HashMap 是 Java 中最常用的数据结构之一,它提供了一种快速的键值对映射关系。下面是 HashMap 的源码解析。
基本结构
HashMap 的底层是一个数组,每个元素叫做 Node。每个 Node 包含一个键值对,以及指向下一个 Node 的指针。
java
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString(){ return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
HashMap 的成员变量包括:
table:Node数组,用于存储键值对。size:HashMap的大小,即键值对的数量。threshold:table的容量上限,当size超过threshold时,table需要扩容。loadFactor:负载因子,默认值为0.75,当size超过threshold * loadFactor时,table需要扩容。
java
transient Node<K,V>[] table;
transient int size;
int threshold;
final float loadFactor;
构造方法
HashMap 有多个构造方法,其中最常用的是带初始容量和负载因子的构造方法。
java
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
在构造方法中,threshold 被初始化为 initialCapacity 的下一个 2 的幂次方。这是因为在 HashMap 中,数组的长度必须是 2 的幂次方,这样可以通过位运算来加速计算。
java
static final int MAXIMUM_CAPACITY = 1 << 30;
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n
python
>>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
scss
`tableSizeFor` 方法使用了位运算来将输入的 `cap` 变成大于等于它的最小的 2 的幂次方,具体来说:
1. 首先将 `cap` 减去 1,得到 `n`。
2. 然后将 `n` 的右移 1 位和原来的 `n` 按位或,这样 `n` 的前两位都变成了 1。
3. 然后将 `n` 的右移 2 位和上一步的结果按位或,这样 `n` 的前四位都变成了 1。
4. 以此类推,最后得到的 `n` 的二进制中,从右往左数第一个 0 变成了 1,而其他位都变成了 1。
例如,如果输入的 `cap` 是 13,经过上述步骤后,`n` 变成了 15,即二进制中的 `1111`。
## put 方法
`put` 方法是向 `HashMap` 中添加键值对的方法,它的实现比较复杂。下面是 `put` 方法的主要流程:
```java
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e;
K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
put 方法首先调用 hash 方法计算键的哈希值,然后调用 putVal 方法将键值对插入到 HashMap 中。
putVal 方法先判断当前的 table 是否为空,如果为空,则调用 resize 方法创建一个新的表。
java
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
然后通过 (n - 1) & hash 计算出键值对应该插入的数组索引,如果该位置上没有节点,就直接插入。
java
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
如果该位置上已经有节点了,则需要判断这个节点是链表节点还是红黑树节点。如果是链表节点,则需要遍历链表,查找是否已经存在该键的节点,如果存在,则更新其值,否则在链表的末尾插入一个新的节点。
java
else {
Node<K,V> e;
K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
如果该位置上已经是红黑树节点,则调用红黑树节点的 putTreeVal 方法进行插入操作。
如果在链表或红黑树中找到了相同键的节点,则更新该节点的值,并返回旧的值。如果没有找到相同键的节点,则将新节点插入到链表末尾,并根据链表长度是否超过了阈值 TREEIFY_THRESHOLD(默认值为 8)来决定是否需要将链表转化为红黑树。
java
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
最后,如果插入新节点后 size 的值大于了阈值 threshold,就调用 resize 方法进行扩容。
java
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
resize 方法
resize 方法是对 HashMap 的扩容操作,主要是将原来的数组扩容为原来的两倍大小,并重新将所有的键值对分配到新的数组中。
java
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
resize 方法首先获取原数组的长度 oldCap,并计算出新数组的长度 newCap,新数组的长度是原数组的两倍,但不超过 MAXIMUM_CAPACITY。
如果原数组的长度为 0,说明这是第一次调用 resize 方法进行扩容,需要根据阈值 threshold 的值来确定新数组的长度 newCap。
然后根据新数组的长度 newCap 计算出新的阈值 newThr,新的阈值是原阈值的两倍,除非新数组的长度超过了 MAXIMUM_CAPACITY,此时阈值设置为 Integer.MAX_VALUE。
接下来,判断 newThr 是否为 0。如果为 0,则根据 newCap 和负载因子 loadFactor 计算新的阈值。
最后,创建新数组 newTab,并将原数组中的所有元素重新分配到新数组中。如果原数组中的元素形成链表,会根据元素的哈希值和新数组的长度来决定该元素在新数组中的位置,如果元素是红黑树节点,则需要调用 split 方法来重新平衡树。如果元素不是链表也不是树,则需要保留元素在原数组中的顺序。
值得注意的是,如果元素在原数组中的位置不发生变化,则无需重新计算该元素的哈希值,也无需重新计算新数组中该元素的位置。而如果元素需要移动到新数组中的其他位置,则需要重新计算该元素的哈希值和在新数组中的位置。
总的来说,resize 方法是一个非常重要的方法,它保证了 HashMap 在插入和删除元素时具有较高的效率,并且能够自动扩容和收缩数组,从而保证了 HashMap 的空间利用率。
put 方法
put 方法是向 HashMap 中插入键值对的方法。其源码如下:
java
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
put 方法首先会调用 hash 方法计算键的哈希值,然后调用 putVal 方法将键值对插入 HashMap 中。
putVal 方法的实现比较复杂,需要根据键的哈希值和在数组中的位置来确定插入的位置。具体来说,putVal 方法首先会检查数组是否为空或长度为 0,如果是,则会调用 resize 方法进行初始化或扩容。接着,根据键的哈希值和数组长度,计算出键在数组中的索引。如果该索引处没有元素,则直接插入新节点。如果该索引处已经存在元素,则需要按照链表或红黑树的方式将新节点插入到原来的节点之后。在插入节点之前,需要检查键是否已经存在于 HashMap 中,如果存在,则会将旧值替换为新值。
如果链表的长度超过了 TREEIFY_THRESHOLD,则会将链表转换成红黑树,以提高插入、查找和删除的效率。
插入完成后,需要根据 size 和 threshold 的比较来决定是否需要扩容。同时还需要调用 `afterNodeInsertion` 方法,以执行相关的回调操作。
由于 put 方法和 putVal 方法的实现比较复杂,我们可以通过一个简单的示例来说明它们的具体行为。
假设我们要向一个 HashMap 中插入以下 6 个键值对:
java
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);
map.put("orange", 4);
map.put("kiwi", 5);
map.put("melon", 6);
当我们调用 put 方法时,会依次调用 putVal 方法将键值对插入到 HashMap 中。在这个过程中,HashMap 会根据键的哈希值和数组中的位置,决定将新节点插入到哪个位置。具体来说,当我们第一次调用 put 方法时,HashMap 的内部结构如下:
在这个 HashMap 中,数组的大小为 16,其中第 2 个和第 6 个位置上已经有了元素。当我们调用 put("apple", 1) 方法时,首先会计算出 "apple" 的哈希值,然后根据这个哈希值和数组的长度,计算出它在数组中的位置为 1。由于数组的第 1 个位置是空的,因此会将键值对插入到这个位置上,得到如下的结果:
接下来,当我们调用 put("banana", 2) 方法时,会根据 "banana" 的哈希值和数组的长度,计算出它在数组中的位置为 5。由于数组的第 5 个位置上已经有了元素,因此需要将新节点插入到链表的尾部。具体来说,会从第 5 个位置开始,依次遍历链表中的每个节点,直到找到一个空的位置或者键与要插入的键相同的节点。在这个示例中,我们需要遍历第 5 个位置上的链表,直到找到一个空的位置,然后将新节点插入到这个位置上。最终,HashMap 的内部结构如下:
当我们继续调用 put 方法时,会依次按照类似的方式将其他键值对插入到 HashMap 中。在这个过程中,由于键值对的数量比较少,因此 HashMap 中的元素都会以链表的形式存储。如果键值对的数量比较多,链表的长度可能会超过 TREEIFY_THRESHOLD,从而导致链表被转换成红黑树,这个过程称为树化。树化的过程会调用 treeifyBin 方法,它会将链表转换为红黑树,并调整 HashMap 中的各个节点的关系,具体实现方式比较复杂,这里不再展开。
总体来说,HashMap 的 put 操作的时间复杂度是 O(1),但是在最坏情况下可能会达到 O(n),这种情况发生的概率比较小,通常情况下不会影响 HashMap 的性能。除了 put 操作之外,HashMap 还支持 get、remove 等操作,它们的实现方式类似于 put 操作,不再赘述。
此外,需要注意的是,HashMap 并不是线程安全的,如果在多线程环境下使用,需要进行额外的同步处理,否则可能会出现意想不到的问题。可以通过使用 ConcurrentHashMap 来实现线程安全的 HashMap。ConcurrentHashMap 的实现方式类似于 HashMap,但是支持多线程并发访问,可以实现高并发的键值对读写操作,具体实现方式也比较复杂,这里不再展开。
除了基本的 put、get、remove 操作,HashMap 还支持其他一些常用的操作,如遍历、清空等。
遍历操作可以使用 keySet、values、entrySet 等方法获取对应的视图,然后使用迭代器遍历。这些视图是动态的,即当 HashMap 中的元素发生变化时,它们会相应地发生变化。
清空操作可以使用 clear 方法,它会将 HashMap 中的所有元素删除,同时将 size 设置为 0。
除此之外,HashMap 还支持一些其他的方法,如 containsKey、containsValue、isEmpty、size 等,它们的使用方式类似于其他集合类。
需要注意的是,HashMap 的所有操作都是基于键的哈希值进行的,因此在使用自定义对象作为键时,需要重写 hashCode 和 equals 方法,以确保它们的哈希值和相等性能够正确地计算。否则可能会导致 HashMap 无法正常工作。
另外,由于哈希冲突的存在,HashMap 内部会维护一个负载因子(load factor),它表示 HashMap 中键值对的数量和桶的数量的比值。当负载因子达到一定阈值时,HashMap 会自动进行扩容操作,以保证桶的使用率不会过高,从而避免哈希冲突的频繁发生,影响 HashMap 的性能。默认情况下,HashMap 的负载因子为 0.75,即当键值对数量达到容量的 75% 时,就会触发扩容操作。
HashMap 的内部实现是基于数组和链表(或红黑树)的,因此它的空间复杂度是 O(n),其中 n 是键值对的数量。由于 HashMap 支持动态扩容,因此它的空间复杂度可以随着元素的增多而增加,但是扩容操作是非常耗时的,因此需要谨慎使用。
除了空间复杂度之外,HashMap 还有一些其他的缺点。首先,由于哈希函数的存在,键的顺序是不确定的,因此 HashMap 不能保证元素的顺序,如果需要保证元素的顺序,可以使用 LinkedHashMap。其次,由于哈希冲突的存在,HashMap 中的元素分布不均,有些桶可能会比其他桶更拥挤,从而影响 HashMap 的性能。为了解决这个问题,可以手动设置初始容量和负载因子,或者使用 ConcurrentHashMap。
总之,HashMap 是一个非常常用的集合类,它提供了高效的键值对存储和查找功能,同时也支持其他常用的操作。在使用 HashMap 时,需要注意它的内部实现方式和相关的性能问题,同时也需要重写 hashCode 和 equals 方法,以确保键的哈希值和相等性能够正确地计算。
除了基本的操作和一些常用的方法之外,HashMap 还有一些特殊的用法和注意事项。
- 多线程操作
HashMap 不是线程安全的,如果多个线程同时对同一个 HashMap 进行操作,可能会导致数据不一致。为了解决这个问题,可以使用 ConcurrentHashMap
- 自定义键类型
HashMap 的键类型不仅可以是基本数据类型和常用类,还可以是自定义的类。但是,如果使用自定义类作为键,需要注意以下几点:
- 自定义类必须重写
hashCode和equals方法,以确保HashMap能够正确地计算键的哈希值和判断键的相等性。 hashCode方法需要满足以下条件:- 多次调用
hashCode方法返回的值必须相等,前提是对象中所用的字段信息没有被修改。 - 如果两个对象通过
equals方法比较返回true,那么它们的hashCode值必须相等。
- 多次调用
equals方法需要满足以下条件:- 自反性:对于任意非空引用值
x,x.equals(x)必须返回true。 - 对称性:对于任意非空引用值
x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。 - 传递性:对于任意非空引用值
x、y和z,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)必须返回true。 - 一致性:对于任意非空引用值
x和y,只要equals方法中所用的比较信息没有被修改,多次调用x.equals(y)就必须始终返回相同的结果。 - 非空性:对于任意非空引用值
x,x.equals(null)必须返回false。
- 自反性:对于任意非空引用值
如果上述条件没有满足,可能会导致 HashMap 在插入和查询时无法正常工作。
- 对象作为值
HashMap 的值类型可以是任意对象,包括基本数据类型的包装类、常用类和自定义类。如果需要将多个值放入同一个键中,可以使用集合类作为值,如 List、Set 等。
- 与其他集合类的转换
HashMap 可以通过构造方法和 putAll 方法与其他集合类进行转换,例如将一个 List 转换为 HashMap:
java
List<String> list = new ArrayList<>();
list.add("one");
list.add("two");
list.add("three");
Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < list.size(); i++) {
map.put(i, list.get(i));
}
也可以将一个 HashMap 转换为 List:
java
Map<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
List<String> list = new ArrayList<>(map.values());
- 性能优化
为了提高 HashMap 的性能,可以考虑以下几点:
- 初始化容量和负载因子:根据实际需要,合理地设置初始容量和负载因子,可以减少扩容的次数和重新哈希的次数,从而提高性能。
- 使用尽量少的哈希冲突:通过优化键的哈希函数和减少哈希冲突的发生,可以减少链表长度,从而提高性能。
- 避免频繁的扩容和重新哈希:当
HashMap中的元素数量达到容量的 75% 时,会触发扩容操作,导致重新计算所有元素的哈希值和重新分配存储位置。因此,可以根据实际需要,预估存储元素的数量,从而避免频繁的扩容和重新哈希。 - 使用迭代器遍历元素:使用
HashMap自带的迭代器遍历元素可以提高性能,因为迭代器不需要在每次访问元素时都进行哈希计算和重新寻找存储位置的操作。 - 避免频繁的调用
hashcode()和equals()方法:这两个方法都需要进行复杂的计算,频繁调用会影响性能,因此可以尽量避免多次调用。
总之,HashMap 是 Java 中一个非常重要的集合类,掌握它的基本用法和原理,可以帮助开发者更加高效地编写 Java 代码。
除了以上的优化建议,还有一些其他的注意事项:
- 确保 key 的唯一性:由于
HashMap通过 key 来定位元素,因此如果两个 key 的哈希值相同,但是equals()方法返回不同的结果,那么它们就会被认为是不同的键。因此,如果要将一个对象用作HashMap的键,必须确保它的hashcode()和equals()方法实现正确。 - 确保 key 的不可变性:由于
HashMap在添加元素时需要计算 key 的哈希值,因此如果 key 的哈希值在添加后发生了变化,那么元素就会被无法找到。因此,如果要将一个对象用作HashMap的键,必须确保它的哈希值不会发生变化,通常需要将 key 声明为不可变类型。 - 多线程安全问题:
HashMap不是线程安全的,如果多个线程同时访问同一个HashMap实例,并且其中至少有一个线程在修改其中的元素,那么就可能会出现线程安全问题,例如死锁、数据不一致等。因此,在多线程环境下使用HashMap时,需要采取适当的线程安全措施,例如使用同步块、使用ConcurrentHashMap等。 HashMap的遍历顺序不保证:由于HashMap中的元素是按照哈希值分散存储的,因此它们的遍历顺序是不确定的。因此,在需要有序访问元素的场合,应该使用TreeMap或者LinkedHashMap等有序集合类。
综上所述,使用 HashMap 时需要考虑多方面的问题,只有了解了其内部实现和性能特点,才能更好地使用它,并在实际应用中取得良好的性能表现。
当然,除了使用 HashMap 本身提供的方法之外,我们还可以使用一些第三方库或工具来进一步优化 HashMap 的性能,例如:
- Google Guava:Guava 提供了一个
MapMaker类,可以用于创建特定的ConcurrentMap,其中包括HashMap的改进版本。它支持自定义缓存失效策略、回收策略等,从而提高HashMap的性能。 - Apache Commons Collections:Commons Collections 提供了多种基于
HashMap的数据结构,例如HashedMap、LinkedMap、ReferenceMap等,每种数据结构都有不同的特点和用途,可以根据实际需要选择。 - Eclipse Collections:Eclipse Collections 提供了多种基于
HashMap的数据结构,例如UnifiedMap、IntObjectHashMap、MutableMap等,它们可以提供比HashMap更好的性能和更多的功能,例如支持并行操作、支持基本类型键值等。
当然,使用这些第三方库需要权衡它们的性能、稳定性和可用性等方面的优缺点,根据实际需要选择。总之,对于大规模的数据处理和高性能的应用场景,使用 HashMap 还需要结合其他技术手段来进一步优化。
好的,下面再补充一些 HashMap 使用的注意事项:
-
初始容量:在创建
HashMap对象时,应该尽量估算预期存储的键值对数量,并将其作为初始容量来设置,这样可以减少HashMap的动态扩容次数,从而提高性能。通常来说,初始容量的大小应该是预期存储的键值对数量的 1.5 倍左右。 -
负载因子:负载因子是
HashMap内部用来判断是否需要扩容的一个参数,它表示当前HashMap中键值对数量与数组长度之间的比值。当负载因子超过一定阈值时,HashMap会自动进行扩容操作。通常来说,负载因子的大小应该在 0.7 左右,这可以使HashMap在空间和时间上取得平衡。 -
扩容操作:当
HashMap中的键值对数量超过阈值时,会自动进行扩容操作,这涉及到数组的重新分配和数据的复制等操作,因此会对性能产生一定的影响。为了避免频繁的扩容操作,可以在创建HashMap时设置一个合适的初始容量,并合理调整负载因子。 -
计算哈希值:
HashMap在添加元素时需要计算键的哈希值,这是一个耗时的操作,因为它需要对键的各个属性进行运算,并且还要保证哈希值的均匀分布。为了提高HashMap的性能,可以考虑对键的哈希值进行缓存或预处理,或者通过使用哈希函数库来提高哈希值的计算速度和均匀性。 -
并发访问:
HashMap是非线程安全的,如果多个线程同时访问同一个HashMap实例,可能会出现线程安全问题,例如死锁、数据不一致等。为了避免这些问题,可以采用以下一些方法:- 使用同步块或锁来控制访问,从而保证同一时间只有一个线程能够修改
HashMap。 - 使用
ConcurrentHashMap等线程安全的哈希表实现,它们可以提供更好的并发性能和更安全的访问方式。 - 将
HashMap对象进行复制,并在每个线程中使用独立的副本,这样可以避免并发访问的问题,但也会增加内存开销。
- 使用同步块或锁来控制访问,从而保证同一时间只有一个线程能够修改
总之,使用 HashMap 需要注意一些细节和性能问题,只有在正确地使用和配置 HashMap 的参数和属性之后,才能发挥它的最佳性能。同时,我们还应该注意键的类型:HashMap 中的键可以是任意类型,但是需要满足以下两个条件:
- 实现了 `equals` 和 `hashCode` 方法,以便在进行键值对比较和哈希值计算时使用。
- 不可变或不会发生改变,否则会导致哈希值的变化和键值对的丢失。
因此,在使用自定义对象作为键时,需要确保实现了 `equals` 和 `hashCode` 方法,并且尽可能将对象设计成不可变的形式,以避免在 `HashMap` 中发生意外的错误。
-
值的类型:
HashMap中的值可以是任意类型,包括 null 值。在使用值时,需要注意它的类型和数据结构,以免出现类型转换异常或运行时错误。 -
遍历操作:
HashMap提供了多种遍历方式,包括迭代器、forEach 和 Stream 等,可以根据实际需要选择合适的方式进行遍历操作。在遍历时,需要注意HashMap的非线程安全性和快速失败机制,以避免在遍历过程中出现并发访问或数据修改的问题。 -
equals 和 hashCode 方法:为了确保
HashMap的正确性和一致性,键对象必须实现equals和hashCode方法。其中,equals方法用于比较两个对象是否相等,通常需要比较对象的各个属性是否相等;hashCode方法用于计算对象的哈希值,通常需要将对象的各个属性进行哈希运算,并结合一个散列函数来计算哈希值。在实现这两个方法时,需要遵循一些规则,例如:- 如果两个对象相等,那么它们的哈希值必须相等。
- 如果两个对象的哈希值相等,那么它们不一定相等,因为可能存在哈希冲突。
- equals 方法必须满足自反性、对称性、传递性、一致性和非空性等条件,否则会导致程序出现不可预测的错误。
以上是使用 HashMap 时需要注意的一些细节和问题,我们可以通过仔细阅读官方文档和源代码,以及参考一些经典的书籍和博客来深入了解 HashMap 的实现原理和最佳实践。