• HashMap实现原理


    概览

    这篇文章,我们打算探索一下Java集合(Collections)框架中Map接口中HashMap的实现。Map虽然是Collctions框架的一部分,但是Map并没有实现Collection接口,而Set和List是实现Collection接口的。

    简单来说,HashMap主要通过key存储value值,并且提供了添加,获取和操作存储value的方法。HashMap的实现基于HashTable。

     
    HashMap内部呈现

    Key-value对在内部是以buckets的方式存储在一起,最终成为一个表。存储和检索操作的时间是固定的,也就是时间复杂度为O(1)。

    这篇文章暂时不过于涉及HashMap的底层,我们先对HashMap有个整体认知。

    前言

    认真阅读了下HashMap的实现方式,也参考了网上别人的一些解析,个人觉得还是有些东西想说。网上有的文章名字为HashMap源码解析,实际上就是给它里面的一些方法加上一些注释而已,有不少都是这样的。

    我自己看源码的时候,发现不是别人不想解析,而是它的实现真的需要亲自研读,多理顺几遍才知道怎么回事。

    我在这里解析的文字描述也较多,不管谁的解析,自己也都要看一下JDK源码的具体实现,我们仅提供参考而已。

     解析思路

    源码不太方便看,先说明一下我的阅读思路。

    1. 把常用的几个方法拷贝到文本编辑器里面。

    2. HashMap中不同的时候会有不同的流程,梳理方法中的逻辑流程。
      就像采用极端法,采用特殊的数据,然后查看方法执行语句。未执行的语句暂时不考虑。

    3. 注释源码...

    我觉得HashMap的实现方式不够好,关键的几个方法里面包含的情况太多了,阅读起来是有难度的,而写程序的目的之一不就是让其他开发者阅读吗?一个方法内部做了太多的事情,违反了代码整洁的规则,一个函数做要尽量少的事情。

    get() 方法

    1、使用

    为了获取存储在hashMap中的对象,我们需要知道与它对应的key。然后通过get方法把对应的key传到参数里。调用HashMap的get方法的时候,也会调用key对象的hashCode方法

        @Test
        public void mapKeyTest(){
            HashMap<MyKey,String> map = new HashMap<MyKey, String>();
            MyKey key1 = new MyKey(1);
            map.put(key1,"value1");
            String retV =  map.get(key1);
        }

    控制台上可以看到两行输出

    调用 hashCode()
    调用 hashCode()

    2、原理

    public V get(Object key){
            Node<K, V> e;
            return(e=getNode(hash(key),key))==null?null:e.value;
    }

    1. 先从数组下标,找到对应的Node

    2. 如果Node里的第一个节点命中,直接返回

    3. 如果有冲突,则通过key.equals(k)去查找对应的entry

    4. 若为树,则在树中通过key.equals(k)查找,O(logn);

    5. 若为链表,则在链表中通过key.equals(k)查找,O(n)。

    // hash值为hash(key),key
    final Node<K,V> getNode(int hash, Object key) {
            Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
            //table不为空,并且tab[(n-1) & hash] != null的时候。
            if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {
    
                //判断取出Node的hash值是否相等。key值相等,那么直接返回。
                //想一想什么情况下,if语句不成立?
                if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                    return first;
    
                //也就是取出的第一个Node的hash值与key计算的hash不等。    
                if ((e = first.next) != null) {
                    if (first instanceof TreeNode)
    
                        //从树中取节点。
                        return ((TreeNode<K,V>)first).getTreeNode(hash, key);
    
                    do {
                        //判断hash值与key值是否相等,一直判断到相等或到节点末端为止。
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            return e;
                    } while ((e = e.next) != null);
                }
            }
            return null;
        }

    put()方法

    1、使用

    Map中通过put方法来存储一个value。

        /**
         * 建立键值对应关系,如果之前已经存在对应的key,
         * 返回之前存储的value,之前如果不存在对应的key,返回null
         */
        public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }

    知识点一: 当Map的调用put方法的时候,key对象被调用hashCode()方法,获得一个hash值供hashmap使用。
    我们创建一个对象来证实一下。

    public class MyKey {
        private int id;
        
        @Override
        public int hashCode() {
            System.out.println("调用 hashCode()");
            return id;
        }
    
        // constructor, setters and getters 
    }
    
       @Test
        public void mapKeyTest(){
            HashMap<MyKey,String> map = new HashMap<MyKey, String>();
            String retV = map.put(new MyKey(1),"value1");
        }

    可以看到控制台的输出信息

    调用 hashCode()

    知识点二: hash()方法计算出的hash值可以标识它在buckets数组中的索引位置。

    HashMap的hash()方法如下:可以与put方法进行关联。

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

    HashMap有一个特点,它可以存储null的key和null的value。当key时null的时候,执行put方法,它会自动分配hash为0. 这也意味着key为null的时候没有hash操作,这样就避免了空指针异常。

    2、原理

    这个中间涉及的逻辑多一些,方法需要分不同的步骤看。
    思路:

    • 对key的hashCode()做hash,然后再计算index;

    • 如果没碰撞直接放到bucket里;

    • 如果碰撞了,以链表的形式存在buckets后;

    • 如果节点已经存在就替换old value(保证key的唯一性)

    • 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;

    • 如果Node的容量满了(超过load factor*current capacity),就要resize。

     public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
    
     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
    
            //如果table为空,就重新创建table
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
            // 如果tab[(n-1) & hash]为空的话,就在tab[(n-1) & hash]位置存储节点。
            // newNode = new Node<>(hash, key, value, next);    
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            else {
    
                //当tab[(n-1)&hash]位置已经存在Node的时候。
                Node<K,V> e; K k;
    
                //如果已经存在的Node与即将要存的key值一样
                // e为存在的Node
                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 {
                    //不满足以上的情况,一直把Node往后插入。
                    //如果插入的节点数量多于TREEIFY_THRESHOLD-1个,变为树形节点
                    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;
                        }
                        //如果再遍历的时候,发现key值相同的时候,就跳出循环。e = p.next,
                        // 这时已经记录e的Node值了
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
    
                // 存在对应的Node时
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
    
            // HashMap内部修改的初始
            ++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;
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
    
            // 留意 i = (n-1)&hash,所以取的时候也这样取
          // newNode = new Node<>(hash, key, value, next);
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            ++modCount;
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
    }

    发生碰撞后,有个红黑树的处理,因为红黑树相对知识点较多,下次单独详细解释。这里可以参考以下,从JDK源码研究红黑树。我解释下关于碰撞冲的循环:

    • 查看是否存在相同的key,存在相同的key跳出循环,覆盖key的value

    • 如果不存在相同的key,在链表末尾插入新的Node

    • 如果链表节点过长,转换为树。

                    for (int binCount = 0; ; ++binCount) {
                        // p.next为null的时候,走到了链表的末端,然后新建一个节点,如果链表的长度太长,转换为树存储。
                        if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null);
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                            break;
                        }
    
                        // 如果链表中存在于要put的key值相同的时候,存储key值,也就是e ,(e = p.next)。
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }

    红黑树的部分,我们下次单独解析

    resize方法

    这个涉及的内容,有不少线需要捋一捋。首先看申明时候会resize()。它们都在调用put的时候执行的。

    • table == null的时候
      if ((tab = table) == null || (n = tab.length) == 0)
              n = (tab = resize()).length;
    • 键值映射的的数目大于临界值的时候。
      if (++size > threshold)
              resize();

    resize具体方法

    final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table;
    
            // 之前的容量可能为0或者为之前的大小
            // threshold可能为null或者为2的n次方
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            int oldThr = threshold;
    
            //新的容量,新的临界目前都为0
            int newCap, newThr = 0;
    
            // 第二次resize的时候。
            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; // double threshold
            }
            //第一次resize()的时候,初始化的操作。
            else if (oldThr > 0) // initial capacity was placed in threshold
                newCap = oldThr;
            else {               // zero initial threshold signifies using defaults
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
    
            //第一次resize(),会进入
            if (newThr == 0) {
                //负载因子 * 初始容量
                float ft = (float)newCap * loadFactor;
    
                //保证临界值不超过最大值。
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }
    
            //真正初始化的操作,新建newCap个数组,临界值初始化。
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
                Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;
    
            // 非 第一次reizie()时
            if (oldTab != null) {
                for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
                    if ((e = oldTab[j]) != null) {
    
                        // 重新计算了一次hash
                        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);
                        // 如果e.next != null 存在hash的Node 链子
                        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;}// 原索引+ oldCapelse{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;}

    如果是第一次resize,我们抽出来会执行到的语句。

    • 初始化容量
    • 初始化threshold,也就是初始化临界值,决定了table的键值对数目到什么时候会再次resize()
    final Node<K,V>[] resize() {
          //第一次的时候table为null
            Node<K,V>[] oldTab = table;
          // oldCap 为 0,threshod为null
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            int oldThr = threshold;
    
            int newCap, newThr = 0;
    
    // 不会走
            if (oldCap > 0) {
                 ....
    // 从这里执行
            else {               // zero initial threshold signifies using defaults
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
    
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
                Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;
            return newTab;
    }

    第二次及后续的resize执行流程

        final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table;
            // oldCap为当前table的长度,  oldThr为上次的table临界值
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            int oldThr = threshold;
    
            int newCap, newThr = 0;
            if (oldCap > 0) {
                // 这个if语句保证容量不超过hashmap的容量上限值。
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                // 如果扩容之后,不超过容量上限,
               //  那么表的大小加倍。临界值加倍
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
                    newThr = oldThr << 1; // double threshold
            }
    
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
                Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;
    
            //第二次扩容的时候,对上次的table如何处理。
            if (oldTab != null) {
    
                // 遍历之前的table,重新hash排序
                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;
                                // oldCap 为 16也就是 10000,
                                //oldCap为16的倍数,这里是hash值为低数字的时候
    
                                if ((e.hash & oldCap) == 0) {
                                    //第一次
                                    if (loTail == null)
                                        loHead = e;
                                    //计算新的next
                                    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;}

    resize中对有碰撞的链表的操作写的很有意思,再叙述一下。在重新分配索引的时候,有重新组建链表的操作。

    举个比较夸张的例子,读者就明白了。

    • e.hash < 2,那么 e.hash&oldCap就等于0,索引为小于之前hash表大小以内的索引。也就是当初的索引不变。

    • e.hash > 2的时候,e.hash&old不等于0,那么它的索引就为当前表的索引再加上新扩容的大小。

    案例图:

    这个图说的是,当hashmap的表大小为2扩充到4的时候,原本挂载在1位置的链表,重新分配之后的样子。

                            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;
                            }

    HashMap中的集合视图

    HashMap提供了三种方式,让我们可以把key和value作为其它集合来使用。

    Set<K> keys = map.keySet()
    Collection<V> values = map.values()
    Set<Entry<K, V>> entries = map.entrySet();

    注意: 在iteators创建完毕后,对map的任何结构修改,都会抛出一个异常。

    @Test
    public void givenIterator_whenFailsFastOnModification_thenCorrect() {
        Map<String, String> map = new HashMap<>();
        map.put("name", "baeldung");
        map.put("type", "blog");
     
        Set<String> keys = map.keySet();
        Iterator<String> it = keys.iterator();
        map.remove("type");
        while (it.hasNext()) {
            String key = it.next();
        }
    }
    
    // 会抛出java.util.ConcurrentModificationException异常

    HashMap中唯一允许的修改是在iterator中移除元素。

    public void givenIterator_whenRemoveWorks_thenCorrect() {
        Map<String, String> map = new HashMap<>();
        map.put("name", "baeldung");
        map.put("type", "blog");
     
        Set<String> keys = map.keySet();
        Iterator<String> it = keys.iterator();
     
        while (it.hasNext()) {
            it.next();
            it.remove();
        }
     
        assertEquals(0, map.size());
    }

    HashMap在iterator上的性能相比于LinkedHashMap和treeMap,性能非常糟糕。最差情况下为O(n),n为hashmap中条目的个数。

    最后

    HashMap内部有很多东西值得探索,这篇仅仅对HashMap做了一层表面的分析。接下来会深入分析。


    参考文献:

    1、https://www.jianshu.com/p/be9ffb76db30

    2、https://www.imooc.com/article/24118

  • 相关阅读:
    fixed 和 fixed
    SVN命令概要
    项目目录的划分
    作用域(scope), 定义空间(declaration space) 和 生存期(lifetime)
    可伸缩性最佳实践:来自eBay的经验(转)
    TCP连接可用性检测
    面向对象设计
    如何截取Http请求
    eBay架构
    .net3.5下的Socket通信框架
  • 原文地址:https://www.cnblogs.com/mmzs/p/8664056.html
Copyright © 2020-2023  润新知