Skip to content

三、String 与包装类(J23–J30) ​

J23 · 知识点:String 不可变性 ​

【题目】关于 String 的说法,正确的是?

A. String 是可变的 B. String s = "a"; s += "b"; 修改了原对象的内容 C. String 被 final 修饰不可继承,且内容不可修改 D. String 与 StringBuilder 的拼接效率相同

答案:C

【考点】String 不可变性的本质:final 类 + 内部字符数组不可变。

【结论】选 C。String 被 final 修饰不可继承,内部字符数组私有且不可变,内容创建后无法修改。

【逐项辨析】

  • A 错:String 是不可变类,任何“修改”操作都会创建新对象。
  • C 正确:String 被 final 修饰(不可继承),内部用 final char[](JDK9 后为 byte[])存储,内容不可修改。
  • B 错:s += "b" 并非修改原对象,而是新建 "ab" 对象并让引用 s 指向它,原对象 "a" 仍驻留常量池待 GC。
  • D 错:String 不可变导致拼接频繁创建中间对象,效率远低于 StringBuilder;循环拼接应优先使用 StringBuilder。

【知识点】 String 不可变性由三层机制保证:① 类被 final 修饰,禁止子类重写破坏不可变性;② 内部字符数组被 final 修饰且不提供修改接口;③ 所有修改方法(substring、concat、replace 等)均返回新字符串而非修改原串。不可变性的优势包括:字符串常量池复用、HashCode 可缓存、线程安全、可作为 Map 键。代价是频繁拼接产生大量临时对象,因此在循环或大量拼接场景下应使用 StringBuilder(单线程)或 StringBuffer(多线程)。

【记忆锚点】 「final 类 final 数组,一改就换新地址」——String 的任何修改操作都返回新对象,原对象纹丝不动。

【易混对比】

特性StringStringBuilderStringBuffer
可变性与继承不可变,final 类可变,final 类可变,final 类
线程安全线程安全(不可变)非线程安全线程安全(synchronized)
适用场景字符串常量、少修改单线程大量拼接多线程大量拼接
典型陷阱循环中用 + 拼接多线程并发修改单线程无必要加锁

【自测】 下列代码创建了几个 String 对象?

java
String s = "a";
s = s + "b";
s = s + "c";

答:共产生多个对象:常量池 "a"、"b"、"c" 各 1 个;运行时堆中生成 "ab"、"abc" 两个新串。若循环拼接则中间对象更多,必须使用 StringBuilder 优化。与 J23 连考。

【知识关联】

  • 同库关联:与 J24(常量池)、J25(拼接)、J26(StringBuilder)、J09(final)同属 String 题群;与 P33(Python 字符串不可变)、P70(驻留)跨语言对照。
  • 实现层:String 内部 final byte[](JDK 9+ compact strings);不可变使常量池共享、线程安全、可缓存 hash。
  • 面试追问:① 不可变带来的好处?② 如何实现「修改」字符串?

【拓展延伸】

  • 变式问法:s.replace 是否改变原串;substring 在 JDK 7u6 后的实现差异。
  • 版本差异:JDK 8 char[] value;JDK 9+ byte[] + coder(Latin1/UTF16)。
  • 工程注意点:循环修改用 StringBuilder;密码用 char[] 便于擦除;intern 滥用会撑爆元空间。

J24 · 知识点:字符串常量池 ​

【题目】String s1 = "abc"; String s2 = new String("abc"); 下列说法正确的是?

A. s1 == s2 B. s1.equals(s2) 为 false C. s1 == s2 为 false,s1.equals(s2) 为 true D. s1 与 s2 指向同一个对象

答案:C

【考点】字面量 → 字符串常量池;new → 堆中新对象;== 比较引用、equals 比较内容。

【结论】选 C。s1 指向字符串常量池中的对象,s2 指向堆中新建的对象,== 比较引用地址故为 false,equals 比较内容故为 true。

