自动装箱和拆箱是什么?Integer 缓存是怎么回事?
简化版
装箱:把基本类型 int 自动转成包装类 Integer(Integer i = 10);拆箱:反过来把 Integer 转回 int(int 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 常量池是同一种「高频不可变值复用」的思路。
补充:
Byte、Short、Long也有[-128,127]缓存,Character缓存0~127;Boolean缓存TRUE/FALSE。但Float、Double没有缓存(取值太连续,缓存没意义)。
二、== 为什么在 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 | 无空值语义,也不需要对象身份 |
List、Map 等泛型参数 | Integer / Long | Java 泛型不能直接使用基本类型 |
| 数据库可空字段 | 包装类 | null 与 0 的业务含义不同 |
| 高频数值计算 | 基本类型或基本类型数组 | 避免装箱、拆箱和对象布局开销 |
因此,看到包装类参与算术、关系比较或条件判断时,要主动在脑中还原一次拆箱;看到基本类型进入泛型容器时,则要意识到发生了装箱。
六、常见误区与追问
- 误区:用
==比较两个Integer的数值。 缓存范围会让错误代码时真时假,值比较应使用equals()。 - 误区:认为
Integer一定能安全赋给int。 只要包装对象可能是null,隐式调用intValue()就可能抛 NPE。 - 误区:在热循环里用包装类累计,觉得语法简短就没有成本。 装箱可能带来分配和 GC,数值计算优先使用基本类型。
- 追问:
new Integer(1) == Integer.valueOf(1)是什么? 前者明确创建新对象,后者返回缓存对象,所以为false;构造器本身也已被标记为废弃。 - 追问:
Integer与Long能直接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 和隐藏分配三个坑。