HashMap
是 Java 中最常用的映射数据结构,它存储键值对(key-value pairs),并允许使用任何非空对象作为键或值。HashMap
的底层原理主要依赖于数组和链表(或红黑树)来实现键值对的存储和检索。
以下是 HashMap
的主要底层原理:
-
数组:
HashMap
内部维护了一个数组,这个数组被称为“桶”(buckets)。每个桶用来存储一个或多个键值对。当创建HashMap
时,如果没有指定初始容量,它会使用一个默认的容量大小(通常是 16)。 -
哈希函数:当向
HashMap
插入一个键值对时,会使用哈希函数来计算键的哈希码(hash code),然后使用这个哈希码来确定键值对应该存储在数组的哪个桶中。 -
链表:如果两个不同的键产生了相同的哈希码或者不同的哈希码映射到了同一个桶,这时会发生“哈希冲突”。
HashMap
通过在数组中使用链表来解决哈希冲突。每个桶中可以存储多个键值对,它们通过链表连接起来。 -
红黑树:在 Java 8 之后,为了提高性能,当链表中的元素数量超过一定阈值(默认为 8)时,链表会被转换为红黑树。红黑树是一种自平衡的二叉搜索树,它可以更高效地处理大量的哈希冲突。
-
扩容:当
HashMap
中的元素数量达到容量和负载因子(load factor)的乘积时,HashMap
会进行扩容操作,即创建一个新的更大的数组,并将所有现有的键值对重新哈希到新数组中。这个过程通常称为“rehashing”。 -
迭代器:
HashMap
提供了迭代器(Iterator),用于遍历映射中的所有键值对。迭代器是 fail-fast 的,这意味着如果在迭代过程中映射结构被修改,迭代器会立即抛出ConcurrentModificationException
。 -
容量和大小:
HashMap
中有两个重要的概念:容量(capacity)和大小(size)。容量指的是数组的长度,而大小指的是映射中实际包含的键值对的数量。下面是一个更准确的简化的
HashMap
内部实现示例:public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, java.io.Serializable {private static final long serialVersionUID = 3624988207751029749L;// 默认容量大小private static final int DEFAULT_INITIAL_CAPACITY = 16;// 最大容量,必须是2的幂private static final int MAXIMUM_CAPACITY = 1 << 30;// 默认负载因子private static final float DEFAULT_LOAD_FACTOR = 0.75f;// 桶数组transient Node<K,V>[] table;// 元素数量transient int size;// 扩容阈值,容量*负载因子transient int threshold;// 负载因子final float loadFactor;// 节点类,用于存储键值对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;}// 实现Map.Entry的方法K getKey() {return key;}V getValue() {return value;}V setValue(V value) {V oldValue = this.value;this.value = value;return oldValue;}// 实现Node的方法// ...}// 构造函数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 = (int)(initialCapacity * loadFactor);this.table = new Node[initialCapacity];}// 默认构造函数public HashMap() {this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);}// 其他方法,如put(K, V),get(K),remove(K)等// ... }
在这个简化的示例中,
HashMap
使用了一个名为Node
的内部类来表示映射中的元素。每个Node
对象包含一个键、一个值、一个哈希码和一个指向下一个Node
的指针。在实际的字节码文件中,Node
类会包含更多的方法,例如toString()
和hashCode()
,这些方法用于支持HashMap
的操作。HashMap
的主要操作,如put(K, V)
、get(K)
和remove(K)
,都是通过计算键的哈希码来定位到相应的桶,然后在桶中的链表(或红黑树)中查找、插入或删除节点。扩容操作会在数组的元素数量达到阈值(
threshold
)时触发,此时会创建一个新的更大的数组,并将所有现有的元素重新哈希到新数组中。这个过程中,原有的链表可能会被转换为红黑树,以提高在大量元素情况下的性能。HashMap
的迭代器也是 fail-fast 的,这意味着在迭代过程中如果映射结构被修改,迭代器会立即抛出ConcurrentModificationException
。继续解释 `HashMap` 的底层原理,我们来看一下几个关键的方法:
1. `put(K, V)`:这是向 `HashMap` 添加一个键值对的方法。它首先计算键的哈希码,然后找到对应的桶。如果桶中没有元素,或者找到了具有相同哈希码的键(发生了哈希冲突),则将新的键值对插入到链表或红黑树中。如果替换了现有的键值对,则会返回被替换的值。
2. `get(K)`:这是获取 `HashMap` 中指定键的值的方法。它通过计算键的哈希码找到对应的桶,然后在链表或红黑树中查找具有相应键的节点。如果找到了键,则返回对应的值;如果没有找到,则返回 `null`。
3. `remove(K)`:这是从 `HashMap` 中删除指定键的方法。它通过计算键的哈希码找到对应的桶,然后在链表或红黑树中删除具有相应键的节点。如果删除了节点,则返回被删除节点的值;如果没有找到键,则返回 `null`。
4. `size()`:这是返回 `HashMap` 中元素数量的方法。它不需要遍历整个桶数组,因为每个桶中的节点数量可以通过数组的长度减去已使用的位置来快速计算。
5. `resize()`:这是扩容方法,它在数组的元素数量达到阈值时被调用。它创建一个新的更大的数组,然后将所有现有的元素重新哈希到新数组中。这个过程中,原有的链表可能会被转换为红黑树。
`HashMap` 的性能取决于哈希函数的质量、链表和红黑树的实现效率以及扩容操作的频率。在理想情况下,哈希冲突应该很少发生,这样可以保持高效的查找、插入和删除操作。然而,在实际应用中,由于键的哈希码可能相互碰撞,哈希冲突是难以避免的。`HashMap` 通过链表和红黑树来处理这些冲突,这样可以保持较高的平均时间复杂度。
需要注意的是,`HashMap` 并不是线程安全的。如果在多线程环境中使用 `HashMap`,并且有线程安全的需求,可以选择使用 `ConcurrentHashMap` 或其他线程安全的替代品。