Skip to content
Go back

IntegerCache——为什么Integer 127 == Integer 127是true而128不是?

IntegerCache:为什么 Integer 127 == Integer 127 是 true,128 不是?

一句话结论(30s)

IntegerCache 的本质是「享元模式」缓存高频小整数以减少对象分配,因为自动装箱 Integer i = 127 被编译成 Integer.valueOf(127),而 valueOf 对 -128~127 返回缓存池中的同一对象,所以 ==(引用比较)在范围内碰巧为 true、范围外为 false。权衡:以固定内存缓存 256 个对象换来高频场景少 new,但 == 是引用比较而非值比较,正确做法永远用 equals()。

核心原理(2min)

主流程:装箱走 valueOf,命中 IntegerCache 的 -128~127 数组则返回同一对象(上界可用 -XX:AutoBoxCacheMax 调高,-128 因是 byte 最小值而固定)。Long/Short/Byte/Character/Boolean 也有缓存,Double/Float 因浮点数连续不可穷举而无缓存。陷阱:三元表达式两端类型不匹配时,二元提升把 Integer 与 int 统一推断成 int,触发 null 拆箱导致 NPE。

底层深入(5-10min)

自动装箱的语法糖

Integer a = 127;  // 编译后: Integer.valueOf(127)
Integer b = 127;
System.out.println(a == b);  // true (同一缓存对象!)

Integer c = 128;
Integer d = 128;
System.out.println(c == d);  // false (各自new的新对象)

Integer i = 127javac 编译成 Integer.valueOf(127)valueOf() 对 -128~127 范围内的值返回缓存池中的同一对象

思考:为什么 a == b 在 127 是 true、128 就 false?因为 == 比的是引用(对象地址),不是值。127 两次装箱都命中 IntegerCache.cache[255] 这同一个对象,地址相同所以 true;128 超出缓存、两次都 new Integer(128),是两个不同对象,地址不同所以 false。判断依据是「对象是不是同一个」,而不是「值相不相等」。

IntegerCache 的初始化

下面是 java.lang.Integer 的真实源码(节选,// 后英文为 JDK 原文注释,中文为解读)。现代 JDK 比经典版本多了 CDS 归档缓存与 AOT 预计算等机制,但核心逻辑不变:low 固定为 -128、high 默认 127,静态初始化时把 [-128, high] 范围内的对象逐个 newcache 数组。

// java.lang.Integer.IntegerCache —— 真实源码(节选)
@AOTSafeClassInitializer
private static final class IntegerCache {
    static final int low = -128;          // 下界固定:byte 的最小值
    @Stable static int high;               // 上界默认 127,可被调高

    @Stable static Integer[] cache;        // 缓存数组
    static Integer[] archivedCache;        // CDS/AOT 归档缓存

    static {
        runtimeSetup();                    // 静态初始化入口
    }

    @AOTRuntimeSetup
    private static void runtimeSetup() {
        // high value may be configured by property
        int h = 127;                       // 默认上界 127
        String integerCacheHighPropValue =
            VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                h = Math.max(parseInt(integerCacheHighPropValue), 127);  // 不低于 127
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(h, Integer.MAX_VALUE - (-low) -1);          // 保证数组不越界
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        Integer[] precomputed;
        if (cache != null) {
            // IntegerCache has been AOT-initialized.
            precomputed = cache;
        } else {
            // Legacy CDS archive support (to be deprecated):
            // Load IntegerCache.archivedCache from archive, if possible
            CDS.initializeFromArchive(IntegerCache.class);
            precomputed = archivedCache;
        }

        cache = loadOrInitializeCache(precomputed);
        archivedCache = cache; // Legacy CDS archive support (to be deprecated)
        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private static Integer[] loadOrInitializeCache(Integer[] precomputed) {
        int size = (high - low) + 1;       // 数组长度 = high - low + 1

        // Use the precomputed cache if it exists and is large enough
        if (precomputed != null && size <= precomputed.length) {
            return precomputed;
        }

        Integer[] c = newCacheArray(size);
        int j = low;                       // 从 -128 开始填
        // If we loading a precomputed cache (from AOT cache or CDS archive),
        // we must use all instances from it.
        // Otherwise, the Integers from the AOT cache (or CDS archive) will not
        // have the same object identity as items in IntegerCache.cache[].
        int precomputedSize = (precomputed == null) ? 0 : precomputed.length;
        for (int i = 0; i < precomputedSize; i++) {
            c[i] = precomputed[i];
            assert j == precomputed[i];
            j++;
        }
        // Fill the rest of the cache.      ← 关键:静态初始化逐个 new 填充缓存
        for (int i = precomputedSize; i < size; i++) {
            c[i] = new Integer(j++);
        }
        return c;
    }

    private static Integer[] newCacheArray(int size) {
        // ValueClass.newReferenceArray requires a value class component.
        if (PreviewFeatures.isEnabled()) {
            return (Integer[]) ValueClass.newReferenceArray(Integer.class, size);
        }
        return new Integer[size];
    }

    private IntegerCache() {}
}

为什么 -128 是固定的? 因为 -128 是 byte 的最小值,缓存范围从 byte 满范围开始,正好覆盖最常用的小整数。high 默认 127,但可通过 -XX:AutoBoxCacheMax=<n>(底层写入 java.lang.Integer.IntegerCache.high 属性)调高,Math.max(..., 127) 保证不会低于默认值,Math.min(h, Integer.MAX_VALUE - (-low) -1) 保证 cache 数组下标不越界。

思考:既然缓存能少 new,为什么不把全部 int 都缓存起来?因为 int 有 2³² 个取值,全缓存内存爆炸且冷数据命中率极低。缓存的价值在「命中率」——-128~127 覆盖了最常用的小整数(循环变量、数组下标、状态标志),用固定 256 个对象的成本换来绝大多数装箱场景零分配,边际收益最高的区间就在这里。

然后是装箱真正走的入口 valueOf,先查缓存:

// java.lang.Integer.valueOf —— 真实源码
@IntrinsicCandidate
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];  // 命中缓存:返回同一对象
    return new Integer(i);                                    // 未命中:才 new 新对象
}

