目录

HashMap-中的-key-值类型

HashMap 中的 key 值类型

在 Java 中, HashMapkey 一般建议使用 String 而不是自定义对象,主要有以下几个原因:

1. String 是不可变对象(Immutable)

  • String 在 Java 中是不可变的,一旦创建就不会改变其哈希值 ( hashCode )。

  • HashMap 依赖 keyhashCode() 计算存储位置,如果 key 是可变对象,修改 key 后,它的 hashCode() 可能会改变,导致 HashMap 无法正确查找该 key ,引发潜在问题(如数据丢失、无法查找等)。

  • 例如:

    Map<List<Integer>, String> map = new HashMap<>();
    List<Integer> key = new ArrayList<>();
    key.add(1);
    map.put(key, "value");
    
    key.add(2); // 修改 key
    System.out.println(map.get(key)); // 可能返回 null

    由于 ArrayListhashCode() 依赖于内容, key 变化后 hashCode 变化,导致 HashMap 无法找到原来的 value

2. StringhashCode() 计算高效且稳定

  • String 在 Java 中的 hashCode() 实现是经过高度优化的,并且被广泛使用,计算效率高。

  • StringhashCode() 计算方式如下:

    public int hashCode() {
        int h = 0;
        for (int i = 0; i < length(); i++) {
            h = 31 * h + charAt(i);
        }
        return h;
    }
  • 由于 Stringfinal 类,它的 hashCode() 计算逻辑不会被子类重写或修改,保证了哈希值的一致性。

3. String 具有良好的分布性,减少 Hash 冲突

  • HashMap 中,良好的 hashCode() 设计可以减少哈希冲突,提高查询效率。
  • StringhashCode() 计算方式能够较好地分布数据,避免大量 key 落在相同的桶(bucket)里。

4. String 适合作为标识符

  • String 直观易读,可以直接表示用户名、ID、类别等,便于代码理解。

  • 例如:

    Map<String, Integer> map = new HashMap<>();
    map.put("Alice", 25);
    map.put("Bob", 30);

    相比于自定义对象, String 更适合作为 key 来表示业务属性。

5. 避免 equals()hashCode() 方法实现错误

  • 使用自定义对象作为 key 时,需要正确实现 equals()hashCode() 方法,否则会导致 HashMap 行为异常,如无法正确查找 key 或发生哈希冲突。

  • 例如:

    class Person {
        String name;
        int age;
    }

    如果 Person 没有正确重写 equals()hashCode() ,那么 HashMap 可能无法正确区分两个 Person 对象,即使它们的 nameage 相同。

6. String 具有内存优化(字符串常量池)

  • String 在 JVM 中有字符串常量池(String Pool),相同字符串可复用,减少内存占用。

  • 例如:

    String s1 = "hello";
    String s2 = "hello";
    System.out.println(s1 == s2); // true共享同一个对象

总结

比较项使用 String 作为 key使用对象作为 key
不可变性不可变,哈希值稳定可能可变,影响 hashCode()
哈希分布性StringhashCode() 分布良好需要自定义 hashCode() ,可能不均匀
易用性直观,易读易维护需要正确实现 equals()hashCode()
性能StringhashCode() 计算高效可能计算复杂,影响 HashMap 效率
内存优化享受 JVM String Pool 优势没有字符串池机制

因此,在 HashMap 中, 推荐优先使用 String 作为 key ,如果必须使用对象作为 key ,需要确保:

  1. 该对象是不可变的;
  2. 正确重写 equals()hashCode() 方法。

HashMap 中,如果 key 是一个对象,并且该对象的 某个属性发生变化 ,那么可能会导致无法通过原来的 key 找到对应的 value

为什么找不到原来的 value

  1. HashMap 依赖 keyhashCode() 计算存储位置
    • HashMap 通过 keyhashCode() 计算存储桶(bucket)的索引。
    • 如果 key 是对象,并且对象的某些属性影响了 hashCode() 计算,那么修改该属性会导致 hashCode() 变化,使得 HashMap 无法在原来的位置找到该 key
  2. HashMap 依赖 equals() 方法查找 key
    • HashMap 中,即使两个对象的 hashCode() 相同,最终还是要通过 equals() 方法确认它们是否相等。
    • 如果 equals() 方法依赖于某些可变属性,而这些属性发生了变化,那么即使 hashCode() 仍然相同,也可能会导致 equals() 失效,使得 HashMap 无法匹配原来的 key

示例代码

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

class Person {
    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person person = (Person) obj;
        return age == person.age && Objects.equals(name, person.name);
    }
}

public class HashMapKeyTest {
    public static void main(String[] args) {
        HashMap<Person, String> map = new HashMap<>();

        Person p = new Person("Alice", 25);
        map.put(p, "Engineer");

        System.out.println("Before change: " + map.get(p)); // 正常获取 "Engineer"

        // 修改 key 对象的属性
        p.age = 30;

        System.out.println("After change: " + map.get(p)); // 可能返回 null
        System.out.println("Contains key: " + map.containsKey(p)); // 可能返回 false
    }
}

分析

  1. Person 作为 key ,它的 hashCode() 依赖于 nameage
  2. 存入 HashMap 后, p 计算的 hashCode() 确定了它在 HashMap 的存储位置。
  3. 修改 p.age 后, hashCode() 发生变化,导致 HashMap 在查找 p 时,计算出的 hashCode() 对应的存储桶位置可能已经改变。
  4. 结果: map.get(p) 可能返回 null ,即无法找到原来的 value

如何解决?

方案 1:使用不可变对象作为 key

class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person person = (Person) obj;
        return age == person.age && Objects.equals(name, person.name);
    }
}
  • 优点 :保证 key 不变,避免 hashCode() 变化导致查找失败。
  • 适用场景 :如果 key 需要稳定,建议使用 final 关键字让属性不可变,或者使用 String 作为 key

方案 2:使用 put() 重新存入修改后的 key

如果 key 发生变化,需要重新 put() 进去:

map.remove(p); // 先删除旧的 key
p.age = 30;    // 修改属性
map.put(p, "Engineer"); // 重新放入 HashMap
  • 适用场景 :如果一定要修改 key ,需要确保 HashMap 结构同步更新。

总结

方案影响适用场景
修改 key 属性hashCode() 变更,导致查找失败不推荐
使用不可变对象key 保持不变,避免问题推荐
使用 remove() 后重新 put()需要额外操作,保证 HashMap 一致性适用于可变对象

👉 最佳实践:建议 HashMapkey 选择 String 或不可变对象,避免因 hashCode() 变化导致查找失败!