杰瑞科技汇

java map key 对象

作为 MapKey 的对象,必须正确实现 equals()hashCode() 方法。

java map key 对象-图1
(图片来源网络,侵删)

下面我们展开详细说明。


为什么 Key 对象需要特殊处理?

Map 的核心是 put(K key, V value)get(Object key) 方法。Map 内部需要根据 Key 来存储和查找 Value,这个过程依赖于两个核心方法:

  1. hashCode(): HashMap 使用这个方法来确定 Key 应该存储在哪个“桶”(bucket)中,它是一个快速、不精确的定位。
  2. equals(Object obj): 当 hashCode() 定位到同一个桶后,HashMap 会使用 equals() 方法来精确比较两个 Key 对象是否真的相同,以避免哈希冲突。

Key 对象没有正确实现这两个方法,Map 的行为就会变得不可预测,最常见的问题就是无法通过 Key 找到对应的 Value


equals()hashCode() 的“契约”

这两个方法必须遵守以下约定:

java map key 对象-图2
(图片来源网络,侵删)
  • 对称性: a.equals(b)trueb.equals(a) 也必须为 true
  • 自反性: a.equals(a) 必须为 true
  • 传递性: a.equals(b)true,且 b.equals(c)truea.equals(c) 也必须为 true
  • 一致性: 多次调用 a.equals(b) 的结果必须一致(前提是对象没有被修改)。
  • 非空性: a.equals(null) 必须返回 false

最重要的约定:

如果两个对象根据 equals() 方法比较是相等的,那么它们调用 hashCode() 方法必须返回相同的整数。

反过来的约定不成立:

如果两个对象的 hashCode() 相同,它们不一定相等(equals() 返回 false),这就是所谓的“哈希冲突”(Hash Collision)。


最佳实践:如何为自定义类实现 Key

假设我们有一个 Person 类,我们想用 Person 对象作为 MapKey

错误的示例 (不实现或错误实现)

class Person {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // 省略 getter/setter
    // 注意:这里没有重写 equals() 和 hashCode()
}
public class Main {
    public static void main(String[] args) {
        Map<Person, String> map = new HashMap<>();
        Person p1 = new Person("Alice", 30);
        Person p2 = new Person("Alice", 30);
        // p1 和 p2 在逻辑上是同一个人,但它们是不同的对象实例
        System.out.println(p1.equals(p2)); // 输出 false (继承自 Object 的 equals)
        map.put(p1, "Engineer");
        // 尝试用 p2 获取值
        // 因为 p1.equals(p2) 是 false,map 认为这是两个不同的 key
        System.out.println(map.get(p2)); // 输出 null,而不是期望的 "Engineer"
    }
}

问题分析

  • p1p2 内容相同,但它们是不同的对象。
  • Person 类没有重写 equals(),所以使用的是 Object 类的 equals(),它比较的是对象的内存地址(),p1.equals(p2) 返回 false
  • HashMap 认为这是两个完全不同的 Keyget(p2) 找不到 put(p1) 时存入的值。

正确的示例 (IDE 自动生成)

现代 IDE(如 IntelliJ IDEA 或 Eclipse)可以一键生成正确的 equals()hashCode() 方法,这是最推荐的方式。

import java.util.Objects;
class Person {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // Getters and Setters
    public String getName() { return name; }
    public int getAge() { return age; }
    // 1. 重写 equals() 方法
    @Override
    public boolean equals(Object o) {
        // 1. 检查是否是同一个对象
        if (this == o) return true;
        // 2. 检查是否为 null 或类型是否不同
        if (o == null || getClass() != o.getClass()) return false;
        // 3. 类型转换
        Person person = (Person) o;
        // 4. 比较所有关键字段
        // 使用 Objects.equals() 可以安全处理 null 值
        return age == person.age && Objects.equals(name, person.name);
    }
    // 2. 重写 hashCode() 方法
    @Override
    public int hashCode() {
        // 使用 Objects.hash() 可以方便地生成基于所有字段的哈希码
        // 它会自动处理 null 值,并且计算结果与 equals() 方法保持一致
        return Objects.hash(name, age);
    }
}
public class Main {
    public static void main(String[] args) {
        Map<Person, String> map = new HashMap<>();
        Person p1 = new Person("Alice", 30);
        Person p2 = new Person("Alice", 30);
        Person p3 = new Person("Bob", 25);
        // 验证 equals()
        System.out.println("p1.equals(p2): " + p1.equals(p2)); // 输出 true
        System.out.println("p1.equals(p3): " + p1.equals(p3)); // 输出 false
        map.put(p1, "Software Engineer");
        // 现在可以成功获取到值
        System.out.println("Get with p2: " + map.get(p2)); // 输出 "Software Engineer"
        // 修改 p3 的字段,使其与 p1 相等
        p3.setName("Alice");
        p3.setAge(30);
        System.out.println("p1.equals(p3) after modification: " + p1.equals(p3)); // 输出 true
        System.out.println("Get with modified p3: " + map.get(p3)); // 输出 "Software Engineer"
    }
}

