目录
基本组成结构
HashMap 是 Map 的一个实现类,它代表的是一种键值对的数据存储形式。Key 不允许重复出现,Value 随意。jdk 8 之前,其内部是由数组+单向链表来实现的,而 jdk 8 对于链表长度超过 8 的链表将转储为红黑树。
大致的数据存储形式如下:
类的继承关系
/**
* HashMap继承了抽象父类AbstractMap
* 实现了Map(定义了一组通用的操作)
* 实现了Cloneable(可进行浅拷贝)
* 实现了Serializable(可序列化)
*/
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
基本成员属性
/**
* 底层存储容器
* Node类型的数组,每个Node元素都是一个链表的头结点,通过它可以访问连接在其后面的所有结点
*/
transient Node<K,V>[] table;
/**
* table的默认容量:16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* table的最大容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认负载因子,用于扩容
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 链表转红黑树的阈值
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 红黑树转链表的阈值
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 最小转红黑树的容量
* (就是说,即使链表长度达到转红黑树的阈值,但总容量没有达到该值,也不会转红黑树)
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 键值总数
*/
transient int size;
/**
* 用于迭代过程中防止结构性破坏的标量
*/
transient int modCount;
/**
* 下一次扩容的阈值 threshold = capacity * load factor
*/
int threshold;
/**
* 负载因子
*/
final float loadFactor;
构造函数
/**
* 自定义初始化容量与负载因子
*/
public HashMap(int initialCapacity, float loadFactor) {
// 初始化容量不能小于0
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 初始化容量大于最大默认容量则取最大默认容量为初始化容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 负载因子不能小于等于0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
// tableSizeFor返回大于等于initialCapacity的最小2的指数幂
this.threshold = tableSizeFor(initialCapacity);
}
/**
* 自定义初始化容量
*/
public HashMap(int initialCapacity) {
// 负载因子取默认值0.75f
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 默认方式
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* 将m中的所有元素添加至HashMap中
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
核心方法
put
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } /** * 计算key的hash地址 * 为了使hash值尽可能分散,将key的hash值的高32位与低32位进行异或运算,得到新的hash值 */ static final int hash(Object key) { int h; // 如果key为null,默认在table[0]的位置,由此看出key是允许为null的 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 1. 如果table还未被初始化,那么初始化它 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 2. 如果为null,说明此索引位置并没有被占用,(n - 1) & hash:得到数组下标 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { // 3. 不为null,说明此处已经被占用,只需要将构建一个结点插入到这个链表的尾部即可 Node<K,V> e; K k; // 3.1 如果当前结点key的hash值和将要插入的结点key的hash值相同, // 且是引用同一个对象或equals方法为真,说明这是一次修改操作,覆盖value值 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 3.2 如果p这个头结点是红黑树结点的话,在红黑树中查找 // 存在则更新value,返回原oldValue,不存在则作为新结点插入,返回null else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {// 3.3 如果不是前两种情况,遍历此链表,将构建一个结点插入到该链表的尾部 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 如果插入后链表长度大于等于 8 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // 在遍历过程中,若发现与某个结点的key值相等,这依然是一次修改操作 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 如果e不是null,说明当前的put操作是一次修改操作,且e指向的就是需要被修改的结点 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // 4. 如果添加后,数组容量达到阈值,进行扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } /** * Replaces all linked nodes in bin at index for given hash unless * table is too small, in which case resizes instead. * table太小,改成扩容操作 */ final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // table太小,不到64,改为扩容操作 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } }
put(K key, V value)方法总结:
1. 先判断table是否初始化,如果没有,则进行初始化。可见table真正初始化是在第一次put的时候。
2. 通过key的hash值计算出新增结点在数组中的位置。如果该位置为null,直接将其存入该位置。
前面通过hash()方法将key的hash值的高32位与低32位进行异或运算,得到新的hash值。这么做的目的是为了使hash值尽可能分散。
然后通过(n – 1) & hash结果得到数组下标,n为table的当前容量,下标范围应该是0~(n-1)范围内的整数。
以n=16为例,(n – 1) & hash可看作:1111 & 01001101010011010100110101001101
由此可以看出,为了让结果覆盖最多的可能,n-1的二进制值应该都为1,因此n的取值应该是2的指数幂,这也是table容量的取值为什么一定是2的指数倍的原因。
3. 如果该位置不为null,说明此处已经被占用,此时需要判断是覆盖还是插入。
3.1 如果当前结点key的hash值和将要插入的结点key的hash值相同,且是引用同一个对象或equals方法为真,说明这是一次修改操作,覆盖value值。
3.2 如果该位置的头结点是红黑树结点的话,在红黑树中查找,存在则更新value,返回原oldValue,不存在则作为新结点插入,返回null。
3.3 如果不是前两种情况,遍历此链表。在遍历过程中,若发现新结点与某个结点的key值相等,覆盖该结点,否则将新结点插入到该链表的尾部。如果插入后链表长度大于等于 8 ,执行treeifyBin(tab, hash)方法。
3.4 在treeifyBin(tab, hash)方法中需要注意,并不是直接将链表裂变成红黑树,而是先判断table是不是小于64,如果是则会进行扩容操作,只有table足够大了(>=64),才会转红黑树。
4. 添加完新结点后,如果数组容量达到阈值,进行扩容操作。
resize
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 说明数组已经初始化完成,此处需要给旧数组扩容 if (oldCap > 0) { // 如果容量达到极限将不再扩容,直接返回旧table 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 } /* 数组未初始化,阈值大于0时 说明使用了构造函数 HashMap(int initialCapacity, float loadFactor)初始化 根据传入的容量initialCapacity计算出一个合适的容量暂存在阈值中 */ else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; // 数组未初始化并且阈值也为0,一切都以默认值进行构造 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; // 旧数组为null则初始化,不为null则进行扩容 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; // 判断e在扩容后的索引是否变化 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; } } } } } // 无论是扩容还是初始化,都返回 newTab return newTab; }
resize()方法总结:
1. 判断是初始化调用还是扩容调用,计算新的数组容量与下一次扩容的阈值,同时创建出新数组。
2. 如果是初始化调用,直接返回新数组;如果是旧数组扩容,还需要将旧数组中的各个结点复制到新数组中,其中包括单个结点,链表结点与红黑树结点的复制。
旧结点在新数组中地址分配过程分析:
单个结点:根据 e.hash & (newCap – 1) 计算出在新数组中的位置。
链表结点:由于扩容后,容量会左移一位,因此可以根据 e.hash & oldCap 来判断扩容后链表元素的 hash 值参与计算的部分是否有变化,无变化的部分在原位置 newTab[index],有变化的部分在扩容后的新半区 newTab[index + oldCap]。
e.hash & oldCap 是如何以判断 hash 值是否变化的?
以oldCap = 16 为例:
index = e.hash & (oldCap – 1) = 010010……1001 & 1111 = 1001
无变化情况: e.hash & oldCap = 010010……01001 & 10000 = 0
有变化情况: e.hash & oldCap = 010010……11001 & 10000 = 1
即通过判断hash值新加入比较的一位是0还是1。
红黑树结点:与链表方式类似,同样将树分裂出一部分到newTab[index + oldCap],如果分裂后的树结点过少,则以链表形式重组。
get
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; // 当table不为null时,且key的hash值所在坐标上不为null时 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // 判断头结点是否匹配 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { // 如果是红黑树,则遍历红黑树查找匹配结点 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; }
get(Object key)方法总结:
1. 当数组不为空时,计算传入参数key的hash值,定位其在数组中的位置。
2. 如果该位置不为null,先通过 == 或 equals方法比较key与该位置头结点的key是否匹配,匹配则返回结点的value值。
3. 如果是链表或红黑树,则通过遍历的方式查找匹配的结点,找到后返回结点的value值。
4. 如果以上情况都没有匹配的结点,直接返回null。
remove
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } 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; // 当table不为null时,且key的hash值所在坐标上不为null时 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; // 查找要删除的结点,找到后用node指向该结点 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { 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为要删除的结点,再判断是否要匹配value if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { // 如果node是红黑树结点 if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); // 如果node是头结点,直接将node.next设为头结点 else if (node == p) tab[index] = node.next; // 如果node是中间结点,node的前结点p的next结点指向node的next结点 else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }
remove(Object key)方法总结:
1. remove方法就是查找 + 删除的过程,首先要根据key值找到要删除的结点,过程与get方法一致。
2. 找到要删除的node结点后,如果是红黑树结点,直接调用红黑树的删除方法;如果是一个头结点,那么用node.next 结点代替它作为头节点存放在 table[index] 中;如果是链表的中间结点,使node的前一结点的next直接指向node.next 结点即可。
3. 成功删除node后,remove方法会返回node.value值;如果未找到要删除的结点,直接返回null。
entrySet
public Set<Map.Entry<K,V>> entrySet() { Set<Map.Entry<K,V>> es; return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; } /** * HashMap的内部类 * 实现了Iterable接口,因此可用来迭代 */ 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); } // foreach遍历EntrySet的时候时间上是会遍历table[] 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); } // 通过modCount禁止多线程并发写操作 if (modCount != mc) throw new ConcurrentModificationException(); } } }
entrySet()方法总结:
entrySet返回了EntrySet对象,EntrySet是HashMap的内部类,实现了Iterable接口,它可以直接操作HashMap底层的数组table,因此可以对HashMap的键值对集合进行迭代。
keySet、values与entrySet作用相似,keySet返回了HashMap的key的集合,values返回value的集合。
另外,由于HashMap是非线程安全的,因此在所有迭代操作过程中,都有modCount变量来限制并发下的结构性破坏操作。
HashMap常见面试题
1. HashMap的原理,内部数据结构?
底层使用哈希表(数组 + 链表),当链表过长会将链表转成红黑树以实现O(logn)时间复杂度内查找。
2. 讲一下HashMap中put方法的过程。
i. 对Key求Hash值,然后再计算下标;
ii. 如果没有碰撞,直接放入桶中;
iii. 如果碰撞了,以链表的方式链接到后面;
iv. 如果链表长度超过阈值(TREEIFY_THRESHOLD == 8),就把链表转成红黑树;
v. 如果节点已经存在就替换旧值;
vi. 如果桶满了(容量 * 负载因子),就需要resize。
3. HashMap中hash函数是怎么实现的?还有哪些hash的实现方式?
i. 高16bit不变,低16bit和高16bit做一个异或运算;
ii. (n – 1)& hash 得到下标;
4. HashMap怎样解决冲突,讲一下扩容过程,加入一个值在原数组中,现在移动了新数组,位置肯定改变了,那是什么定位到在这个值新数组中的位置?
i. 将新节点追加到链表上;
ii. 容量扩充为原来的二倍,然后对每个节点重新计算哈希值;
iii. 这个值只可能在两个地方,一个是在原下标的位置,另一种情况时在下标为<原下标 + 原容量>的位置。
5. 抛开HashMap,hash冲突有哪些解决办法?
i. 链地址法(HashMap使用了该方式);
ii. 再哈希法(产生冲突时计算另一个hash函数地址,直到没有冲突为止);
iii. 开放定址法。
6. 针对HashMap中某个Entry链太长,查找的时间复杂度可能达到O(n),如何优化?
链表转为红黑树,JDK 8已经实现。