Integer 的 == 在 100 时是 true,在 1000 时是 false

一个权限校验的判断,本地跑得好好的,到同事机器上偶发不生效:

java 复制代码
Integer userLevel = map.get("level");
if (userLevel == 3) {
    // 允许操作
}

同一个包、同一份配置、同一个 JDK 大版本,他那边一直进得去,我这边有时候进不去。

问题在两处叠加:== 比较的是对象引用,而 Integer 的引用有时相同有时不同,取决于值落在不在缓存区间。

javadoc 到底承诺了什么?

Integer.valueOf(int) 的说明里有这句:

This method will always cache values in the range -128 to 127, inclusive, and may cache other values outside of this range.

「总是缓存 -128 到 127(含端点),可能缓存这个范围之外的其他值。」

前半个承诺是硬的,后半个是软的。于是有了那个著名现象:

java 复制代码
Integer a = 100, b = 100;
System.out.println(a == b);   // true  都在缓存里,同一个对象

Integer c = 1000, d = 1000;
System.out.println(c == d);   // false 超出缓存,各自新建

100 命中缓存,两次装箱拿到同一个实例,引用相等成立。1000 不在保证范围内,两次装箱各建一个对象,引用不相等。

写法 是否走缓存 结果
Integer a = 100 是,自动装箱调 valueOf 引用相同
Integer c = 1000 否,超出保证范围 引用不同
new Integer(100) 不走 一定是新对象
Integer.valueOf(100) 是 引用相同
int x = 1000; Integer y = x 走 valueOf 引用不同

第三行值得单独说:用构造函数创建包装类型已经标记为待移除,javadoc 明确建议改用 valueOf。它一定不返回缓存对象,所以「用构造函数更可靠」这种直觉是反的。

为什么 127 是那条线?

一个字节能表示的范围。用 byte 存「小整数」是 JVM 早期的优化,缓存一个字节范围内的 Integer 能覆盖绝大多数计数器、状态码和索引。

这个区间不是随便定的,但也不是可移植的约定。javadoc 写的是「总是缓存」,意思是实现必须至少缓存这么多,可以更多。

区间上下对称,是因为负数也要缓存。业务里负数用得少,但规范不为此开特例。

为什么本地和同事机器表现不同?

缓存命中与否取决于值的来源。从 Map 里取出的对象可能是别处缓存的,也可能是新建的,两条路径拼在一起,结果就依赖运行时的数据分布。

同一个数值走不同路径,引用就可能不同。所以「我这里是好的」不能说明代码是对的。

所以同一段代码在不同实现、不同启动参数下可能表现不同,而它不会报错。

这不是 bug,是什么?

是装箱的代价。

Integer 是对象,int 是基本类型。a == b 比较两个引用是不是指向同一个对象,a.equals(b) 比较值。只有基本类型之间的 == 才是数值比较。

自动装箱把 int 变成 Integer 时调的是 valueOf,而 valueOf 恰好查缓存。所以「有时相等」不是随机,是缓存命中与否。

根源是「同一个符号,两套语义」,而不是某个实现有缺陷。换一个 JVM 也一样。

这类「同一运算符、不同语义」的条目,我整理成了一张对照表,底稿是用 AI墨衍 把 javadoc 条目按「类型 / 运算符的语义 / 该用什么」三列对齐的,几个项目共用一份。

正确的写法有哪三种?

第一种,比较值:

java 复制代码
if (userLevel != null && userLevel == 3) { }   // 拆箱后比较
if (Integer.valueOf(3).equals(userLevel)) { }  // 显式值比较,null 安全

第一种依赖拆箱:包装类型与基本类型比较时,包装类型会被拆箱,变成数值比较。前提是不为空,否则抛空指针。

第二种用 equals,实参可以为空而不抛异常,适合从 Map 里取值这种「可能为空」的场景。

第三种,干脆别用包装类型:

java 复制代码
int userLevel = Optional.ofNullable(map.get("level")).orElse(0);

把空的可能性在入口处理掉,后面全部用 int,== 就永远是对的。

如果这个值要参与算术,用 int 最省事;如果必须保留空值语义,就用包装类型配 equals。两种都比等号可靠。

缓存区间可以调大吗?

JVM 有个参数能把上界放大。有人用它来「顺手修掉」这个问题,这是危险的做法。

它依赖启动参数,换个环境就失效。上界可调而下界固定,区间不对称。更麻烦的是,它让代码的正确性依赖运行参数,评审时看不出来。

真正该做的是把 == 换成 equals 或拆箱比较。缓存参数只用于性能调优,不该用于修 bug。

如果线上已经依赖这个参数,等于把代码的正确性押在了启动脚本上。

同类问题还在哪里出现?

Long 也有缓存,保证范围同样是 -128 到 127,Long.valueOf 的 javadoc 用的是同样的措辞。Short、Byte、Character 也都有。

所以「Long 值小的时候 == 为 true」这个现象同样成立,而且更容易被忽略,因为大家默认 Long 是「大数类型」,不会想到它有缓存。

String 的坑是另一个版本:字面量在常量池里,构造函数创建的不在,intern() 又回到池里。判断逻辑一样:== 比引用,equals 比值。

判断方法可以统一:先看这个类型是不是引用类型,再看它有没有缓存或常量池。两个条件都成立,等号就不可靠。

代码里怎么拦住?

检查项 检测手段 说明
两个包装类型用等号比较 IDE 检查或静态分析 最直接的一条
用构造函数创建包装类型 编译告警(已标记待移除) 一律换 valueOf
从 Map 取值后直接比较 代码评审 优先 equals 或先拆箱判空
用等号比较 Long 静态分析同类规则 常被漏掉

前两行用静态分析就能覆盖,加进 CI 之后这类问题基本绝迹。

静态分析报出来的是形状,不是语义:两个包装类型之间出现等号,一律可疑。这条规则不需要理解业务,误报也低。

小结

一条判断管住全部:== 在 Java 里比的是引用,不是值,而包装类型恰好横跨两个世界。

基本类型比「哪一个」,引用类型比「是不是同一个」,而 Integer 会自动装箱、会查缓存、还会在不该拆箱的时候拆箱。

缓存让这件事变得「时真时假」,也让 bug 更难复现。真正要记的不是 127 这个数字,而是「包装类型不要用等号比值」。

相关推荐
尘世中一位迷途小书童6 小时前
和 AI 打交道会撞见的一整串词:从大模型到 Agent、RAG、MCP、Skill,一次串清
前端·人工智能
lusklusklusk6 小时前
Python 全局变量与局部变量区别——global 关键字、__main__ 块与作用域示例
开发语言·前端·python
产品经理爱开发6 小时前
AxureShow(艾可秀)国内通用的html托管
前端·人工智能·html·ai编程
jktantan6 小时前
前端也需要事务:Vue Router 乐观更新、回滚与“过期事务”
前端·javascript·vue.js
怕浪猫6 小时前
K3.1 直追 Astra,先补 13%
前端·javascript·面试
天若有情6736 小时前
【前端趣味项目】把防抖玩出新花样!一个带隐藏后门的毫秒级“读秒挑战”
前端·javascript·html·防抖·外挂·趣味demo·读秒挑战
Liben7 小时前
Weex开发踩坑记录
android·前端·ios
jerrywus7 小时前
AI 写的代码能跑,为什么接手的人想跑?
前端·agent·claude
山雀~7 小时前
Web 多语言(i18n)技术方案详解
前端