1. 从“127相等、128不等”说起1.1 一个让新手摸不着头脑的代码片段我记得很清楚第一次在项目里遇到这个场景是在做用户积分兑换的时候。当时有个接口要判断用户本次获得的积分是否达到了某个档位我顺手写了类似这样的代码Integer scoreA 128; Integer scoreB 128; if (scoreA scoreB) { // 走档位 A 的逻辑 } else { // 走档位 B 的逻辑 }结果怎么跑都不对明明两个值都是128可程序就是走进了else分支。我当时第一反应是“是不是数据源传错了”排查了半天最后才发现问题不在数据而是出在了这个比较方式上。后来我把两个值改成127再试嘿居然又相等了。同一个写法127能过128就翻车这就是传说中的“128陷阱”。这个陷阱不只坑新手。我见过不少工作了三四年的同事在code review的时候看到这类代码照样要愣一下。原因在于它涉及Java里几个看起来基础但特别容易忽略的机制自动装箱、包装类的缓存策略、以及和equals的区别。这三件事单独拎出来都知道但组合在一起的时候绝大多数人是没有直觉的。1.2 这个陷阱到底“坑”在哪儿先说结论Integer在比较的时候如果两个值都在-128到127之间用比较返回的是true一旦超出这个范围比如128、129、1000用比较返回的就是false。而equals方法无论什么范围只要数值相等就返回true。这不是Java的bug而是JDK刻意设计的一个缓存优化。但问题在于这个设计对调用方是“不可见的”编译器不会给你任何提示IDE默认也不会报警。你写Integer a 128; Integer b 128;的时候表达式表面上就是两个int字面量赋给了两个Integer变量下意识会觉得这就是两个数字在比较。可实际上这个赋值动作背后藏了一次方法调用而比较的是两个对象的引用地址不是数值本身。这就像你手里有两张写着“128”的纸条其中一张是从官方统一印发的纸堆里拿的另一张是临时手写的你觉得它们俩“应该一样”但问的是“这两张纸条是不是同一张纸”。只有在缓存范围内JVM才保证了“同一个数值对应同一张纸”。所以这个陷阱真正的杀伤力在于它不是必然触发而是条件触发。程序在小数值下一切正常一旦数据量大了、数值超过了127行为就悄悄变了。这种“间歇性灵异事件”在线上排查时最让人头疼——单测都是小数据全绿一上生产数据一大问题立刻冒出来。这也是为什么“128陷阱”能成为Java面试里的高频考点它考察的不仅是知识点记忆更是对语言底层机制的理解深度。2. 底层原理拆解IntegerCache与自动装箱2.1 自动装箱编译器帮你调用的valueOf要理解这个陷阱第一步得看清楚赋值语句背后发生了什么。在Java 5之前你不能直接写Integer a 128;你得写Integer a Integer.valueOf(128);。Java 5引入了自动装箱autoboxing机制允许基本类型和包装类型之间直接赋值、直接参与运算编译器会在背后自动插入转换代码。所以你写的Integer a 128;编译后等价于Integer a Integer.valueOf(128);Integer a 128;这句看起来人畜无害的代码实际执行了一次方法调用。既然是方法调用方法内部怎么处理、有没有缓存、返回值是不是同一个对象就完全取决于valueOf的实现。注意自动装箱是“语法糖”它在编译期生效运行期你还是在对Integer对象做操作。这也是为什么很多人会忽略它的存在——你写的是“像基本类型一样”的代码但对象本质没有变。2.2 valueOf方法源码缓存边界为什么是-128到127我们直接看Integer.valueOf的源码JDK里写得很清楚public static Integer valueOf(int i) { if (i IntegerCache.low i IntegerCache.high) return IntegerCache.cache[i (-IntegerCache.low)]; return new Integer(i); }逻辑不复杂如果传入的int值落在IntegerCache.low到IntegerCache.high这个闭区间内直接从缓存数组里取现成的对象否则new一个新的Integer对象。那IntegerCache又是什么它是Integer类里的一个私有静态内部类在类加载的时候会执行静态初始化块把-128到127这256个数值对应的Integer对象全部提前创建好放进一个数组里private static class IntegerCache { static final int low -128; static final int high; static final Integer[] cache; static { // high 的值可以通过 JVM 参数 -XX:AutoBoxCacheMaxxxx 调整 int h 127; // ... 默认 high 127 cache new Integer[(high - low) 1]; int j low; for (int k 0; k cache.length; k) cache[k] new Integer(j); } }这段代码有几个值得注意的点。第一默认缓存范围是-128到127共256个对象。为什么是这个范围因为byte类型的取值范围恰好就是-128到127这个范围内的整数在程序里出现频率极高作为ID、状态码、小计数器等场景满天飞。缓存它们能显著减少对象创建降低GC压力。第二缓存的high值实际上可以通过JVM参数-XX:AutoBoxCacheMax调整。我做过测试启动JVM时加上-XX:AutoBoxCacheMax256那么256以内的数值用比较也会返回true。low值是写死-128改不了因为IntegerCache.low是final常量。这个参数在线上调优的时候偶尔会用比如某个QPS极高的服务频繁比较小范围数值可以适当调大缓存上限。但注意调大有成本缓存数组要占堆内存每个Integer对象16字节左右如果调到100万那要占16MB不值得。第三每次调用valueOf只要值在缓存范围内拿到的都是同一个对象引用。这就是为什么Integer a 127; Integer b 127; a b是true——因为a和b指向的是同一个缓存对象。而Integer a 128; Integer b 128;两个对象分别new出来的堆上两个不同的内存地址自然返回false。2.3 不止Integer其他包装类型的缓存机制也不一样这个陷阱在Integer上最出名但Long、Short、Byte、Character这几个包装类也有类似逻辑只不过容易被忽略。我整理了一张表方便对照包装类型缓存范围说明Byte全部范围-128到127因为byte总共就256个值全缓存了Short-128到127缓存了一部分高范围不缓存Integer-128到127可调默认缓存可以通过JVM参数调整上限Long-128到127同上不可调整Character0到127缓存ASCII码范围大于127的字符不缓存Float/Double无缓存浮点数不缓存每次都是新对象我特别提醒一下Character它的缓存范围是0到127对应ASCII码。所以Character c1 a; Character c2 a; c1 c2结果也是true但换成中文汉字中、文结果就是false了因为中文字符的UTF-16编码都远超127。这个在实际开发中容易踩坑尤其在做字符处理、解析文本的时候。还有Float和Double压根没有缓存因为浮点数的范围太大了搞缓存没意义。所以对这两个类型任何时候都不要用比较要么用equals要么先转成Double.compare()再比。3. 实际项目中的影响与正确处理方式3.1 线上最容易触发的三个场景从我的经验看“128陷阱”在线上最常见的触发点有三个。第一个是状态码或业务码的比较。比如订单状态、审核状态、用户类型这种字段数据库里存的是int用MyBatis、Hibernate之类的框架映射到Java实体类时如果实体类字段声明成了Integer那从数据库里查出来再经过自动装箱一旦某个状态码超过127用比较就会出问题。更恶心的是这些状态码通常都是从常量类里取出来的写代码的人根本不会想到要检查常量值是多少。第二个是缓存数据的比较。比如从Redis里取积分值、从本地缓存里取配置项取出来的是Integer然后跟另一个Integer做等值判断。Redis序列化反序列化之后对象的创建路径完全不可控比较更是玄学。第三个是集合元素比较。很多人写了list.get(0) list.get(1)这种代码如果两个元素数值都在缓存范围内碰巧就对了一旦超过127马上翻车。还有些人用Map判等map.get(a) map.get(b)同样是雷。3.2 正确的比较姿势那遇到Integer等值判断到底该怎么写核心原则就一条包装类型之间做值比较一律用equals或者Objects.equals不要用。我推荐的是Objects.equals原因在于它做了null安全处理。比如你判断两个Integer是否相等直接用Objects.equals(a, b)就算a或b是null也不会抛NullPointerException返回的结果也符合直觉一个为null另一个不为null返回false两个都为null返回true。还有一个思路是把Integer转成基本类型再比比如a.intValue() b.intValue()。这样比较的就是原始数值不存在对象引用的问题。但要注意调用intValue()之前必须确保对象不为null否则照样空指针。如果你用的是Java 7及以上还有一种写法Integer.compare(a, b) 0它返回的是两个int值按数值比较的结果等于0表示相等同时也避免了自动拆箱可能带来的NPE问题前提是参数不能为null。我还见过有人用a.toString().equals(b.toString())来比较这个能出结果但我强烈不推荐——凭空多出字符串对象的创建性能差而且可读性也差。正确做法很简单别绕弯子。这里列出几种比较方式的效果对比比较方式127 vs 127128 vs 128null vs 4说明a btruefalsefalse比较引用非数值a.equals(b)truetrueNPE正确但需判nulla.intValue() b.intValue()truetrueNPE正确但需判nullObjects.equals(a, b)truetruefalse最推荐null安全3.3 从源头规避实体类字段类型怎么选比“怎么写比较”更进一步的思路是“从一开始就别给自己挖坑”。在设计实体类、DTO的时候如果字段本身就是整数且不会超过int的范围我更倾向于直接用基本类型int而不是Integer。为什么因为基本类型天然没有null的概念两个int变量直接用比那比的就是数值没有任何歧义。但这里有个权衡取舍。数据库字段如果允许为null那映射到Java实体类时用Integer更合适因为可以表达“未赋值”的状态。比如用户表里有个“推荐人ID”有的人没有推荐人存的就是null这时候你用int映射就会变成0语义就变了。所以我的原则是业务上必填、不允许为null的整数用int允许为空、有特殊语义的用Integer。前者避坑后者保真。另外一个值得注意的点在使用Spring等框架时JSON反序列化出来的Integer对象比如{“score”: 128}经过Jackson反序列化后得到的score是什么Jackson内部默认也是走Integer.valueOf所以128会被new Integer(128)处理同样不在缓存里。也就是说哪怕你不是自己手动new的、不是自己手动装箱的只要经过了JSON反序列化超范围的数值也是一堆不同的对象。这进一步说明比较是完全不可依赖的。4. 常见问题排查与面试延伸4.1 这张速查表建议保存我把平时容易踩坑的情况整理成了下面这个排查速查表写代码之前扫一眼心里有数场景建议做法理由比较两个Integer值是否相等Objects.equals(a, b)null安全语义正确比较两个int值是否相等基本类型没有引用一说Integer和int混比a b也可以Integer会自动拆箱比如Integer(128) 128是true因为右侧基本类型触发左侧拆箱判断Integer是否为0a ! null a 0先判null再让Integer自动拆箱比较从List/Map取值比较先判null再用equals集合里元素可能是null也可能是任何缓存状态new Integer(128) 与 valueOf(128)永远用valueOf或自动装箱直接new一定不走缓存其中“Integer和int混比”这条经常被误解。比如Integer a 128; int b 128; System.out.println(a b); // 输出 true能猜到这个结果吗看到两边一个是Integer一个是int编译器会强制把Integer拆箱成int所以比较的是128 128自然为true。也就是说“128陷阱”只在比较的两个对象都是包装类型的时候才触发。这个细节面试里经常拿来当变体考察。4.2 线上排查怎么确认是不是“128陷阱”的锅如果你在线上遇到类似的诡异比较结果排查路径其实很清晰。第一步看代码里比较的变量声明类型是Integer还是int。如果两个都是Integer第二步看比较符是还是equals。如果是第三步看实际运行时两个值是多少是否超出当前包装类型的缓存范围。只要超出范围那基本可以锁定是“128陷阱”。定位到问题之后修复很简单把换成Objects.equals然后补一个针对超范围数值的单测用例防止回归。我通常还会在单测里加上边界值测试一个127一个128确保修复后两种值都能通过。排查的时候有一个容易被误导的点本地测试是好的线上是坏的。原因很简单本地测试数据全是小数值或者正好在缓存范围内线上数据一变大超出边界问题就暴露了。这再次说明单测最好覆盖边界值尤其是像缓存边界这种“截断点”。4.3 面试题背后的考点从记忆到理解的转变“128陷阱”为什么在Java面试里出镜率这么高因为它考察的维度非常立体。一个候选人如果能把这个知识点讲透彻通常说明他具备三个层次的功底。第一层知道现象。知道127相等、128不等这说明有基本的经验积累或者至少看了面经。第二层理解原理。能说出IntegerCache、valueOf方法、自动装箱机制说明读过源码有一定的钻研精神。第三层懂得应用。能结合线上实际场景谈怎么规避、怎么排查、怎么选型这就能看出项目经验和系统思考能力。面试官也喜欢在这个知识点上做延伸提问常见的有这样几类new Integer(127) new Integer(127)的结果是什么——false因为new必然创建新对象不走缓存。Integer a 127; Integer b new Integer(127); a b的结果是什么——false一个来自缓存一个来自new。Integer a 127; int b 127; a b的结果是什么——true因为发生了拆箱。IntegerCache的缓存上限能不能调——能通过-XX:AutoCacheMax但low固定-128。Long有没有类似机制——有范围同样-128到127但不可调。这几个问题如果都能答上来说明不是死记硬背而是真的理解了这套机制。面试官想要的就是这种“知其所以然”的人。还有一个特别有意思的变体考察的是反射。你可以在运行时通过反射修改IntegerCache的缓存数组内容比如把cache[128]对应的值改掉那以后所有Integer.valueOf(128)返回的对象其实际数值都会变成你改过的“假值”。这种操作在正常业务代码里永远不要做但它能帮你直观理解缓存机制到底是怎么回事——缓存里的对象是可以被外部修改的一旦改了整个JVM范围内的128都会变成别的数非常恐怖。最后说点实在的。我在实际项目中处理过太多次这种“看起来是数据问题、其实是比较方式问题”的case了每次排查到最后都是同一个结论包装类型比较别用。这句话说一百遍都不为过。如果你正在写新代码直接养成用Objects.equals的习惯如果你刚好在改一个线上bug先怀疑“128陷阱”再动别的——大概率能省下好几个小时的排查时间。
