自动装箱与拆箱
你有没有想过这个奇怪的现象?
java
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false同一个类,同一个操作符,为什么 100 和 200 的结果不一样?
这背后是 Java 最容易被忽视的语法糖:自动装箱与拆箱。
什么是装箱?什么是拆箱?
装箱(Boxing):基本类型 → 包装类 拆箱(Unboxing):包装类 → 基本类型
java
// 手动装箱(JDK 5 之前唯一方式)
Integer i = Integer.valueOf(10);
// 手动拆箱(JDK 5 之前唯一方式)
int n = i.intValue();
// 自动装箱(JDK 5+ 语法糖)
Integer i = 10; // 编译器自动转为 Integer.valueOf(10)
// 自动拆箱(JDK 5+ 语法糖)
int n = i; // 编译器自动转为 i.intValue()你写的 Integer i = 10;,编译器悄悄帮你调用了 Integer.valueOf(10)。
拆穿缓存的真相
回到开头的例子。为什么 100 和 200 结果不同?
java
public class BoxingTruth {
public static void main(String[] args) {
// 自动装箱调用 valueOf()
Integer a = 100; // → Integer.valueOf(100)
Integer b = 100; // → Integer.valueOf(100)
// 100 在缓存范围内,返回同一个对象
System.out.println(a == b); // true
// 200 超出缓存范围,每次都 new
Integer c = 200; // → Integer.valueOf(200) → new Integer(200)
Integer d = 200; // → Integer.valueOf(200) → new Integer(200)
System.out.println(c == d); // false
}
}Integer.valueOf() 源码:
java
private static class IntegerCache {
static final int low = -128;
static final int high = 127; // 可通过 -Djava.lang.Integer.IntegerCache.high 调整
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
}所以:
- 范围 -128 ~ 127:返回缓存对象,
==比较可能为true - 范围 之外:每次创建新对象,
==比较必为false
什么时候会触发自动装箱/拆箱?
1. 赋值时
java
Integer i = 10; // int → Integer,自动装箱
int n = i; // Integer → int,自动拆箱2. 方法参数传递
java
public class ParameterDemo {
public static void main(String[] args) {
Integer wrapper = 42;
printValue(wrapper); // 自动拆箱:wrapper → 42
}
static void printValue(int value) { // 参数是基本类型
System.out.println("value = " + value);
}
// 反过来
static void getWrapper() {
returnInteger(100); // 自动装箱:100 → Integer.valueOf(100)
}
static Integer returnInteger(Integer value) { // 参数是包装类
return value;
}
}3. 运算时
java
Integer a = 10;
Integer b = 20;
// 运算时自动拆箱,计算完再自动装箱
Integer sum = a + b; // a.intValue() + b.intValue() → 30 → Integer.valueOf(30)4. 集合操作
java
List<Integer> list = new ArrayList<>();
list.add(100); // 自动装箱:int 100 → Integer.valueOf(100)
list.add(200); // 自动装箱:int 200 → Integer.valueOf(200)
int first = list.get(0); // 自动拆箱:Integer → int这是最常见的场景,也是最容易被忽略性能问题的地方。
隐藏的陷阱
陷阱一:空指针异常
java
Integer nullWrapper = null;
// 自动拆箱 → 调用 null.intValue() → NPE
int value = nullWrapper; // java.lang.NullPointerException
// 运算时拆箱也会炸
int sum = nullWrapper + 10; // NPE防御策略:
java
// 方式一:显式检查
if (nullWrapper != null) {
int value = nullWrapper;
}
// 方式二:使用 Optional(JDK 8+)
Optional<Integer> optional = Optional.ofNullable(nullWrapper);
int value = optional.orElse(0);
// 方式三:三元运算符(巧妙但可读性差)
int value = (nullWrapper != null) ? nullWrapper : 0;陷阱二:包装类参与运算
java
public class CalculationTrap {
public static void main(String[] args) {
// 看似简单的计算
Integer a = 1;
Integer b = 2;
Integer c = 3;
// 哪个会 NPE?
System.out.println(a * b * c); // ❌ NPE!
}
}为什么 a * b 不 NPE?因为乘法会触发拆箱。但如果写成:
java
Integer result = a * b * c; // 编译错误:不兼容的类型或者:
java
Integer result = a; // OK
result += b; // 实际上是 result = result + b,每次都拆箱
result += c; // 每次都拆箱陷阱三:泛型与基本类型
java
// ❌ 编译错误:泛型不允许基本类型
List<int> list = new ArrayList<>();
// ✅ 必须用包装类
List<Integer> list = new ArrayList<>();这意味着当你用 List<Integer> 存储 1000 个数字时,内部是 1000 个 Integer 对象,而不是 1000 个 int。
陷阱四:方法参数的不确定性
java
public class OverloadDemo {
public static void main(String[] args) {
method(10); // 调用哪个?
}
// 重载方法
static void method(int i) {
System.out.println("int: " + i);
}
static void method(Integer i) {
System.out.println("Integer: " + i);
}
}如果你用 method(10),调用的是 int 版本,因为 10 是字面量,编译器选择不需要装箱的版本。
但如果是变量:
java
int x = 10;
method(x); // 调用 int 版本
Integer y = 10;
method(y); // 调用 Integer 版本(10 在缓存范围内)性能影响:循环中的装箱
这是生产环境中最常见的性能问题:
java
public class PerformanceDemo {
// ❌ 错误示范:循环中频繁装箱
static long wrongSum() {
long start = System.nanoTime();
Long sum = 0L; // 注意是 Long,不是 long
for (int i = 0; i < 1_000_000; i++) {
sum += i; // 每次:i 装箱 → 相加 → 拆箱 → 装箱 → 赋值
}
long end = System.nanoTime();
System.out.println("Wrong: " + (end - start) + "ns, sum = " + sum);
return sum;
}
// ✅ 正确示范:使用基本类型
static long correctSum() {
long start = System.nanoTime();
long sum = 0L; // 基本类型
for (int i = 0; i < 1_000_000; i++) {
sum += i; // 纯基本类型运算,无装箱
}
long end = System.nanoTime();
System.out.println("Correct: " + (end - start) + "ns, sum = " + sum);
return sum;
}
public static void main(String[] args) {
wrongSum();
correctSum();
// 典型结果:wrong 比 correct 慢 10-50 倍
}
}实战建议:
| 场景 | 推荐类型 | 原因 |
|---|---|---|
| 集合元素 | 包装类 | 必须用 |
| 循环计数器 | 基本类型 | 避免频繁装箱 |
| 数据库映射 | 包装类 | null 表示无值 |
| 数值计算 | 基本类型 | 性能优先 |
| JSON 序列化 | 包装类 | 标准化 |
装箱 vs 拆箱一览表
| 操作 | 触发条件 | 实际调用 |
|---|---|---|
| 装箱 | Integer i = 10 | Integer.valueOf(10) |
| 拆箱 | int n = wrapper | wrapper.intValue() |
| 运算 | wrapper + 1 | wrapper.intValue() + 1,结果再装箱 |
| 比较 | wrapper == 10 | 先拆箱:wrapper.intValue() == 10 |
要点回顾
- 自动装箱/拆箱是编译器行为,不影响运行时的逻辑
valueOf()对 -128~127 有缓存,new Integer()没有- 包装类
==比较地址,缓存范围内可能「碰巧」相等 null拆箱会 NPE,运算时拆箱也会 NPE- 性能敏感代码避免在循环中装箱
教训:
Long sum = 0L; for (...) { sum += i; }这种写法,线上流量一大,GC 就会找你谈话。
