知源笔记

Java 装箱与拆箱 (Boxing / Unboxing)

2026 年 11 月 4 日

1是什么

Java 的泛型、集合、反射等机制只能操作对象,而 int、long、char 这类基本类型不是对象。为了让两者互通,JDK 为每个基本类型提供了一个包装类 (wrapper class):

基本类型包装类值位宽
booleanBoolean—
byteByte1 字节
shortShort2 字节
charCharacter2 字节
intInteger4 字节
longLong8 字节
floatFloat4 字节
doubleDouble8 字节
  • 装箱 (boxing):把基本类型值转成对应的包装类对象。
  • 拆箱 (unboxing):把包装类对象里的值取出来,变成基本类型。

从 Java 5 开始,这两种转换在赋值、方法调用、算术运算、比较等场景下由编译器自动插入,称为自动装箱 / 自动拆箱 (autoboxing / auto-unboxing)。

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

List<Integer> list = new ArrayList<>();
list.add(1);            // 自动装箱,1 -> Integer.valueOf(1)
int first = list.get(0); // 自动拆箱

2编译器到底做了什么

自动装箱/拆箱完全是编译期行为,没有任何运行时“魔法”。用 javap -c 反编译可以看到插入的调用:

static Integer box(int i)   { return i; }
static int     unbox(Integer i) { return i; }
static java.lang.Integer box(int);
  Code:
     0: iload_0
     1: invokestatic  #2   // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
     4: areturn

static int unbox(java.lang.Integer);
  Code:
     0: aload_0
     1: invokevirtual #3   // Method java/lang/Integer.intValue:()I
     4: ireturn

由此可得两条结论,后面几乎所有坑都源于此:

  1. 装箱调用的是 Integer.valueOf(int),不是 new Integer(int)。
  2. 拆箱调用的是 intValue(),这是一个实例方法——对象为 null 时必然抛 NullPointerException。

new Integer(1)、new Long(1L) 等包装类构造器自 Java 9 起已标记为 @Deprecated,应改用 valueOf 或直接赋值。

3缓存:valueOf 不是每次都 new

Integer.valueOf(int) 内部维护了一个静态缓存数组,命中缓存时返回同一个对象:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

各包装类的缓存范围 (在 JDK 8 上实测):

包装类缓存范围是否可调整
Booleantrue / false 两个常量否
Byte全部 256 个值 (-128 ~ 127)否
Short-128 ~ 127否
Character0 ~ 127 ('\u0000' ~ '\u007f')否
Integer-128 ~ 127 (high 默认 127)是,-XX:AutoBoxCacheMax=N
Long-128 ~ 127否
Float / Double无缓存—

实测输出:

System.out.println(Integer.valueOf(-128) == Integer.valueOf(-128)); // true
System.out.println(Integer.valueOf(128)  == Integer.valueOf(128));  // false
System.out.println(Short.valueOf((short) 200) == Short.valueOf((short) 200)); // false
System.out.println(Character.valueOf((char) 65) == Character.valueOf((char) 65)); // true
System.out.println(Double.valueOf(1.0) == Double.valueOf(1.0)); // false

Integer 的上限可以通过 JVM 参数放大 (下限固定为 -128):

java -XX:AutoBoxCacheMax=1000 Main
Integer x = 128, y = 128;
System.out.println(x == y); // 未加参数:false;加了上面参数:true

注意:缓存范围是实现细节,不要在业务代码里依赖 127 以内 == 恰好为 true。它只会让你的程序在某个值上“碰巧正确”。

4常见坑

4.1用 == 比较包装类

== 对两个引用比较的是地址,对 Integer/int 混合比较才会拆箱比数值。

Integer a = 128, b = 128;
System.out.println(a == b);        // false —— 超出缓存,两个不同对象
System.out.println(a.equals(b));   // true

Integer c = 127, d = 127;
System.out.println(c == d);        // true —— 命中缓存,同一对象(纯属巧合)

正确的做法:

a.equals(b)                          // 包装类之间
Objects.equals(a, b)                 // 允许 null
Integer.compare(a, b) == 0           // 或 a.intValue() == b.intValue()

4.2不同类型包装类的 equals 恒为 false

Integer.equals 的实现是 obj instanceof Integer && value == ((Integer) obj).intValue(),类型不匹配直接返回 false:

Integer i = 1;
Long l = 1L;
System.out.println(i.equals(l));  // false,且不抛异常,极难发现

对比之下,Integer 与 long 用 == 比较时会各自拆箱后做数值提升,结果是 true:

Integer i = 1;
System.out.println(i == 1L);  // true

而两个不同类型的包装类直接用 == 是编译错误:

Integer i = null; Long l = null;
System.out.println(i == l);   // 编译错误:incomparable types: Integer and Long

4.3null 拆箱 → NullPointerException

这是生产环境最常见的空指针来源之一:错误栈指向的往往是一行看不出任何解引用的代码。

Integer count = map.get("missing");   // null
int c   = count;                      // NPE
int sum = count + 1;                  // NPE
if (count > 0) { }                    // NPE(> 触发拆箱)—— 应为 count != null && count > 0

Integer x = null;
switch (x) { ... }                    // NPE,switch 的表达式也会拆箱

例外是 == / != 与 null 比较时不拆箱:

Integer x = null;
System.out.println(x == null);  // true,安全
System.out.println(x == 1);     // NPE

4.4三元运算符的隐式拆箱

