跳至主要內容

程序员小富大约 4 分钟

大家好,我是小富。

《十万个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(整个范围)整个范围都安全
Character0 ~ 127同左
BooleanTRUE / 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,永远不要用 ==。


我是小富,下期见。

上次编辑于: