在Java中,HashMap 是一种基于哈希表的数据结构,它要求键(Key)必须满足以下条件:
- 不可变性(Immutability) :键对象必须是不可变的,或者在作为键期间其状态不能发生变化。这是因为
HashMap依赖于键的哈希码(hashCode)和相等性(equals)来判断键的唯一性和存储位置。如果键对象在插入后发生了变化,可能会导致哈希码改变,从而无法正确找到或删除对应的值。 - 一致性(Consistency) :键对象的
hashCode()和equals()方法必须保持一致。也就是说,如果两个对象通过equals()方法比较为相等,那么它们的hashCode()必须相同;反之,如果hashCode()相同,equals()不一定返回true,但hashCode()不同则equals()必须返回false。
实际上,在其他语言里面,也是一样的。
为什么 List 不能作为 HashMap 的键?
List 是一个可变集合,它的内容可以随时被修改。如果将 List 作为 HashMap 的键,可能会导致以下问题:
- 哈希码不一致 :
List的hashCode()方法是基于其内容的。如果List的内容发生变化,其哈希码也会随之改变。这会导致HashMap无法正确找到或删除对应的值,因为HashMap使用的是插入时的哈希码来定位存储位置。 - 违反契约 :
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 接口的实现类(如 ArrayList、LinkedList 等)的 hashCode() 方法是通过遍历列表中的所有元素,并根据每个元素的哈希码计算得出的。具体来说,List 的 hashCode() 实现遵循以下规则:
List.hashCode() 的计算规则
List 的 hashCode() 方法通常基于以下公式计算:
ini
s = 1
for (E e : list) {
s = 31 * s + (e == null ? 0 : e.hashCode())
}
return s
详细解释:
-
初始值:
- 计算开始时,
s被初始化为1。
- 计算开始时,
-
遍历元素:
-
对于列表中的每一个元素
e,执行以下步骤:- 如果元素
e为null,则其哈希码视为0。 - 否则,使用元素
e的hashCode()方法获取其哈希码。 - 更新
s的值为31 * s + 当前元素的哈希码。
- 如果元素
-
-
返回结果:
- 遍历完所有元素后,
s即为列表的最终哈希码。
- 遍历完所有元素后,
为什么选择 31?
- 性能优化 :31 是一个奇素数,且
31 * i可以被优化为(i << 5) - i,这在某些 JVM 实现中可以提高计算效率。 - 减少冲突:选择一个适当的素数有助于减少不同对象产生相同哈希码的概率,从而提高哈希表的性能。
示例代码
以下是一个简化的 ArrayList 的 hashCode() 实现示例:
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;
}
非用不可的话?
-
使用不可变列表:
- 为了避免上述问题,建议在使用
List作为键时,先将其转换为不可变的列表。例如,可以使用Collections.unmodifiableList或者创建一个不可变的自定义列表类。
arduinoList<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 的哈希码 - 为了避免上述问题,建议在使用
-
自定义键类:
- 另一种方法是创建一个不可变的自定义键类,封装
List并确保其哈希码在对象生命周期内不变。
typescriptpublic 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"); - 另一种方法是创建一个不可变的自定义键类,封装
总结
List 的 hashCode() 方法通过遍历列表中的每个元素,并结合元素的哈希码和乘数 31 来计算最终的哈希值。这种计算方式确保了哈希码的分布均匀性,但由于 List 的可变性,使用时需谨慎,避免在作为键后修改列表内容。推荐的做法是使用不可变的列表或自定义不可变键类,以确保哈希码的一致性和 HashMap 的正确性。