前言
先看一段能让人怀疑人生的代码:
Integera=127,b=127;System.out.println(a==b);// trueIntegerc=128,d=128;System.out.println(c==d);// false同样的写法,只是把数值从127改成128,==的结果就从true翻成了false。第一次见到的人几乎都会愣一下:这不科学。
其实一点都不玄。背后是两个知识点的叠加:==比的到底是什么,以及Integer有个-128~127的缓存。这篇文章把这两点讲透,看完你不仅能解释 127/128,还能躲开一整类"包装类型比较"的坑。
环境说明:本文基于 JDK 8。
一、先复现
把几种情况放一起,现象更清楚:
publicclassIntegerEqualsDemo{publicstaticvoidmain(String[]args){Integera=127,b=127;System.out.println("127 == 127 : "+(a==b));// trueIntegerc=128,d=128;System.out.println("128 == 128 : "+(c==d));// falseIntegere=newInteger(127),f=newInteger(127);System.out.println("new 127 : "+(e==f));// falseSystem.out.println("equals : "+c.equals(d));// true}}输出:
127 == 127 : true 128 == 128 : false new 127 : false equals : true三个反直觉的点:
127 == 127是true,128 == 128却是false;- 连
new Integer(127) == new Integer(127)都是false——明明值一样; - 但只要改用
equals,全都是true。
问题就一个:==到底在比什么,为什么它对Integer这么"不讲道理"?
二、根因
2.1==比引用,equals比内容
这是总纲,记住这一句能解决一大半问题:
- 对基本类型(
int、long、double…),==比的是值。 - 对引用类型(所有对象,包括
Integer),==比的是引用地址——也就是"是不是同一个对象",而不是值相不相等。
Integer是对象,所以c == d问的其实是"c和d是不是指向同一个对象",而不是"它们的值相不相等"。new Integer(127) == new Integer(127)为false就是这个道理:new了两次,是两个不同的对象,地址自然不同,哪怕值都是 127。
而equals被Integer重写成了比较内部的int值,所以只要值相等就返回true。
那问题来了:c、d又没new,只是Integer c = 128;,为什么也是两个对象?这就要说到自动装箱。
2.2 自动装箱:Integer a = 127背后发生了什么
Integer a = 127;这行,等号右边是int,左边是Integer,编译器会自动帮你"装箱",实际编译成:
Integera=Integer.valueOf(127);注意,是Integer.valueOf(127),不是new Integer(127)。这个区别是全部谜题的钥匙。
2.3IntegerCache:-128~127返回同一个缓存对象
看Integer.valueOf的源码(JDK 8):
publicstaticIntegervalueOf(inti){if(i>=IntegerCache.low&&i<=IntegerCache.high)returnIntegerCache.cache[i+(-IntegerCache.low)];// 命中缓存,返回同一个对象returnnewInteger(i);// 超出范围,new 新对象}Integer内部维护了一个静态缓存IntegerCache,默认缓存-128到127这 256 个整数对应的Integer对象。逻辑很直白:
- 值在
-128~127之间:直接返回缓存里同一个对象; - 超出这个范围:
new一个新对象。
到这里,127/128 的反转就彻底解释清楚了:
Integer a = 127, b = 127;→ 两次valueOf(127)都命中缓存,返回的是同一个对象,a == b自然true;Integer c = 128, d = 128;→ 128 超出缓存范围,两次各new一个新对象,c == d就是false。
而new Integer(127)是显式new,绕过了缓存,所以哪怕在 127 也照样是两个对象,==为false。
一张图看清valueOf的分支和缓存范围:
三、正解
结论很简单:包装类型比较值,一律用equals,别用==。
Integerc=128,d=128;// 有坑:比的是对象地址,结果随数值大小变化System.out.println(c==d);// false// 正解:比的是值,永远可靠System.out.println(c.equals(d));// true几个补充:
- 想用
==也行,但要先拆箱成基本类型:c.intValue() == d.intValue(),或者让其中一边是int(见下面第四节)。 - 注意
equals的空指针:如果变量可能为null,c.equals(d)会 NPE。用java.util.Objects.equals(c, d)更安全,它内部做了 null 判断。 - 同类陷阱:
Long、Short、Byte、Character都有类似缓存,Double、Float没有缓存(浮点值域连续,缓存无意义)。另外String有字符串常量池,==也有类似的坑——都是"引用比较"惹的祸。
四、常见误区与面试高频问答
Q:为什么缓存范围偏偏是-128~127?能改吗?
这是一个字节(byte)能表示的范围,也是实践中小整数最常用的区间,缓存它们收益最高。上界可以通过 JVM 参数-XX:AutoBoxCacheMax=<size>调大(下界固定-128)。调大后,原本128 == 128为false的结果可能变成true——这也从侧面证明了它就是缓存在起作用。
Q:基本类型int之间用==有问题吗?
没有。基本类型的==比的就是值,128 == 128(两个int)永远是true。坑只出现在包装类型上。
Q:Integer和int用==比会怎样?
Integerc=128;intx=128;System.out.println(c==x);// true当==一边是基本类型、一边是包装类型时,包装类型会自动拆箱成基本类型,于是比的是值,结果反而"正常"了。也就是说Integer == int是安全的,Integer == Integer才有坑。
Q:既然equals更可靠,==是不是就没用了?
不是。判断"是不是同一个对象"(比如判断单例、判断引用是否被重新赋值)就得用==。==和equals是两个不同的问题:==问"是不是同一个",equals问"值是不是相等"。选哪个取决于你要问什么。
Q:为什么阿里手册强制包装类之间比较用equals?
正是因为本文这个坑。用==比较包装类,结果会随数值是否落在缓存范围而变化——127对、128错,这种"时对时错"的 bug 极难排查,还很容易在测试时用小数据蒙混过关、上线用大数据翻车。统一用equals就彻底避开了。
总结
128 == 128为false,不是 JVM 有 bug,而是两个机制叠加的必然结果:
==对引用类型比的是对象地址(是不是同一个对象),不是值;equals才比值。Integer a = 127会自动装箱成Integer.valueOf(127),而valueOf对-128~127返回缓存的同一个对象,超出范围则new新对象。- 所以
127命中缓存是同一个对象(==为true),128超范围是两个对象(==为false);new Integer绕过缓存,永远是新对象。
一句话记忆:==比地址、equals比值;Integer只缓存-128~127,超了就是两个对象——包装类型比较,永远用equals。