我用 == 比较两个明明相等的 Integer,小数值时返回 true、大数值时却返回 false,我对着这个像在看天气的诡异判断查了大半天的深度复盘
这是一个让我对 Java "==" 和"自动装箱"刻骨铭心的故事。我在做一段逻辑,要比较两个 Integer 类型的值,看它们是否相等。我图顺手,直接用了 == 去比——在我朴素的认知里,== 不就是判断"两个值相不相等"嘛,a == b,只要它俩的值一样,就该返回 true。逻辑再简单不过了。
可运行结果,把我整懵了:这个 == 判断,竟然像在"看天气"一样,时而对、时而错!当两个 Integer 的值都比较小的时候(比如都是 100),a == b 返回 true,没问题;可当它俩的值大一点的时候(比如都是 1000),a == b 竟然返回了 false!明明值是完全相等的两个 Integer,怎么会因为"值的大小"不同,而得出截然相反的比较结果?我当时整个人都是懵的:100 == 100 是 true,1000 == 1000 却是 false?这是什么"玄学"?难道数值大小还能影响相等判断?我反复确认,值确实都相等,可结果就是随着数值大小"变脸"。直到我去深究 Java 的 Integer 自动装箱机制,才恍然大悟,补上了关于 == 最重要的一课:问题的核心,在两个地方。第一,== 用在对象(Integer 是对象)身上时,它比较的根本不是"值",而是"两个引用,是不是指向同一个对象(即内存地址是否相同)"!第二,Java 为了性能,对自动装箱的 Integer,做了一个缓存:它把 -128 到 127 之间的小整数,预先创建好、缓存起来、反复复用。所以,当我写 Integer a = 100; Integer b = 100; 时,因为 100 在缓存范围内,a 和 b 拿到的,是缓存里同一个 Integer 对象——它俩指向同一个地址,所以 a == b 自然是 true。可当我写 Integer a = 1000; Integer b = 1000; 时,因为 1000 超出了缓存范围,Java 会各自 new 一个新的 Integer 对象——a 和 b,是两个不同的对象、两个不同的地址,所以 a == b 就成了 false!我那个看似"看天气"的诡异结果,真相是:== 比的是"是不是同一个对象",而 Integer 缓存,让小数值"碰巧"是同一个对象、大数值是不同对象——我误把 == 当成了"值比较",才会被这个缓存的"实现细节",耍得团团转。
故障现场:== 比的是引用,而 Integer 有缓存
我把这个"看天气的相等判断"的现场,用代码摊开给你看:
// ✗ 灾难: 用 == 比较 Integer(对象)
Integer a = 100, b = 100;
System.out.println(a == b); // true ← 小值, 碰巧"是同一个对象"
Integer c = 1000, d = 1000;
System.out.println(c == d); // false ← 大值, 是两个不同的对象!
// 同样的值, == 的结果却因大小而不同, 看起来像"玄学"。
// 真相1: == 用在"对象"上, 比的是"引用(地址)", 不是"值"!
// a == b 问的是: "a 和 b 是不是指向同一个对象?", 不是 "a 和 b 的值相不相等?"
// 真相2: Integer 自动装箱有"缓存"(IntegerCache), 范围 -128 ~ 127
// Integer a = 100; // 自动装箱 Integer.valueOf(100)
// valueOf 内部: 如果值在 [-128,127], 返回"缓存里的同一个对象";
// 否则, new 一个新的 Integer 对象。
// → 100 在缓存内 → a、b 是缓存里同一个对象 → a==b 为 true
// → 1000 超出缓存 → c、d 各 new 一个 → 两个不同对象 → c==d 为 false
// 验证一下:
Integer x = 127, y = 127;
System.out.println(x == y); // true (127 在缓存内)
Integer m = 128, n = 128;
System.out.println(m == n); // false (128 刚好超出缓存!)
// 基本类型 int 则没这问题(== 直接比值):
int p = 1000, q = 1000;
System.out.println(p == q); // true (int 是基本类型, == 比的就是值)
// 根因: == 对对象比引用(是否同一个对象); Integer 缓存让小值复用同一对象。
// 误把 == 当"值比较", 就会被这个缓存细节坑得"时对时错"。
看着这段代码,我才算真正理解了这个"看天气"的相等判断,背后的原理。问题的核心,藏在两个我都没吃透的机制里。第一个,也是最根本的:== 运算符,用在对象(而 Integer 是一个对象,不是基本类型)身上时,它比较的,根本不是"值",而是"引用"——也就是说,a == b 问的,是"a 和 b 这两个引用,是不是指向同一个对象(内存地址相同)",而不是"a 和 b 的值,相不相等"。第二个机制,是 Integer 的自动装箱缓存:Java 为了性能,有一个 IntegerCache,它把 -128 到 127 这个范围内的小整数,预先创建好、缓存起来;当你写 Integer a = 100(自动装箱,等价于 Integer.valueOf(100))时,valueOf 内部会判断:如果这个值在缓存范围内,就返回缓存里那个现成的、同一个对象;如果超出范围,才 new 一个新的对象。这两个机制一叠加,那个"玄学"现象就完全解释通了:当我写 Integer a = 100; Integer b = 100; 时,100 在缓存范围内,所以 a 和 b,拿到的是缓存里同一个 Integer 对象,它俩指向同一个地址,于是 ==(比引用)就是 true;而当我写 Integer c = 1000; Integer d = 1000; 时,1000 超出了缓存范围,Java 给 c 和 d 各 new 了一个新对象,它俩是两个不同的对象、两个不同的地址,于是 == 就成了 false。这就是为什么 127 == 127 是 true(在缓存内)、而 128 == 128 却是 false(刚好超出缓存这个边界)。归根结底,我犯的错,是误把 == 当成了"值比较";而 == 对对象,比的是"是不是同一个对象"。Integer 的缓存,这个本是为性能而设的实现细节,恰好让"小数值碰巧是同一个对象、大数值是不同对象",于是,我那个错误的 == 用法,就被这个缓存细节,耍得"时对时错"、像在看天气。
第一件事:搞懂 == 比引用、equals 比值
定位到根源,我必须把"== 和 equals 的根本区别",以及"基本类型 vs 包装类型"的差异,彻底搞清楚:
== 比引用, equals 比值; 基本类型 vs 包装类型
# == 运算符:
# - 用在"基本类型"(int/long/double/char/boolean)上: 比"值"。
# int a=1000, b=1000; a==b → true (直接比值)
# - 用在"对象/引用类型"(Integer/String/任何对象)上: 比"引用(是否同一个对象)"!
# Integer a, b; a==b → 比的是 a、b 是否指向同一个对象, 不是值!
# equals() 方法:
# - 是对象的方法, 用来比较"逻辑上的值是否相等"。
# - Integer.equals / String.equals 等, 都重写成了"比较值"。
# Integer a=1000, b=1000; a.equals(b) → true (比值, 正确!)
# 所以, 比较"对象的值是否相等", 永远用 equals, 不用 ==:
# ✗ if (integerA == integerB) // 比引用, 会被缓存坑
# ✓ if (integerA.equals(integerB)) // 比值, 正确
# ✓ if (str1.equals(str2)) // 字符串也是! 别用 == 比字符串内容
# Integer 缓存(IntegerCache): -128 ~ 127
# - Integer.valueOf(x): x 在 [-128,127] → 返回缓存的同一对象; 否则 new。
# - 自动装箱 Integer a = 100 走的就是 valueOf。
# → 这就是"小值 == 为 true、大值为 false"的原因(缓存内复用同一对象)。
# (类似缓存: Long、Short、Byte、Character 也有; Boolean 也缓存)
# 关键认知: "值相等(equals)" 和 "引用相等/同一个对象(==)" 是两码事!
# - 两个对象, 值可以相等(equals true), 但不是同一个对象(== false)。
# - 别用 == 去问"值相等吗"——那是 equals 的活。
# 一句话: 比对象的值, 用 equals; == 只用于基本类型、或判断"是不是同一个对象/null"。
原理终于刻进脑子里了。第一,要分清 == 在"基本类型"和"对象"上的天壤之别:用在基本类型(int/long/double 等)上,== 比的是值(int a=1000,b=1000; a==b 是 true);但用在对象/引用类型(Integer/String/任何对象)上,== 比的是引用——即"这两个引用,是不是指向同一个对象",而不是值!第二,要知道,比较对象的"值",有专门的方法 equals():equals() 是对象的方法,用来比较"逻辑上的值是否相等";Integer.equals、String.equals 等,都重写成了"比较值"(Integer a=1000,b=1000; a.equals(b) 就是 true,正确)。所以,结论很清晰:比较"对象的值是否相等",永远用 equals,不用 ==——这不仅对 Integer,对 String 也一样(千万别用 == 去比字符串内容,要用 str1.equals(str2))。而 Integer 的缓存,正是那个让我栽跟头的实现细节:Integer.valueOf(x),在 x 处于 [-128,127] 时返回缓存的同一对象、否则 new;自动装箱 Integer a = 100 走的就是 valueOf——这就是"小值 == 为 true、大值为 false"的根源(类似的缓存,Long/Short/Byte/Character/Boolean 也有)。由此,我领悟到一个最关键的认知:"值相等(equals)"和"引用相等 / 是不是同一个对象(==)",是两码事!两个对象,它们的值可以相等(equals 为 true),但它们不是同一个对象(== 为 false)。所以,别再用 == 去问"值相等吗"——那是 equals 的活。一句话:比对象的值,用 equals;而 ==,只用于基本类型、或判断"是不是同一个对象 / 是不是 null"——这,是我用一次"看天气"的相等判断,补上的、最该铭记的一课。
第二件事:正解——比对象的值,永远用 equals
搞懂了根因——"== 比引用、Integer 有缓存"——正解就一目了然了:比较对象(Integer、String 等)的"值"是否相等,永远用 equals();== 只用在基本类型(比值)、或判断"是不是同一个对象 / 是不是 null"上。
// 正解1: 比 Integer 的值, 用 equals
Integer c = 1000, d = 1000;
System.out.println(c.equals(d)); // true ✓ 不管值大小, 比的都是值
// 正解2: 比 String 内容, 也用 equals(同样别用 ==!)
String s1 = new String("hi"), s2 = new String("hi");
System.out.println(s1 == s2); // false (两个不同对象)
System.out.println(s1.equals(s2)); // true ✓ 比内容
// 正解3: 防 NPE, 用 Objects.equals 或常量在前
Integer x = null;
// x.equals(5) // ✗ x 是 null → NPE!
System.out.println(Objects.equals(x, 5)); // ✓ false, 不抛 NPE
System.out.println(Integer.valueOf(5).equals(x)); // ✓ 把"非null"放前面
// 正解4: 如果本来就该用基本类型, 就别用包装类型
int p = 1000, q = 1000;
System.out.println(p == q); // true ✓ 基本类型 == 比值, 没缓存问题
// → 能用 int 就用 int, 别无谓地用 Integer(还省了装箱开销)
// == 正确的用武之地:
// - 基本类型比值: int/long/double/char/boolean
// - 判断是不是同一个对象(确实想比引用时)
// - 判断是不是 null: if (obj == null) ← null 判断就该用 ==
// 核心: "比值用 equals, 比引用/null 用 =="。
// 对 Integer/String 等对象, 想比"值相等", 一律 equals, 别碰 ==。
这个正解的核心,就一句口诀:"比值用 equals,比引用 / 判 null 用 =="。正解1(比 Integer 的值,用 equals):c.equals(d),无论值的大小,比的都是值,所以永远正确,不会再被缓存坑。正解2(比 String 内容,也用 equals):这是同源的坑——new String("hi") == new String("hi") 是 false(两个不同对象),要用 .equals() 比内容;千万别用 == 比字符串内容。正解3(防 NPE):用 equals 时要注意,如果调用者可能是 null(x.equals(5) 而 x 是 null),会抛 NPE——可以用 Objects.equals(x, 5)(它做了 null 处理),或者把"确定非 null"的那个放在前面调用。正解4(本就该用基本类型,就别用包装类型):如果一个值,本来就该是 int,那就用 int——基本类型的 == 直接比值、没有缓存问题,还省了装箱的开销;别无谓地用 Integer。而 == 真正的用武之地,是:基本类型比值、确实想判断"是不是同一个对象"、以及判断"是不是 null"(if (obj == null) 这种 null 判断,就该用 ==)。归根结底:对 Integer、String 这些对象,只要你想比的是"值相等",就一律用 equals,别去碰 ==。我那次的错误,正是用 == 去问了一个本该用 equals 来回答的"值相等吗"的问题。
下面这张图,对比了"用 == 比对象"和"用 equals 比对象"两条路径:
这张图的对比很清楚:左边红色那条,用 == 比 Integer,比的是引用(是不是同一个对象),而结果又取决于"值在不在缓存内"——在缓存内碰巧 true、超出缓存就 false,于是结果"时对时错、看天气";右边绿色那条,用 equals,比的是值,不管两个对象是不是同一个,只要值相等就 true,永远正确。两条路的根本分野,在于你比的是"值"还是"引用"——而你想要的,几乎总是前者。
第三件事:== 和装箱还会在哪些地方坑你
填平了 Integer 这个坑,我系统排查了 == 和自动装箱,还会在哪些地方,带来意外:
// == 和自动装箱 还会坑你的地方:
// 1. Integer/Long 等的 == 比较(本文): 缓存内 true、缓存外 false。
// → 一律用 equals 比值。
// 2. String 的 == 比较
String a = "hi"; // 字符串常量池, a、b 指向池里同一个
String b = "hi";
System.out.println(a == b); // true (常量池复用)
String c = new String("hi"); // new 强制新建对象
System.out.println(a == c); // false (不同对象) → 所以别用 == 比字符串!
// 3. 自动拆箱时的 NPE
Integer i = null;
int j = i; // ✗ 自动拆箱 i.intValue() → NPE! (null 拆箱)
// → 包装类型可能是 null, 拆箱前要判空
// 4. 三元表达式里的意外拆箱
Integer x = true ? Integer.valueOf(1) : Double.valueOf(2.0);
// ✗ 三元两个分支类型不同 → 统一拆箱成 double → x 可能变成 1.0 的装箱! 类型被悄悄改
// 5. 集合里基本类型自动装箱的性能 / 比较
List list = ...;
if (list.contains(1000)) ... // contains 内部用 equals, 这个是对的;
// 但大量装箱有性能开销, 热点路径注意。
// 6. == 比较两个包装类型做"业务相等判断"——最常见的线上 bug 之一
// if (order.getUserId() == user.getId()) // ✗ 都是 Long, 大概率 == 比错!
// if (order.getUserId().equals(user.getId())) // ✓
// 共同点: 把"对象的值比较", 错用成了 == (引用比较); 以及装箱/拆箱的隐式行为。
// 原则: 对象比值用 equals; 留意自动装箱/拆箱(尤其 null 拆箱 NPE)。
这一排查,让我对 == 和自动装箱的"雷区",有了全面的警觉。除了 Integer 的 ==(本文),还有几个高频坑:String 的 ==(字符串常量 "hi" == "hi" 因常量池复用是 true,但 new String("hi") 就是 false——所以别用 == 比字符串);自动拆箱的 NPE(Integer i = null; int j = i; 会因对 null 拆箱而抛 NPE——包装类型可能是 null,拆箱前要判空);三元表达式里的意外拆箱(两个分支类型不同时,会被统一拆箱/转型,类型被悄悄改掉);以及最常见的线上 bug 之一——用 == 比较两个包装类型做"业务相等判断"(比如 order.getUserId() == user.getId(),两个都是 Long,大概率会 == 比错!应该用 .equals())。这些坑的共同点是:把"对象的值比较",错用成了 ==(引用比较);以及自动装箱/拆箱的隐式行为(尤其是 null 拆箱的 NPE)。所以,核心原则就两条:对象比值,一律用 equals;并时刻留意自动装箱/拆箱的隐式发生(特别是包装类型可能为 null 时的拆箱)。把这两条刻在心里,== 和装箱的这些隐蔽的坑,就再也绊不倒你了。
第四件事:equals 与 hashCode 的契约
这次踩坑,把我引到了一个更重要的话题:既然比值用 equals,那自定义类的 equals 该怎么写?还有那个总和它一起出现的 hashCode?我把这套"契约"系统地搞清楚了:
// 自定义类: 重写 equals, 必须同时重写 hashCode!
class Point {
int x, y;
// 默认的 equals(继承自 Object)= 比引用(==)! 所以两个值相同的 Point 不 equals。
// 要让"值相同就算相等", 必须重写 equals:
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point)) return false;
Point p = (Point) o;
return x == p.x && y == p.y; // 比值
}
// ✗ 只重写 equals、不重写 hashCode = 灾难!
@Override public int hashCode() { // ✓ 必须一起重写
return Objects.hash(x, y);
}
}
// 为什么 equals 和 hashCode 必须一起重写? "equals/hashCode 契约":
// 1. 两个对象 equals 为 true → 它们的 hashCode 必须相等。
// 2. (hashCode 相等, equals 不一定为 true——允许哈希冲突)
// → 违反契约的后果: 在 HashMap/HashSet 里行为错乱!
// 反例(只重写 equals, 没重写 hashCode):
Set set = new HashSet<>();
set.add(new Point(1, 2));
System.out.println(set.contains(new Point(1, 2)));
// ✗ 可能返回 false! 因为 HashSet 先用 hashCode 定位桶,
// 两个 Point 的 hashCode 不同(用了默认的、基于地址的)→ 定位到不同的桶
// → 根本不会去比 equals → 找不到! (即使 equals 写对了也没用)
// 正确: equals 和 hashCode 一起重写, 且基于"相同的字段"。
// 现代偷懒: 用 record(Java 16+, 自动生成 equals/hashCode/toString)
// record Point(int x, int y) {} // ✓ 自动正确实现
// 核心: equals 和 hashCode 是一对"契约", 必须同时重写、基于相同字段。
// 只写一个, 会让对象在 HashMap/HashSet 里行为诡异。
这一深挖,让我对"对象相等"有了更完整的认识。对于自定义类,Object 默认的 equals,其实就是 ==(比引用)——所以,如果你想让"值相同的两个对象算相等",就必须重写 equals(在里面逐个比较字段)。但这里有一个极易被忽略、却后果严重的规则:重写 equals,就必须同时重写 hashCode!这是 Java 规定的"equals/hashCode 契约":第一,两个对象如果 equals 为 true,那它们的 hashCode 必须相等;第二,hashCode 相等时,equals 不一定为 true(允许哈希冲突)。违反这个契约的后果,在 HashMap/HashSet 里会暴露得淋漓尽致:如果你只重写了 equals、没重写 hashCode,那么 set.contains(new Point(1,2)) 可能返回 false(即使你的 equals 写得完全正确)!因为 HashSet 是先用 hashCode 定位"桶(bucket)"的——两个值相同的 Point,如果用的是默认的、基于地址的 hashCode,它们的 hashCode 就不同,会被定位到不同的桶里,于是 HashSet 根本不会去比 equals,自然就找不到了。所以,正解是:equals 和 hashCode 要一起重写,且基于相同的字段;现代 Java 里更省事的办法,是用 record(Java 16+,它会自动、正确地生成 equals/hashCode/toString)。归根结底:equals 和 hashCode 是一对必须共同遵守的"契约",只重写其中一个,就会让对象在哈希集合里行为诡异。把 ==、equals、hashCode 的关系,整理成一张表:
| 机制 | 比/管什么 | 对象上的默认行为 | 怎么用 |
|---|---|---|---|
| == | 引用(是否同一对象) | 比地址 | 基本类型/判 null |
| equals | 逻辑值是否相等 | 默认=比引用 | 比对象的值,常重写 |
| hashCode | 哈希桶定位 | 默认基于地址 | 重写 equals 必须一起重写 |
| record | 自动值语义 | 自动生成 | Java 16+ 偷懒首选 |
第五件事:别被"实现细节"误导,要抓住"语义本质"
这次踩坑,在认知层面给了我最大的纠偏——它让我警惕"被实现细节带偏"。我把这层反思,沉淀了下来:
认知纠偏: 别被"实现细节"误导, 要抓住"语义本质"
# 我的误解(错误的):
# 我看到"100 == 100 为 true", 就以为"== 能比 Integer 的值"——
# 我是被 Integer 缓存这个"实现细节"(碰巧让小值相等)给"误导"了,
# 误以为那是 == 的"正常语义"。
# 真相: == 的"语义"是比引用; 缓存只是个"巧合的实现细节"
# - == 的语义本质: 对对象, 永远比"是不是同一个对象"。
# - "100==100 为 true": 不是因为 == 会比值, 而是缓存让它们碰巧是同一个对象。
# - 我把这个"巧合"(实现细节), 错当成了"规律"(语义), 于是被坑。
# 普遍的坑: 用"碰巧成立的测试结果", 去推断"语义", 而非看清"真正的语义"
# - 小数据测 == 成功了 → 误以为 == 能比值(其实是缓存巧合)。
# - 某次没触发并发 → 误以为代码线程安全(其实只是没撞上)。
# - 某次 append 没扩容 → 误以为切片独立(其实只是 cap 碰巧够)。
# → "碰巧能用" ≠ "语义正确"。靠巧合建立的认知, 迟早在边界崩塌。
# 正确的习惯:
# 1. 理解一个操作的"语义本质"(它被定义成做什么), 而非凭"测试碰巧的结果"。
# 2. 警惕"实现细节"(缓存、优化、巧合)制造的假象, 别把它当成规律。
# 3. 用"能暴露语义"的边界数据测试(如 == 要测 1000 而不只是 100)。
核心: 抓住操作的"语义本质", 别被"碰巧成立的实现细节"误导。
== 的语义是比引用——这才是真相, 缓存让小值相等只是个会骗人的巧合。
这层反思,是这次踩坑给我最高维度的收获。复盘我的误解,根源是:我看到"100 == 100 为 true",就以为"== 能比 Integer 的值"——我是被 Integer 缓存这个"实现细节"(它碰巧让小值相等)给误导了,误把它当成了 == 的"正常语义"。可真相是:== 的语义本质,是比引用;"100==100 为 true",不是因为 == 会比值,而是因为缓存让它们碰巧是同一个对象。我把这个"巧合"(实现细节),错当成了"规律"(语义),于是栽了跟头。而这,是一个极其普遍的坑——用"碰巧成立的测试结果",去推断"语义":小数据测 == 成功了,就误以为 == 能比值(其实是缓存的巧合);某次没触发并发问题,就误以为代码线程安全(其实只是没撞上);某次 append 没扩容,就误以为切片是独立的(其实只是 cap 碰巧够)——"碰巧能用"不等于"语义正确";靠巧合建立起来的认知,迟早会在边界情况上崩塌。由此,我立下了几条习惯:第一,去理解一个操作的"语义本质"(它被定义成做什么),而不是凭"测试碰巧的结果"去猜;第二,警惕"实现细节"(缓存、优化、巧合)制造的假象,别把它当成规律;第三,用"能暴露语义"的边界数据去测试(就像测 ==,要用 1000 这种能暴露问题的、超出缓存的值,而不只是碰巧也对的 100)。归根结底:抓住操作的"语义本质",别被"碰巧成立的实现细节"误导。== 的语义,就是比引用——这才是真相;而缓存让小值相等,只是一个会骗人的、危险的巧合。把"被实现细节误导"和"抓住语义本质"两种状态对比成一张表:
| 维度 | 被实现细节误导(踩坑) | 抓住语义本质(掌握) |
|---|---|---|
| 对 == | 100==100 真 → 以为能比值 | 知道它本质比引用 |
| 认知来源 | 碰巧成立的测试结果 | 操作被定义的语义 |
| 对巧合 | 当成规律 | 警惕它是假象 |
| 测试 | 只测碰巧对的小值 | 测能暴露语义的边界 |
| 典型受害 | ==/线程安全/切片扩容 | 提前看清真相 |
一套"该用 == 还是 equals"的决策流程
把这次踩坑的全部教训,我浓缩成了一张"要比较两个东西时,该用 == 还是 equals"的决策图,贴在了团队的 Java 规范里:
这张图,把我"血泪换来"的整套方法论,串成了一条可执行的路径:比较前先问是基本类型还是对象——基本类型(int/long/double)直接用 == 比值;对象(Integer/String/自定义类)则看你想比什么:比值用 equals(可能为 null 时用 Objects.equals 防 NPE),判断是不是同一个对象才用 ==,判断是不是 null 用 == null。这条"先分基本/对象、再分比值/比引用"的决策链,现在是我们团队写每一个比较时的准则。
我立下的几条 Java 相等比较规矩
这次"看天气的相等判断"的踩坑,让我把 Java 相等比较的注意事项,认真地立成了几条规矩:
- 比对象的值,永远用 equals,不用 ==。
Integer、Long、String等对象,比值一律equals。 - == 只用于基本类型、判同一对象、判 null。对象的
==比的是引用,不是值。 - 记牢 Integer 缓存 -128~127。这是
==比Integer"小值 true 大值 false"的根源,别依赖它。 - equals 防 NPE。调用者可能为 null 时用
Objects.equals,或把非 null 放前面。 - 重写 equals 必须同时重写 hashCode。基于相同字段,否则在 HashMap/HashSet 里行为错乱;Java 16+ 用 record。
- 能用基本类型就别用包装类型。避免装箱开销和
==/拆箱 NPE 的坑。 - 抓语义本质,别被实现细节误导。用能暴露语义的边界数据测试,"碰巧能用"不等于"语义正确"。
写在最后
这次"我用 == 比 Integer、小值对大值错"的经历,是我在 Java 路上,一次很经典、也很受用的成长。它教给我的,远不止"比对象用 equals"这一条具体的技术经验,更是一种认知上的警醒——别被"实现细节"制造的假象误导,要去抓住一个操作的"语义本质"。我那次的坑,根源就在于,Integer 缓存这个为性能而设的实现细节,恰好让"小值用 == 碰巧相等",于是我便误以为"== 能比 Integer 的值"——我把一个"巧合",错当成了"规律",直到大数值时,这个错误的认知,才在边界上轰然崩塌。
所以,当你使用一个操作、一个 API 时,请别凭"它在我这几个测试里碰巧成立"就推断它的行为,而要去搞清楚它真正的、被定义的"语义"是什么;并且,要警惕那些缓存、优化、巧合制造的"假象",用能暴露真实语义的边界数据去验证它。就像 Java 的 ==,你只要真正理解了"它对对象比的是引用、而非值",就绝不会用它去比 Integer 的值,也就不会被那个"看天气"的结果,折磨得百思不得其解。透过实现细节的表象、抓住操作的语义本质,用边界数据去检验认知而非依赖巧合,是从一个"会写语法"的开发,走向一个"懂原理、认知扎实"的工程师,必经的修炼。愿你写的每一个相等判断,都精准无误;也愿你我,永远不被那些会骗人的巧合带偏,始终抓得住事物的本质。共勉。
—— 别看了 · 2026