我用自定义对象当 HashMap 的 key,两个字段完全一样的对象却被当成了不同的键、get 永远返回 null,我对着这个去重失效的 Map 排查了大半天的复盘

我用自定义的 Point(有 x、y)当 HashMap 的 key,put(new Point(1,2),"A") 后再用另一个内容一模一样的 new Point(1,2) 去 get,竟返回 null;HashSet 放两个相同的 Point 也没去重、都进去了。内容明明完全相同,怎么就认不出?深挖才懂:我没重写 equals 和 hashCode——不重写时继承 Object 的默认实现,equals 比的是内存地址(等价于 ==)、hashCode 基于地址,两个 new Point 是不同对象、不同地址,于是 equals 不等、hashCode 也不同。而 HashMap/HashSet 是"先用 hashCode 定位桶、再用 equals 在桶内精确比对",hashCode 不同就跑到错误的桶里去找,自然找不到。这篇从 equals 与 hashCode 的契约(equals 相等则 hashCode 必相等、必须成对、基于同一组字段)讲起,到成对重写/record/Lombok/IDE 生成的正解、重写的细节坑(只写一个、参数类型、可变字段当 key)、真实暴雷场景(contains/remove 也失灵)、几行对照实验,以及那句最戳心的——计算机从不想当然,你以为天经地义的常识必须显式写进代码,机器才懂。

我用自定义对象当 HashMap 的 key,两个字段完全一样的对象却被当成了不同的键、get 永远返回 null,我对着这个去重失效的 Map 排查了大半天的复盘

这是一个让我对 Java 的 equalshashCode 真正敬畏起来的故事。我有个需求,要用一个自定义的类(比如 Point,有 xy 两个字段)当 HashMap 的 key,做一些缓存和去重。我写了个 Point 类,放进 HashMap,逻辑看着天经地义。可运行起来,诡异的事发生了:我用 new Point(1, 2) 当 key 存了个值进去,然后又用另一个 new Point(1, 2)(字段一模一样)去 get,结果返回的竟然是 null!明明内容完全相同啊,怎么就取不到?同样地,我往 HashSet 里放 Point 去重,结果两个字段完全一样的 Point,竟然都被存了进去、根本没去重

我当时彻底懵了:两个 Point 的 x、y 都是 1 和 2,它们不应该"相等"吗?为什么 HashMap 不认它们是同一个 key?我顺着这个现象深挖,才终于揭开真相,补上了我对 Java 集合一个最根本的认知漏洞:问题的核心,是我用自定义对象当 key/元素,却没有重写 equals()hashCode() 这两个方法。我一直想当然地以为,"两个对象,只要字段值一样,Java 就认为它们相等";可真相是:Java 里,如果你不重写,那么从 Object 继承来的默认 equals(),比较的是"两个对象是不是同一个内存地址"(即 ==),而默认的 hashCode(),是基于对象的内存地址算出来的这就意味着:我那两个 new Point(1, 2),虽然字段值相同,但它们是两个不同的对象、占着两块不同的内存地址;所以,默认的 equals 认为它们不相等,默认的 hashCode算出了两个不同的哈希值HashMap/HashSet 的工作方式,正是"先用 hashCode 定位到桶,再用 equals 在桶里精确比对":当我用第二个 Point(1,2)get 时,它算出的 hashCode 和第一个不一样,于是 HashMap 直接跑到了一个错误的桶里去找,自然找不到,返回 null;HashSet 也因为 hashCode 不同,把两个"内容相同"的对象,当成两个不同的元素,都存了进去我这才痛彻地明白:在 Java 里,"对象内容相等"不是天经地义的,而是需要你自己通过重写 equals()定义的;而一旦你要把这个对象,用在任何基于哈希的集合(HashMapHashSet 等)里,你就必须同时重写 hashCode(),且要保证"两个 equals 相等的对象,hashCode 必须也相等"这条铁律equalshashCode,是一对"必须成对出现、缺一不可"的孪生兄弟;只重写一个、或都不重写,就会让你的基于哈希的集合,以各种诡异的方式悄悄失灵

故障现场:没重写 equals/hashCode,HashMap 认不出相同的 key

我把这个"去重失效"的现场,用代码摊开给你看:

// ✗ 灾难: 自定义对象当 key, 没重写 equals 和 hashCode
class Point {
    int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
    // ✗ 没有重写 equals() 和 hashCode()!
}

Map<Point, String> map = new HashMap<>();
map.put(new Point(1, 2), "A");

