HashMap源码阅读笔记(基于jdk1.8)

一、HashMap概述:  

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

  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

二、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进行关联的。性能

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

  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上一个同级节点,红黑树内容较多,有兴趣的能够自行百度,不在赘述。spa

  在来讲说hash算法,HashMap中使用的算法以下线程

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

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

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倍的状况,会浪费空间。

 三、HashMap的存取:

  3.一、存储:

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.二、扩容:

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.三、链表转换为红黑树

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.四、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.五、读取:

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.六、迭代器

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对象的操做数,便不会抛出异常

  

相关文章
相关标签/搜索