【逐项辨析】

  • A 错:s1 == s2 为 false,s1 在常量池,s2 在堆,地址不同。
  • B 错:String 重写了 equals,按字符逐一比较内容,"abc".equals("abc") 为 true。
  • C 正确:s1 与 s2 引用不同(== false),内容相同(equals true)。
  • D 错:s2 通过 new 关键字强制在堆中新建对象,与常量池对象不是同一个。

【知识点】 字符串常量池是 JVM 堆中的一块特殊内存区域(JDK7 起从永久代移至堆),用于存储字面量创建的字符串。String s1 = "abc" 时,JVM 先检查常量池:若存在 "abc" 则直接复用,不存在则创建并放入常量池。String s2 = new String("abc") 则一定在堆中新建 String 对象,同时若常量池无 "abc" 也会创建一份(构造参数的字面量触发)。因此 new String("abc") 最多可能创建 2 个对象(堆 1 个 + 常量池 1 个)。intern() 方法可将堆中字符串的引用放入常量池并返回常量池引用。

【记忆锚点】 「字面量进池,new 必堆中;== 看地址,equals 看内容」

【易混对比】

  • 进阶考法:String s3 = new String("abc").intern(); 此时 s3 == s1 为 true,因为 intern() 返回常量池引用。
  • 与 J25 关联:常量拼接 "a" + "bc" 编译期折叠为 "abc",直接指向常量池;变量拼接则运行时生成堆对象。

【自测】

java
String s1 = new String("a") + new String("b");
String s2 = "ab";

问 s1 == s2 结果?

答:false。变量拼接在运行时通过 StringBuilder 完成,s1 指向堆中新对象;s2 指向常量池。与 J24、J25 连考。

【知识关联】

  • 同库关联:与 J23/J25/J28(Integer 缓存类似思想)、J63(运行时数据区:堆与元空间/方法区)相关。
  • 实现层:JDK 7 起字符串常量池在堆;ldc 从运行时常量池取引用;intern() 尝试放入池并返回规范引用。
  • 面试追问:① new String("a") 创建了几个对象?② intern 在 JDK 6/7+ 的差异?

【拓展延伸】

  • 变式问法:"a"+"b" 与 "ab" ==;多个相同字面量是否同一对象。
  • 版本差异:JDK 6 池在永久代易 OOM;JDK 7+ 移到堆;后续 G1/ZGC 下池回收行为需关注。
  • 工程注意点:避免大量动态 intern;用 String.intern 做去重要评估内存;配置类/常量用 static final 编译期折叠。

J25 · 知识点:字符串拼接与编译期优化 ​

【题目】执行下列代码,正确的是?

java
String s1 = "ja";
String s2 = "va";
String s3 = "java";
String s4 = s1 + s2;   // 变量拼接
String s5 = "ja" + "va"; // 常量拼接

A. s3 == s4 B. s4 == s5 C. s3 == s5 D. s4.equals(s5) 为 false

答案:C

【考点】编译期常量折叠 vs 运行期动态拼接(JDK8 及以前 StringBuilder,JDK9+ invokedynamic)。

【结论】选 C。常量拼接 "ja" + "va" 在编译期被折叠为 "java",s5 与 s3 指向常量池同一对象;变量拼接 s1 + s2 在运行期生成堆中新对象。

【推导过程】

  1. String s3 = "java"; 字面量直接放入常量池,s3 指向常量池对象。
  2. String s4 = s1 + s2; s1、s2 是变量,编译期无法确定值,运行时生成新对象:JDK8 及以前底层转为 new StringBuilder().append(s1).append(s2).toString(),toString() 在堆中新建对象;JDK9+ 改为 invokedynamic + StringConcatFactory。
  3. String s5 = "ja" + "va"; 两个字面量常量,编译期常量折叠为 "java",s5 直接指向常量池中的已有对象,与 s3 相同。
  4. 因此 s3 == s5 为 true,s3 == s4 为 false,s4 == s5 为 false;s4.equals(s5) 比较内容,为 true。