// ✗ 用另一个内容相同的 Point 去取
String v = map.get(new Point(1, 2));
System.out.println(v);   // ✗ null! 取不到, 尽管内容一模一样

// ✗ HashSet 去重也失效
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println(set.size());   // ✗ 2! 没去重, 两个"相同"的都进去了

// 为什么会这样?
//   - 不重写时, 继承自 Object 的默认行为:
//     equals(): 比较"是不是同一个对象"(即 ==, 比内存地址)。
//     hashCode(): 基于对象的内存地址计算。
//   - 两个 new Point(1,2) 是不同对象、不同地址:
//     → equals 认为不相等; hashCode 算出不同的值。

// HashMap/HashSet 的查找机制(两步):
//   1. 先用 hashCode() 定位到"桶(bucket)"。
//   2. 再在桶里用 equals() 逐个精确比对。
//   - 第二个 Point hashCode 不同 → 定位到错误的桶 → 根本找不到 → null。

// 根因: 自定义对象没重写 equals/hashCode, 默认按内存地址比较;
//   内容相同的两个对象被当成不同的 key/元素, 基于哈希的集合失效。

看着这段代码,我才算彻底想明白了这场"去重失效"的根源。问题的核心,是我用自定义对象 Point 当 key/元素,却没重写 equals()hashCode()不重写时,会继承 Object默认行为:equals() 比较的是"是不是同一个对象"(即 ==、比内存地址);hashCode() 是基于对象的内存地址算的而我那两个 new Point(1,2),是不同对象、不同地址,所以 equals 认为它们不相等、hashCode 算出了不同的值结合 HashMap/HashSet两步查找机制——第一步用 hashCode() 定位到"桶",第二步在桶里用 equals() 精确比对——第二个 Point 因为 hashCode 不同,直接定位到了错误的桶,根本找不到、返回 null;HashSet 也因 hashCode 不同,把两个"内容相同"的对象当成不同元素都存了进去。归根结底:自定义对象没重写 equals/hashCode,默认按内存地址比较;内容相同的两个对象被当成不同的 key/元素,基于哈希的集合就失效了——这,就是根源。

第一件事:搞懂 equals 与 hashCode 的契约

定位到根源,我必须把 equalshashCode 的关系与契约,从根上彻底搞清楚:

equals 与 hashCode: 必须成对重写, 且要满足契约

# 默认行为(不重写时, 继承自 Object):
#   - equals(o): 等价于 ==, 比较"是否同一个对象"(内存地址)。
#   - hashCode(): 基于对象地址, 不同对象几乎必不同。
#   → 所以"内容相同的两个新对象", 默认既不 equals, hashCode 也不同。

# HashMap/HashSet 怎么用这俩?
#   - 存/取时: 先 hashCode() 算出该去哪个桶, 再在桶内用 equals() 精确匹配。
#   - 所以两个"相等"的 key, 必须先有"相同的 hashCode"才能落到同一个桶!

# 核心契约(必须遵守):
#   1. 若 a.equals(b) 为 true, 则 a.hashCode() == b.hashCode() 必须成立!
#      —— 这是为什么"只重写 equals 不重写 hashCode"会出大问题:
#         两个 equals 的对象 hashCode 却不同 → 落到不同桶 → Map 找不到。
#   2. hashCode 相同, equals 不一定为 true(哈希冲突, 允许)。
#   3. equals 要满足: 自反、对称、传递、一致性。

# 三种错误写法的后果:
#   - 都不重写: 按地址比, 内容相同也认不出(本文)。
#   - 只重写 equals, 不重写 hashCode: ✗ 违反契约1!
#     Map/Set 里两个 equals 的对象因 hashCode 不同落不同桶 → 仍找不到。
#   - 只重写 hashCode, 不重写 equals: 落到同一桶, 但桶内 equals 按地址比 → 仍认不出。

# 关键认知: equals 和 hashCode 是"孪生兄弟", 要重写就一起重写。
#   - 且两者要基于"同一组字段"(参与 equals 比较的字段, 也要参与 hashCode)。

# 核心: equals 定义"内容相等", hashCode 定位桶; 二者必须成对重写、满足
#   "equals 相等则 hashCode 必相等"的契约, 否则基于哈希的集合失效。

