以前很长一段时间,我以为“Integer 和 int 用 == 比较”只是新手面试题,实际代码里没人会这么写。直到有一次我们小组接了一个数据清理的老项目,里面有一段状态判断代码,从 1 到 12 跑得好好的,忽然客户反馈某个功能在状态切换到 13 时失效。查了半天,最后定位到一个 Integer 对象的 == 比较上。那天的排查过程让我对 Integer a = 100, b = 100; a == b 这种八股问题产生了全新认识——它不只是考试题,它背后藏着 Java 自动装箱、对象缓存、JVM 参数、反序列化行为、代码评审习惯这一整条链路。这篇文章我打算把这条链路完完整整拆开讲一遍,从现象到源码再到实战,尽量让看过的人以后再遇到类似的比较问题时,能直接做出正确判断。
1. 先从那个经典的“100 为 true、200 为 false”现象说起
先把这个已经被讨论无数遍的代码摆出来:
java复制Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false
不管你之前有没有运行过,我建议你先亲手跑一遍。很多人在第一次看到这个结果时,第一反应是“编译器是不是对不同常量做了特殊处理”,第二反应是“Integer 不是对象吗,对象用 == 比较的不是地址吗,为什么两个地址会相等”。这两个疑问都非常正常,因为它们指向了同一个真相:Integer 对象其实会走一个“装箱后的对象缓存”逻辑,而 100 恰好落进了缓存范围,200 又没有落进去。
更有意思的是,如果你把代码改成这样:
java复制Integer a = new Integer(100);
Integer b = new Integer(100);
System.out.println(a == b); // false
那么即使值是 100,结果也会是 false。因为 new 关键字强行创建了两个全新对象,两个对象的堆内存地址当然不同。
也就是说,同样的值、同样的“对象比较”,会因为对象的诞生路径不同而出现完全不同的结果。这里马上就会引出一个核心结论:用 == 比较包装类对象,本质上是在比较对象的引用是否指向同一个内存对象,而不是在比较它们的数值是否相等。 之所以 Integer a = 100 和 Integer b = 100 能指向同一个对象,是因为 JVM 内部维护了一个叫 IntegerCache 的缓存池,自动装箱时优先从池子里拿对象。这个池子默认覆盖了 -128 到 127,所以 100 能命中池子,200 则每次都会新建对象。
我刚入行那几年,对这种现象的理解也就到“记住 127 边界”为止。后来踩了生产环境的坑才意识到,真正危险的从来不是记住这个边界,而是不知道哪些代码路径会绕过这个“默认约定”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 拆开自动装箱:Integer a = 100 在字节码层面到底做了什么
要理解为什么 100 会“走缓存”,得先理解自动装箱的本质。Java 是编译型语言,我们写的是 Integer a = 100,但 javac 编译出来的字节码并不是“把 100 这个 int 直接塞给 Integer 引用”,而是调用了一个静态方法:Integer.valueOf(int)。
你可以用 javap -c 验证这一点:
bash复制javap -c TestInteger.class
假设有这样一个类:
java复制public class TestInteger {
public static void main(String[] args) {
Integer a = 100;
Integer b = 200;
}
}
反编译后你会看到类似下面的输出:
text复制public static void main(java.lang.String[]);
Code:
0: bipush 100
2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: astore_1
6: sipush 200
9: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
12: astore_2
13: return
注意看
