HashMap源码阅读笔记

1、HashMap概述:

HashMap是基于Map接口的一个非同步实现,此实现提供key-value形式的数据映射,支持null值。

HashMap的常量和重要变量如下:

 
DEFAULT_INITIAL_CAPACITY = 16
 
Node数组的默认长度
 
MAXIMUM_CAPACITY = 1073741824
 
Node数组的最大长度
 
DEFAULT_LOAD_FACTOR = 0.75F
 
负载因子,调控控件与冲突率的因数
 
TREEIFY_THRESHOLD = 8
 
链表转换为树的阈值,超过这个长度的链表会被转换为红黑树
 
UNTREEIFY_THRESHOLD = 6
 
当进行resize操作时,小于这个长度的树会被转换为链表
 
MIN_TREEIFY_CAPACITY = 64
 
链表被转换成树形的最小容量,如果没有达到这个容量只会执行resize进行扩容
 
Node<K, V>[] table
 
储存元素的数组
 
Set<Map.Entry<K, V>> entrySet
 
set数组,用于迭代元素
 
int size
 
存放元素的个数,但不等于数组的长度
 
int modCount
 
每次扩容和更改map结构的计数器
 
int threshold
 
临界值,当实际大小(容量*负载因子)超过临界值的时候,会进行扩容
 
float loadFactor
 
负载因子,默认为0.75F

2、HashMap实现原理:

在jdk1.8中,HashMap是采用数组+链表+红黑树的形式实现。如下图:

其中链表的实现如下:

复制代码
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;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }
复制代码

可以看到,node中包含一个next变量,这个就是链表的关键点,hash结果相同的元素就是通过这个next进行关联的。

接下来看看红黑树的实现:

复制代码
  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);
        }
   ......
}
复制代码

红黑树比链表多了四个变量,parent父节点、left左节点、right右节点、prev上一个同级节点,红黑树内容较多,有兴趣的可以自行百度,不在赘述。

在来说说hash算法,HashMap中使用的算法如下

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

这个hash先将key右移了16位,然后与key进行异或,这里还涉及到后面put方法中的另一次&操作,

tab[i = (n - 1) & hash]

tab既是table,n是map集合的容量大小,hash是上面方法的返回值。因为通常声明map集合时不会指定大小,或者初始化的时候就创建一个容量很大的map对象,所以这个通过容量大小与key值进行hash的算法在开始的时候只会对低位进行计算,虽然容量的2进制高位一开始都是0,但是key的2进制高位通常是有值的,因此先在hash方法中将key的hashCode右移16位在与自身异或,使得高位也可以参与hash,更大程度上减少了碰撞率。

构造方法如下:

public HashMap() //默认构造方法 public HashMap(int initialCapacity)//参数为初始大小 public HashMap(int initialCapacity, float loadFactor)//参数为初始大小,负载因子

这里又涉及到另一个算法:

复制代码
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;
    }
复制代码

这个算法很有意思,通过给定的大小cap,计算大于等于cap的最小的2的幂数。连续5次右移运算乍一看没有什么意思,但仔细一想2进制都是0和1啊,这就有问题了,第一次右移一位,就表示但凡是1的位置右边的一位都变成了1,第二次右移两位,上次已经把有1的位置都变成连续两个1了,是不是感觉很神奇,如此下来5次运算正好将int的32位都转了个遍,以最高的一个1的位置为基准将后面所有位数都变为1,然后在进行n+1,不就变成了2的幂数。这里还有一点要注意的是第一行的cap-1,这是因为如果cap本身就是2的幂数,会出现结果是cap的2倍的情况,会浪费空间。

 3、HashMap的存取:

3.1、存储:

复制代码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {    //put方法的实际执行者
        //hash,key的hash值;onlyIfAbsent,是否改变原有值;evict,LinkedHashMap回调时才会用到。
            Node<K,V>[] tab; 
        Node<K,V> p; 
        int n, i;
            if ((tab = table) == null || (n = tab.length) == 0)
                    n = (tab = resize()).length;    //table为空或长度为0时,对table进行初始化,分配内存
           if ((p = tab[i = (n - 1) & hash]) == null)
                  tab[i] = newNode(hash, key, value, null);    //当put的key在map中不存在时,直接new一个Node存入table。
           else {    //当key在map中存在时,进入此分支。
                  Node<K,V> e; K k;
                if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))    //此处的p是table中的第n-1个元素,每一次必检查此元素的key是否与put的key相同,如果相同则替换value
                       e = p;
                else if (p instanceof TreeNode)    //检查p是否是TreeNode类型。
                      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                else {    //如果第n-1个元素不符合,则一次遍历table中其余元素,直到找到相应key值。
                    for (int binCount = 0; ; ++binCount) {
                        if ((e = p.next) == null) {    //此处主要为了防止hash出现重复,只有上边tab[i = (n - 1) & hash]中存在元素且不是put的元素时才会进入这个分支。
                               p.next = newNode(hash, key, value, null);
                                if (binCount >= TREEIFY_THRESHOLD - 1) // 此处判断的意义是当链表长度超过8时,转换为红黑树,在1.8以前是没有的,链表查询的复杂度是O(n),而红黑树是O(log(n)),但是如果hash结果不均匀会极大的影响性能  treeifyBin(tab, hash);
                                 break;
                    }
                      if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))    //进入此分支标志已找到与put的key值相同的元素,元素变量为e。
                          break;
                      p = e;
                }
            }
           if (e != null) { // 将对应key的value替换为put的value,同时返回旧value,只有存在key时才会返回!
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);    
                return oldValue;
           }
        }
        ++modCount;
        if (++size > threshold)
            resize();    //对table进行扩容  afterNodeInsertion(evict);    
        return null;
    }
