大家好,我是小富。
《十万个why》系列持续更新中
先看一段代码,猜猜输出:
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // ???
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // ???
如果你觉得两个都是 true,那你就中招了。
实际输出:第一个 true,第二个 false。
同样是两个 Integer 变量,同样的值,同样用 == 比较,127 就是相等的,128 就不等了。如果你第一次看到这个结果,肯定觉得 Java 出 bug 了。
但这不是 bug,而是一个精心设计的"缓存陷阱"。
自动装箱背后发生了什么
Integer a = 127; 这行代码看起来很简单,但 Java 编译器偷偷做了一件事——自动装箱(autoboxing)。它等价于:
Integer a = Integer.valueOf(127);
关键就在 Integer.valueOf() 这个方法的实现:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
看到了吗?如果值在 IntegerCache.low(-128)到 IntegerCache.high(默认 127)之间,直接从缓存数组里取一个现成的 Integer 对象返回。否则,new 一个新对象。
所以真相是
Integer a = 127; // valueOf(127) → 从缓存取,返回 IntegerCache.cache[255]
Integer b = 127; // valueOf(127) → 从缓存取,返回 IntegerCache.cache[255]
// a 和 b 指向同一个对象!
System.out.println(a == b); // true(同一个对象,地址相同)
Integer c = 128; // valueOf(128) → 128 > 127,new Integer(128)
Integer d = 128; // valueOf(128) → 128 > 127,new Integer(128)
// c 和 d 是两个不同的对象!
System.out.println(c == d); // false(不同对象,地址不同)
== 比较的是对象的内存地址,不是值。127 在缓存范围内所以是同一个对象,128 不在缓存范围所以是不同对象。
这个缓存是怎么初始化的
JVM 启动时,IntegerCache 这个内部类的静态代码块就会预先创建 -128 到 127 共 256 个 Integer 对象,放在一个数组里:
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer[] cache;
static {
int h = 127;
// 可以通过 JVM 参数调整上限
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.IntegerCache.high");
if (integerCacheHighPropValue != null) {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
h = Math.min(i, Integer.MAX_VALUE - (-low) - 1);
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for (int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}
}
有意思的是,上限可以通过 JVM 参数 -XX:AutoBoxCacheMax= 调整。比如你设置 -XX:AutoBoxCacheMax=1000,那 1000 以内的 Integer 都能用 == 正确比较。但下限 -128 是写死的,不能改。
不只是 Integer,其他包装类也有坑
| 类型 | 缓存范围 | == 安全范围 |
|---|---|---|
| Integer | -128 ~ 127 | 同左 |
| Long | -128 ~ 127 | 同左 |
| Short | -128 ~ 127 | 同左 |
| Byte | -128 ~ 127(整个范围) | 整个范围都安全 |
| Character | 0 ~ 127 | 同左 |
| Boolean | TRUE / FALSE | 永远安全 |
| Float | 无缓存 | 永远不安全 |
| Double | 无缓存 | 永远不安全 |
Float 和 Double 没有缓存,所以:
Double a = 1.0;
Double b = 1.0;
System.out.println(a == b); // false!即使是 1.0 也是 false
更隐蔽的坑:运算结果的装箱
Integer a = 100;
Integer b = 20;
Integer c = a + b; // 先拆箱计算 100+20=120,再装箱 valueOf(120)
Integer d = 120; // valueOf(120)
System.out.println(c == d); // true,因为 120 在缓存范围内
// 换个数字
Integer e = 100;
Integer f = 50;
Integer g = e + f; // 拆箱计算 150,装箱 new Integer(150)
Integer h = 150; // new Integer(150)
System.out.println(g == h); // false!150 超出缓存范围
运算结果是否在缓存范围内,决定了 == 的结果。这种 bug 在代码里极其隐蔽——测试用例用小数字测都没问题,上线之后数据大了就翻车。
线上事故案例
// 订单状态比较
public boolean isOrderPaid(Integer orderStatus) {
Integer PAID = 1;
return orderStatus == PAID; // 1 在缓存范围内,凑巧能工作
}
// 但如果状态值设计成了大数字
public boolean isRefundApproved(Integer refundStatus) {
Integer APPROVED = 200;
return refundStatus == APPROVED; // 200 不在缓存范围,永远返回 false!
}
第二个方法永远返回 false,退款永远无法通过审批。而且这种 bug 用小数字的状态值测试时完全正常,只有特定状态码才会触发。
正确做法
永远用 .equals() 比较包装类型的值。
Integer a = 128;
Integer b = 128;
System.out.println(a.equals(b)); // true,始终比较的是数值
// 如果可能为 null,用 Objects.equals
System.out.println(Objects.equals(a, b)); // true,还能防 NPE
或者拆箱后用 ==:
System.out.println(a.intValue() == b.intValue()); // true
阿里巴巴 Java 开发手册里也明确规定:
所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
总结
这个问题的本质是:Java 为了优化频繁使用的小整数对象创建,引入了 IntegerCache 缓存。缓存范围内的值共享同一个对象(== 为 true),缓存范围外每次都创建新对象(== 为 false)。
从设计角度看,这个优化是合理的——大部分程序里出现频率最高的整数就是 0 附近的小数字,缓存它们确实节省了大量堆内存分配。但它和自动装箱结合在一起,就成了一个"看起来对但某些情况下会炸"的定时炸弹。
记住:包装类型比较,永远用 equals,永远不要用 ==。
我是小富,下期见。
