zoukankan      html  css  js  c++  java
  • HashMap源码阅读笔记(基于jdk1.8)

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

      

  • 相关阅读:
    flume sink两种类型 file_rool 自定义sing com.mycomm.MySink even if there is only one event, the event has to be sent in an array
    为什么引入进程20年后,又引入线程?
    As of Flume 1.4.0, Avro is the default RPC protocol.
    Google Protocol Buffer 的使用和原理
    Log4j 2
    统一日志 统一订单
    网站行为跟踪 Website Activity Tracking Log Aggregation 日志聚合 In comparison to log-centric systems like Scribe or Flume
    Percolator
    友盟吴磊:移动大数据平台的架构、实践与数据增值
    Twitter的RPC框架Finagle简介
  • 原文地址:https://www.cnblogs.com/ayanami-rei/p/5998097.html
Copyright © 2011-2022 走看看