← 返回题目列表

自动装箱和拆箱是什么?Integer 缓存是怎么回事?

高频 中等 第 8 / 32 题 更新于 2026/07/25
Java 基础装箱拆箱Integer

简化版

装箱:把基本类型 int 自动转成包装类 IntegerInteger i = 10);拆箱:反过来把 Integer 转回 intint n = i)。编译器帮你偷偷调 Integer.valueOf()i.intValue()Integer 缓存valueOf-128~127 的值会复用缓存对象,所以这个范围内 == 比较为 true,超出就是两个新对象、== 为 false。

详细版

自动装箱/拆箱是编译器的语法糖:

Integer i = 10;        // 装箱:编译器改写成 Integer.valueOf(10)
int n = i;             // 拆箱:编译器改写成 i.intValue()

List<Integer> list = new ArrayList<>();
list.add(3);           // 自动装箱,int 3 → Integer
int x = list.get(0);   // 自动拆箱,Integer → int

Integer 缓存机制Integer.valueOf(int) 内部维护了一个 [-128, 127] 的缓存数组(IntegerCache),落在这个区间的值直接返回同一个缓存对象,不新建:

Integer a = 127, b = 127;
System.out.println(a == b);   // true,复用缓存里同一对象

Integer c = 128, d = 128;
System.out.println(c == d);   // false,超出缓存,各 new 一个新对象

所以包装类比较值一定用 equals(),不要用 ==,否则会踩这个 128 的坑。

完整版教学

一、为什么设计缓存

-128~127 是实际编程里最常用的一段小整数(循环计数、状态码、小数量)。为这些高频值每次都 new 一个 Integer 对象很浪费。于是 JDK 用一个预先建好的缓存池,让 valueOf 在这个范围内复用对象,省内存、省 GC。这和 String 常量池是同一种「高频不可变值复用」的思路。

补充:ByteShortLong 也有 [-128,127] 缓存,Character 缓存 0~127Boolean 缓存 TRUE/FALSE。但 FloatDouble 没有缓存(取值太连续,缓存没意义)。

二、== 为什么在 127 和 128 表现不同

Integer a = 127 触发装箱 = Integer.valueOf(127) → 命中缓存,返回池里那个对象。两次都命中同一个,所以 a == b 为 true(地址相同)。

Integer c = 128 = Integer.valueOf(128) → 超出缓存范围,new Integer(128) → 每次都是新对象,地址不同,c == d 为 false。

a.equals(b) 永远比的是数值,所以无论多少都返回正确结果。这再次说明:引用类型比值用 equals()

三、最危险的坑:拆箱空指针(NPE)

比缓存更容易在生产出事的是自动拆箱触发的 NPE

Map<String, Integer> map = new HashMap<>();
int count = map.get("不存在的key");   // get 返回 null,自动拆箱 null.intValue() → NPE!

map.get() 没命中返回 null(是 Integer 类型),赋给 int 时自动拆箱调 null.intValue(),直接抛 NullPointerException。这个坑很隐蔽,因为代码看起来完全正常。

还有三目运算也会触发隐式拆箱:

Integer x = null;
Integer result = true ? 0 : x;   // 三目两分支类型不一致会统一拆箱,可能 NPE

记忆点:包装类可能为 null,拆箱前要判空。用包装类接收可能不存在的值时,别直接赋给基本类型。

四、包装类 vs 基本类型怎么选

  • 基本类型int):有默认值 0、不能为 null、性能好、无对象开销。局部变量、计数、性能敏感处优先用。
  • 包装类Integer):可以为 null(能表达「没有值」)、能放进集合(泛型不支持基本类型)。数据库字段映射、集合元素、需要区分「0」和「未设置」时用。

一个典型场景:数据库某字段可空,实体类就该用 Integer 而非 int——否则 null 会被拆箱成异常,或丢失「未设置」的语义。

五、装箱的隐藏成本怎么估算

装箱不仅是语法变化,还可能分配对象。假设循环执行 100 万次,变量写成 Integer sum,表达式 sum += i 每轮都会先拆箱做加法,再把结果装箱;一旦结果越过缓存区间,就可能产生大量短命对象。即使逃逸分析有机会消除部分分配,也不应把程序性能建立在 JIT 恰好优化成功上。

Integer sum += i
≈ intValue(sum) + i
≈ Integer.valueOf(计算结果)
场景推荐类型原因
计数、求和、数组下标int / long无空值语义,也不需要对象身份
ListMap 等泛型参数Integer / LongJava 泛型不能直接使用基本类型
数据库可空字段包装类null0 的业务含义不同
高频数值计算基本类型或基本类型数组避免装箱、拆箱和对象布局开销

因此,看到包装类参与算术、关系比较或条件判断时,要主动在脑中还原一次拆箱;看到基本类型进入泛型容器时,则要意识到发生了装箱。

六、常见误区与追问

  • 误区:用 == 比较两个 Integer 的数值。 缓存范围会让错误代码时真时假,值比较应使用 equals()
  • 误区:认为 Integer 一定能安全赋给 int 只要包装对象可能是 null,隐式调用 intValue() 就可能抛 NPE。
  • 误区:在热循环里用包装类累计,觉得语法简短就没有成本。 装箱可能带来分配和 GC,数值计算优先使用基本类型。
  • 追问:new Integer(1) == Integer.valueOf(1) 是什么? 前者明确创建新对象,后者返回缓存对象,所以为 false;构造器本身也已被标记为废弃。
  • 追问:IntegerLong 能直接 equals() 吗? 不能,Integer.valueOf(1).equals(Long.valueOf(1))false,因为包装类的 equals() 同时检查类型和值。
  • 追问:缓存上限永远只能是 127 吗? 规范保证至少缓存 -128~127;某些 JVM 可扩大 Integer 缓存上限,因此绝不能依赖扩展范围编写逻辑。

再看一个数量级:假设 100 万次循环中的结果都超过缓存范围,并且优化器未消除对象,每轮就可能产生一个临时 Integer。即便每个对象按十几字节粗略估算,累计分配也会达到十几 MB,随后还要由 GC 回收。

long sum = 0L;       // 只操作基本类型
for (int i = 0; i < 1_000_000; i++) sum += i;

这也是基准测试必须预热 JVM、消费计算结果并使用 JMH 的原因:否则常量折叠或逃逸分析可能让测试测不到真实装箱成本。

七、加强记忆

把整条链串起来记:基本类型进入对象世界时,编译器用 valueOf 装箱;包装类参与计算时,用 xxxValue 拆箱。valueOf 可能命中小整数缓存,所以对象身份不能代表数值相等,比较值要用 equals();拆箱则要求对象非空,因此边界数据要先判空。最后按语义选类型:需要 null 或泛型容器才用包装类,纯计算、计数和下标优先基本类型,这样同时避开身份比较、NPE 和隐藏分配三个坑。