我用 == 比较两个明明相等的 Integer,小数值时返回 true、大数值时却返回 false,我对着这个像在看天气的诡异判断查了大半天的深度复盘

我用 == 比较两个 Integer,本以为值相等就 true。结果它像看天气:Integer a=100,b=100 时 a==b 是 true,可 a=1000,b=1000 时却变成 false!同样相等的值,竟因大小不同给出相反结果。深究才懂两件事:一,== 用在对象上比的是引用(是不是同一个对象),不是值;二,Integer 自动装箱有缓存,-128~127 复用同一对象,超出就各 new 一个——所以小值碰巧同一对象 == 为 true,大值不同对象 == 为 false。这篇从 == 比引用/equals 比值、Integer 缓存讲起,到一律用 equals 比值的正解、String 的 ==/拆箱 NPE 等坑、equals 与 hashCode 契约,以及那句最戳心的——别被实现细节(缓存)制造的假象误导,要抓住操作的语义本质。

我用 == 比较两个明明相等的 Integer,小数值时返回 true、大数值时却返回 false,我对着这个像在看天气的诡异判断查了大半天的深度复盘

这是一个让我对 Java "==" 和"自动装箱"刻骨铭心的故事。我在做一段逻辑,要比较两个 Integer 类型的值,看它们是否相等。我图顺手,直接用了 == 去比——在我朴素的认知里,== 不就是判断"两个值相不相等"嘛,a == b,只要它俩的值一样,就该返回 true。逻辑再简单不过了。

可运行结果,把我整懵了:这个 == 判断,竟然像在"看天气"一样,时而对、时而错!当两个 Integer 的值都比较的时候(比如都是 100),a == b 返回 true,没问题;可当它俩的值大一点的时候(比如都是 1000),a == b 竟然返回了 false!明明值是完全相等的两个 Integer,怎么会因为"值的大小"不同,而得出截然相反的比较结果?我当时整个人都是懵的:100 == 100true,1000 == 1000 却是 false?这是什么"玄学"?难道数值大小还能影响相等判断?我反复确认,值确实都相等,可结果就是随着数值大小"变脸"。直到我去深究 Java 的 Integer 自动装箱机制,才恍然大悟,补上了关于 == 最重要的一课:问题的核心,在两个地方。第一,== 用在对象(Integer 是对象)身上时,它比较的根本不是"值",而是"两个引用,是不是指向同一个对象(即内存地址是否相同)"!第二,Java 为了性能,对自动装箱的 Integer,做了一个缓存:它把 -128127 之间的小整数,预先创建好、缓存起来、反复复用。所以,当我写 Integer a = 100; Integer b = 100; 时,因为 100 在缓存范围内,ab 拿到的,是缓存里同一个 Integer 对象——它俩指向同一个地址,所以 a == b 自然是 true。可当我写 Integer a = 1000; Integer b = 1000; 时,因为 1000 超出了缓存范围,Java 会各自 new 一个新的 Integer 对象——ab,是两个不同的对象、两个不同的地址,所以 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,它把 -128127 这个范围内的小整数,预先创建好、缓存起来;当你写 Integer a = 100(自动装箱,等价于 Integer.valueOf(100))时,valueOf 内部会判断:如果这个值在缓存范围内,就返回缓存里那个现成的、同一个对象;如果超出范围,才 new 一个新的对象。这两个机制一叠加,那个"玄学"现象就完全解释通了:当我写 Integer a = 100; Integer b = 100; 时,100 在缓存范围内,所以 ab,拿到的是缓存里同一个 Integer 对象,它俩指向同一个地址,于是 ==(比引用)就是 true;而当我写 Integer c = 1000; Integer d = 1000; 时,1000 超出了缓存范围,Java 给 cd new 了一个新对象,它俩是两个不同的对象、两个不同的地址,于是 == 就成了 false这就是为什么 127 == 127true(在缓存内)、而 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==btrue);但用在对象/引用类型(Integer/String/任何对象)上,== 比的是引用——即"这两个引用,是不是指向同一个对象",而不是值!第二,要知道,比较对象的"值",有专门的方法 equals():equals() 是对象的方法,用来比较"逻辑上的值是否相等";Integer.equalsString.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)"和"引用相等 / 是不是同一个对象(==)",是两码事!两个对象,它们的值可以相等(equalstrue),但它们不是同一个对象(==false)。所以,别再用 == 去问"值相等吗"——那是 equals 的活。一句话:比对象的值,用 equals;而 ==,只用于基本类型、或判断"是不是同一个对象 / 是不是 null"——这,是我用一次"看天气"的相等判断,补上的、最该铭记的一课。

第二件事:正解——比对象的值,永远用 equals

