HashMap 构造器
HashMap 共有四个构造器:
public HashMap(int initialCapacity, float loadFactor) {
// 对于传入的初始容量(loadFactor) 及 负载因子(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);
}
// 调用了 HashMap(int initialCapacity, float loadFactor),其中 loadFactor 取默认值 DEFAULT_LOAD_FACTOR
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
// 容量及负载因子皆使用默认值
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; }
// 调用 putMapEntries 将值存入当前 hashmap
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
在上面构造器中存在一个方法【tableSizeFor】,这个方法的作用是:返回给定目标容量的二次方大小。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
换句话说,HashMap 的默认容量为16,而容量是以2的次方扩充的(即使是自定义传入,也一定会经过转换,如传入30,则返回32),一是为了提高性能使用足够大的数组,二是为了能使用位运算代替取模预算;
HashMap 的实现原理
数据存储方式示例图:
在 JDK1.8 中,HashMap采用【位桶(数组table)+链表+红黑树】实现,当链表长度超过阈值(8)时,将链表转换为红黑树(table长度大于等于64),这样大大减少了查找时间;链表长度大于8时转化为红黑树,小于6时转化为链表;
HashMap put 方法:
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
其中 hash 方法:
// 计算 hash code
static final int hash(Object key) {
int h;
// key.hashCode 的调用方法:Object 中的原生方法 public native int hashCode();
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
重头戏,putVal 方法:
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
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;
}
头大,分解来看:
总体结构:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
/**
* tab 用于保存 table 引用
* 1、若 tab 为 null 或者 tab 的长度为 0,则调用 resize 方法进行初始化或者扩容
*/
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/**
* 2、到了这一步,tab 一定存在
* i = (n - 1) & hash 确定元素存放在 tab 中的下标,p = tab[i]
*/
// 2.1、若 p 为 null,表示当前 tab 的 i 位置空,则可以直接直接构建 Node 插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else { // 2.2、若 p 不为 null,表示 tab 的 i 位置已经有值,则继续进行内部判断
Node<K,V> e; K k;
// ... 后续单独理解
}
/**
* modCount 自增,记录操作行为次数
* 3、++size > threshold,即判断下一次增加一个结点后size是否大于最大容量,如果是,则进行一次扩容
*/
++modCount;
if (++size > threshold)
resize();
// 插入成功时会调用的方法(默认实现为空)
afterNodeInsertion(evict);
return null;
}
从 1 ~ 3 共三个步骤中,理解 putVal 方法大的执行方向。其中最复杂的是 2.2 中 else 中 的内容:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// ...
else {
Node<K,V> e; K k;
// 已知:p = tab[i],是 tab[i] 中链表或者红黑树第一个结点
// 如果 p.hash 和 p.key 与传入参数中的 hash 和 key 相同,表示对应 key 已经存在,则直接使用原结点,只需要后面改变value即可
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果 p 是红黑树结点类型,则将其插入 tab[i] 位置中的红黑树中
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { // p 是链表结点类型,则将其插入 tab[i] 位置中的链表中
// 循环,尾插法
for (int binCount = 0; ; ++binCount) {
// 链表尾部
if ((e = p.next) == null) {
// 构建新结点,并修改 p 的 next 指向
p.next = newNode(hash, key, value, null);
/**
* TREEIFY_THRESHOLD:将链表转换为红黑树的阈值(默认为8),超过该阈值执行 treeifyBin 方法
* 注意:执行 treeifyBin 方法并不代表一定会将链表转换为红黑树,它会根据 table 的总长度来决定,即:
* 只有当 table 的长度大于等于 64 后,才会执行转换红黑树操作,否则只会对 table 进行扩容
*/
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 这里会判断整条链表上的结点的key、Hash值与插入的元素的key、Hash值是否相等(前面只判断了链表中的第一个结点 p)
// 如果相等,同前面一样,表示已经存在key值相同的结点【e = p.next,其中 e 已经赋值了】,则直接退出循环
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) {
V oldValue = e.value;
/**
* public V put(K key, V value) {
* return putVal(hash(key), key, value, false, true);
* }
* 这里 onlyIfAbsent 为 false,则 !onlyIfAbsent 为 true,进而执行 e.value = value 【新值替换旧值】
*/
if (!onlyIfAbsent || oldValue == null)
e.value = value;
/**
* 在HashMap中:void afterNodeAccess(Node<K,V> p) { }
* 实际上,afterNodeAccess 是提供给LinkedHashMap类【继承HashMap】使用,LinkedHashMap 可以保证输入输出顺序的一致性
* 类似的还有 afterNodeInsertion、afterNodeRemoval 这两个方法
*/
afterNodeAccess(e); // 这里默认实现为空
return oldValue;
}
}
// ...
return null;
}
putVal 方法中还涉及一些其他方法,如:
- resize:初始化或加倍表大小。如果 table 为空,则会根据字段阈值中保持的初始容量目标进行分配。
- treeifyBin:判断是否需要将链表替换为红黑树
a. replacementTreeNode:将链表结点 Node 转换为树结点 TreeNode
b. treeify:形成从该节点链接的节点的树 - Node:链表结点
- TreeNode:红黑树结点,关于红黑树的实现及对应操作,后续有机会再另讲
putVal 方法流程图(仅用于辅助理解源码):
其他
关于其他的方法,如 get、entrySet、ketSet 等,都可以在理解上述代码后再去看源码即可
关于红黑树:需要首先理解红黑树概念,再回头来看这里的源码更有效果(TODO:红黑树及HashMap红黑树源码理解)
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
/**
* Returns root of tree containing this node.
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
// ...省略几百行
}
补充1:HashSet
HashSet 的底层实现是 HashMap,来看 HashSet 的无参构造器:
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
add 方法:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
这里是把传入的值,作为 map 的 key 传入,而 value 统一为 PRESENT【private static final Object PRESENT = new Object();】
HashSet 之所以可以保证数据不会重复,其关键在于调用了 HashMap 的 put 方法:
// ...
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); // 通过hash计算【add】方法传入的e的下标i,若在table[i]不存在则构建结点保存
else {
// 如果结点存在,则判断 key 与 hash 值是否都相同,具体流程此处不再赘述
Node<K,V> e; K k;
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// ...
}
// ...
也就是说,HashSet 利用 HashMap 中 key 值不能重复的特性来保证其存入的值不会重复。
HashSet 中其他方法基本都基于 HashMap 的方法,此处不再赘述。