0Pricing
Java Academy · 课时

equals/hashCode 契约

为什么二者必须一致

equals/hashCode 契约 是 CoddyKit 上的免费 Java Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Java Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Java Academy 课程共包含 4 节课。

两个方法,一个契约

每个 Java 对象都会从 Object 继承 equals() 和 hashCode()。

重写其中一个方法时,几乎总是必须重写另一个方法。它们构成了基于哈希的集合所依赖的严格契约。

public class Main {
    public static void main(String[] args) {
        Object a = new Object();
        Object b = new Object();
        System.out.println(a.equals(b));
        System.out.println(a.hashCode() == b.hashCode());
    }
}

核心规则

契约规定:

  • 如果 a.equals(b) 为真,那么 a.hashCode() 必须等于 b.hashCode()。
  • 如果哈希码不同,则可以确定对象不相等。

相等的对象必须具有相等的哈希码。反之则不必成立。

public class Main {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hel" + "lo";
        System.out.println(s1.equals(s2));
        System.out.println(s1.hashCode() == s2.hashCode());
    }
}

不相等的对象可以共享哈希值

两个不相等的对象可以共享同一个哈希码。这只是一次冲突,映射会使用 equals() 处理它。

因此,唯一被禁止的情况是:相等的对象具有不同的哈希码。

public class Main {
    public static void main(String[] args) {
        // Equal value, unequal objects, same hash is fine
        System.out.println("FB".hashCode() == "Ea".hashCode());
        System.out.println("FB".equals("Ea"));
    }
}

只重写 equals 的类

这是一个有问题的类。它重写了 equals(),但没有重写 hashCode()。

现在,逻辑上相等的两个点拥有从 Object 继承而来的不同哈希码,违反了契约。

public class Main {
    static class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
        @Override public boolean equals(Object o) {
            if (!(o instanceof Point p)) return false;
            return x == p.x && y == p.y;
        }
        // BUG: no hashCode override
    }
    public static void main(String[] args) {
        Point a = new Point(1, 2);
        Point b = new Point(1, 2);
        System.out.println("equals: " + a.equals(b));
        System.out.println("same hash: " + (a.hashCode() == b.hashCode()));
    }
}

HashSet 中的错误

这个有问题的类在 HashSet 中会失败。集合会在错误哈希值选定的桶中查找,因此永远找不到相等的元素。

最终会出现本应被拒绝的重复项。

import java.util.HashSet;
import java.util.Set;

public class Main {
    static class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
        @Override public boolean equals(Object o) {
            if (!(o instanceof Point p)) return false;
            return x == p.x && y == p.y;
        }
    }
    public static void main(String[] args) {
        Set<Point> set = new HashSet<>();
        set.add(new Point(1, 2));
        set.add(new Point(1, 2));
        System.out.println("size = " + set.size());
    }
}

修复它

重写这两个方法,并根据同一组字段推导它们。这样相等的对象会共享一个哈希码,并落入同一个桶。

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

public class Main {
    static class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
        @Override public boolean equals(Object o) {
            if (!(o instanceof Point p)) return false;
            return x == p.x && y == p.y;
        }
        @Override public int hashCode() { return Objects.hash(x, y); }
    }
    public static void main(String[] args) {
        Set<Point> set = new HashSet<>();
        set.add(new Point(1, 2));
        set.add(new Point(1, 2));
        System.out.println("size = " + set.size());
    }
}

equals 必须具有自反性

正确的 equals() 遵守四个性质。第一条是自反性:x.equals(x) 必须为真。

这听起来显而易见,但如果忘记处理同一引用的情况,自定义逻辑就可能破坏这一性质。

public class Main {
    public static void main(String[] args) {
        String x = "java";
        System.out.println(x.equals(x));
    }
}

对称性与传递性

还有两条规则:

  • 对称:如果 a.equals(b),那么 b.equals(a)。
  • 传递:如果 a.equals(b) 且 b.equals(c),那么 a.equals(c)。

在类层次结构中混用类型经常会破坏对称性,因此请仔细比较类。

public class Main {
    public static void main(String[] args) {
        String a = "x", b = "x", c = "x";
        boolean symmetric = a.equals(b) == b.equals(a);
        boolean transitive = a.equals(b) && b.equals(c) && a.equals(c);
        System.out.println("symmetric: " + symmetric);
        System.out.println("transitive: " + transitive);
    }
}

一致

第四条规则是一致:只要对象没有发生变化,重复调用就会返回相同结果。

这就是为什么您应当根据不可变字段实现 equals 和 hashCode。在插入后修改 hashCode 使用的字段会破坏映射。

import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        // Immutable record gives consistent equals/hashCode automatically
        record Id(int value) {}
        Id id = new Id(7);
        System.out.println(Objects.equals(id, new Id(7)));
        System.out.println(id.hashCode() == new Id(7).hashCode());
    }
}

记录类会替您完成这些工作

Java record 会根据其所有组件自动生成 equals() 和 hashCode()。

对于值类型数据,优先使用记录类。这样无需额外工作即可满足契约,并且始终保持同步。

import java.util.HashSet;
import java.util.Set;

public class Main {
    record Point(int x, int y) {}
    public static void main(String[] args) {
        Set<Point> set = new HashSet<>();
        set.add(new Point(1, 2));
        set.add(new Point(1, 2));
        System.out.println("size = " + set.size());
    }
}

可变键陷阱

如果将对象放入映射后,修改了 hashCode 使用的字段,您将再也找不到它。

映射会在旧桶中查找;对象现在会哈希到新桶。该条目变成了一个丢失的幽灵。

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class Main {
    static class Box {
        int id;
        Box(int id) { this.id = id; }
        @Override public boolean equals(Object o) {
            return o instanceof Box b && b.id == id;
        }
        @Override public int hashCode() { return Objects.hash(id); }
    }
    public static void main(String[] args) {
        Map<Box, String> m = new HashMap<>();
        Box key = new Box(1);
        m.put(key, "value");
        key.id = 99; // mutated after insertion
        System.out.println(m.get(key));
    }
}

快速检查

请测试您对这一契约的掌握程度。

回顾

您已了解 equals/hashCode 契约:

  • 相等的对象必须具有相等的哈希码。
  • equals 必须具有自反性、对称性、传递性和一致性。
  • 两个方法都应基于同一组不可变字段。
  • 记录类可以免费生成正确的实现。

接下来,您将亲手编写正确的 hashCode。

import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        System.out.println("Contract recap: " + Objects.hash(1, 2, 3));
    }
}

常见问题解答

「equals/hashCode 契约」课时是免费的吗?

是的 — 「equals/hashCode 契约」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Java Academy 课程的其余内容,请升级到 CoddyKit PRO。 Java Academy 课程共包含 4 节课。

「equals/hashCode 契约」这节课中我会学到什么?

为什么二者必须一致 你通过在浏览器中直接运行的动手代码来练习 Java Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Java Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Java Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「equals/hashCode 契约」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Java Academy 课中编写并运行代码吗?

能。每节 Java Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. HashMap 的工作原理
  2. equals/hashCode 契约
  3. 实现 hashCode
  4. 树化与性能
← 返回 Java Academy