搞懂了根因——"== 比引用、Integer 有缓存"——正解就一目了然了:比较对象(IntegerString 等)的"值"是否相等,永远用 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)xnull),会抛 NPE——可以用 Objects.equals(x, 5)(它做了 null 处理),或者把"确定非 null"的那个放在前面调用。正解4(本就该用基本类型,就别用包装类型):如果一个值,本来就该是 int,那就int——基本类型的 == 直接比值、没有缓存问题,还省了装箱的开销;别无谓地用 Integer== 真正的用武之地,是:基本类型比值、确实想判断"是不是同一个对象"、以及判断"是不是 null"(if (obj == null) 这种 null 判断,就该用 ==)。归根结底:IntegerString 这些对象,只要你想比的是"值相等",就一律用 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 契约":第一,两个对象如果 equalstrue,那它们的 hashCode 必须相等;第二,hashCode 相等时,equals 不一定为 true(允许哈希冲突)。违反这个契约的后果,在 HashMap/HashSet 里会暴露得淋漓尽致:如果你只重写了 equals、没重写 hashCode,那么 set.contains(new Point(1,2)) 可能返回 false(即使你的 equals 写得完全正确)!因为 HashSethashCode 定位"桶(bucket)"的——两个值相同的 Point,如果用的是默认的、基于地址的 hashCode,它们的 hashCode不同,会被定位到不同的桶里,于是 HashSet 根本不会去比 equals,自然就找不到了。所以,正解是:equalshashCode一起重写,且基于相同的字段;现代 Java 里更省事的办法,是用 record(Java 16+,它会自动、正确地生成 equals/hashCode/toString)。归根结底:equalshashCode 是一对必须共同遵守的"契约",只重写其中一个,就会让对象在哈希集合里行为诡异。==equalshashCode 的关系,整理成一张表:

机制 比/管什么 对象上的默认行为 怎么用
== 引用(是否同一对象) 比地址 基本类型/判 null
equals 逻辑值是否相等 默认=比引用 比对象的值,常重写
hashCode 哈希桶定位 默认基于地址 重写 equals 必须一起重写
record 自动值语义 自动生成 Java 16+ 偷懒首选

第五件事:别被"实现细节"误导,要抓住"语义本质"

这次踩坑,在认知层面给了我最大的纠偏——它让我警惕"被实现细节带偏"。我把这层反思,沉淀了下来:

认知纠偏: 别被"实现细节"误导, 要抓住"语义本质"

# 我的误解(错误的):
#   我看到"100 == 100 为 true", 就以为"== 能比 Integer 的值"——
#   我是被 Integer 缓存这个"实现细节"(碰巧让小值相等)给"误导"了,
#   误以为那是 == 的"正常语义"。

# 真相: == 的"语义"是比引用; 缓存只是个"巧合的实现细节"
#   - == 的语义本质: 对对象, 永远比"是不是同一个对象"。
#   - "100==100 为 true": 不是因为 == 会比值, 而是缓存让它们碰巧是同一个对象。
#   - 我把这个"巧合"(实现细节), 错当成了"规律"(语义), 于是被坑。

# 普遍的坑: 用"碰巧成立的测试结果", 去推断"语义", 而非看清"真正的语义"
#   - 小数据测 == 成功了 → 误以为 == 能比值(其实是缓存巧合)。
#   - 某次没触发并发 → 误以为代码线程安全(其实只是没撞上)。
#   - 某次 append 没扩容 → 误以为切片独立(其实只是 cap 碰巧够)。
#   → "碰巧能用" ≠ "语义正确"。靠巧合建立的认知, 迟早在边界崩塌。

# 正确的习惯:
#   1. 理解一个操作的"语义本质"(它被定义成做什么), 而非凭"测试碰巧的结果"。
#   2. 警惕"实现细节"(缓存、优化、巧合)制造的假象, 别把它当成规律。
#   3. 用"能暴露语义"的边界数据测试(如 == 要测 1000 而不只是 100)。

核心: 抓住操作的"语义本质", 别被"碰巧成立的实现细节"误导。
  == 的语义是比引用——这才是真相, 缓存让小值相等只是个会骗人的巧合。