【逐项辨析】

  • A错:s3 指向常量池,s4 指向堆中新对象,== 比较引用为 false。
  • B错:s4 在堆,s5 在常量池,引用地址不同。
  • C正确:s3 与 s5 均指向常量池中的 "java",是同一对象。
  • D错:s4 与 s5 内容均为 "java",equals 为 true。

【知识点】 编译期常量折叠(Constant Folding):当字符串拼接的操作数都是编译期常量(字面量、final 修饰的字符串常量)时,Javac 直接在编译阶段计算结果并放入常量池。运行期动态拼接涉及变量时,编译器无法预知值,只能生成字节码在运行时执行。JDK8 及以前编译为 StringBuilder 链式 append,JDK9 引入 JEP 280,改用 invokedynamic 指令配合 StringConcatFactory,运行时动态生成最优拼接策略(可能仍是 StringBuilder、StringConcatHelper 或数组拷贝)。无论哪种实现,变量拼接的结果都是运行时新建的堆对象,不会进入常量池(除非显式调用 intern())。

【记忆锚点】 「变量拼接走运行,常量折叠在编译;变量一定生新对象,常量可能复用旧池」

【易混对比】

拼接形式编译期行为运行期行为结果位置== 常量池对象
变量 + 变量不折叠JDK8 StringBuilder / JDK9 invokedynamic堆false
常量 + 常量直接折叠无常量池true
final 常量 + 常量直接折叠无常量池true
变量 + 常量不折叠动态拼接堆false

【自测】

java
final String s1 = "ja";
String s2 = "va";
String s3 = s1 + s2;
String s4 = "java";

问 s3 == s4 结果?

答:false。s1 虽是 final,但 s2 不是,整体非常量表达式,编译期不折叠,运行期动态拼接生成堆对象。若 s2 也声明为 final,则编译期折叠,s3 == s4 为 true。与 J25 连考。

【知识关联】

  • 同库关联:与 J07(运行时拼接)、J26(Builder)、J24(编译期常量折叠)衔接。
  • 实现层:编译期常量表达式可折叠进常量池;运行时 JDK8 用 StringBuilder,JDK9+ invokedynamic。
  • 面试追问:① 哪些算编译期常量?② 循环拼接为何要用 Builder?

【拓展延伸】

  • 变式问法:final String a="x"; final String b="y"; "xy"==a+b;或非 final 拼接是否进池。
  • 版本差异:JDK 9 indy 策略可调;JMH 下不同策略性能不同。
  • 工程注意点:日志避免无条件字符串拼接;国际化消息用 MessageFormat;大文本用 StringBuilder/Writer。

J26 · 知识点:StringBuilder 与 StringBuffer ​

【题目】关于 StringBuilder 和 StringBuffer,下列说法正确的是?

A. StringBuilder 线程安全 B. 两者都是可变类,但都可以被继承 C. StringBuffer 线程安全,其方法被 synchronized 修饰 D. 两者内容不可变

答案:C

【考点】可变字符序列的线程安全性对比。

【结论】选 C。StringBuffer 的 append 等方法被 synchronized 修饰,线程安全;StringBuilder 无同步锁,单线程性能更高。

【逐项辨析】

  • A 错:StringBuilder 未加锁,非线程安全,多线程并发修改可能丢数据或抛异常。
  • C 正确:StringBuffer 方法被 synchronized 修饰,同一时刻仅允许一个线程执行修改操作,线程安全但加锁有性能开销。
  • B 错:两者均为 final 类,不可被继承。
  • D 错:两者均继承 AbstractStringBuilder,内部 char[] / byte[] 可变,append、insert、reverse 等操作直接修改内部数组。