原理终于清晰了。默认行为(不重写时):equals(o) 等价于 ==(比内存地址),hashCode() 基于对象地址——所以"内容相同的两个新对象",默认既不 equalshashCode 也不同HashMap/HashSet 怎么用它俩?存/取时,hashCode() 算该去哪个桶,再在桶内用 equals() 精确匹配——所以两个"相等"的 key,必须先有"相同的 hashCode"才能落到同一个桶!这就引出了核心契约:第一(最重要):若 a.equals(b) 为 true,则 a.hashCode() == b.hashCode() 必须成立!(这正是为什么"只重写 equals 不重写 hashCode"会出大问题——两个 equals 的对象 hashCode 却不同,落到不同桶,Map 照样找不到);第二,hashCode 相同、equals 不一定为 true(哈希冲突,允许);第三,equals 要满足自反、对称、传递、一致性三种错误写法,后果各异:都不重写(按地址比,本文);只重写 equals 不重写 hashCode(违反契约 1,落不同桶仍找不到);只重写 hashCode 不重写 equals(落同一桶,但桶内 equals 按地址比、仍认不出)。由此,我刻下一个关键认知:equalshashCode 是"孪生兄弟",要重写就一起重写,且两者要基于"同一组字段"。归根结底:equals 定义"内容相等"、hashCode 定位桶;二者必须成对重写、满足"equals 相等则 hashCode 必相等"的契约,否则基于哈希的集合就会失效。

第二件事:正解——成对重写 equals 和 hashCode

搞懂了原理,正解就清晰了:同时重写 equalshashCode,且让它们基于同一组字段

// ✓ 正解一: 手动成对重写(基于同一组字段 x, y)
class Point {
    int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;                  // 同一对象
        if (o == null || getClass() != o.getClass()) return false;  // 类型检查
        Point p = (Point) o;
        return x == p.x && y == p.y;                  // ✓ 比较字段值
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);                   // ✓ 用同一组字段算 hash
    }
}
// 现在: map.get(new Point(1,2)) 能取到; HashSet 也能正确去重了。

// ✓ 正解二: 用 record(Java 16+, 最省事!)
//   record 自动生成基于所有字段的 equals/hashCode/toString/构造器
record Point(int x, int y) {}
//   new Point(1,2).equals(new Point(1,2)) → true, 开箱即用!

// ✓ 正解三: 用 Lombok @EqualsAndHashCode 自动生成
//   @EqualsAndHashCode
//   class Point { int x, y; }

// ✓ 正解四: IDE 自动生成(Alt+Insert → equals() and hashCode())

// 重写要点:
//   - equals 和 hashCode 必须基于"同一组字段"(参与相等判断的字段)。
//   - 改了类的字段, 记得同步更新这俩(或重新生成)。
//   - 别只挑一部分字段, 除非你明确知道相等的语义。

// 核心: 成对重写 equals 和 hashCode 且基于同一组字段;
//   优先用 record(Java16+)/Lombok/IDE 生成, 别手写易漏。

修复的方案,简单而明确:同时重写 equalshashCode,并让它们基于同一组字段正解一,手动成对重写:equals 里先判同一对象、再判类型、最后比较字段值;hashCode 里用 Objects.hash(x, y) 基于同一组字段算哈希。这样,map.get(new Point(1,2)) 就能取到,HashSet 也能正确去重了。但手写容易漏、容易写错,所以更推荐用工具:正解二,record(Java 16+,最省事!):record Point(int x, int y) {}自动生成基于所有字段的 equals/hashCode/toString/构造器,开箱即用;正解三,Lombok 的 @EqualsAndHashCode 自动生成;正解四,IDE 自动生成(Alt+Insert)。重写时的要点是:equalshashCode 必须基于"同一组字段";改了类的字段,记得同步更新这俩(或重新生成);别只挑一部分字段,除非你明确知道相等的语义归根结底:成对重写 equalshashCode 且基于同一组字段;优先用 record(Java 16+)/Lombok/IDE 生成,别手写易漏。

第三件事:重写 equals/hashCode 的几个坑

就算知道要重写,真写起来,还有几个容易踩的细节坑,我也梳理清楚了:

重写 equals/hashCode 的细节坑

# 坑1: 只重写一个(最常见!)
#   - 只 equals 不 hashCode → Map/Set 失效(违反契约)。
#   - 只 hashCode 不 equals → 桶内比对仍按地址 → 失效。
#   → 必须成对!

# 坑2: equals 的参数类型写错
#   - 正确: public boolean equals(Object o)  ← 参数必须是 Object
#   - 错误: public boolean equals(Point o)   ← 这是"重载"不是"重写"!
#     编译能过, 但集合调用的是 Object 版 → 你的没生效。
#   → 加 @Override 注解, 写错会编译报错, 能挡住这个坑。

