包装类型、自动装箱与缓存
包装类型把基本类型的值放进对象中,使它们能够用于泛型、集合和需要引用类型的 API。自动装箱减少了转换代码,但也引入了对象身份、空值和隐式转换问题。
1. 基本类型与包装类型
Java 为八种基本类型提供了对应的包装类:
| 基本类型 | 包装类型 |
|---|---|
boolean | Boolean |
byte | Byte |
short | Short |
char | Character |
int | Integer |
long | Long |
float | Float |
double | Double |
基本类型变量不能为 null,也不是对象。包装类型是引用类型,可以为 null,并提供解析、比较和进制转换等方法:
int port = Integer.parseInt("8080");
String hex = Integer.toHexString(255);
int compared = Integer.compare(10, 20);
泛型只能使用引用类型,所以集合元素需要写成 Integer,不能写成 int:
List<Integer> numbers = List.of(1, 2, 3);
2. 自动装箱与拆箱
自动装箱把基本类型表达式转换为对应包装类型,自动拆箱执行相反转换:
Integer boxed = 101; // 自动装箱
int value = boxed; // 自动拆箱
这段代码可以近似理解为:
Integer boxed = Integer.valueOf(101);
int value = boxed.intValue();
“近似理解”强调语义,不要求编译器一定生成完全相同的源代码形态。装箱和拆箱也会出现在赋值、方法调用、算术运算和条件表达式中。
2.1 算术运算会触发拆箱
Integer count = 10;
int next = count + 1;
计算 count + 1 前,count 会被拆箱为 int。结果是否重新装箱,取决于接收位置的类型。
循环和大量数值运算如果不需要对象语义,优先使用基本类型。无意的装箱会增加对象或引用处理成本,集合中的包装元素也会带来额外内存开销。
2.2 null 拆箱会抛出异常
Integer count = null;
// int value = count; // NullPointerException
// int next = count + 1; // NullPointerException
拆箱需要从包装对象取得基本值。引用为 null 时没有对象可调用,因此抛出 NullPointerException。
这种错误也可能藏在条件表达式中:
Boolean enabled = null;
// if (enabled) { // 自动拆箱,抛出 NullPointerException
// start();
// }
如果三态值确实存在,应当明确处理 null;如果业务只有真和假,使用 boolean 可以让无效状态更早消失。
3. 包装对象的缓存与身份
部分基本值装箱后允许复用已有包装对象。Java 语言规范保证,对特定常量表达式进行装箱时,常见小值会得到相同引用,其中整数范围包括 -128 到 127:
Integer first = 127;
Integer second = 127;
System.out.println(first == second); // true
范围外的值不应对对象身份作任何假设:
Integer first = 128;
Integer second = 128;
System.out.println(first == second); // 结果不应作为业务判断依据
System.out.println(first.equals(second)); // true
实现可以缓存更多值,但程序不能依赖具体 JVM 参数或缓存范围。包装值比较使用 equals,数值排序使用 compare 或先明确处理空值后拆箱。
3.1 == 的结果取决于两侧类型
两个包装类型引用使用 == 时,比较对象身份:
Integer first = 1000;
Integer second = 1000;
System.out.println(first == second); // 不应期待为 true
一侧是基本类型时,包装对象会先拆箱,再比较数值:
Integer boxed = 1000;
int primitive = 1000;
System.out.println(boxed == primitive); // true
如果 boxed 为 null,第二种比较会在拆箱时抛出 NullPointerException。同一个 == 表达式因为静态类型不同而具有不同语义,这也是包装类型判断容易出错的原因。
4. 包装类型的使用场景
4.1 泛型与集合
List<Integer>、Map<Long, User> 等泛型参数必须是引用类型。读写集合时可能发生自动装箱和拆箱:
List<Integer> numbers = new ArrayList<>();
numbers.add(101); // 装箱
int first = numbers.get(0); // 拆箱
性能敏感且数据量很大的数值处理,需要把对象数量和内存布局纳入评估,而不是只比较算法复杂度。
4.2 表达缺失值
包装类型可以用 null 表示“未提供”,例如更新请求中的 Integer retryCount 可以区分未传值和明确传入 0。
这个能力也带来空值传播。进入核心业务对象后,通常应把“未提供”转换为默认值、明确的状态类型或校验错误,不要让可能为空的包装类型在计算链路中继续流动。
4.3 框架和通用 API
反射、ORM、序列化框架和以 Object 为边界的 API 经常需要包装类型。是否使用包装类型,应由接口是否需要对象或空值语义决定,不要统一把所有字段都改成包装类型。
5. 常见问题
5.1 Integer.valueOf(1) 与 new Integer(1) 有什么区别
valueOf 可以复用缓存对象。显式调用包装类构造方法会要求创建新对象,并且这些构造方法已经被弃用。正常代码使用 valueOf、自动装箱或解析方法,不手动 new 包装对象。
5.2 包装类型为什么适合做不可变值
标准包装类都是不可变的,创建后内部表示的基本值不会改变。加减运算会拆箱计算,再得到新的基本值或包装对象,不会修改原包装对象。
5.3 方法参数用 int 还是 Integer
参数不允许缺失并且只做数值运算时,使用 int 更清楚,也不会发生空拆箱。只有接口需要表达 null、使用泛型或对接要求引用类型的框架时,才使用 Integer,并在边界明确处理空值。
5.4 Integer 与 Long 能否通过 equals 判断数值相等
不能:
System.out.println(Integer.valueOf(1).equals(Long.valueOf(1))); // false
包装类的 equals 同时要求类型和数值匹配。跨数值类型比较时,应先根据业务需要转换到共同且不会丢失信息的表示。
6. 面试题
6.1 什么是自动装箱和拆箱,可能产生哪些问题
出现公司:百度、阿里巴巴
考察重点
- 基本类型与包装类型之间发生了什么转换。
- 哪些赋值和运算会隐式触发转换。
- 缓存身份、空值拆箱和额外对象成本。
相关内容:第 2 节“自动装箱与拆箱”、第 3 节“包装对象的缓存与身份”。
参考回答
自动装箱把基本类型转换为对应包装类型,通常通过 valueOf 取得对象;自动拆箱从包装对象中取得基本值。它们会出现在赋值、方法调用、比较和算术运算中。
使用时要注意三点:包装对象使用 == 比较的是引用身份,结果可能受缓存影响;null 在拆箱时会抛出 NullPointerException;大量不必要的装箱会增加对象和内存成本。数值不能为空时优先使用基本类型,包装值判断相等使用 equals。
6.2 为什么两个值为 127 的 Integer 使用 == 可能为 true
出现公司:实在智能、百度
考察重点
- 装箱缓存与对象身份的关系。
- Java 规范保证的范围与实现可扩展的范围。
- 为什么业务判断不应依赖缓存。
相关内容:第 3 节“包装对象的缓存与身份”。
参考回答
对结果位于 -128 到 127 的整数常量表达式进行装箱时,Java 规范保证重复装箱得到不可区分的相同引用,所以两个 Integer 使用 == 可能为 true。实现还可以缓存更多值,但范围外不能依赖引用身份。
== 对两个包装引用比较的是对象身份,不是稳定的数值判断。无论数值是否落在缓存范围,包装值的内容比较都应使用 equals。