包装类型
包装类型把基本类型的值表示为对象,供泛型、集合和需要引用类型的 API 使用。本文从基本类型与包装类型的区别入手,再讲解装箱、拆箱和对象缓存。最后说明三元表达式与方法重载中的隐式转换,帮助判断比较结果和空指针异常。
1. 面试核心
先记住这几条,再读正文- 引用类型用途。 包装类型是引用类型,既可作为泛型参数,也可用
null表示未提供的值。 - 空值拆箱。 自动拆箱从包装对象取出基本值,引用为
null时会抛出NullPointerException。卡 · 自动装箱与拆箱 - 整数缓存。
Integer.valueOf保证缓存 -128 到 127 的对象,相同值会返回相同引用。卡 · 包装对象缓存 - 相等类型要求。
Integer.equals只有在对方也是Integer且数值相同时才返回true。 - 表达式求值。 数值三元表达式根据分支类型决定是否拆箱,即使赋值给包装类型也会触发空值拆箱。卡 · 三元表达式与重载选择
2. 基本类型与包装类型
基本类型保存数值或布尔值,包装类型则让这些值能够作为对象使用。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);
上例中的 port 为 8080,hex 为 "ff",compared 为负数,表示 10 小于 20。
泛型参数要求引用类型,所以整数集合使用 Integer:
List<Integer> numbers = List.of(1, 2, 3);
3. 自动装箱与拆箱
基本值传给需要对象的位置时,需要装箱;包装值用于基本类型运算时,则需要拆箱。Java 可以自动完成这两种转换:
// 自动转换
{
Integer boxed = 101; // 自动装箱
int value = boxed; // 自动拆箱
}
// 对照理解:显式取得包装对象与基本值
{
Integer boxed = Integer.valueOf(101);
int value = boxed.intValue();
}
上面的 valueOf 表示取得包装对象,intValue 表示取出其中的整数值。两种写法用于对照转换语义,编译器实际生成的是字节码。除赋值外,方法调用、算术运算和条件表达式也会触发这些转换。
3.1 算术运算中的拆箱
包装对象参与加减运算时,程序会先取出基本值,再进行计算:
Integer count = 10;
int next = count + 1;
计算 count + 1 前,count 会被拆箱为 int。结果是否重新装箱,取决于接收位置的类型。
循环中有大量数值运算,且无需表示空值时,优先使用基本类型。装箱会增加对象或引用处理成本,集合中的包装元素也会占用额外内存。
3.2 null 拆箱
包装引用可以为空,所以看起来普通的赋值或加法也会抛异常:
Integer count = null;
// int value = count; // NullPointerException
// int next = count + 1; // NullPointerException
Boolean enabled = null;
// if (enabled) { // 自动拆箱,抛出 NullPointerException
// start();
// }
这些赋值、加法和条件判断都要从包装对象取出基本值。引用为 null 时没有可调用的对象,所以都会在拆箱时抛出 NullPointerException。
业务确实区分真、假、未提供时,应明确处理 null。如果业务只有真和假,使用 boolean 就能从类型上排除空值。
4. 包装对象缓存
对象缓存通过复用已有包装对象,减少重复创建。Java 语言规范保证特定常量表达式装箱后共享引用,整数常量的范围包括 -128 到 127:
// 保证范围内:127
{
Integer first = 127;
Integer second = 127;
System.out.println(first == second); // true
}
// 保证范围外:128
{
Integer first = 128;
Integer second = 128;
System.out.println(first == second); // 结果不应作为业务判断依据
System.out.println(first.equals(second)); // true
}
上例中,两次装箱的 127 共享同一对象;两次装箱的 128 则应使用 equals 判断数值。
Integer.valueOf 也保证缓存 -128 到 127 的对象,实现还可以扩展缓存范围。因此,范围外的值是否共享引用,取决于具体实现与配置。比较包装值时使用 equals,排序时使用 compare,或先处理空值再拆箱比较。
4.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
第一个比较检查引用是否相同,第二个比较会先拆箱,所以相同数值会得到 true。如果 boxed 为 null,与基本类型比较时就会因拆箱抛出 NullPointerException。因此,同一个 == 运算符会因两侧静态类型不同,分别比较对象身份或数值。
5. 使用场景
选择包装类型,主要是为了满足泛型、缺失值或框架接口的要求。下面分别说明这些场景。
5.1 泛型与集合
List<Integer>、Map<Long, User> 等泛型参数必须是引用类型。下面的集合读写会触发装箱和拆箱:
List<Integer> numbers = new ArrayList<>();
numbers.add(101); // 装箱
int first = numbers.get(0); // 拆箱
处理大量数值时,除了算法复杂度,还要评估包装对象数量和内存布局。同样数量的数值,保存为对象引用与直接保存基本值,会有不同的内存成本。
5.2 缺失值语义
包装类型可以用 null 表示未提供。例如,更新请求中的 Integer retryCount 能区分未传值与明确传入 0。
进入核心计算前,应把缺失值处理好,避免空值沿计算过程继续传递。可以根据业务选择以下三种处理方式:
- 缺失表示使用默认配置时,补上默认值。
- 缺失有独立业务含义时,转换成明确的状态类型。
- 参数为必填项时,直接报告校验错误。
5.3 框架和通用 API
反射、对象关系映射(ORM)和序列化框架中,部分接口通过 Object 接收或返回数据。这些接口需要用包装对象表示基本值。选择字段类型时,应依据接口是否需要对象或空值,只做数值计算的必填字段则使用基本类型。
6. 三元表达式与重载选择
三元表达式中的隐式转换,会让赋给包装变量的表达式也触发空指针异常。数值三元表达式会结合两个分支的类型决定结果,接收变量的包装类型不会跳过这一步。
Integer missing = null;
boolean useMissing = true;
// Integer value = useMissing ? missing : 0; // NullPointerException
Integer value = useMissing ? missing : Integer.valueOf(0); // null
第一种表达式的结果类型为 int,所以选中 missing 后要先拆箱。它的值是 null,因此在赋给 Integer 变量之前就抛出异常。第二种表达式的两个分支都是 Integer,所以保留引用,结果为 null。
重载选择也有先后顺序。编译器先尝试无需装箱的匹配,例如把 int 拓宽为 long。只有这一阶段没有适用方法,才考虑装箱。
static String choose(long value) { return "long"; }
static String choose(Integer value) { return "Integer"; }
// choose(1) 返回 "long":int 到 long 的拓宽先于装箱匹配。
7. 常见问题
7.1 valueOf 与包装类构造器有什么区别?
valueOf 可复用缓存。显式构造器创建新对象,且包装类构造器已被弃用。新代码使用工厂、自动装箱或解析方法。
7.2 对包装值做加减会修改原对象吗?
原对象保持不变。标准包装类在创建后,内部保存的基本值就固定了。加减运算先拆箱计算,再返回基本值或包装结果;变量重新赋值时,改变的是变量保存的值或引用。
7.3 方法参数应该用 int 还是 Integer?
数值必填且只做计算时,使用 int 更直接,也能避免空值拆箱。泛型、可空值或框架接口需要引用时,使用 Integer,并在进入计算前处理空值。
7.4 Integer 与 Long 能用 equals 比较数值吗?
Integer 与 Long 即使表示相同数值,equals 也会返回 false:
System.out.println(Integer.valueOf(1).equals(Long.valueOf(1))); // false
包装类的 equals 同时检查类型与值。跨数值类型比较时,应转换到共同且无信息损失的表示。
8 · 面试题卡片
8.1 什么是自动装箱和拆箱,可能产生哪些问题
出现公司:阿里巴巴
考察重点
- 说明基本类型与包装类型之间发生了什么转换。
- 说明哪些赋值和运算会隐式触发转换。
- 说明对象缓存如何影响引用比较,以及空值拆箱和大量装箱会带来什么问题。
参考回答
自动装箱把基本值转换为包装对象,自动拆箱则从包装对象取出基本值。
比如,整数装箱可理解为调用 Integer.valueOf,拆箱可理解为调用 intValue。赋值、方法调用、比较和算术运算都会触发这些转换。
使用时要理解三类问题:
- 两个包装引用用
==比较对象身份,因此缓存会影响结果。比较同类型包装值的内容,应使用equals。 - 空引用没有可取出的基本值,所以拆箱会抛出
NullPointerException。 - 大量装箱会增加对象或引用处理和内存成本。必填且只用于计算的数值,适合使用基本类型。
8.2 为什么两个值为 127 的 Integer 使用 == 可能为 true
出现公司:实在智能
考察重点
- 说明装箱缓存为什么会让两个变量指向同一对象。
- 区分 Java 规范保证的缓存范围与实现自行扩展的范围。
- 解释业务判断为什么应比较数值,而非依赖缓存产生的对象身份。
相关内容:包装对象缓存。
参考回答
两个值为 127 的 Integer 如果来自常量装箱或 Integer.valueOf,就会共享缓存对象。
此时两个引用指向同一对象,所以 == 返回 true。
规范保证 -128 到 127 的整型常量装箱共享引用,Integer.valueOf 也保证该范围的缓存。实现可以缓存更多值,但范围外的对象身份取决于实现与配置。显式调用构造器创建新对象时,即使数值相同,也具有不同身份。
因此,== 的结果取决于对象是否共享。业务需要比较包装值的内容时,应使用 equals,这样缓存范围的变化就不会影响判断。
8.3 三元表达式接收为 Integer,为什么仍会空指针?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:说明三元表达式如何根据分支类型触发拆箱,以及拆箱与赋值装箱的先后顺序。
相关内容:三元表达式与重载选择。
参考回答
因为数值三元表达式会先按分支类型求值,赋值目标是 Integer 也会发生拆箱。
useMissing ? missing : 0 的结果类型为 int。选中值为 null 的 missing 时,程序先拆箱,随即抛出异常,还没有执行赋值时的装箱。两边都为 Integer 时,表达式保留引用,所以选中的 null 可以直接赋给接收变量。