# 坑3: hashCode 用了"会变的字段"
#   - 对象放进 HashSet/HashMap 后, 又改了参与 hashCode 的字段:
#     → hashCode 变了, 对象"迷失"在错误的桶里, 再也找不到/删不掉!
#   → 当 key 的对象, 参与 hashCode 的字段最好是不可变的。

# 坑4: equals 和 hashCode 用了"不同的字段集"
#   - equals 比 x,y 但 hashCode 只算 x → 两个 equals 对象可能 hashCode 不同
#     (其实这里不会, 但若 equals 比 x, hashCode 算 x,y 才危险) → 必须一致。

# 坑5: 继承体系里的 equals(对称性/getClass vs instanceof)
#   - 父子类混用时 equals 容易违反对称性, 是个深水区, 谨慎设计。

# 坑6: 可变对象当 key
#   - 同坑3, 用作 key 的对象, 状态变了哈希就乱 → 优先用不可变对象当 key。

# 核心: 必成对重写、equals 参数是 Object(加@Override)、hashCode 用不可变字段、
#   两者字段集一致; 可变对象别当 key。

这些细节坑,个个都很隐蔽。坑一,只重写一个(最常见!):只 equalshashCode(违反契约)、或只 hashCodeequals(桶内仍按地址比),都会失效——必须成对!坑二,equals 的参数类型写错:正确的是 public boolean equals(Object o)(参数必须是 Object),如果写成 equals(Point o),那是"重载"不是"重写",编译能过、但集合调用的是 Object 版、你的没生效!——@Override 注解,写错就编译报错,能挡住这个坑。坑三,hashCode 用了"会变的字段":对象放进集合后又改了参与 hashCode 的字段,hashCode 一变,对象就"迷失"在错误的桶里、再也找不到/删不掉——当 key 的对象,参与 hashCode 的字段最好不可变坑四,两者用了不同的字段集(必须一致);坑五,继承体系里 equals 的对称性(深水区,谨慎);坑六,可变对象当 key(同坑三,优先用不可变对象当 key)。归根结底:必成对重写、equals 参数是 Object(加 @Override)、hashCode 用不可变字段、两者字段集一致;可变对象别当 key。

下面这张图,是这次"HashMap 取不到值"的成因与解法:

第四件事:重写 equals/hashCode 的几种方式对比

这次踩坑后,我把生成 equals/hashCode 的几种方式,横向比了一遍,按场景对号入座。

方式 写法 优点 注意
record(Java16+) record Point(int x,int y){} 最省事, 自动且正确, 不可变 是不可变数据类, 不能改字段
Lombok @EqualsAndHashCode 注解一行 简洁, 可选字段 需引 Lombok, 注意继承配置
IDE 生成 Alt+Insert 生成 标准、可控、无依赖 改字段要重新生成
Objects.hash / equals 手写用工具方法 清晰、JDK 自带 仍要自己写, 别漏字段
手写全部逻辑 纯手工 完全可控 ✗ 易漏易错, 不推荐

把它们排在一起,选择就清楚了。能用 record 就用 record(Java 16+):一行定义,equals/hashCode/toString/构造器全自动且正确,还天生不可变——最适合做"值对象"和 Map 的 key用不了 record 时:Lombok 的 @EqualsAndHashCode 一个注解搞定(注意继承场景的配置);IDE 生成(Alt+Insert)标准、可控、无额外依赖;手写时务必用 Objects.hash()Objects.equals() 这些 JDK 自带的工具方法,别自己徒手算哈希。纯手工写全部逻辑,是最不推荐的(易漏字段、易写错对称性)。这张表给我的最大启发是:对于 equals/hashCode 这种"有严格契约、又容易写错"的样板代码,最好的策略是"不要自己写"——交给 record、Lombok 或 IDE 去生成;把"容易出错的机械劳动",交给不会犯错的工具,自己只负责"选对要参与比较的字段"这个真正需要思考的决策。

第五件事:这个坑在真实项目里的几种暴雷场景

顺着这次的教训,我把 equals/hashCode 缺失在真实项目里暴雷的典型场景,系统排查了一遍。它们往往很隐蔽、不报错、只是"结果不对"