【知识点】 StringBuilder 与 StringBuffer 均继承自 AbstractStringBuilder,底层使用可变字符数组(JDK9 前为 char[],JDK9 后为 byte[] + 编码标识),默认初始容量 16,扩容时创建新数组并拷贝(newCapacity = oldCapacity * 2 + 2)。StringBuffer 在所有修改方法上加 synchronized 同步锁,保证线程安全,但锁竞争导致并发性能下降;StringBuilder 去掉同步锁,单线程场景下效率显著更高(通常快 2~5 倍)。两者都是 final 类,防止子类破坏封装。实际开发中,单线程字符串拼接优先使用 StringBuilder;仅在多线程共享且频繁修改的字符缓冲区场景下使用 StringBuffer(但此类场景更推荐使用 java.util.concurrent 下的并发工具或 ThreadLocal<StringBuilder>)。

【记忆锚点】 「Builder 无锁跑得快,Buffer 加锁保安全;都是 final 不能继承,底层数组可扩展」

【易混对比】

维度StringStringBuilderStringBuffer
内容可变性不可变可变可变
线程安全安全(不可变)不安全安全(synchronized)
可否继承不可(final)不可(final)不可(final)
单线程性能低(频繁创建对象)高中(锁开销)
多线程适用适用不适用适用
JDK 引入1.01.51.0

【自测】 多线程环境下,下列哪种方式拼接字符串最安全? A. StringBuilder B. StringBuffer C. synchronized(StringBuilder) 包装 D. String

答:B。StringBuffer 内建方法级同步;D 虽线程安全但每次拼接产生新对象,不适合大量拼接。与 J26 连考。

【知识关联】

  • 同库关联:与 J23/J25 相邻;与多线程 J53/J54 对比——StringBuffer 方法 synchronized,StringBuilder 无锁。
  • 实现层:两者底层均为可扩容 byte/char 数组;扩容约 2 倍 + 新增长度。
  • 面试追问:① 何时必须 StringBuffer?② 扩容策略是什么?

【拓展延伸】

  • 变式问法:单线程选谁;二者是否线程安全。
  • 版本差异:语义稳定;JDK 11+ 等有细微实现优化。
  • 工程注意点:默认 StringBuilder;预估容量 new StringBuilder(64) 减少扩容;不要把 Builder 当共享可变全局。

J27 · 知识点:String 常用方法 ​

【题目】String s = "Hello World"; 表达式 s.substring(6).charAt(1) 的结果是? A. 'r' B. 'W' C. 'o' D. 'l'

答案:C

【考点】substring 与 charAt 的索引规则(含头不含尾、从 0 开始)。

【结论】选 C。substring(6) 截取索引 6 到末尾得到 "World",charAt(1) 取新串索引 1 的字符为 'o'。

【推导过程】

  1. 原串 "Hello World" 的字符索引:0='H', 1='e', 2='l', 3='l', 4='o', 5=' ', 6='W', 7='o', 8='r', 9='l', 10='d'。
  2. s.substring(6) 从索引 6 开始截取到末尾(含头不含尾,省略 endIndex 默认到末尾),得到新字符串 "World"。
  3. "World" 的索引:0='W', 1='o', 2='r', 3='l', 4='d'。
  4. .charAt(1) 取索引 1,即 'o'。

【逐项辨析】

  • C正确:substring(6) 得 "World",charAt(1) 得 'o'。
  • B错:'W' 是 substring(6) 结果的首字符(索引 0),但题目问的是再取 charAt(1)。
  • A错:'r' 是 "World" 的索引 2,需 charAt(2) 才能得到。
  • D错:'l' 是 "World" 的索引 3,不符合题意。

【知识点】 String 的 substring(int beginIndex) 方法返回从 beginIndex 开始到字符串末尾的新字符串,原字符串保持不变(不可变性)。charAt(int index) 返回指定索引处的 char 值,索引从 0 开始。当 beginIndex 为负数或大于字符串长度时,substring 抛 StringIndexOutOfBoundsException;当 index 为负数或大于等于字符串长度时,charAt 同样抛该异常。JDK7 之前 substring 共享原串的 char[] 数组(通过偏移量和长度引用),可能导致内存泄漏;JDK7 起改为拷贝新数组,与原串彻底解耦。

【记忆锚点】 「substring 含头不含尾,charAt 零起算;原串不动新串出,组合题里步步看」

