Skip to content

自动装箱与拆箱

你有没有想过这个奇怪的现象?

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&lt;Integer&gt; 存储 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 = 10Integer.valueOf(10)
拆箱int n = wrapperwrapper.intValue()
运算wrapper + 1wrapper.intValue() + 1,结果再装箱
比较wrapper == 10先拆箱:wrapper.intValue() == 10

要点回顾

  • 自动装箱/拆箱是编译器行为,不影响运行时的逻辑
  • valueOf() 对 -128~127 有缓存,new Integer() 没有
  • 包装类 == 比较地址,缓存范围内可能「碰巧」相等
  • null 拆箱会 NPE,运算时拆箱也会 NPE
  • 性能敏感代码避免在循环中装箱

教训:Long sum = 0L; for (...) { sum += i; } 这种写法,线上流量一大,GC 就会找你谈话。

基于 VitePress 构建