暴雷场景 现象 根因
自定义对象当 HashMap key put 进去 get 不到(null) 没重写 equals/hashCode(本文)
HashSet 给对象去重 "相同"对象没去重, 全进去了 同上
List.contains(对象) 明明有却返回 false contains 用 equals, 没重写就按地址
List.remove(对象) 删不掉 remove 也用 equals 匹配
两个对象比较是否相等 内容一样却 a.equals(b)=false 没重写 equals
DTO/实体放进 Set 去重 去重不生效, 数据重复 实体类没重写

这张表,让我看清了这个坑暴雷的广度它们无一例外,都源于同一件事:把一个"没重写 equals/hashCode"的对象,用在了"需要判断对象内容相等"的场景里而这些场景,远不止 HashMap:HashSet 去重List.contains/List.remove(它们内部都用 equals 匹配,没重写就按地址比,导致"明明有却找不到、删不掉")、直接 a.equals(b) 比较DTO/实体放进 Set 去重——全都会因为缺失 equals/hashCode悄悄出错它们最阴险的地方在于:它们不报错、不抛异常,只是"结果默默地不对"——get 返回 null、去重没生效、contains 返回 false、数据悄悄重复;这种"无声的错误",往往要等到数据出了问题、被用户发现,才被回溯到这个根上。它给我的最大启发是:只要你定义了一个"有'相等'语义"的类(尤其是值对象、DTO、实体、要放进集合的对象),就应该条件反射地为它成对地补上 equals/hashCode(或用 record)——这应该成为一种下意识的习惯,而不是等到"集合行为诡异了"才回头补救。

第六件事:定义一个类时,我现在会怎么决策

现在,每当我定义一个新的类,脑子里都会过一遍这张决策图——核心就一问:这个类的对象,需要"按内容判断相等"吗?

这张图的灵魂,是把判断前置到"定义类的那一刻"。第一问:这个类的对象,需要"按内容判断相等"吗?——如果它只是个行为对象/服务(比如某个 Service),不需要按内容比,那默认按地址就行;但如果它是值对象、DTO、实体、或要放进集合,就必须成对重写 equals + hashCode第二问:用什么方式?——Java 16+ 且数据不可变,直接用 record(最省事);有 Lombok 用 @EqualsAndHashCode;否则 IDE 生成 / Objects.hash;都要基于同一组字段、字段尽量不可变最后一问:要当 HashMap 的 key 吗?——是的话,务必确保参与 hashCode 的字段不可变,免得放进去后字段一改就"迷失"。这套判断,让我以后定义类时,不再等到集合行为出问题才补救,而是在源头就把"相等语义"想清楚、定义好

我立下的几条规矩

这场"HashMap 取不到值"的事故,换来了我写 Java 时,刻进骨子里的几条铁律:

  1. equals 和 hashCode 必须成对重写。它们是孪生兄弟,只写一个会让基于哈希的集合失效——这是契约,不是建议。
  2. 契约第一条:equals 相等,则 hashCode 必须相等。违反它,HashMap/HashSet 就会定位到错误的桶、找不到。
  3. 两者基于同一组字段。参与 equals 比较的字段,必须也参与 hashCode 计算,保持一致。
  4. 优先用 record/Lombok/IDE 生成,别手写。这是易错的样板代码,交给工具,自己只决定"哪些字段参与相等"。
  5. equals 参数必须是 Object,且加 @Override。写成具体类型是重载不是重写,@Override 能在编译期挡住这个坑。
  6. 当 key 的对象,hashCode 字段要不可变。放进集合后改了哈希字段,对象会"迷失"在错误的桶里,找不到也删不掉。
  7. 定义值对象/DTO/实体时,条件反射地补上。别等集合行为诡异了才回头查——在源头就想清楚相等语义。

附:几行代码看清 equals/hashCode 的作用

口说无凭。下面这几段对照,能让你亲眼看清 equals/hashCode 重写前后的天壤之别,跑一遍胜过千言:

import java.util.*;

