自动装拆箱

核心概念

自动装箱(Autoboxing):基本类型 → 包装类型 自动拆箱(Unboxing):包装类型 → 基本类型

这是Java编译器提供的语法糖,让基本类型和包装类之间可以无缝转换。


一、底层原理

1.1 装箱:valueOf()

// 源码
Integer num = 100;

// 编译器自动转换为
Integer num = Integer.valueOf(100);

valueOf()方法实现:

public static Integer valueOf(int i) {
    // 范围内直接返回缓存对象
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    // 超出范围创建新对象
    return new Integer(i);
}

1.2 拆箱:xxxValue()

// 源码
Integer num = 100;
int x = num;

// 编译器自动转换为
int x = num.intValue();

intValue()方法实现:

public int intValue() {
    return value;  // 直接返回内部存储的int值
}

二、自动装拆箱的触发场景

2.1 装箱场景

① 赋值操作

Integer a = 10;  // 自动装箱:Integer.valueOf(10)

② 方法参数

public void print(Integer num) { }

print(20);  // 自动装箱:print(Integer.valueOf(20))

③ 集合操作

List<Integer> list = new ArrayList<>();
list.add(30);  // 自动装箱:list.add(Integer.valueOf(30))

④ 三元表达式

Integer result = true ? 100 : 200.5;
// 100先装箱为Integer,再拆箱为int,最后转为double,再装箱为Double
// 实际结果类型是Number(Integer和Double的公共父类)

2.2 拆箱场景

① 赋值操作

Integer a = 10;
int b = a;  // 自动拆箱:a.intValue()

② 运算操作

Integer a = 10;
Integer b = 20;
int sum = a + b;  // 自动拆箱:a.intValue() + b.intValue()

③ 比较操作

Integer a = 10;
if (a > 5) { }  // 自动拆箱:a.intValue() > 5

④ 方法返回

public int getValue() {
    Integer num = 100;
    return num;  // 自动拆箱:return num.intValue()
}

三、核心陷阱与解决方案

3.1 空指针异常(NPE)⚠️

陷阱代码

Integer num = null;
int x = num;  // 💥 NullPointerException

// 原因:编译后变为
int x = num.intValue();  // null.intValue() 报错

真实场景

// 数据库查询结果可能为null
public void calculateBonus(Integer userId) {
    Integer bonus = userDao.getBonus(userId);  // 可能返回null
    int total = bonus + 100;  // 💥 NPE
}

解决方案

// 方案1:null检查
Integer bonus = userDao.getBonus(userId);
int total = (bonus != null ? bonus : 0) + 100;

// 方案2:使用包装类运算(会自动装箱)
Integer total = bonus + 100;  // bonus为null时,运算前会报错,提前发现问题

// 方案3:Optional(推荐)
int total = Optional.ofNullable(userDao.getBonus(userId))
                    .orElse(0) + 100;

3.2 缓存池引发的 == 判断错误

// ✅ 缓存范围内(-128~127)
Integer a = 100;
Integer b = 100;
System.out.println(a == b);  // true(共享同一个对象)

// ❌ 超出缓存范围
Integer x = 200;
Integer y = 200;
System.out.println(x == y);  // false(两个不同对象)

正确做法:

// 包装类比较,永远用equals()
Integer a = 200;
Integer b = 200;
System.out.println(a.equals(b));  // true

3.3 性能问题:频繁装拆箱

问题代码

// ❌ 每次循环都装箱一次,创建10000个Integer对象
Long sum = 0L;
for (int i = 0; i < 10000; i++) {
    sum += i;  // sum.longValue() + i,结果再装箱为Long
}

优化方案

// ✅ 使用基本类型
long sum = 0L;
for (int i = 0; i < 10000; i++) {
    sum += i;  // 纯基本类型运算,无装箱开销
}

性能对比

// 测试:累加1000万次
// 包装类版本:耗时约800ms,创建1000万个对象,触发多次GC
// 基本类型版本:耗时约20ms,无对象创建

四、equals() vs ==

4.1 核心区别

比较方式 基本类型 包装类型
== 比较值 比较引用(地址)
equals() 不支持 比较值(重写后)

4.2 典型案例

// 案例1:缓存池内
Integer a = 127;
Integer b = 127;
System.out.println(a == b);        // true(同一对象)
System.out.println(a.equals(b));   // true(值相等)

// 案例2:缓存池外
Integer x = 200;
Integer y = 200;
System.out.println(x == y);        // false(不同对象)
System.out.println(x.equals(y));   // true(值相等)

// 案例3:混合比较
Integer m = 100;
int n = 100;
System.out.println(m == n);        // true(m自动拆箱后比较值)

记忆法则:

包装类比较,永远用equals() 除非明确要判断"是否同一对象"(如单例模式)


五、高频面试题

Q1: 什么是自动装箱和自动拆箱?底层如何实现?

答:

  • 装箱int → Integer,编译器调用Integer.valueOf()
  • 拆箱Integer → int,编译器调用intValue()
  • 本质:编译器的语法糖,简化代码编写

Q2: 自动拆箱时为什么可能出现空指针异常?

答:

Integer num = null;
int x = num;  // 拆箱调用num.intValue(),null.xxx()报NPE

避免方式:拆箱前检查null,或使用Optional

Q3: 为什么频繁装拆箱会影响性能?该如何优化?

答:

  • 问题:每次装箱都创建新对象(超出缓存范围),增加GC压力
  • 优化
    1. 高频运算使用基本类型
    2. 预估容量,减少集合扩容导致的装箱
    3. 使用@Primitive注解(Lombok)或原始类型集合(Trove/Eclipse Collections)

Q4: Integer a = 128, b = 128,a == b 为 false 的原因?

答:

  • 128超出缓存池范围(-128~127)
  • 两次装箱创建了两个不同的对象
  • ==比较的是引用地址,不同对象返回false
  • 正确写法:a.equals(b)

Q5: Integer.valueOf(127) == Integer.valueOf(127) 为什么为 true?

答:

  • 127在缓存池范围内
  • valueOf()返回的是缓存池中的同一个对象
  • ==比较引用,指向同一对象返回true

延伸:缓存池详见 03-缓存池机制(Cache Pool)

Q6: == 和 equals 在比较 Integer 对象时有何区别?

答:

方法 比较内容 使用场景
== 对象引用(地址) 判断是否同一对象
equals() 对象的值 判断值是否相等

推荐:包装类比较统一用equals()


六、最佳实践

✅ 推荐做法

// 1. 基本类型优先
public int calculate(int a, int b) {
    return a + b;
}

// 2. 包装类比较用equals()
Integer a = 200;
Integer b = 200;
if (a.equals(b)) { }

// 3. 空值检查
Integer num = getFromDatabase();
int value = (num != null) ? num : 0;

// 4. 避免循环装箱
long sum = 0L;  // 而不是Long sum = 0L

❌ 避免做法

// 1. 循环中装箱
Long sum = 0L;
for (int i = 0; i < 10000; i++) {
    sum += i;  // 每次都装箱
}

// 2. 包装类用 ==
Integer a = 200;
Integer b = 200;
if (a == b) { }  // 错误,应该用equals()

// 3. 忽略null检查
Integer num = null;
int x = num;  // NPE

相关链接


记忆口诀

装箱拆箱编译糖,valueOf和intValue 缓存池内同对象,超出范围各自造 包装比较用equals,基本类型用双等 空值拆箱抛NPE,循环装箱性能糟


⬅️ 基本类型与包装类 🏠 00-Java ➡️ 缓存池机制(Cache Pool)