每日计算机知识-List为什么不能做HashMap的Key?

在Java中,HashMap 是一种基于哈希表的数据结构,它要求键(Key)必须满足以下条件:

  1. 不可变性(Immutability) :键对象必须是不可变的,或者在作为键期间其状态不能发生变化。这是因为 HashMap 依赖于键的哈希码(hashCode)和相等性(equals)来判断键的唯一性和存储位置。如果键对象在插入后发生了变化,可能会导致哈希码改变,从而无法正确找到或删除对应的值。
  2. 一致性(Consistency) :键对象的 hashCode()equals() 方法必须保持一致。也就是说,如果两个对象通过 equals() 方法比较为相等,那么它们的 hashCode() 必须相同;反之,如果 hashCode() 相同,equals() 不一定返回 true,但 hashCode() 不同则 equals() 必须返回 false

实际上,在其他语言里面,也是一样的。

为什么 List 不能作为 HashMap 的键?

List 是一个可变集合,它的内容可以随时被修改。如果将 List 作为 HashMap 的键,可能会导致以下问题:

  1. 哈希码不一致ListhashCode() 方法是基于其内容的。如果 List 的内容发生变化,其哈希码也会随之改变。这会导致 HashMap 无法正确找到或删除对应的值,因为 HashMap 使用的是插入时的哈希码来定位存储位置。
  2. 违反契约HashMap 依赖于键的 hashCode()equals() 方法的一致性。如果 List 的内容发生变化,hashCode()equals() 的结果可能会不一致,从而违反 HashMap 的契约。

示例

arduino 复制代码
import java.util.HashMap;
import java.util.List;
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        HashMap<List<String>, String> map = new HashMap<>();
        List<String> key = Arrays.asList("a", "b");
        map.put(key, "value");

        // 修改 key 的内容
        key.add("c");

        // 由于 key 的哈希码发生了变化,无法正确获取值
        System.out.println(map.get(key)); // 输出: null
    }
}

在上面的例子中,key 是一个 List,在插入 HashMap 后,key 的内容被修改了。由于 List 是可变的,修改后其哈希码发生了变化,导致 HashMap 无法正确找到对应的值。

hashcode的计算方法

在 Java 中,List 接口的实现类(如 ArrayListLinkedList 等)的 hashCode() 方法是通过遍历列表中的所有元素,并根据每个元素的哈希码计算得出的。具体来说,ListhashCode() 实现遵循以下规则:

List.hashCode() 的计算规则

ListhashCode() 方法通常基于以下公式计算:

ini 复制代码
s = 1
for (E e : list) {
    s = 31 * s + (e == null ? 0 : e.hashCode())
}
return s

详细解释:

  1. 初始值

    • 计算开始时,s 被初始化为 1
  2. 遍历元素

    • 对于列表中的每一个元素 e,执行以下步骤:

      • 如果元素 enull,则其哈希码视为 0
      • 否则,使用元素 ehashCode() 方法获取其哈希码。
      • 更新 s 的值为 31 * s + 当前元素的哈希码
  3. 返回结果

    • 遍历完所有元素后,s 即为列表的最终哈希码。

为什么选择 31?

  • 性能优化 :31 是一个奇素数,且 31 * i 可以被优化为 (i << 5) - i,这在某些 JVM 实现中可以提高计算效率。
  • 减少冲突:选择一个适当的素数有助于减少不同对象产生相同哈希码的概率,从而提高哈希表的性能。

示例代码

以下是一个简化的 ArrayListhashCode() 实现示例:

csharp 复制代码
public int hashCode() {
    int hashCode = 1;
    for (E e : this) {
        hashCode = 31 * hashCode + (e == null ? 0 : e.hashCode());
    }
    return hashCode;
}

具体实现示例

ArrayList 为例,其 hashCode() 方法的实际实现如下:

csharp 复制代码
public int hashCode() {
    int h = 1;
    for (E e : this)
        h = 31 * h + (e == null ? 0 : e.hashCode());
    return h;
}

非用不可的话?

  1. 使用不可变列表

    • 为了避免上述问题,建议在使用 List 作为键时,先将其转换为不可变的列表。例如,可以使用 Collections.unmodifiableList 或者创建一个不可变的自定义列表类。
    arduino 复制代码
    List<String> original = new ArrayList<>(Arrays.asList("a", "b"));
    List<String> immutableKey = Collections.unmodifiableList(original);
    Map<List<String>, String> map = new HashMap<>();
    map.put(immutableKey, "value");
    
    // 现在即使尝试修改 original,也不会影响 immutableKey 的哈希码
  2. 自定义键类

    • 另一种方法是创建一个不可变的自定义键类,封装 List 并确保其哈希码在对象生命周期内不变。
    typescript 复制代码
    public final class ImmutableListKey {
        private final List<String> list;
    
        public ImmutableListKey(List<String> list) {
            this.list = List.copyOf(list); // Java 10+: creates an unmodifiable copy
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof ImmutableListKey)) return false;
            ImmutableListKey that = (ImmutableListKey) o;
            return Objects.equals(list, that.list);
        }
    
        @Override
        public int hashCode() {
            return list.hashCode();
        }
    }
    
    // 使用自定义键类
    List<String> original = Arrays.asList("a", "b");
    ImmutableListKey key = new ImmutableListKey(original);
    Map<ImmutableListKey, String> map = new HashMap<>();
    map.put(key, "value");

总结

ListhashCode() 方法通过遍历列表中的每个元素,并结合元素的哈希码和乘数 31 来计算最终的哈希值。这种计算方式确保了哈希码的分布均匀性,但由于 List 的可变性,使用时需谨慎,避免在作为键后修改列表内容。推荐的做法是使用不可变的列表或自定义不可变键类,以确保哈希码的一致性和 HashMap 的正确性。

相关推荐
canonical_entropy7 分钟前
最小变更成本 vs 最小信息表达:第一性原理的比较
后端
渣哥7 分钟前
代理选错,性能和功能全翻车!Spring AOP 的默认技术别再搞混
javascript·后端·面试
间彧23 分钟前
Java泛型详解与项目实战
后端
间彧33 分钟前
PECS原则在Java集合框架中的具体实现有哪些?举例说明
后端
间彧35 分钟前
Java 泛型擦除详解和项目实战
后端
间彧39 分钟前
在自定义泛型类时,如何正确应用PECS原则来设计API?
后端
间彧40 分钟前
能否详细解释PECS原则及其在项目中的实际应用场景?
后端
武子康1 小时前
大数据-132 Flink SQL 实战入门 | 3 分钟跑通 Table API + SQL 含 toChangelogStream 新写法
大数据·后端·flink
李辰洋1 小时前
go tools安装
开发语言·后端·golang
wanfeng_091 小时前
go lang
开发语言·后端·golang