当条件表达式的两个分支一个是包装类、一个是基本类型时,按二元数值提升规则,结果类型是基本类型,包装类分支会被拆箱——即使你的目标变量是 Object:

Integer maybeNull = null;
Integer r1 = true ? maybeNull : 0;   // NPE
Object  r2 = true ? maybeNull : 0;   // 同样是 NPE

只要两个分支都是包装类,就不会拆箱:

Integer r3 = true ? maybeNull : Integer.valueOf(0);  // null,安全

另一个相关的类型提升陷阱:

Object o = true ? 1 : 2.0;
System.out.println(o.getClass().getName());  // java.lang.Double,结果是 2.0 而不是 1

4.5重载解析:宽化优先于装箱

Java 的重载选择分阶段进行:先找不需要装箱/拆箱的严格匹配 (允许 int -> long 这类宽化),找不到才进入允许装箱/拆箱的阶段,最后才是可变参数。所以下面这段会选中 f(long) 而不是 f(Integer):

static void f(long x)    { System.out.println("f(long)"); }
static void f(Integer x) { System.out.println("f(Integer)"); }

f(1);  // 输出 f(long):int -> long 的宽化属于严格匹配,优先于装箱

null 不能匹配基本类型形参,因此只能落到包装类版本上,方法体内若直接拆箱就会 NPE。

static void g(int x)     { System.out.println("g(int)"); }
static void g(Integer x) { System.out.println("g(Integer) " + x); }

g(1);     // g(int)      —— 直接匹配基本类型
g(null);  // g(Integer) null —— 编译通过,进入方法后一旦拆箱就 NPE

4.6List.remove 的重载歧义

List 有 remove(int index) 和 remove(Object o) 两个重载,传 int 时选索引版本:

List<Integer> list = new ArrayList<>(Arrays.asList(10, 20, 30));

list.remove(1);                      // 删除下标 1 的元素 -> [10, 30]
list.remove(Integer.valueOf(10));    // 删除值 10 的元素 -> [20, 30]

一行 list.remove(i) (i 是 int) 如果本意是“删掉这个值”,就会静默删错元素。

4.7赋值时的类型限制

只有编译期常量且落在目标类型范围内时,才允许窄化后装箱:

Byte b = 1;        // 可以,常量 1 在 byte 范围内
Short s = 1;       // 可以
Character c = 'a'; // 可以
Long l = 1L;       // 可以(1L 是 long 字面量)

Long bad = 1;      // 编译错误:int cannot be converted to Long
Float f2 = 1;      // 编译错误:int cannot be converted to Float
Byte b2 = 200;     // 编译错误:超出 byte 范围

4.8包装类“不可变”带来的循环累加开销

Integer/Long 是 final 且字段不可变,sum += i 等价于每次新建对象:

Long sum = 0L;
for (int i = 0; i < 1_000_000; i++) {
    sum += i;   // 每次都拆箱 + 相加 + 装箱,产生约 100 万个临时对象
}

正确写法是全程使用基本类型 long sum = 0L;。

5性能影响

装箱的成本有三块:对象分配 (堆/Young GC 压力)、内存占用 (64 位 HotSpot 开启指针压缩时,Integer = 12 字节对象头 + 4 字节 int 值 = 16 字节,而 int 只占 4 字节),以及拆箱时的额外方法调用 (JIT 内联后很小,但未充分预热或分支复杂时仍可见)。

本机 JDK 8 上的一组小规模对照 (非严谨 JMH 基准,仅供感受量级):

场景耗时
Integer[] 增量累加 500 万次约 17 ms
int[] 同等操作约 4 ms
HashMap<Integer, Integer> 计数约 70 ms

结论:单纯数组/循环内的装箱拆箱开销约 3~4 倍;而把计数放进 HashMap<Integer, Integer> 后,除装箱本身还要叠加哈希计算与对象头占用,实测约慢一个数量级 (上表中约 17 倍)。在热点路径 (批量计算、序列化、流式处理) 上尤其明显。

6实践建议

  • 泛型 (集合、Stream、Optional<T>) 的类型参数只能是包装类;局部变量和循环累加器一律用基本类型。
  • 包装类比较数值用 equals / Objects.equals / Integer.compare,永不用 ==。
  • 可能为 null 的包装类,参与运算或比较前显式判空;三元表达式两侧保持类型一致。
  • 方法返回“可能没有值”时,用 OptionalInt/OptionalLong 或返回 null 并明确文档化,不要靠调用方猜。
  • 涉及金额、精度用 BigDecimal,不要用 Double (BigDecimal.valueOf(double) 而非 new BigDecimal(double))。
  • 对性能敏感的代码若发现大量 Integer 临时对象,优先考虑 int[]、long、或专门的集合库 (如 fastutil 的基本类型集合)。
  • 不要依赖 Integer 缓存范围写业务逻辑;需要判断身份时用 == 之外的手段。

7小结

自动装箱/拆箱让 int 与 Integer 之间的代码更简洁,代价是把“引用是否相等”和“数值是否相等”混在了一起,并引入了编译器隐式插入的拆箱点。记住三条即可规避绝大多数问题:

  1. 装箱 = valueOf,可能返回缓存对象,因此 == 的结果在小范围内是“真”,在范围外是“假”。
  2. 拆箱 = xxxValue(),对象为 null 就抛 NPE,且可能发生在你看不见的地方 (算术、比较、三元、switch)。
  3. 不同包装类型之间 equals 恒为 false,跨类型比较先统一到同一个类型。
2026 年 11 月 4 日