跳到主要内容
约 8 分钟3 张卡

包装类型

包装类型把基本类型的值表示为对象,供泛型、集合和需要引用类型的 API 使用。本文从基本类型与包装类型的区别入手,再讲解装箱、拆箱和对象缓存。最后说明三元表达式与方法重载中的隐式转换,帮助判断比较结果和空指针异常。

1. 面试核心​

先记住这几条,再读正文
  • 引用类型用途。 包装类型是引用类型,既可作为泛型参数,也可用 null 表示未提供的值。
  • 空值拆箱。 自动拆箱从包装对象取出基本值,引用为 null 时会抛出 NullPointerException。卡 · 自动装箱与拆箱
  • 整数缓存。 Integer.valueOf 保证缓存 -128 到 127 的对象,相同值会返回相同引用。卡 · 包装对象缓存
  • 相等类型要求。 Integer.equals 只有在对方也是 Integer 且数值相同时才返回 true。
  • 表达式求值。 数值三元表达式根据分支类型决定是否拆箱,即使赋值给包装类型也会触发空值拆箱。卡 · 三元表达式与重载选择

2. 基本类型与包装类型​

基本类型保存数值或布尔值,包装类型则让这些值能够作为对象使用。Java 为八种基本类型分别提供了包装类:

基本类型包装类型
booleanBoolean
byteByte
shortShort
charCharacter
intInteger
longLong
floatFloat
doubleDouble

基本类型变量直接保存基本值,没有 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?​

7.4 Integer 与 Long 能用 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 可以直接赋给接收变量。

9. 参考资料​