自动装箱与拆箱
自动装箱与拆箱
自动装箱(Autoboxing)和自动拆箱(Unboxing)是 Java 编译器提供的语法糖,用来在基本类型和包装类型之间自动转换。
- 自动装箱:
int -> Integer - 自动拆箱:
Integer -> int
int a = 10;
Integer b = a; // 自动装箱
Integer c = 20;
int d = c; // 自动拆箱编译器大致会把上面的代码转换成:
Integer b = Integer.valueOf(a);
int d = c.intValue();为什么需要自动装箱
Java 集合只能直接存对象,不能直接存基本类型,因此包装类很常见:
List<Integer> list = new ArrayList<>();
list.add(1); // 实际发生了自动装箱如果没有自动装箱,代码会变成:
list.add(Integer.valueOf(1));自动装箱的意义就是让代码更简洁。
自动装箱一定会创建新对象吗
不一定。
自动装箱本质上通常调用的是 Integer.valueOf(),而不是 new Integer():
Integer a = 100;大致等价于:
Integer a = Integer.valueOf(100);这里是否创建新对象,要看 Integer.valueOf() 是否命中缓存。
Integer 缓存机制
Integer 内部有一个 IntegerCache,默认会缓存 -128 ~ 127 之间的整数对象。
可以简单理解为:
public static Integer valueOf(int i) {
if (i >= -128 && i <= 127) {
return IntegerCache.cache[i + 128];
}
return new Integer(i);
}所以:
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true127 在缓存范围内,a 和 b 复用的是同一个对象。
Integer a = 128;
Integer b = 128;
System.out.println(a == b); // false128 超出默认缓存范围,通常会创建两个不同对象,因此引用不同。
[!IMPORTANT] 缓存对象本身也是堆中的对象,只是不是这次装箱时新建的,而是直接复用了缓存里的对象。
new Integer() 不走缓存
如果显式使用 new,就一定会创建新对象:
Integer a = new Integer(100);
Integer b = new Integer(100);
System.out.println(a == b); // false即使值在 -128 ~ 127 范围内,也不会复用缓存。
自动拆箱做了什么
自动拆箱本质上会调用包装类对应的取值方法,例如:
Integer a = 10;
int b = a;大致等价于:
int b = a.intValue();这也是为什么包装类为 null 时,拆箱会抛出空指针异常:
Integer a = null;
int b = a; // NullPointerException因为本质上执行的是:
a.intValue();包装类参与运算时会发生什么
包装类参与算术运算时,会先拆箱成基本类型,运算结束后如果赋值给包装类,再重新装箱。
Integer a = 1;
Integer b = a + 1;大致等价于:
Integer a = Integer.valueOf(1);
Integer b = Integer.valueOf(a.intValue() + 1);这个过程包含:
a先拆箱成int- 执行
+ 1运算 - 运算结果再装箱成
Integer
所以 a + 1 这种看起来很简单的写法,底层其实是“拆箱 -> 运算 -> 装箱”。
a++ 的本质
Integer a = 1;
a++;大致等价于:
Integer tmp = a;
int value = tmp.intValue();
value = value + 1;
a = Integer.valueOf(value);因此如果 a 为 null,同样会在拆箱阶段抛出 NullPointerException。
== 和 equals() 的区别
这是自动装箱/拆箱最容易出错的地方。
基本类型和包装类型比较
int a = 2;
Integer b = 2;
System.out.println(a == b); // true这里会发生自动拆箱,实际比较的是两个 int 值:
System.out.println(a == b.intValue());两个包装类型比较
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true这里是两个对象比较,== 比较的是引用地址。之所以结果是 true,不是因为 == 在比较值,而是因为两个对象都命中了缓存,刚好引用相同。
Integer a = 200;
Integer b = 200;
System.out.println(a == b); // false因为 200 不在默认缓存范围内,通常是两个不同对象。
正确做法
比较包装类的值,应该优先使用 equals():
Integer a = 200;
Integer b = 200;
System.out.println(a.equals(b)); // true[!WARNING] 不要依赖
Integer缓存范围去写==判断,这属于实现细节,业务代码里很容易埋坑。
常见面试陷阱
1. 为什么 Integer 127 == 127 为 true,Integer 128 == 128 为 false
原因不是值本身特殊,而是:
127命中了IntegerCache128默认没有命中缓存==比较的是引用
2. 为什么 int 和 Integer 用 == 往往是 true
int a = 2;
Integer b = 2;
System.out.println(a == b); // true因为 b 会自动拆箱成 int,最终比较的是值。
3. 为什么下面代码会空指针
Integer a = null;
System.out.println(a == 1);因为比较前需要先拆箱:
a.intValue() == 1而 a 是 null,所以抛出 NullPointerException。
总结
- 自动装箱本质上通常调用
valueOf() - 自动拆箱本质上通常调用
xxxValue() Integer默认会缓存-128 ~ 127- 自动装箱不一定新建对象,可能复用缓存
new Integer()一定会创建新对象- 包装类参与运算时会先拆箱,再运算,再装箱
- 比较包装类的值时,优先使用
equals() - 包装类为
null时,拆箱操作要特别小心
自动装箱/拆箱是编译器提供的语法糖,本质上分别是调用包装类的 valueOf() 和 xxxValue();Integer 在 -128 ~ 127 范围内会复用缓存对象,因此 == 有时看起来“能比较值”,但本质上比较的仍然是引用。
