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 = 127 被 javac 编译成 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] 范围内的对象逐个 new 进 cache 数组。
// 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 = 127 和 Integer 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 |
| Character | 0 ~ 127(ASCII) | ❌ |
| Boolean | TRUE / FALSE | N/A |
| Double | 无缓存 | — |
| Float | 无缓存 | — |
Double/Float 无缓存的原因:浮点数连续不可穷举——0.1 和 0.2 之间有无限多个浮点数,“缓存常用值”对浮点数没有意义(不存在”常用值”这个概念)。
思考:为什么 Character 缓存 0
127 而 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,而不是返回null或Integer?因为三元运算符要求两端结果类型一致,Java 的二元提升把Integer和int统一提升成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?
结论:三元运算两端类型不一致时,二元提升把 Integer 和 int 统一提升成 int。因为要提升成 int 就必须对 null 拆箱,null.intValue() 抛 NPE。