valueOf 先判断 i 是否落在 [low, high] 区间内:命中就用 i - low(即 i + 128)作下标返回缓存数组里的同一个对象;否则才真正 new Integer(i)Integer a = 127Integer b = 127 之所以 == 为 true,就是因为两次装箱都命中了 cache[255] 这同一个元素;而 128 超出缓存范围,两次都 new 出新对象,== 自然是 false。

思考Integer i = 127 为什么编译成 valueOf(127) 而不是 new Integer(127)?因为 new Integer 每次都 new 新对象,装箱要是走 new 就完全错过缓存;valueOf 是缓存的唯一入口,编译器把自动装箱统一编译成 valueOf,就是为了让高频小整数自动享受享元复用。这也提醒我们:手写代码时也应优先 valueOf 而非 new Integer

哪些包装类有缓存?

类型缓存范围可配置上界?
Integer-128 ~ 127(默认)-XX:AutoBoxCacheMax
Long-128 ~ 127
Short-128 ~ 127
Byte-128 ~ 127(全范围)N/A
Character0 ~ 127(ASCII)
BooleanTRUE / FALSEN/A
Double无缓存
Float无缓存

Double/Float 无缓存的原因:浮点数连续不可穷举——0.1 和 0.2 之间有无限多个浮点数,“缓存常用值”对浮点数没有意义(不存在”常用值”这个概念)。

思考:为什么 Character 缓存 0127 而 Integer 是 -128127?因为 char 无符号(065535),最常用的是 ASCII 0127 这一小段;Boolean 只有 true/false 两个取值,各缓存一个就 100% 命中。缓存范围的设计原则都是同一个——「高频、有限、可穷举」才值得缓存,无限连续的值(浮点)无法穷举,缓存无从谈起。

三层运算触发意外拆箱

Integer x = flag ? null : 1;
// 整行被推断为 int 表达式 → null 拆箱为 int → NPE!

三元运算两端类型不匹配时,Java 的二元提升规则将 null(Integer)和 1(int)统一推断为 int——导致 null.intValue() 调用,NPE。

思考flag ? null : 1 为什么会 NPE,而不是返回 nullInteger?因为三元运算符要求两端结果类型一致,Java 的二元提升把 Integerint 统一提升成 int(拆箱方向),于是 null 需要先拆箱成 int 才能与 1 对齐,null.intValue() 就抛 NPE。要规避就显式把一端声明成 Integer(如 flag ? null : Integer.valueOf(1)),让整行表达式保持为 Integer

总结

IntegerCache 是用享元模式减少高频小整数的对象分配。== 对引用比较,不是值比较——缓存范围内 == 碰巧为 true,范围外 == 为 false。永远用 equals() 比较 Integer,不依赖 == 的巧合。

章末提问

Q1:Integer 127 == Integer 127 为什么是 true,128 为什么是 false? 结论:因为自动装箱走 Integer.valueOf(),它对 -128~127 命中缓存返回同一对象。所以 127 两次装箱是同一个 cache[255]== 比引用为 true;128 超出缓存各自 new,引用不同为 false。

Q2:为什么缓存范围默认是 -128~127?-128 为什么固定? 结论:-128 是 byte 最小值,这个区间恰好覆盖 byte 满量程、命中最常用的小整数。因为下界 low = -128 是硬编码常量不可调,只有上界 high 能通过 -XX:AutoBoxCacheMax 调高。

Q3:怎么调大 Integer 缓存上界?有什么代价? 结论:用 JVM 参数 -XX:AutoBoxCacheMax=<n>,底层写入 java.lang.Integer.IntegerCache.high 属性。因为缓存是类静态初始化时一次性 new 出来的,调大上界会让启动时多分配 (high-low+1) 个 Integer 对象,占内存且拖慢一点类初始化。

Q4:Double/Float 为什么没有缓存? 结论:因为浮点数连续不可穷举,0.1 和 0.2 之间有无穷多个值,不存在「高频常用值」的概念。缓存的前提是「取值有限且高频」,整数区间满足,浮点数不满足。

Q5:Integer x = flag ? null : 1 为什么 NPE? 结论:三元运算两端类型不一致时,二元提升把 Integerint 统一提升成 int。因为要提升成 int 就必须对 null 拆箱,null.intValue() 抛 NPE。


Share this post on:

Previous Post
Java四种引用类型——强软弱虚与Cleaner的堆外内存释放链
Next Post
Compact Strings——JDK 9如何将String内存占用减半?