关键点

  • equals(): 定义了对象的“逻辑相等性”,对于 Person 类,我们认为 nameage 相同的两个人就是同一个人。
  • hashCode(): 必须与 equals() 保持一致,因为 p1.equals(p2)truep1.hashCode() 必须等于 p2.hashCode(),这样,HashMap 才能将它们定位到同一个桶,并最终通过 equals() 确认它们是同一个 Key

可变对象作为 Key 的风险

可变对象作为 MapKey非常危险的。

class Person {
    // ... (同上,包含 equals 和 hashCode)
}
public class MutableKeyProblem {
    public static void main(String[] args) {
        Map<Person, String> map = new HashMap<>();
        Person p = new Person("Charlie", 40);
        map.put(p, "Manager");
        System.out.println("Before change: " + map.get(p)); // 输出 "Manager"
        // !!!!!!危险的操作:修改了作为 Key 的对象 !!!!!!
        p.setAge(41);
        // 现在再也找不回值了!
        System.out.println("After change: " + map.get(p)); // 输出 null
    }
}

问题分析

  1. put(p, "Manager") 时,HashMap 计算 p 的哈希码(基于 name="Charlie", age=40),并将 "Manager" 存储在对应的桶中。
  2. p.setAge(41) 后,对象 p 的状态被改变了。
  3. get(p) 时,HashMap 重新计算 p 的哈希码(基于 name="Charlie", age=41),这个新的哈希码指向了一个完全不同的桶
  4. HashMap 在新的桶里找不到任何东西,返回 null

最佳实践

尽量使用不可变对象作为 MapKey

如果一个对象被用作 Key,那么在它被放入 Map 之后,其决定 equals()hashCode() 结果的字段(所有字段)就不应该再被修改。String 类是作为 Key 的完美选择,因为它就是不可变的。


不同 Map 实现对 Key 的要求

Map 实现 对 Key 的要求 特点
HashMap 必须正确实现 equals()hashCode() 基于哈希表,性能最高(O(1)),不保证有序。
TreeMap Key 对象的类必须实现 Comparable 接口,或者构造时提供一个 Comparator 基于红黑树,Key 会根据自然排序或自定义排序规则进行排序,查询时间复杂度为 O(log n)。
LinkedHashMap 必须正确实现 equals()hashCode() 继承自 HashMap,同时维护了一个双向链表,可以记录 Key 的插入顺序或访问顺序。
Hashtable 必须正确实现 equals()hashCode() HashMap 的线程安全版本,性能较差,不推荐使用。

TreeMap 示例

import java.util.TreeMap;
class Person implements Comparable<Person> {
    private String name;
    private int age;
    // ... constructor, getters, equals, hashCode ...
    @Override
    public int compareTo(Person other) {
        // 先按年龄排序,如果年龄相同再按名字排序
        int ageCompare = Integer.compare(this.age, other.age);
        return ageCompare != 0 ? ageCompare : this.name.compareTo(other.name);
    }
}
public class TreeMapExample {
    public static void main(String[] args) {
        Map<Person, String> map = new TreeMap<>();
        map.put(new Person("Alice", 30), "Engineer");
        map.put(new Person("Bob", 25), "Designer");
        map.put(new Person("Charlie", 30), "Manager");
        // 遍历 Map,Key 会按年龄和名字排序
        for (Map.Entry<Person, String> entry : map.entrySet()) {
            System.out.println(entry.getKey().getName() + " -> " + entry.getValue());
        }
        // 输出:
        // Bob -> Designer
        // Alice -> Engineer
        // Charlie -> Manager
    }
}
  1. 核心规则: 任何作为 Map Key 的自定义对象,都必须同时且正确地重写 equals()hashCode()
  2. 契约保证: equals() 相等的对象,hashCode() 必须相等。
  3. 最佳实践: 优先使用不可变对象(如 String, 包装类)作为 Key,以避免因对象状态改变而导致 Map 失效。
  4. 工具辅助: 使用 IDE 自动生成 equals()hashCode() 方法,确保正确性和效率。
  5. 不同实现: 注意 HashMapTreeMapKey 的不同要求(哈希 vs. 排序)。
分享:
扫描分享到社交APP
上一篇
下一篇