自动装箱与拆箱有哪些坑?Integer 缓存是怎么回事?
结论:自动装箱(autoboxing)由编译器插入 Integer.valueOf(int),拆箱插入 intValue()。Integer.valueOf 对 -128 ~ 127 的值走 IntegerCache,返回缓存中的同一对象;超出范围每次 new 一个。于是 == 比较包装类型的结果随数值大小而变,必须用 equals;拆箱遇到 null 会抛 NullPointerException,最典型的是 Map.get 返回 null 直接赋给 int。
1. 缓存机制
Integer.valueOf(100)
└── 100 在 [-128, 127] → 返回 IntegerCache.cache[100 + 128](同一对象)
Integer.valueOf(1000)
└── 超出范围 → new Integer(1000)(每次新对象)
IntegerCache 在 Integer 静态内部类中初始化:
low = -128(固定),high = 127(可用 -XX:AutoBoxCacheMax=N 调大)
启动时一次性创建 high - low + 1 个对象,常驻堆
Long、Short、Byte、Character 也有类似缓存(范围多为 -128127,127);Character 是 0Float、Double 没有缓存。
2. 结果对照表
| 表达式 | 结果 | 原因 |
|---|---|---|
Integer a=127, b=127; a==b | true | 都在缓存内 |
Integer a=128, b=128; a==b | false | 超出缓存,各自 new |
new Integer(100) == new Integer(100) | false | 显式 new,必是新对象 |
Integer a=100; a == 100 | true | 与基本类型比较时 a 拆箱 |
Long g=100L; g.equals(100) | false | 实参被装箱成 Integer,类型不同 |
Integer a=null; a == 1 | NPE | 与 int 比较触发拆箱 |
Long a=127L, b=127L; a==b | true | Long 也有缓存 |
3. 代码:三个高频坑
import java.util.HashMap;
import java.util.Map;
public class BoxingDemo {
public static void main(String[] args) {
Integer a = 127, b = 127;
Integer c = 128, d = 128;
System.out.println(a == b); // true,命中缓存
System.out.println(c == d); // false,两个对象
System.out.println(c.equals(d)); // true,比较值
Integer e = 1000;
int f = 1000;
System.out.println(e == f); // true,e 被拆箱
Long g = 100L;
System.out.println(g.equals(100)); // false,Integer vs Long
// 坑 1:Map 取值直接拆箱
Map<String, Integer> stock = new HashMap<>();
// int n = stock.get("apple"); // NPE:null 不能拆箱
Integer raw = stock.get("apple");
int safe = raw == null ? 0 : raw; // 正确写法
// 坑 2:三元表达式触发拆箱
Integer flag = null;
// int r = true ? flag : 0; // NPE:结果类型被提升为 int
int r = true ? (flag == null ? 0 : flag) : 0;
// 坑 3:循环里装箱,产生大量短命对象
Long sum = 0L;
for (int i = 0; i < 1_000_000; i++) {
sum += i; // 每次都装箱 + 拆箱
}
System.out.println(sum);
}
}
三元表达式那一行值得展开:true ? flag : 0 中一边是 Integer、一边是 int,Java 的二元数值提升(binary numeric promotion)会把结果类型定为 int,于是 flag 被强制拆箱——null 立刻 NPE。修法是把两边都保持为 Integer(如 flag == null ? Integer.valueOf(0) : flag)。
4. 正确使用建议
| 场景 | 建议 |
|---|---|
| 比较包装类型 | 一律 equals 或 Objects.equals,不要 == |
| 集合中存数值 | 用包装类型没问题,但注意 null |
| 频繁累加 | 用基本类型或 LongAdder,别用 Long 做循环变量 |
| 参数/返回值 | 明确是否允许 null,允许就用 Integer,不允许用 int |
| 缓存范围依赖 | 不要把 == 的偶然成立当成规范,换 JDK 版本或调大缓存就会变 |
| 性能敏感代码 | 避免在循环内装箱,必要时用 int[] 或专用集合库 |
5. 常见追问
| 追问 | 回答要点 |
|---|---|
| 缓存范围能调吗? | 上界可用 -XX:AutoBoxCacheMax=N 调整,下界 -128 固定 |
为什么 Double 没有缓存? | 浮点值域太大,缓存意义不大,且 == 比较本就不该用 |
| 自动装箱发生在编译期还是运行期? | 编译期插入 valueOf/xxxValue() 调用,运行期执行 |
装箱会创建对象,为什么 a == b 有时 true? | 缓存对象是复用的,两个引用指向同一缓存实例 |
| 拆箱 NPE 怎么定位? | 堆栈里通常只有一行,看反编译后的 intValue() 调用;静态检查工具会提示 |
包装类型能做 switch 吗? | 可以,switch 会自动拆箱,null 同样 NPE |
小结
自动装箱是"语法糖 + 缓存"的组合。记住三个数字与一句话:缓存范围 -128~127、比较用 equals、拆箱前判 null。看到 Integer == Integer 或 int x = map.get(k) 就该警觉。