先看 HashMap 类注释是怎么介绍 HashMap 的。
/**
* Hash table based implementation of the Map interface. This
* implementation provides all of the optional map operations, and permits
* null values and the null key. (The HashMap
* class is roughly equivalent to Hashtable, except that it is
* unsynchronized and permits nulls.) This class makes no guarantees as to
* the order of the map; in particular, it does not guarantee that the order
* will remain constant over time.
*
* This implementation provides constant-time performance for the basic
* operations (get and put), assuming the hash function
* disperses the elements properly among the buckets. Iteration over
* collection views requires time proportional to the "capacity" of the
* HashMap instance (the number of buckets) plus its size (the number
* of key-value mappings). Thus, it's very important not to set the initial
* capacity too high (or the load factor too low) if iteration performance is
* important.
*
*
An instance of HashMap has two parameters that affect its
* performance: initial capacity and load factor. The
* capacity is the number of buckets in the hash table, and the initial
* capacity is simply the capacity at the time the hash table is created. The
* load factor is a measure of how full the hash table is allowed to
* get before its capacity is automatically increased. When the number of
* entries in the hash table exceeds the product of the load factor and the
* current capacity, the hash table is rehashed (that is, internal data
* structures are rebuilt) so that the hash table has approximately twice the
* number of buckets.
*
*
As a general rule, the default load factor (.75) offers a good
* tradeoff between time and space costs. Higher values decrease the
* space overhead but increase the lookup cost (reflected in most of
* the operations of the HashMap class, including
* get and put). The expected number of entries in
* the map and its load factor should be taken into account when
* setting its initial capacity, so as to minimize the number of
* rehash operations. If the initial capacity is greater than the
* maximum number of entries divided by the load factor, no rehash
* operations will ever occur.
*
*
If many mappings are to be stored in a HashMap
* instance, creating it with a sufficiently large capacity will allow
* the mappings to be stored more efficiently than letting it perform
* automatic rehashing as needed to grow the table. Note that using
* many keys with the same {@code hashCode()} is a sure way to slow
* down performance of any hash table. To ameliorate impact, when keys
* are {@link Comparable}, this class may use comparison order among
* keys to help break ties.
*
*
Note that this implementation is not synchronized.
* If multiple threads access a hash map concurrently, and at least one of
* the threads modifies the map structurally, it must be
* synchronized externally. (A structural modification is any operation
* that adds or deletes one or more mappings; merely changing the value
* associated with a key that an instance already contains is not a
* structural modification.) This is typically accomplished by
* synchronizing on some object that naturally encapsulates the map.
*
* If no such object exists, the map should be "wrapped" using the
* {@link Collections#synchronizedMap Collections.synchronizedMap}
* method. This is best done at creation time, to prevent accidental
* unsynchronized access to the map:
* Map m = Collections.synchronizedMap(new HashMap(...));
*
* The iterators returned by all of this class's "collection view methods"
* are fail-fast: if the map is structurally modified at any time after
* the iterator is created, in any way except through the iterator's own
* remove method, the iterator will throw a
* {@link ConcurrentModificationException}. Thus, in the face of concurrent
* modification, the iterator fails quickly and cleanly, rather than risking
* arbitrary, non-deterministic behavior at an undetermined time in the
* future.
*
*
Note that the fail-fast behavior of an iterator cannot be guaranteed
* as it is, generally speaking, impossible to make any hard guarantees in the
* presence of unsynchronized concurrent modification. Fail-fast iterators
* throw ConcurrentModificationException on a best-effort basis.
* Therefore, it would be wrong to write a program that depended on this
* exception for its correctness: the fail-fast behavior of iterators
* should be used only to detect bugs.
*
*
This class is a member of the
*
* Java Collections Framework.
*
* @param the type of keys maintained by this map
* @param the type of mapped values
*
* @author Doug Lea
* @author Josh Bloch
* @author Arthur van Hoff
* @author Neal Gafter
* @see Object#hashCode()
* @see Collection
* @see Map
* @see TreeMap
* @see Hashtable
* @since 1.2
*/
译文:
Map 接口的基于哈希表的实现,它实现了 Map 接口所有的抽象方法,并允许存储 null 键和 null 值。HashMap 与 HashTable 非常相似,除了它是非线程安全的和允许存储 null。HashMap 不保证元素的顺序,因为底层是哈希表和重新散列,它甚至不能保证在一段时间内顺序是一致的。
HashMap 为基本操作(get 和 set)提供了恒定的时间性能。假设哈希函数将元素平均的分配到 bucket 之中,对集合视图的迭代与 HashMap 中 bucket 的数量和 entry 的数量成正比,所以如果迭代性能很重要,那么不要将初始容量设置的太高(或者负载因子设置的太低),这一点非常重要。
HashMap 中有两个影响其性能的参数:初始容量和负载因子。容量指的是哈希表中 bucket 的数量,初始容量就是哈希表被创建时的大小;负载因子是衡量哈希表在自动扩容之前运行达到的满量的指标,当哈希表中 entry 的数量超过负载因子和当前容量的乘积时,哈希表会自动扩容为之前容量的两倍,即以新的容量创建新的哈希表,此时需要将旧的 entry 重新哈希到新的哈希表中,这个过程叫做哈希表的重建。
作为一般规则,默认负载因子(0.75)在时间和空间成本上提供了良好的平衡,更高的负载因子可以减少空间开销,但增加了查找成本。在设置初始容量的时候,应该要考虑 HashMap 的预期条目数和负载因子的值,以最大限度减少哈希表重建的次数,如果最大条目数小于初始容量乘以负载因子,即初始容量大于最大条目数除以负载因子,则不会发生哈希表重建。
需要注意的是,如果多个键的 hashCode() 方法返回相同的值,会减低哈希表性能,因为它可能导致多个键散列到同一个桶中,从而形成长链表或者红黑树(JDK 8 之后链表长度超过一定的阈值会自动转换成红黑树),为了减轻这种影响,可以让键实现 Comparable 接口,HashMap 会使用键之间的比较顺序来辅助排序,以帮助解决键的散列冲突。
需要注意的是,HashMap 是非线程安全的,如果多个线程同时访问一个 HashMap 实例,至少有一个线程在修改它的结构(结构修改指的是添加和删除一个或多个 entry,改变一个已经存在的 key 的 value 并不算结构修改),则必须对它进行外部同步,这通常是通过对一些封装了 HashMap 的对象进行同步来实现的,如果不存在这样的对象,则应使用
Collections#synchronizedMap
方法来包装 HashMap,这最好在创建时完成,以防止意外的对 HashMap 进行非同步的访问Map m = Collections.synchronizedMap(new HashMap(...))
。HashMap 所有的 “collection view methods” 返回的迭代器都是 fail-fast 的,如果在创建 iterator 之后,HashMap 在任意时间被修改了结构(除了 iterator 自己的 remove 方法),iterator 将会抛出 ConcurrentModificationException。因此,面对并发修改,iterator 会快速而干净的失败,而不是冒着在未来不确定的时间出现任意的、不确定行为的风险。
值得注意的是,iterator 的 fail-fast 行为无法得到保证,因为只有在迭代的时候才会检查 modCount 的值是否改变,如果改变了会抛出 ConcurrentModificationException,所以当 modCount 改变(发生并发修改)的时候并不能保证马上检测到 modCount 的变化,抛出 ConcurrentModificationException。
注释已经介绍了 HashMap 具有的所有关键特性,我们在了解了这些特性之后,再来看看 HashMap 的源码是如何实现这些特性的。
首先看看 HashMap 中定义的成员变量:
/**
* 初始容量的默认值,必须是 2 的倍数
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 负载因子的默认值
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 容量的最大值
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 触发 bucket 树化(链表转红黑树)的 table 的最小容量
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 链表中元素的个数超过这个值,链表会自动转红黑树
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 红黑树中的元素个数低于这个值,红黑树会自动转链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
上面介绍的是静态成员变量,通常用来作为其他非静态成员变量的默认值,下面看一下非静态成员变量。
/**
* HashMap 修改结构的次数,迭代器的 remove 不算,它和迭代器的 fail-fast 有关
*/
transient int modCount;
/**
* entry 的个数
*/
transient int size;
/**
* 哈希表,存储 entry
*/
transient Node<K,V>[] table;
/**
* 下次动态扩容的大小,它的值是 capacity * load factor.
*/
int threshold;
/**
* 负载因子
*/
final float loadFactor;
/**
* entry 的集合视图
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* HashMap 中的值的集合视图,这个变量定义在 AbstractMap 中
*/
transient Collection<V> values;
/**
* HashMap 中的值的集合视图,这个变量定义在 AbstractMap 中
*/
transient Set<K> keySet;
我们在使用 HashMap 的时候,通常是先实例化,然后调用 put 方法存储数据,之后调用 get 方法得到数据,或者使用 iterator 遍历数据,如下:
@Test
public void test07(){
Map<String, String> map = new HashMap<>();
map.put("001", "bob");
map.put("002", "john");
map.put("003", "slice");
String value = map.get("001");
System.out.println(value);
}
HashMap 的无参数构造方法没有什么好分析的,它只有一行设置负载因子的代码,我们用的最多的是 put 方法,HashMap#put 的键和值都可以为 null,我们来看看 put 方法做了些什么。
public V put(K key, V value) {
// 将操作委托给了 putVal 方法
return putVal(hash(key), key, value, false, true);
}
// 这是一个 final 方法,防止用户重写该方法
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)
/*
* 如果 table 为 null 或者 table 的长度为 0 的时候,调用 resize() 方法扩容哈希表,如果是第一次调用 put 方法
* 会进入这个 if 条件中
*/
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
/*
* 计算 key 的哈希值 i,从 tab[i] 中获取指定哈希值对应的 Node 对象 p,如果 p 为 null,则说明没有发生哈希冲突
* 使用 key value 构建 Node 并填充到 tab[i]
*/
tab[i] = newNode(hash, key, value, null);
else {
// 如果 p 不为 null,则发生了哈希冲突,链表和红黑树有不同的解决哈希冲突的逻辑,需要分开处理
/*
* 如果发生了哈希冲突
*/
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// key 相同
e = p;
else if (p instanceof TreeNode)
// 红黑树解决哈希冲突
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 链表解决哈希冲突,需要从头节点遍历,直到找到有相同 key 的 节点或者到达了尾节点
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// p 是尾节点,此时 e = null,构建新的 Node 插入到 p 的后面
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 检查是否要将链表转红黑树
treeifyBin(tab, hash);
break;
}
// e.key 和 key 相等,找到相同的节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// e 不为 null,则 e 和新插入具有相同的 key,用新的 value 覆盖 e.value,并返回旧的 value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 给子类提供的回调函数
afterNodeAccess(e);
return oldValue;
}
}
// 因为 put 方法会修改 HashMap 的结构,所以 modCount 自增
++modCount;
/*
* 检查 HashMap 的容量 size 是否超过了阈值 threshold,如果超过了就扩容
* 从这里可以看出每次调用 put 方法都会检查是否需要扩容,且扩容是发生在数据插入 HashMap 之后的
* 这里是不是在插入数据之前检查并扩容会好一点?因为这样就可以减少一次插入数据的损耗了
* 因为如果扩容的就可以不将数据插入到旧哈希表,而是在扩容之后插入到新哈希表,但是整体逻辑可能就复杂了,JDK 的实现可读性更高
*/
if (++size > threshold)
resize();
// 给子类提供的回调函数
afterNodeInsertion(evict);
return null;
}
然后我们来看看 get 方法,get 方法根据 key 从 Map 中得到指定的 value,如果 key 不存在则返回 null,返回 null 并不一定就代表 key 不存在,也有可能这个 key 对应的 value 就是 null。
// 委托给 getNode 方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
// getNode 方法也是一个 final 方法,防止子类重写该方法
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 因为哈希冲突的缘故,所以可能需要对比多个值,首先比较第一个 node
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 如果第一个 node 的 key 不相等,则分从红黑树和链表两种数据结构获取下一个 node 继续比较,直到最后一个 node
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
接下来看看 remove 方法,remove 方法用来删除 HashMap 中存在的 entry。
// 类似的,委托给 removeNode 方法
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
// remove 方法的代码乍一看和 get 方法的代码很相似,因为你要想 remove 它,必须先找到 它
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 检查第一个 node 是否相等
node = p;
else if ((e = p.next) != null) {
// 在红黑树和链表中查找要删除的 node,如果找到了就赋值给局部变量 node
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 如果 node != null,说明找到了要删除的节点
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
// 红黑树结构的删除,这里面还涉及了
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
// 如果找到的就是第一个节点,则将 table 中的 bucket 指向 node 的下一个节点
tab[index] = node.next;
else
// 链表结构的删除
p.next = node.next;
// 修改了 HashMap 的结构,modeCount 自增
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
除了用户常用的 put、get 和 remove 方法之外,还有一个很重要的方法是 resize,它负责哈希表的重建。
// 同样是 final 方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// oldCap 表示旧容量大小,这个只有在初始化的时候为 0,其他情况都会大于 0
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
// 如果老容量大于等于最大容量 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)
// 如果当前 bucket 只有一个元素
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
/*
* 如果当前 bucket 使用红黑树解决哈希冲突
* 这里会检查红黑树的元素个数是否小于阈值 UNTREEIFY_THRESHOLD,如果小于会将红黑树转链表
*/
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 如果当前 bucket 使用链表解决哈希冲突
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;
}
这三个方法也是 HashMap 中比较常用的方法,它们分别返回 HashMap 中键、值和键值对(Entry)的集合,这些方法返回的集合是视图,它们是基于原始 HashMap 数据的快照,如果修改它们会影响到原始的 HashMap,反之亦然。这三个方法与 entrySet、values 和 keySet 三个成员变量相关。
这三个方法的实现逻辑类似,这里只分析 entrySet() 方法,它返回 HashMap 中键值对(Entry)的集合视图,我们一般用它来遍历 HashMap 中的键值对。
@Test
public void test08(){
Map<String, String> map = new HashMap<>();
map.put("001", "bob");
map.put("002", "john");
map.put("003", "slice");
Set<Map.Entry<String, String>> entries = map.entrySet();
Iterator<Map.Entry<String, String>> iterator = entries.iterator();
while(iterator.hasNext()){
Map.Entry<String, String> entry = iterator.next();
String key = entry.getKey();
String value = entry.getValue();
System.out.println("key: " + key + ",value: " + value);
}
}
我们接着看 entrySet() 方法在 HashMap 中源代码:
// 返回的是一个 EntrySet 实例
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
//EntrySet 实现了 AbstractSet,它支持 iterator、contains、remove 和 clear 方法,但是不支持 add 和 addAll 方法,因为它没有重写这两个方法
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Node<K,V> candidate = getNode(hash(key), key);
return candidate != null && candidate.equals(e);
}
public final boolean remove(Object o) {
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Object value = e.getValue();
return removeNode(hash(key), key, value, true, true) != null;
}
return false;
}
public final Spliterator<Map.Entry<K,V>> spliterator() {
return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
// EntrySet 的 iterator 方法返回一个 EntryIterator 实例,它集成了 HashIterator 类
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
/**
* 到这里才看到 EntrySet 操作的底层数据结构,它通过操作 HashMap 的哈希表 table,来提供 Entry 的 Set 视图,
* 这也是为什么 EntrySet 被称为视图的原因,因为底层并没有构建这样一个 Set,它是通过操作 table 来实现这些功能的
*/
abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry
int expectedModCount; // for fast-fail
int index; // current slot
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}