【易混对比】

  • substring 重载:substring(int beginIndex, int endIndex) 截取 [beginIndex, endIndex),如 s.substring(6, 8) 得 "Wo"。
  • 常见陷阱:s.substring(6).charAt(0) 得 'W';s.charAt(6) 也得 'W',但前者创建了新对象,后者没有。
  • 索引越界:s.substring(11) 合法(返回空串 ""),但 s.charAt(11) 抛异常。

【自测】"Hello World".substring(0, 5).charAt(4) 的结果是?

答:'o'。substring(0,5) 截取 [0,5) 得 "Hello",charAt(4) 取最后一个字符 'o'。与 J27 连考。

【知识关联】

  • 同库关联:与 J23–J26 一体;与 P33–P40(Python 字符串方法)对照表式记忆。
  • 实现层:charAt/indexOf 等为 O(n) 扫描;JDK 9+ Latin1 可用 intrinsics 加速。
  • 面试追问:① substring 复杂度?② 如何高效判断空串/null?

【拓展延伸】

  • 变式问法:split 正则注意点;replace 与 replaceAll 区别。
  • 版本差异:JDK 8 vs 9+ 底层表示变化影响部分方法分配。
  • 工程注意点:判空用 s == null || s.isEmpty() 或工具类;正则 split 注意转义;日志脱敏用 substring 注意越界。

J28 · 知识点:Integer 缓存(128 陷阱) ​

【题目】Integer a = 127, b = 127, c = 128, d = 128; 下列比较结果正确的是?

A. a == b 为 true,c == d 为 true B. a == b 为 false,c == d 为 false C. a == b 为 true,c == d 为 false D. 编译错误

答案:C

【考点】包装类缓存机制:自动装箱调用 valueOf,默认缓存 -128~127。

【结论】选 C。127 落在默认缓存区间 [-128, 127] 内,a 与 b 指向 IntegerCache 中的同一对象;128 超出区间,c 与 d 分别新建对象。

【逐项辨析】

  • A 错:c == d 为 false,128 超出缓存,c、d 是两个不同的堆对象。
  • B 错:a == b 为 true,127 命中缓存,a、b 是同一对象。
  • C 正确:127 在缓存内(== true),128 在缓存外(== false)。
  • D 错:自动装箱是合法语法,不会编译错误。

【知识点】 自动装箱 Integer a = 127 编译后等价于 Integer a = Integer.valueOf(127)。Integer.valueOf(int) 的源码逻辑为:若值落在 [-128, 127] 区间内,则直接返回 IntegerCache.cache[] 中预创建的缓存对象;否则执行 new Integer(i) 创建新对象。该缓存机制旨在减少高频小整数的对象创建开销。Integer 缓存上限可通过 JVM 启动参数 -XX:AutoBoxCacheMax=<size> 调整。其他包装类的缓存范围:Byte、Short、Long 固定 [-128, 127];Character 固定 [0, 127];Boolean 固定缓存 TRUE/FALSE;Float、Double 无缓存。

【记忆锚点】 「自动装箱调 valueOf,-128 到 127 走缓存;一出范围就 new,== 立刻现原形」

【易混对比】

包装类缓存范围可否调整典型陷阱
Integer[-128, 127]可(-XX:AutoBoxCacheMax)128 陷阱
Byte[-128, 127]不可全部命中缓存
Short[-128, 127]不可同 Integer
Long[-128, 127]不可同 Integer
Character[0, 127]不可负值无缓存
Booleantrue / false不可始终命中
Float / Double无无任何值都新建对象
  • 进阶考法:若 Integer a = 128, b = 128; 改用 Integer.valueOf(128) 结果不变;若改为 new Integer(128) 则无论值大小都新建对象(但 JDK9 起已废弃)。

【自测】

java
Integer a = 100;
Integer b = 100;
Integer c = 200;
Integer d = 200;
System.out.println(a == b);
System.out.println(c == d);

输出结果?

