手撕源码----jdk 8.0 HashMap源码1
在我们手撕jdk 8.0 HashMap源码之前需要知道源码中这几个常量的意义:
* DEFAULT_INITIAL_CAPACITY : HashMap 的默认容量 : 16 * DEFAULT_LOAD_FACTOR : HashMap的默认加载因子 : 0.75 * threshold : 扩容的临界值 = 容量 * 填充因子 : 16 *0.75 = 12 * TREEIFY_THRESHOLD : Bucket中链表长度大于该默认值,转化为红黑树 : 8 * MIN_TERRIFY_CAPACITY : 桶(Bucket)中的Node被树化时最小的hash表容量 : 64
HashMap 底层用了 数组 + 链表 + 红黑树实现。
其中数组table : Node<K,V> 存放的是链表的头结点(非空)
Node是HashMap的内部类,有如下字段:
hash; //哈希值 key; //键 value; //值 next; //next指针,类型是Node<K,V>
Node是链表的节点元素,这里是jdk8,采用的尾插法。
Map接口其实还定义了Entry接口,里面主要是getValue()和getKey()方法,获得Map容器中的键值对元素。
我们先来看HashMap的构造函数:
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); } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
HashMap主要就这三个构造函数
第一个构造函数主要就是给填充因子和数组长度赋值
第二个构造函数是只有一个参数,就是给数组长度赋值。
第三个是无参构造函数,填充因子和数组长度全部是默认值。
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { if (table == null) { // pre-size float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) threshold = tableSizeFor(t); } else if (s > threshold) resize(); for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }
这个方法则是用来实现putAll()和HashMap的构造函数
如果当前的内部数组是null(用于构造函数),我们观察传入的Map
float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) threshold = tableSizeFor(t);
主要看这段代码
为什么要算 s / loadFactor?
因为这里用于构造函数,一开始的构造函数里的内置数组就是Null,而根据公式:
threshold = capacity * loadFactor
所以:
capacity = (float) threshold / loadFactor
这里其实是根据传入的map对象反推我们现在造的HashMap对象的capacity
下面就进行判断,如果我们反推出来的capacity大于限定数组最大长度就改为最大长度,否则不变。
如果这个capacity大于threshold(扩容阈值)就根据t重新确定扩容阈值。
而下面那段代码:
else if (s > threshold) resize();
则是我们调用putAll()要执行的代码,如果传入的长度大于扩容阈值,那么就要更新HashMap的capacity和threshold。
最后调用:
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); }
把传入的Map添加进容器,这里还会调用putVal方法,我们后面再分析。
下面看get(Object key)方法:
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
这里它调用了getNode方法,根据key的哈希值和key对象找到Node节点,如果没找到返回Null,否则返回key对应的value值
那么getNode方法是怎么样的呢:
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) { 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; }
我们首先看这一段代码:
(tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null)
这里首先是要做赋值,把当前HashMap对象内置的table赋给局部的tab数组,tab的长度赋给n,根据哈希算法找到我们要找的key的头结点在数组中的位置。
并且还要进行判断,防止你这个数组是空的然后再引用产生空指针异常了,赋值并且判断后就进入查找了。
if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first;
如果头节点的hash值和我们要找的key的哈希值相等并且key equals k 那么就说明找到了,返回头结点,没找到接着往下找。
if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key);
如果当前这个不是链表结构而是红黑树,就调用红黑树的查找方式查找key。
do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); }
经过刚才的红黑数判断后,说明这应该是链表结构,就根据链表结构使用do - while 遍历链表
根据hash值和equals()配套查找与k相同的key,找到了返回Node节点,没找到返回null。
那么这个方法我们就看完了。
再看下一个方法:boolean containsKey(Object key) 根据key查找是否有这样的key的键值对
public boolean containsKey(Object key) { return getNode(hash(key), key) != null; }
这里也是要使用我们上面的getNode方法,没找到说明没有,返回false,找到了说明有返回true。
我们再来看一个常用方法: put(K key,K value) 方法
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
这个方法主要调用了putVal方法,我们看看putVal方法是个什么情况:
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; }
好家伙,这个方法太长了,我们一段一段研究。
首先还是来介绍一下参数:
参数:
hash – 哈希值
key – 键
value – 值
onlyIfAbsent – 如果是true,不改变节点的value值
evict – 如果是false,说明正处于创建模式
Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
首先定义几个变量,tab : Node[] , p 是当前的节点,n是数组长度。
这段代码主要判断是不是出于创建模式,如果是创建模式就创建tab数组并且重新赋给n值。
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
如果根据哈希值没有在数组中找到这个键值对,就可以在哈希值对应的数组位置新建一个节点。
否则,如果找到的话:
Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
引用e指向头节点.
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; }
根据binCount来计数,e始终指向p的下一个节点,p用来遍历。
如果e指向的节点是空,说明这条链表没有和key相同的键,就可以插入了,并且如果当前计数(节点个数)大于TREEIFY_THRESHOLD(数组中链表长度的默认最大值),就要进行一次判断要不要把这条链表转化为红黑树存储。
如果我们在遍历这条链表的时候发现这条链表有和key相同的值了,就跳出循环。
if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } }
如果e不为空:存在一个节点其key与我们传入的key相同,就要根据onlyIfAbsent判断要不要更改这个节点的value值,并且返回旧的value值。
++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null;
如果e为空说明我们是插入进去的,不要忘了长度以及修改次数都要+1,并且进行一次容量判断。
下面来看看常用的putAll(Map map) 方法,把Map容器中的元素插入到当前HashMap容器
public void putAll(Map<? extends K, ? extends V> m) { putMapEntries(m, true); }
这就要调用我们之前介绍的putMapEntries()方法,并且这不是创建模式。
未完待续......