复制代码

从上面的源码可以看出,首先会判断key的hash与map容量-1的与计算值,如果数组中这个位置没有元素则直接插入,反之则在遍历此位置的元素链表,直到在最后插入。这个地方有一个链表的阈值(默认是8),如果一个链表的长度达到了阈值,则调用treefyBin()将链表转换成红黑树。

3.2、扩容:

复制代码
final Node<K,V>[] resize() {    //扩容方法
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;        //map现在的容量
        int oldThr = threshold;                            
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {    //如果现在的容量大于等于设定的最大容量则不会进行扩容
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1;         // 如果现在的容量扩大为2倍依然没有超过最大容量,并且现在的容量大于等于数组的默认长度,将map的容量扩大为2倍  }
        else if (oldThr > 0)  //hashMap有一个构造方法会指定初始大小,这时候就会用到这个分支,对map进行初始化
            newCap = oldThr;
        else {               //这个分支是默认的初始化参数分支,比如用new hashMap()生成一个对象,就会进入这个分支初始化
            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) {        //扩容操作,将oldTab的元素依次添加到newTab
            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;
                            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;
    }
复制代码

在这个方法里有几个比较有意思的地方,首先是最大容量MAXIMUN_CAPACITY,这个值实际就是int的最大值,个人推测应该是为了配合put时的hash算法,因为计算key值hash的算法返回值是int型,如果容量超过int的阈值,在进行与运算时碰撞率会增大很多倍。然后是扩容的方式,这里是new了一个数组!这也是HashMap不安全的关键之一。当超过一个线程对一个HashMap对象进行put的时候如果触发了resize方法,后执行的那一个会把之前执行的结果覆盖掉!也就是先put的值不会真的存入map中。

3.3、链表转换为红黑树

复制代码
final void treeifyBin(Node<K,V>[] tab, int hash) { //将链表转换为红黑树 int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) //如果map的容量小于64(默认值),会调用resize扩容,不会转换为红黑树  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); //Node转换为TreeNode 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); //调用TreeNode的树排序方法  }
    }
复制代码

这里需要注意的是当map容量小于64时,就算链表超过了8也不会转换为红黑树。

3.4、Map克隆

复制代码
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { //主要用于clone方法和putAll方法和一个构造方法HashMap(Map<? extends K, ? extends V> m)。 int s = m.size(); if (s > 0) { if (table == null) { // 如果是一个new的map,即table变量还没有初始化,先通过参数的map.size和负载因子计算所需的map大小。 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) //如果计算出的大小大于map的实际存储容量,则重新计算存储容量 threshold = tableSizeFor(t);
            } else if (s > threshold) //如果参数map大小大于实际存储容量,则将map容量变为2倍  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);
            }
        }
    }
复制代码

 

3.5、读取:

复制代码
final Node<K,V> getNode(int hash, Object key) {    //get的实际实现方法
        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) {        //通过hash定位到一个桶,并且有元素存在
            if (first.hash == hash &&  ((k = first.key) == key || (key != null && key.equals(k))))        //总是检查桶中的第一个元素
                return first;
            if ((e = first.next) != null) {    //如果第一个元素不符合,则继续搜索
                if (first instanceof TreeNode)    //如果桶中是红黑树,进入此分支,实际执行的是TreeNode中的find方法,从根节点开始向下搜寻
                    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;
    }
复制代码

看过put方法后再来看get就容易很多了,首先用put中的hash算法定位到数组中的位置,如果有元素且key值相同直接返回,如果有元素且key值不同那就要向下遍历了,如果是链表就一直遍历next,如果是红黑树则调用getTreeNode方法查找节点。

3.6、迭代器

复制代码
abstract class HashIterator { //KeySet和EntrySet的迭代器的父类 Node<K,V> next; // 下一个元素 Node<K,V> current; // 删除方法会调用 int expectedModCount; // 就是hashMap中的modCount int index; // 元素序号  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);
            }
        }
  .......
}
复制代码

HashIterator是KerSet和EntrySet的父类,这个迭代器中有一个属性是expectedModCount要特别注意,这个变量等价于HashMap对象中的modCount,如果在迭代器没有执行完的期间对map进行增删,会抛出异常阻止继续迭代,代码如下:

复制代码
 final Node<K,V> nextNode() { //获取下一个元素 Node<K,V>[] t;
            Node<K,V> e = next; if (modCount != expectedModCount) //在迭代器执行期间只能用下面的remove删除元素,否则会抛出异常,不能增加元素 throw new ConcurrentModificationException(); if (e == null) //如果没有下一个会抛出异常 throw new NoSuchElementException(); if ((next = (current = e).next) == null && (t = table) != null) { //如果next没有取到值,通过index继续向下遍历,直到取到元素为止 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; //关键语句,这里做了一个同步,否则与正常的删除没有区别,这个同步避免了上面的异常抛出 }
复制代码

与普通的删除方法就差在最后这一行上,这里同步了迭代器和HashMap对象的操作数,便不会抛出异常