在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
的正确性。