public class Demo {
    // 版本A: 没重写
    static class PointBad {
        int x, y;
        PointBad(int x, int y) { this.x = x; this.y = y; }
    }
    // 版本B: 成对重写
    static class PointGood {
        int x, y;
        PointGood(int x, int y) { this.x = x; this.y = y; }
        @Override public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof PointGood)) return false;
            PointGood p = (PointGood) o;
            return x == p.x && y == p.y;
        }
        @Override public int hashCode() { return Objects.hash(x, y); }
    }

    public static void main(String[] args) {
        // ===== 没重写 =====
        System.out.println(new PointBad(1,2).equals(new PointBad(1,2)));  // false ✗
        Map mBad = new HashMap<>();
        mBad.put(new PointBad(1,2), "A");
        System.out.println(mBad.get(new PointBad(1,2)));                  // null ✗
        Set sBad = new HashSet<>();
        sBad.add(new PointBad(1,2)); sBad.add(new PointBad(1,2));
        System.out.println(sBad.size());                                 // 2 ✗ 没去重

        // ===== 成对重写 =====
        System.out.println(new PointGood(1,2).equals(new PointGood(1,2)));// true ✓
        Map mGood = new HashMap<>();
        mGood.put(new PointGood(1,2), "A");
        System.out.println(mGood.get(new PointGood(1,2)));               // "A" ✓
        Set sGood = new HashSet<>();
        sGood.add(new PointGood(1,2)); sGood.add(new PointGood(1,2));
        System.out.println(sGood.size());                               // 1 ✓ 去重了

        // 看 hashCode 差异:
        System.out.println(new PointBad(1,2).hashCode() == new PointBad(1,2).hashCode());  // false
        System.out.println(new PointGood(1,2).hashCode() == new PointGood(1,2).hashCode());// true
    }
}

// 核心: 一跑便知 —— 没重写时 equals=false/get=null/Set不去重/hashCode不同;
//   成对重写后 equals=true/get取到/Set去重/hashCode相同。眼见为实。

这段对照,把 equals/hashCode 的作用,赤裸裸地摆在了面前同样的"内容相同的两个点":版本 A(没重写),equalsfalsemap.get 返回 nullSet 没去重(size=2)、两个对象 hashCode 不同;而版本 B(成对重写),equalstruemap.get 取到了 "A"Set 正确去重(size=1)hashCode 相同一一对应的对比,比任何文字都更能说明:hashCode 相同,是 HashMap/HashSet 能正确工作的前提;而 equals 为 true,是它们能精确匹配的保证;两者缺一,集合就失灵这,正是我想用这段代码,留给每一个 Java 开发者的最后一课:当你对"集合为什么行为诡异"感到困惑时,别去猜——写这样一段最小的对照实验,把重写前后的 equalsgetsizehashCode 都打印出来,让 Java 亲口告诉你差别在哪。一次"眼见为实"的实验,胜过十遍"道听途说"的记忆;而 equals/hashCode 这对孪生兄弟的契约,也会在你这一次次的亲手验证中,被你刻进肌肉记忆

写在最后

回头看,这场由"没重写 equals/hashCode"引发的、HashMap 取不到值的事故,真正教给我的,是一个比"要成对重写"本身更深的道理:计算机从不"想当然";那些在你看来"天经地义、不言自明"的概念,在机器眼里,必须被精确地、明确地定义出来,它才"知道""两个 x、y 都相同的点,当然是同一个点啊"——这在我的直觉里,是再自然不过的常识;可在 Java 看来,它对"相等"一无所知,除非我亲手用 equals 告诉它"什么叫相等"、用 hashCode 告诉它"怎么给相等的东西算出一致的指纹"。我之前的错误,正是把"我以为的常识",当成了"机器默认就懂的常识",从而省略了那个"把常识翻译给机器听"的关键步骤。所以,编程的一个核心修养,就是时刻分清"什么是我脑子里的隐含假设"和"什么是程序里被显式表达了的逻辑":机器只执行后者,而绝不会替你脑补前者;凡是你希望它遵守的规则、你认为理所当然的语义,你都必须把它显式地、无歧义地写进代码里,否则,它就会用它自己那套"默认的、机械的"逻辑,给你一个意想不到的结果真正的严谨,在于对"机器不懂我的常识"这件事,保持永久的清醒,并耐心地、把每一个常识,都翻译成它能懂的、精确的代码别让"想当然"的常识,成为代码里沉默的漏洞——这,是我用一次"HashMap 失灵"的事故,换来的、关于 Java、也关于编程本质的、最朴素也最深刻的领悟。如果这篇复盘,能让你在下一次定义一个值对象时,条件反射地为它补上 equalshashCode,那我对着那个返回 nullget 熬的这大半天,就值了。

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

我在 Go 的循环里用 defer 关闭打开的文件,自以为每轮都妥妥地释放了,结果批量处理几千个文件时却报了 too many open files,我排查了大半天的复盘

2026-6-2 2:34:51

技术教程

我的转账接口在高峰期偶尔会报 Deadlock found、事务莫名其妙被回滚,我对着这个时有时无的数据库死锁排查了大半天才搞懂加锁顺序的复盘

2026-6-2 2:46:48

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