答:true 和 false。100 在缓存区间,200 超出。与 J28 连考。

【知识关联】

  • 同库关联:与 J29/J30(拆箱)、J24(字符串池思想一致)、J06(三元装箱类型)相关。
  • 实现层:IntegerCache 默认 -128~127(堆外/堆内实现随版本);valueOf 优先走缓存;new Integer 永远新对象且 JDK9+ 废弃。
  • 面试追问:① 缓存上限如何修改?② Long/Short 缓存范围?

【拓展延伸】

  • 变式问法:Integer a=127,b=127; a==b;a=128 时结果。
  • 版本差异:可通过 -XX:AutoBoxCacheMax 调整上限(实现细节,非标准 API)。
  • 工程注意点:包装类比较用 equals;避免 new Integer;缓存范围外 == 会踩坑。

J29 · 知识点:包装类与基本类型的比较 ​

【题目】执行下列代码,正确的是?

java
Integer a = new Integer(100);
Integer b = new Integer(100);
int c = 100;

A. a == b 为 true B. a == c 为 true C. b.equals(c) 为 false D. 以上都正确

答案:B

【考点】包装类 == 比较引用;包装类与基本类型比较时自动拆箱比数值;equals 自动装箱比较内容。

【结论】选 B。包装类与基本类型 == 比较时,包装类自动拆箱按数值比较;同类型包装类 == 比较引用地址;equals 一律按内容比较。

【推导过程】

  1. Integer a = new Integer(100); 在堆中创建 Integer 对象(值 100)。
  2. Integer b = new Integer(100); 在堆中创建另一个 Integer 对象(值 100)。
  3. int c = 100; 基本类型变量 c,值为 100。
  4. 判断 A:a == b 比较引用地址,a 与 b 是两个不同对象 → false。
  5. 判断 B:a == c 是包装类与基本类型比较,触发自动拆箱,a 拆为 int 100,100 == 100 → true。
  6. 判断 C:b.equals(c) 中 c 是基本类型 int,传入 equals(Object) 时自动装箱为 Integer(100),Integer.equals 比较内部 int 值,100 == 100 → true,故 C 说 false 是错的。
  7. 判断 D:A 错、C 错,因此“以上都正确”不成立。

【逐项辨析】

  • A错:显式 new 创建两个独立对象,== 比较引用为 false(即使值相同)。
  • B正确:包装类与基本类型混用 == 时,编译器插入自动拆箱指令,转为基本类型数值比较。
  • C错:b.equals(c) 为 true。equals 接收 Object,int 自动装箱为 Integer,再按数值比较。
  • D错:A、C 均不成立,故 D错误。

【知识点】 Java 的 == 运算符在包装类之间的行为:① 两个包装类对象比较时,== 比较堆中引用地址;② 包装类与基本类型比较时,触发自动拆箱(Unboxing),转为基本类型数值比较。equals 方法在包装类中均被重写,按封装的数值内容比较,且参数为 Object 类型,因此基本类型会先自动装箱(Boxing)再传入。需注意:自动拆箱在 null 引用上会触发 NullPointerException(见 J30);new Integer() 自 JDK9 标记为 deprecated、JDK16 标记 forRemoval,应使用 Integer.valueOf() 利用缓存。

【记忆锚点】 「同类 == 比地址,混用 == 比数值;equals 永远比内容,null 拆箱要炸锅」

【易混对比】

  • 若改为 Integer a = 100, b = 100;(自动装箱走 valueOf),则 a == b 为 true(缓存命中),与显式 new 行为截然不同。
  • 若改为 Integer a = null; int c = 100; System.out.println(a == c); 则抛 NullPointerException(null 拆箱)。
  • 进阶考法:new Integer(100) == new Integer(100) 一定 false;Integer.valueOf(100) == Integer.valueOf(100) 一定 true。

【自测】

java
Integer x = 200;
Integer y = 200;
int z = 200;
System.out.println(x == y);
System.out.println(x == z);

输出结果?

