自动装箱与拆箱有哪些坑?Integer 缓存是怎么回事?

#Java#面试八股

自动装箱与拆箱有哪些坑?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,Character 是 0127);Float、Double 没有缓存。

2. 结果对照表

表达式结果原因
Integer a=127, b=127; a==btrue都在缓存内
Integer a=128, b=128; a==bfalse超出缓存,各自 new
new Integer(100) == new Integer(100)false显式 new,必是新对象
Integer a=100; a == 100true与基本类型比较时 a 拆箱
Long g=100L; g.equals(100)false实参被装箱成 Integer,类型不同
Integer a=null; a == 1NPE与 int 比较触发拆箱
Long a=127L, b=127L; a==btrueLong 也有缓存

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) 就该警觉。

笔记加载中…