三、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 的任何修改操作都返回新对象,原对象纹丝不动。
【易混对比】
| 特性 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性与继承 | 不可变,final 类 | 可变,final 类 | 可变,final 类 |
| 线程安全 | 线程安全(不可变) | 非线程安全 | 线程安全(synchronized) |
| 适用场景 | 字符串常量、少修改 | 单线程大量拼接 | 多线程大量拼接 |
| 典型陷阱 | 循环中用 + 拼接 | 多线程并发修改 | 单线程无必要加锁 |
【自测】 下列代码创建了几个 String 对象?
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",直接指向常量池;变量拼接则运行时生成堆对象。
【自测】
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 · 知识点:字符串拼接与编译期优化
【题目】执行下列代码,正确的是?
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 在运行期生成堆中新对象。
【推导过程】
String s3 = "java";字面量直接放入常量池,s3 指向常量池对象。String s4 = s1 + s2;s1、s2 是变量,编译期无法确定值,运行时生成新对象:JDK8 及以前底层转为new StringBuilder().append(s1).append(s2).toString(),toString() 在堆中新建对象;JDK9+ 改为 invokedynamic + StringConcatFactory。String s5 = "ja" + "va";两个字面量常量,编译期常量折叠为 "java",s5 直接指向常量池中的已有对象,与 s3 相同。- 因此 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 |
【自测】
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 不能继承,底层数组可扩展」
【易混对比】
| 维度 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 内容可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 安全(不可变) | 不安全 | 安全(synchronized) |
| 可否继承 | 不可(final) | 不可(final) | 不可(final) |
| 单线程性能 | 低(频繁创建对象) | 高 | 中(锁开销) |
| 多线程适用 | 适用 | 不适用 | 适用 |
| JDK 引入 | 1.0 | 1.5 | 1.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'。
【推导过程】
- 原串
"Hello World"的字符索引:0='H', 1='e', 2='l', 3='l', 4='o', 5=' ', 6='W', 7='o', 8='r', 9='l', 10='d'。 s.substring(6)从索引 6 开始截取到末尾(含头不含尾,省略 endIndex 默认到末尾),得到新字符串"World"。"World"的索引:0='W', 1='o', 2='r', 3='l', 4='d'。.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] | 不可 | 负值无缓存 |
| Boolean | true / false | 不可 | 始终命中 |
| Float / Double | 无 | 无 | 任何值都新建对象 |
- 进阶考法:若
Integer a = 128, b = 128;改用Integer.valueOf(128)结果不变;若改为new Integer(128)则无论值大小都新建对象(但 JDK9 起已废弃)。
【自测】
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 · 知识点:包装类与基本类型的比较
【题目】执行下列代码,正确的是?
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 一律按内容比较。
【推导过程】
Integer a = new Integer(100);在堆中创建 Integer 对象(值 100)。Integer b = new Integer(100);在堆中创建另一个 Integer 对象(值 100)。int c = 100;基本类型变量 c,值为 100。- 判断 A:
a == b比较引用地址,a 与 b 是两个不同对象 → false。 - 判断 B:
a == c是包装类与基本类型比较,触发自动拆箱,a 拆为 int 100,100 == 100 → true。 - 判断 C:
b.equals(c)中 c 是基本类型 int,传入 equals(Object) 时自动装箱为 Integer(100),Integer.equals 比较内部 int 值,100 == 100 → true,故 C 说 false 是错的。 - 判断 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。
【自测】
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 → int | int b = a; | 是 | a 为 null 则 NPE |
| Integer → Integer | Integer 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>。
【自测】 下列代码哪行会抛异常?
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 或默认值工具。