2026-10-26 Integer 缓存:127 与 128 的分界线
这段代码大概是学 Java 时最容易被当成"玄学"的一处:
Integer a = 127, b = 127; Integer c = 128, d = 128; System.out.println(a == b); // true System.out.println(c == d); // false同样的写法,只差 1,结果就反了。我把它跑了一遍,又用 JVM 参数把这条线挪了挪。
一、先看结果
=== 1. 最经典的两行 ===
Integer a=127, b=127; a==b -> true
Integer c=128, d=128; c==d -> false
== 比的是引用,不是值。所以第一行输出 true,说明 a 和 b 指向同一个对象;第二行 false,说明 c 和 d 是两个不同的对象,只是值相同。
为什么 127 会共用对象、128 不共用?
二、因为有个缓存
Integer 内部有一个静态的缓存数组,默认存了 -128 到 127 这 256 个 Integer 对象。
自动装箱(Integer a = 127)实际调用的是 Integer.valueOf(127)。这个方法的逻辑很简单:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
在区间内就直接返回缓存里那个现成的对象,超出区间才 new 一个。
所以 127 两次都拿到同一个对象,== 为 true;128 两次都是新的,== 为 false。
三、把这条线量出来
我不信文档,直接逐个值试:同一个值取两次,看是不是同一个对象。
=== 2. 各包装类的缓存区间(逐个值试对象身份)===
Integer 缓存区间 = [-128, 127]
Long 缓存区间 = [-128, 127]
Short 缓存区间 = [-128, 127]
Byte 缓存区间 = [-128, 127]
Character 缓存区间 = [0, 127]
Double 没有缓存(每次都是新对象)
再用反射把 IntegerCache 的上下界读出来,和扫描结果对上了:
=== 3. IntegerCache 的实际上下界(反射读)===
low = -128
high = 127
顺带两个容易忽略的点:
=== 4. new 出来的对象不进缓存 ===
new Integer(127) == Integer.valueOf(127) -> false
new Integer(127).equals(127) -> true
=== 5. 和 int 比较时会拆箱 ===
Integer x=128; int y=128; x==y -> true
第四组说明:new 是绕开缓存的,它一定产生新对象。这就是为什么 new Integer(127) == Integer.valueOf(127) 是 false,而 .equals() 是 true。
第五组说明:一边是 Integer、一边是 int 时,== 会把 Integer 自动拆箱,变成数值比较。所以 128 那里也是 true——同一对数字,两边声明成什么类型,答案不一样。
四、这条线是可以挪的
IntegerCache.high 默认 127,但它可以由 JVM 参数改。加一个 -XX:AutoBoxCacheMax=1000 再跑一遍:
JVM 参数 = -XX:AutoBoxCacheMax=1000
Integer a=127, b=127; a==b -> true
Integer c=128, d=128; c==d -> true ← 变了
Integer 缓存区间 = [-128, 1000]
low = -128
high = 1000
128 变成 true 了。而且注意 Long、Short、Character 三行没变——这个参数只影响 Integer,别的包装类不吃它。
这条实测结果的用处不在调参,在于说明一件事:== 的结果依赖 JVM 启动参数。同一个程序在别人机器上可能跑出另一个答案。这种代码根本不该写。
五、那到底该怎么比
三条,都是踩过坑之后的说法。
包装类比较值,一律用 equals。 或者先 .intValue() 转成 int 再比。别用 ==,除非你确定要比较"是不是同一个对象"。
new Integer(...) 已经废弃了。 现在的 JDK 里这个构造器被标了 @Deprecated(forRemoval = true),编译会警告。原因正是它绕开缓存、制造了"相同值不同对象"的语义。要包装一个数就用 Integer.valueOf()。
Double 和 Float 没有缓存。 上面扫描那一行写得很清楚。但这不意味着它们可以随便用 ==——浮点数比较本身还有精度问题,那是另一件事。
六、结论
127 和 128 的差别不是语言规定的,是实现给的优化:为了省掉大量小整数对象,Integer 缓存了 -128 到 127。自动装箱走 valueOf 就吃到这个优化,new 就吃不到。
== 比的是对象,equals 比的是值。这条规则在基本类型上不成立(基本类型没有对象),在包装类型上成立,两者混用时会自动拆箱——所以它才会这么容易错。
附:验证代码
import java.lang.reflect.Field;
import java.util.function.IntFunction;
public class IntegerCacheTest {
/** 同一个值取两次,看是不是同一个对象;顺便扫出连续缓存区间 */
static void scan(String name, IntFunction<Object> box, int from, int to) {
int lo = Integer.MAX_VALUE, hi = Integer.MIN_VALUE;
for (int v = from; v <= to; v++) {
if (box.apply(v) == box.apply(v)) {
lo = Math.min(lo, v);
hi = Math.max(hi, v);
}
}
if (lo == Integer.MAX_VALUE) {
System.out.printf("%-10s 没有缓存(每次都是新对象)%n", name);
} else {
System.out.printf("%-10s 缓存区间 = [%d, %d]%n", name, lo, hi);
}
}
public static void main(String[] args) throws Exception {
scan("Integer", v -> Integer.valueOf(v), -1000, 1000);
scan("Long", v -> Long.valueOf(v), -1000, 1000);
scan("Short", v -> Short.valueOf((short) v), -1000, 1000);
scan("Byte", v -> Byte.valueOf((byte) v), -128, 127);
scan("Character", v -> Character.valueOf((char) v), 0, 1000);
scan("Double", v -> Double.valueOf(v), -1000, 1000);
Class<?> cls = Class.forName("java.lang.Integer$IntegerCache");
Field low = cls.getDeclaredField("low");
Field high = cls.getDeclaredField("high");
low.setAccessible(true);
high.setAccessible(true);
System.out.println("low = " + low.getInt(null));
System.out.println("high = " + high.getInt(null));
}
}
反射读
java.lang.Integer$IntegerCache需要加
--add-opens java.base/java.lang=ALL-UNNAMED,否则 JDK 9+ 的模块封装不让读。
浙公网安备 33010602011771号