这层反思,是这次踩坑给我最高维度的收获。复盘我的误解,根源是:我看到"100 == 100true",就以为"== 能比 Integer 的值"——我是被 Integer 缓存这个"实现细节"(它碰巧让小值相等)给误导了,误把它当成了 == 的"正常语义"。可真相是:==语义本质,是比引用;"100==100true",不是因为 == 会比值,而是因为缓存让它们碰巧是同一个对象。我把这个"巧合"(实现细节),错当成了"规律"(语义),于是栽了跟头。而这,是一个极其普遍的坑——用"碰巧成立的测试结果",去推断"语义":小数据测 == 成功了,就误以为 == 能比值(其实是缓存的巧合);某次没触发并发问题,就误以为代码线程安全(其实只是没撞上);某次 append 没扩容,就误以为切片是独立的(其实只是 cap 碰巧够)——"碰巧能用"不等于"语义正确";靠巧合建立起来的认知,迟早会在边界情况上崩塌。由此,我立下了几条习惯:第一,去理解一个操作的"语义本质"(它被定义成做什么),而不是凭"测试碰巧的结果"去猜;第二,警惕"实现细节"(缓存、优化、巧合)制造的假象,别把它当成规律;第三,用"能暴露语义"的边界数据去测试(就像测 ==,要用 1000 这种能暴露问题的、超出缓存的值,而不只是碰巧也对的 100)。归根结底:抓住操作的"语义本质",别被"碰巧成立的实现细节"误导== 的语义,就是比引用——这才是真相;而缓存让小值相等,只是一个会骗人的、危险的巧合。把"被实现细节误导"和"抓住语义本质"两种状态对比成一张表:

维度 被实现细节误导(踩坑) 抓住语义本质(掌握)
对 == 100==100 真 → 以为能比值 知道它本质比引用
认知来源 碰巧成立的测试结果 操作被定义的语义
对巧合 当成规律 警惕它是假象
测试 只测碰巧对的小值 测能暴露语义的边界
典型受害 ==/线程安全/切片扩容 提前看清真相

一套"该用 == 还是 equals"的决策流程

把这次踩坑的全部教训,我浓缩成了一张"要比较两个东西时,该用 == 还是 equals"的决策图,贴在了团队的 Java 规范里:

这张图,把我"血泪换来"的整套方法论,串成了一条可执行的路径:比较前先问是基本类型还是对象——基本类型(int/long/double)直接用 == 比值;对象(Integer/String/自定义类)则看你想比什么:比equals(可能为 null 时用 Objects.equals 防 NPE),判断是不是同一个对象才用 ==,判断是不是 null== null这条"先分基本/对象、再分比值/比引用"的决策链,现在是我们团队写每一个比较时的准则。

我立下的几条 Java 相等比较规矩

这次"看天气的相等判断"的踩坑,让我把 Java 相等比较的注意事项,认真地立成了几条规矩:

  1. 比对象的值,永远用 equals,不用 ==。IntegerLongString 等对象,比值一律 equals
  2. == 只用于基本类型、判同一对象、判 null。对象的 == 比的是引用,不是值。
  3. 记牢 Integer 缓存 -128~127。这是 ==Integer "小值 true 大值 false"的根源,别依赖它。
  4. equals 防 NPE。调用者可能为 null 时用 Objects.equals,或把非 null 放前面。
  5. 重写 equals 必须同时重写 hashCode。基于相同字段,否则在 HashMap/HashSet 里行为错乱;Java 16+ 用 record。
  6. 能用基本类型就别用包装类型。避免装箱开销和 ==/拆箱 NPE 的坑。
  7. 抓语义本质,别被实现细节误导。用能暴露语义的边界数据测试,"碰巧能用"不等于"语义正确"。

写在最后

这次"我用 ==Integer、小值对大值错"的经历,是我在 Java 路上,一次很经典、也很受用的成长。它教给我的,远不止"比对象用 equals"这一条具体的技术经验,更是一种认知上的警醒——别被"实现细节"制造的假象误导,要去抓住一个操作的"语义本质"。我那次的坑,根源就在于,Integer 缓存这个为性能而设的实现细节,恰好让"小值用 == 碰巧相等",于是我便误以为"== 能比 Integer 的值"——我把一个"巧合",错当成了"规律",直到大数值时,这个错误的认知,才在边界上轰然崩塌。

所以,当你使用一个操作、一个 API 时,请别凭"它在我这几个测试里碰巧成立"就推断它的行为,而要去搞清楚它真正的、被定义的"语义"是什么;并且,要警惕那些缓存、优化、巧合制造的"假象",用能暴露真实语义的边界数据去验证它。就像 Java 的 ==,你只要真正理解了"它对对象比的是引用、而非值",就绝不会用它去比 Integer 的值,也就不会被那个"看天气"的结果,折磨得百思不得其解。透过实现细节的表象、抓住操作的语义本质,用边界数据去检验认知而非依赖巧合,是从一个"会写语法"的开发,走向一个"懂原理、认知扎实"的工程师,必经的修炼。愿你写的每一个相等判断,都精准无误;也愿你我,永远不被那些会骗人的巧合带偏,始终抓得住事物的本质。共勉。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我只是对一个切片做了 append,结果它悄悄改掉了另一个切片的数据,我盯着这个幽灵修改查了大半天才搞懂共享底层数组的深度复盘

2026-6-1 22:50:04

技术教程

我的列表页明明感觉只查了一次数据库,慢查询日志里却冒出几百条 SQL,页面慢得像蜗牛,最后揪出是 ORM 的 N+1 查询在背后疯狂打库的深度复盘

2026-6-1 23:02:01

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索