答:false 和 true。200 超出缓存,x 与 y 是不同对象;x == z 触发拆箱,按数值比较为 true。与 J29、J28 连考。

【知识关联】

  • 同库关联:与 J28/J30/J06/J03 构成类型系统题群。
  • 实现层:== 对包装类型若为对象则比引用;一方为基本类型则拆箱比数值。
  • 面试追问:① Integer a=1; long b=1; a==b 结果?② 两个 new Integer(1) 的 ==?

【拓展延伸】

  • 变式问法:混合 == 与 equals 的真假组合表。
  • 版本差异:语义稳定。
  • 工程注意点:统一用 equals 或拆成基本类型再比;方法参数尽量基本类型避免无谓装箱。

J30 · 知识点:自动拆箱与空指针 ​

【题目】下列代码会抛出 NullPointerException 的是?

A. Integer a = null; Integer b = a; B. Integer a = 1; int b = a; C. Integer a = null; int b = a; D. Integer a = null; if (a == null) {}

答案:C

【考点】自动拆箱在 null 上触发 NPE;引用赋值不拆箱。

【结论】选 C。Integer 引用为 null 时赋值给基本类型 int,触发自动拆箱并隐式调用 intValue(),在 null 上调用实例方法抛出 NullPointerException。

【逐项辨析】

  • C正确:int b = a 触发自动拆箱,编译器等价转换为 int b = a.intValue();,a 为 null 时抛 NullPointerException。
  • B错:a 为 1(非 null),自动拆箱调用 intValue() 正常,b = 1。
  • A错:Integer b = a 是引用类型之间的赋值,仅复制引用地址,不涉及拆箱操作,安全。
  • D错:对引用类型进行 null 判空是合法且安全的操作,不会触发任何异常。

【知识点】 自动拆箱(Unboxing)是 Java 5 引入的语法糖,当包装类对象出现在需要基本类型的上下文中时,编译器自动插入 xxxValue() 调用,如 Integer → int 调用 intValue(),Long → long 调用 longValue() 等。若包装类引用为 null,调用 xxxValue() 相当于在 null 引用上调用实例方法,必然抛出 NullPointerException。该异常是线上故障高频诱因,典型场景包括:从数据库或 Map<String, Integer> 中取出 null 值后直接参与算术运算、赋值给基本类型变量、或作为方法参数传入需要基本类型的方法。

【记忆锚点】 「包装转基本,编译加 intValue();左边是 int,右边 null 就 NPE」

【易混对比】

场景代码示例是否拆箱结果
Integer → intint b = a;是a 为 null 则 NPE
Integer → IntegerInteger b = a;否安全,仅引用赋值
Integer 判空if (a == null)否安全
Integer 运算int c = a + 1;是a 为 null 则 NPE
Integer 比较a == 1是a 为 null 则 NPE
方法传参foo(int x) 传入 Integer a是a 为 null 则 NPE
  • 防御式编程:从可能返回 null 的容器或接口取值后,先判空再拆箱,或使用 Java 8 的 Optional<Integer>。

【自测】 下列代码哪行会抛异常?

java
Map<String, Integer> map = new HashMap<>();
map.put("k", null);
int val = map.get("k");

答:第 3 行抛 NullPointerException。map.get("k") 返回 null,赋值给 int val 时自动拆箱失败。与 J30 连考。

【知识关联】

  • 同库关联:与 J28/J29 直接连考;与 Spring 参数绑定/Optional 使用中的拆箱 NPE 面试常问。
  • 实现层:拆箱生成 invokevirtual intValue(),引用为 null 时 NPE。
  • 面试追问:① 三元/运算中的隐式拆箱 NPE 场景?② 如何安全处理 Integer?

【拓展延伸】

  • 变式问法:Integer x=null; int y=x;;Integer x=null; Integer y=x==null?0:x;。
  • 版本差异:语义稳定。
  • 工程注意点:DTO 字段避免基本类型以免 JSON 缺字段拆箱炸;用 Optional 或默认值工具。

持续学习,持续积累。