每日计算机知识-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 的正确性。

相关推荐
AI智能科技用户7946329782 分钟前
okcc呼叫中心两个sip对接线路外呼任务怎么设置才能一个任务对应yigesip中继?
人工智能·后端
懒虫虫~14 分钟前
Spring源码中关于抽象方法且是个空实现这样设计的思考
java·后端·spring
雷渊22 分钟前
DDD的分层架构是怎么样的?
后端
会有猫30 分钟前
阿里云OSS挂载到Linux
后端
雷渊34 分钟前
聊一聊贫血模型和充血模型区别
后端
瀚海澜生40 分钟前
NSQ 深入剖析(一):架构原理与安装实战
后端
大鹏dapeng1 小时前
Gone 框架的服务注册与发现:打造高效微服务架构
后端·go·github
LemonDu1 小时前
线上救急-AWS限频
后端·算法·架构
AronTing1 小时前
装饰模式:动态扩展对象功能的优雅设计
java·后端·设计模式
雷渊1 小时前
DDD的落地流程
后端