从源码理解LinkedMap和HashMap的区别

前面几篇从源码角度分析了HashMap put和遍历过程HashMap数据结构以及put过程HashMap遍历原理,通过上面两篇分析HahMap遍历数据的顺序是随机的,LinkedHashMap和HashMap唯一的不同是后者遍历的是有序的。

数据结构

HashMap使用的是数组+单链表的数据结构(本质上是数组,数组的每个元素是一个单向链表),LinkedHashMap使用的是数组+双向链表数据结构。
单链表中的节点只拥有指向下个节点的指针,而双向链表中的节点同时拥有指向前一个节点和后一个节点的指针。
HashMap中定义了HashMapEntry对象来表示一个节点,该对象中有k(key),v(value),hash,next四个属性,其中next为指向下个节点的指针。
LinkedHashMap定义了LinkedHashMapEntry来表示节点,LinkedHashMapEntry继承自HashMapEntry,在它的基础上增加了before,after属性,分别指向上一个和下一个节点。

源码分析

HashMap的构造方法的最后调用了init()的空方法,LinkedHashMap中重写了init方法,这个方法中实例化了一个空的头节点。

  @Override
    void init() {
        header = new LinkedHashMapEntry<>(-1, null, null, null);
       //header的前一个节点和后一个节点都指向自己
        header.before = header.after = header;
    }

LinkedHashMap的put方法

LinkedHashMap和HashMap的使用方法相同。
先回顾一下HashMap的put流程:
1、先得到Key的hash值,然后根据hash值计算出该Key应该放在数组中的哪个位置。
2、根据第一步找的数组的位置,遍历该位置的链表,如果发现和当前key相同的hash值表示该key已经在链表中存在,则将旧的Value替换成新的。
3、如果当前HashMap中的size>16*0.75 则对数组进行扩容,然后拷贝旧数组数据到新的数组中(调用transfer方法)。
4、创建HashMapEntry对象插入到链表头部。(调用createEntry方法)

LinkedHashMap和HashMap put数据的第一步和第二步相同。HashMap调用transfer方法将旧表中数据拷贝到新表中,而在LinkedHashMap中重写了transfer方法。

  @Override
    void transfer(HashMapEntry[] newTable) {
        int newCapacity = newTable.length;
        //遍历旧表数据 当header.after=header时表示遍历到最后
        for (LinkedHashMapEntry<K,V> e = header.after; e != header; e = e.after) {
            //计算出该key在新表的索引
            int index = indexFor(e.hash, newCapacity);
            //将之前表中第一个节点 作为下一个节点
            e.next = newTable[index];
             //将节点放到数组的头位置
            newTable[index] = e;
        }
    }

HashMap的第四步调用createEntry方法创建一个HashMapEntry并加入到链表中,LinkedLinkedHashMap中重写了createEntry方法。

 void createEntry(int hash, K key, V value, int bucketIndex) {
         //取出第一个节点
        HashMapEntry<K,V> old = table[bucketIndex];
        //创建LinkedHashMapEntry next指向 旧的头部
        LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
        //将创建节点 放在表的头部
        table[bucketIndex] = e;
        e.addBefore(header);//传入头节点
        size++;
    }
        //找到旧节点的前一个节点作为新建的e的前一个节点
  private void addBefore(LinkedHashMapEntry<K,V> existingEntry) { 
          //此处existingEntry=header
            after = existingEntry;//after=header
            //第一个节点 header.before=header 所以before=header
            before = existingEntry.before;//before=header
            //this为加入链表的节点
            before.after = this;//header.after=e;
            after.before = this;//header.before=e;
        }

总结:LinkedHashMap的put过程,基本上和HashMap的put流程相同,因为LinkedHashMap使用的数据结构是双向链表,所以LinkedHashMap在数组扩容(transfer)和创建节点(createEntry)进行了特殊处理。

LinkedHashMap的遍历

HashMap遍历过程是遍历数组+单链表数据结构,找到第一个不为null的节点返回,然后根据该节点指向下个节点指针得到下一个节点信息。因为HashMap中put数据是根据Key的hash值来计算节点存放的位置的,所以HashMap遍历顺序是随机的。
LinkedHashMap遍历的方法和HashMap相同。

LinkedHashMap<String, String> hashMap = new LinkedHashMap();
Iterator<Map.Entry<String, String>> iterator1 = hashMap.entrySet().iterator();
         while (iterator1.hasNext()){
                 iterator1.next();
                }

在HashMap中在HashIterator的构造方法中找到第一个不为null的点,然后按照节点顺序依次遍历。
LinkedHashMap中重写了newEntryIterator方法实例化了一个EntryIterator继承自LinkedHashIterator,和其他地方不同LinkedHashIterator没有继承HashIterator,直接继承了Iterator。
上面分析LinkedHashMap put数据时将节点放到了header的后面,所以在遍历时只需要从header节点开始就可以按照顺序输出了。

private abstract class LinkedHashIterator<T> implements Iterator<T> {
        LinkedHashMapEntry<K,V> nextEntry    = header.after;//从header头部开始一次向下遍历
        LinkedHashMapEntry<K,V> lastReturned = null;

        /** * The modCount value that the iterator believes that the backing * List should have. If this expectation is violated, the iterator * has detected concurrent modification. */
        int expectedModCount = modCount;

        public boolean hasNext() {
            //下一个节点 为header遍历结束
            return nextEntry != header;
        }

        public void remove() {
            if (lastReturned == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();

            LinkedHashMap.this.remove(lastReturned.key);
            lastReturned = null;
            expectedModCount = modCount;
        }

        Entry<K,V> nextEntry() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (nextEntry == header)
                throw new NoSuchElementException();

   LinkedHashMapEntry<K,V> e = lastReturned = nextEntry;
            nextEntry = e.after;
            return e;
        }
    }

总结:

HashMap put数据是根据Key的hash值计算应该放到数组的第几个位置然后把数据放进去,第一个放入的数据和第二个放入的数据因为key的hash值不同,所以在数组中的位置也不同。而HashMap的遍历是找到数组中第一个不为null的节点,然后根据指向下个节点的信息可以遍历出所有节点。这样就导致HashMap的遍历顺序是随机的。

LinkedHashMap put过程和在数组中的存放的位置和HashMap相同,在LinkedHahMap中定义了header节点,放入的节点的前一个节点指向header。LinkedHashMap遍历就是根据header依次遍历,所以LinkedHashMap的遍历顺序是有序的(LinkedHasMap可以用来实现Lru算法)。

    原文作者:长腿欧巴的痘痘
    原文地址: https://blog.csdn.net/DZ266912/article/details/78278570
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