散列表&HashMap、LinkedHashMap源码深入解读

散列表&HashMap、LinkedHashMap源码深入解读


前言

  散列表的英文叫"Hash Table",我们一般也叫做"哈希表"或者"Hash表",你一定听过它,但是你是不是真的理解这种数据结构呢?散列表用的是数组支持下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。可以说,没有数组就没有散列表。那它具体又是如何实现的呢
  我们知道散列表将元素散落在各个槽中,无法通过添加的顺序来遍历每一个元素。如果我们想要按照元素添加的先后顺序遍历它该如何实现呢?
  LRU是一种缓存淘汰算法,按照最近最少使用的原则淘汰数据,HashMap是一种内存数据结构,如果内存不够用了,我们该如何实现基于LRU的算法来淘汰最近最少使用的数据呢?以下内容将一一为你解答,文章可能比较长,请耐心阅读

散列表

  散列表具备三个核心要素,key键,就是存储数据的关键字,用它来标识一个数据。key通过一个散列函数计算后就得到一个hash值,这个hash值通过取模运算后得到的就是hash表的槽位,我们知道hash表就是一个数组,槽位就是数组的某个下标位置,也叫做bucket(桶)。
在这里插入图片描述

散列函数

  从上图中我们可以看到,散列函数在散列表中起着非常关键的作用。它是一个函数,我们把它定义成hash(key),其中key表示元素的键值,hash(key)得到的值表示key经过散列函数计算得到的散列值。有了散列值我们就确定元素该放在数组哪个位置上了。那么该如何构造散列函数呢?设计散列函数一般满足以下三点:

1.散列函数计算得到的散列值必须是一个非负整数
2.如果key1==key2,那么hash(key1)==hash(key2)
3.如果key!=key2,那么hash(key1)!=hash(key2)

其中第一点很好理解,因为数组的下标是从0开始的,而且必须是整数。第二点也很好理解,相同的key经过相同的函数得到的值应该是相同的。第三点理解起来可能会有点问题。这个要求看起来是合情合理的,不同key经过相同的函数计算得到的值理应不相等,但是在真实的情况下,想要找到一个不同的key对应的散列值不一样的散列函数几乎是不可能的,即便像业界著名的 MD5、SHA、CRC 等hash算法,也无法完全避免这种散列从突。散列从突是指不同的key经过散列函数后得到的hash值相同,从而导致他们会落在同一个槽位上,而且由于数组的大小有限,即使两个不同的hash值,在经过取模运算后也有可能落在同一个槽位上。先看一下HashMap的hash函数实现:

    //hash函数,通过key计算hash值
    //key是可以为null的
    static final int hash(Object key) {
    
    
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

散列冲突

  既然散列冲突是无法避免的,那我们该如何解决这种冲突问题呢?常用的散列冲突解决方法有两类,开放寻址法链表法

1.开放寻址法

  开放寻址法的核心思想就是,如果出现了散列冲突,我就重新探测一个空闲的位置,将其插入。先介绍一种比较简单的探测方法,线性探测
  当我们往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止,再将其插入。如下图所示:
在这里插入图片描述
查找的过程跟插入类似。我们先通过散列函数求出要查找元素的键值对应的散列值,然后比较对应的数组下标的元素与要查找的元素是否相等。如果相等,说明就是我们要找的元素;否则就按顺序继续往后查找,如果遍历到数组中空闲位置还没找到,则说明要查找的元素不在散列表中。
  散列表还支持删除的操作,但是删除的操作有点特别,我们不能单纯的把要删除的元素设置为空。这是为什么呢?因为在查找的时候,一旦我们找到一个空闲位置的时候,我们就认为散列表中不存在该数据。但是,如果这个空闲位置是我们后来删除的,就会导致原来的查找算法失效。本来存在的数据会被认为不存在。该如何解决呢?我们可以将删除的元素特殊标记为keepOn,当查找遇到keepOn的位置的时候并不是停下来,而是继续往下探测。如下图所示:
在这里插入图片描述
当散列表中插入的数据越来越多时,散列冲突发生的可能性就会越来越大,空闲位置越来越少,线性探测的时间就会越来越久。极端情况下,我们可能要探测整个散列表,所以最坏情况下的时间复杂度为O(n)。同理,在删除和查找时,也可能会线性探测整张散列表,才能找到要查找或者删除的数据。
  对于开放寻址冲突解决方法,除了线性探测方法之外,还有另外两种比较经典的探测方法,二次探测双重散列。所谓二次探测跟线性探测很像,线性探测每次探测的步长是1,那它探测的下标序列就是hash(key)+0,hash(key)+1,hash(key)+2……而二次探测的步长就变成原来的二次方,也就是说它探测的下标序列就是hash(key)+0,hash(key)+12 ,hash(key)+22 ……所谓双重散列就是不仅要使用一个散列函数。我们使用一组散列函数hash1(key),hash2(key),hash3(key)……我们先用第一个散列函数,如果计算得到的存储位置已经被占用,再用第二个散列函数,依次类推,直到找到空闲的存储位置。
  不管使用哪种探测方法,当散列表中空闲位置不多的时候,散列冲突的概率就会大大提高。为了尽可能保证散列表操作的效率,一般情况下,我们会尽可能保证散列表中有一定比例的空闲槽位。我们用装载因子来表示空位的多少。其可以用公式表示为:装载因子=散列表中元素个数/散列表的长度,装载因子越大,说明空闲位置越少,冲突的概率就会越大。

2.链表法

   链表法是一种更加常用的散列冲突解决办法,相比开放寻址法,它要简单很多。如下图所示,每个槽位(桶)会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中。
在这里插入图片描述
当插入的时候,我们只要通过散列函数计算出对应的散列槽位,将其插入到对应链表中即可。当查找、删除一个元素时,我们同样通过散列函数计算出对应的槽,然后遍历链表查找或者删除。
   散列表的基本知识就介绍到这里,接下来我们结合上面讲的内容来看一下HashMap是如何来实现一个工业级的散列表的。

HashMap源码探索

  HashMap在我们的日常开发中经常使用,它是一个k、v结构的集合类,通过key来存储或者查找对应的value。但它是线程不安全的,所以我们一般都是在单线程环境中使用,如果多线程并发访问我们就需要手动做并发同步。接下来我们来看一下它的源码实现。

数据域

常量字段

 //默认hash表初始容量,必须是2的n次方,2^4
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
    //hash表最大容量,2^30
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //树化的阈值,当链表的长度达到该值时转化成红黑树
    static final int TREEIFY_THRESHOLD = 8;
    //退化成链表的阈值,当红黑树的节点个数达到该值时退化成链表
    static final int UNTREEIFY_THRESHOLD = 6;
    //树化的hash表最小容量,当链表的长度达到8、hash表的长度小于该值时,先进行扩容而不是树化
    static final int MIN_TREEIFY_CAPACITY = 64;

对象字段

     //hash表,元素是一个Node类型,
    //transient修饰不能参与序列化,序列化时hash表单独处理
    //接下来的源码分析中hash表也会叫做数组
    transient Node<K,V>[] table;

    //hash表中所有Entry元素集合
    transient Set<Map.Entry<K,V>> entrySet;

    //哈希表中键值对(Entry)的总数
    transient int size;

    //修改次数,发生删除或添加修改结点时次数加一
    transient int modCount;

    //下一次触发扩容时的阈值大小
    //还有一种情况就是当hash表没有初始化,那么这个值就表示hash表初始化长度
    int threshold;

    //负载因子
    final float loadFactor;

  HashMap底层基于hash表,即数组,数组元素类型是Node,Node就是一个Entry,不仅封装了key、value字段还封装了key的hash值和next指针指向下一个Node(发生了hash从突)。

   //具体的Entry,hash表中的元素封装成entry
    static class Node<K,V> implements Map.Entry<K,V> {
    
    
        //通过hash函数计算得到的key的hash值
        final int hash;
        //元素的key
        final K key;
        //元素的value
        V value;
        //下一个node的地址,指向下一个node
        //当槽位发生从突时,next不为空
        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;
        }

        //先判断内存地址是否相等
        //如果内存地址不相等,再判断key和value是否都相等
        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;
        }
    }

所以HashMap解决冲突使用了链表法,我们知道hash表正常情况下查找的时间复杂度是O(1),但是链表法在极端的情况下,所有Node都发生了hash从突,这时候所有的Node都在一个链表上,时间复杂度就退化成了O(N),如果是在高并发的场景下会导致服务器CPU负载升高,影响整体服务性能。所以HashMap做了优化,当链表的长度达到8并且hash表总的元素个数大于等于64时,将链表变成一颗红黑树,红黑树是一棵平衡的二叉搜索树,查找、修改的时间复杂度为O(LogN)。
在这里插入图片描述
简单介绍一下红黑树:

  • 根节点是黑色的;
  • 每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存储数据;
  • 任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的;
  • 每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点;

构造方法

初始化传入hash表初始容量、负载因子

 //构造函数传入初始容量大小、负载因子
    public HashMap(int initialCapacity, float loadFactor) {
    
    
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        //不能大于最大容量
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //负载因子必须大于0且是必须是Number
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

tableSizeFor方法返回一个2的n次方大小的值,并且该值是最接近(大于等于)cap的

 //返回一个2的n次方大小的值,并且该值是最接近(大于等于)cap的
    //位运算性能最高,我们来分析一下它的原理
    //int是32位,我们假设最高位的index是32,最低位的index是1
    //那么对于n,它肯定在某个index位上存在一个1(n!=0),我们找到这个1所在index的最高位,记做maxIndex
    //假设极端的情况,这个1的位置是32,即maxIndex=32,接下来开始运算,1xxxx... ...xxx
    //n |= n >>> 1,n >>> 1后31位置上也变成了1,|=运算后32位置、31位置都变成了1,不用管后面位置的值,11xxx...xxx
    //n |= n >>> 2,n >>> 2后30、29位置都变成了1,|=运算后32、31、30、29都变成了1,不用管后面的值,1111xxx...xxx
    //n |= n >>> 4,1111 1111 XXX...XXX
    //n |= n >>> 8,1111 1111 1111 1111 xxx...xxx
    //n |= n >>> 16,1111 1111 1111 1111 1111 1111 1111 1111
    //好了,现在maxIndex后面的位置都变成了1,如果maxIndex不是32呢,其结果也一样,都会把maxIndex后面的位置变成1
    //最后n+1就会把maxIndex以及它后面的位置都变成0,maxIndex前面的位置变成1,也就是2^maxIndex
    //如果cap本身就是2的N次方呢,那返回的还是cap,如果n是0的话,那么最后n+1就返回2^0.
    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;
    }

传入外部hash表的构造函数

    //传入外部map
    public HashMap(Map<? extends K, ? extends V> m) {
    
    
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        //将外部map的元素结点node添加到当前map中
        putMapEntries(m, false);
    }

看一下具体的putMapEntries方法

 //外部map的key、value的类型必须和当前map的key、value类型相同,或是其子类
    //evict表示是否可以淘汰最近未被使用的元素结点,在LInkedHashMap中用到
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    
    
        int s = m.size();
        if (s > 0) {
    
    
            //哈希表初始化
            if (table == null) {
    
     // pre-size
                //根据负载因子反向计算需要申请的哈希表的初始大小
                float ft = ((float)s / loadFactor) + 1.0F;
                //不能超过最大容量
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    //如果新申请的容量大于当前的扩容阈值,则需要重新计算扩容阈值
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)
                //如果外部hash表元素个数大于扩容阈值则直接触发扩容操作
                resize();
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
    
    
                K key = e.getKey();
                V value = e.getValue();
                //遍历外部hash表,将元素添加进当前的hash表,如果存在相同的key则覆盖对应的value
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

resize扩容操作

//扩容操作,就是创建新的hash表,长度扩大一倍,hash表初始化也是在这一步完成的
    final Node<K,V>[] resize() {
    
    
        Node<K,V>[] oldTab = table;
        //原hash表的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
    
    
            if (oldCap >= MAXIMUM_CAPACITY) {
    
    
                //如果原hash表长度已经到达最大长度上限
                //设置扩容阈值为最大正整数
                //返回原先的hash表,不能再扩容了
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //原hash表长度大于默认初始容量(16) & 申请新hash表的长度为老hash表的两倍大小,并且新hash表长度小于最大长度上限
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //新的扩容阈值=老的阈值*2
                //因为oldThr=oldCap*loadFactor
                //所以newThr=newCap*loadFactor=2*oldCap*loadFactor=2*oldThr
                newThr = oldThr << 1;
        }
        else if (oldThr > 0)
            //hash表没有初始化,而扩容阈值大于0,那么扩容阈值就用作hash表初始化长度
            newCap = oldThr;
        else {
    
    
            //如果原hash表长度、阈值都不大于0,说明hash表还没有初始化
            //取默认的初始化值
            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];
        //新的hash表创建好了
        table = newTab;
        //接下来是核心的数据搬移操作了
        if (oldTab != null) {
    
    
            //遍历老的hash表
            for (int j = 0; j < oldCap; ++j) {
    
    
                Node<K,V> e;
                //如果当前槽位存在结点的话需要将它搬到新的hash表
                if ((e = oldTab[j]) != null) {
    
    
                    //原槽位置空
                    oldTab[j] = null;
                    //如果该槽位只有一个结点元素,那就直接hash取模(取新的hash表的模)搬移到新的hash表即可
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //如果该槽位上的结点类型是TreeNode说明是一颗红黑树,需要将整棵树搬移到新hash表
                    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;
                            //等于0的话说明该结点应该放到低位链表
                            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);
                        //低位链表在新hash表的下标位置和原hash表一样都是j
                        if (loTail != null) {
    
    
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //高位链表在新hash表中的下标位置是原hash表的位置+原hash表大小
                        if (hiTail != null) {
    
    
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

再看一下TreeNode的split方法,因为红黑树的结点也维护了链表的数据结构,所以其原理和上面的链表数据的搬移操作差不多

 //TreeNode继承自Node,他的每个结点同时也维护了一个双向链表的数据结构
        final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    
    
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            //低位链表头结点、尾结点
            TreeNode<K,V> loHead = null, loTail = null;
            //高位链表头结点、尾结点
            TreeNode<K,V> hiHead = null, hiTail = null;
            //记录高低位链表的结点个数
            int lc = 0, hc = 0;
            //遍历每个结点,因为也是一个双向链表结构,所以可以通过next指针遍历每一个结点
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
    
    
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                //当前结点 & bit等于0的话就添加到低位链表,也是一个双向的链表,bit就是原hash表的长度
                if ((e.hash & bit) == 0) {
    
    
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                }
                //否则的话就是应该添加到高位链表,也是一个双向链表
                else {
    
    
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    ++hc;
                }
            }

            if (loHead != null) {
    
    
                //低位链表在新hash表的位置index保持和原hash表的位置一致
                //如果长度小于等于树退化的阈值就将此树退化成链表
                if (lc <= UNTREEIFY_THRESHOLD)
                    tab[index] = loHead.untreeify(map);
                else {
    
    
                    //否则就将此链表搬移到新的hash表
                    tab[index] = loHead;
                    //说明原来的树被一分为二了,需要重新树化
                    if (hiHead != null)
                        loHead.treeify(tab);
                }
            }
            if (hiHead != null) {
    
    
                //高位链表在新hash表的位置index是原hash表的index+原hash表的长度
                //小于等于树退化的阈值的就退化成一个链表
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
    
    
                    //搬移到新的hash表对应的槽位
                    tab[index + bit] = hiHead;
                    //说明原来的树被一分为二了,需要重新树化
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
        }

重点分析一下数据搬移的操作原理。hash表的长度size为2的n次幂即1<<n,那么size-1的低位都是1,所以hash值&(size-1)的范围就是[0,size-1],正好均匀落在hash表的每个桶内。当hash表扩容后,长度size为原来的两倍,即1<<n+1,那么在搬移数据的时候我们只需判断n位置所在的bit位是0还是1,将数据分为低位和高位,如果是0就是低位,那么它搬移后新的位置index还是和老的hash表的位置一样,如果是1的话就是高位,那么它搬移后的位置就是原先hash表位置加上原hash表的size。见下图,其中bit就是原hash表的size:
在这里插入图片描述
将原hash表的结点分为低位结点和高位结点两类结点后,分别重新组合成新的链表,再整体搬移到新hash表对应的槽位上
  再来看putMapEntries方法最后一步操作putVal,其中有两个方法afterNodeAccess和afterNodeInsertion在LinkedHashMap中实现了,后面会分析

//真正添加元素结点的方法,onlyIfAbsent为false的话覆盖已存在的key的value
    //evict为false表示正在创建hash表,为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;
        //如果hash表长度是0的话说明还没初始化,初始化在扩容方法resize()里完成
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //hash取模计算key所在的槽位,判断该槽位是否存在结点p
        //n是2的n次幂,所以n-1的二进制低位都是1,&计算出来的值肯定是介于0到(n-1)之间
        if ((p = tab[i = (n - 1) & hash]) == null)
            //如果该槽位是空的话就把key,value包装成node添加到该槽位
            tab[i] = newNode(hash, key, value, null);
        else {
    
    
            //否则该槽位上已存在结点p
            //判断p结点与待插入结点的key是否相等,hash值相等的情况下key不一定相等,hash不相等key肯定不相等
            //相等的话记录e,表示存在key相同的结点
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果该槽位上是一颗红黑树,则查找红黑树中是否存在key相等的结点
            //存在的话会返回该结点并记录e,不存在的话该新结点就插入红黑树中
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
    
    
                //否则该槽位就是一个链表结构,遍历链表
                for (int binCount = 0; ; ++binCount) {
    
    
                    //如果找不到key相等的结点就把新结点插入链表尾部
                    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,终止循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //存在和待插入元素相同key的结点e
            if (e != null) {
    
     // existing mapping for key
                V oldValue = e.value;
                //onlyIfAbsent参数表示只有在e结点的value不存在的情况下才会替换成新结点的value
                //如果是false的话原结点e的value都会被替换成新结点的value
                //如果原结点e的value是空的话,替换成新的结点的value
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //这个方法是在LinkedHashMap中真正实现,用来将该结点e搬移到链表最后面
                //表示该结点最近被访问过了,如果此时要回收hash表上的结点的话,该结点就会躲过一劫
                //因为回收是从头结点开始回收的
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //添加新元素成功了,增加修改次数
        ++modCount;
        if (++size > threshold)
            //size加1如果大于扩容阈值的话就扩容
            resize();
        //这个方法是被LinkedHashMap真正实现
        //如果evict是true的话淘汰头结点(最近最少使用的那个结点)
        afterNodeInsertion(evict);
        return null;
    }

这里如果发生了hash从突,并且链表的长度达到了树化的阈值,则需要转化成红黑树

//将链表转化红黑树
    final void treeifyBin(Node<K,V>[] tab, int hash) {
    
    
        int n, index; Node<K,V> e;
        //如果hash表长度小于最小树化长度阈值(64),则先进行扩容而不是树化
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        //如果hash取模对应的槽位index存在结点e
        //说明此槽位上的链表需要树化
        else if ((e = tab[index = (n - 1) & hash]) != null) {
    
    
            //头结点、尾结点
            TreeNode<K,V> hd = null, tl = null;
            do {
    
    
                //遍历每一个结点,将他们包装成TreeNode,TreeNode继承自Node
                //它的内部不仅有next指针,还有prev指针,并且维护了一个和转化
                //前链表顺序相同的双向链表结构,所以它既是红黑树,又是双向链表
                TreeNode<K,V> p = replacementTreeNode(e, null);
                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);
        }
    }

其他常用方法

根据key查找对应的value

//根据key获取对应的value
    public V get(Object key) {
    
    
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

//根据hash值、key获取对应结点
    final Node<K,V> getNode(int hash, Object key) {
    
    
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //如果hash表不为空,并且hash取模后对应的槽位上存在结点
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
    
    
            //根据hash、key判断,如果对应槽位上的结点就是要查找的结点,直接返回该结点
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //如果该槽位存在多个结点
            if ((e = first.next) != null) {
    
    
                //如果结点是TreeNode类型,说明是一颗红黑树,通过红黑树的搜索方法查找对应的结点并返回
                if (first instanceof TreeNode)
                    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);
            }
        }
        //找不到返回null
        return null;
    }

根据key删除对应的键值对

//根据指定的key删除对应的k、v键值对
    public V remove(Object key) {
    
    
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

//根据hash值、key删除对应的结点,如果value不为空且matchValue为true,则删除的条件还必须是value也相等
    //先查到到要删除的结点,然后再删除,如果没找到返回null
    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
    
    
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //先判断hash表不为空,且hash取模后对应的槽位上存在结点
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
    
    
            Node<K,V> node = null, e; K k; V v;
            //判断槽位上的第一个结点是否是要查找的结点,是的话记录node
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
    
    
                //如果第一个结点不是要查找的结点,且该槽位不止一个结点,则继续探测
                //如果TreeNode结点类型就从红黑树中查找对应的结点并记录node
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
    
    
                    //否则是链表结构,顺序遍历查找
                    do {
    
    
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
    
    
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //如果找到了待删除的结点node,判断是否需要比较value的值,并且value也相等
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
    
    
                //如果是红黑树,删除node结点
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                //p记录的是node前一个结点,如果node==p则说明
                //p是hash槽的第一个结点,删除node,node后面的结点变成hash槽第一个结点
                else if (node == p)
                    tab[index] = node.next;
                else
                    //否则从链表中删除node
                    p.next = node.next;
                //修改次数加1
                ++modCount;
                //size减1
                --size;
                //这个方法在LinkedHashMap中实现了
                //删除在链表中的node结点
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

清空hash表

//清空hash表所有槽位上的结点
    public void clear() {
    
    
        Node<K,V>[] tab;
        modCount++;
        if ((tab = table) != null && size > 0) {
    
    
            size = 0;
            for (int i = 0; i < tab.length; ++i)
                tab[i] = null;
        }
    }

迭代器

常用的key、value、entry迭代器

//key迭代器
    final class KeyIterator extends HashIterator
        implements Iterator<K> {
    
    
        //返回结点的key
        public final K next() {
    
     return nextNode().key; }
    }

    //value迭代器
    final class ValueIterator extends HashIterator
        implements Iterator<V> {
    
    
        //返回结点的value
        public final V next() {
    
     return nextNode().value; }
    }

    //entry迭代器
    final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
    
    
        //返回整个结点,即Entry
        public final Map.Entry<K,V> next() {
    
     return nextNode(); }
    }

看一下核心的HashIterator


    abstract class HashIterator {
    
    
        //指向下一个返回的结点
        Node<K,V> next;        // next entry to return
        //当前的结点
        Node<K,V> current;     // current entry
        //更新标志位
        int expectedModCount;  // for fast-fail
        //当前hash表的槽位
        int index;             // current slot

        HashIterator() {
    
    
            //记录当前变更次数
            expectedModCount = modCount;
            //hash表
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            //找到第一个不为空的槽位的index下标并赋值给next
            if (t != null && size > 0) {
    
     // advance to first entry
                do {
    
    } while (index < t.length && (next = t[index++]) == null);
            }
        }

        public final boolean hasNext() {
    
    
            return next != null;
        }
        //返回下一个结点
        final Node<K,V> nextNode() {
    
    
            Node<K,V>[] t;
            Node<K,V> e = next;
            //如果hash表发生了变动则抛异常
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            //当前要返回的结点不存在,说明已经遍历完了,抛异常
            if (e == null)
                throw new NoSuchElementException();
            //如果下一个结点空,说明当前slot槽位上已经到链表尾部了,则继续遍历下一个非空
            //slot上的结点链表,直到最后一个index位置
            if ((next = (current = e).next) == null && (t = table) != null) {
    
    
                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();
            //如果hash表结点发生了变动,抛异常
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            //当前结点指针清空
            current = null;
            //当前结点的key
            K key = p.key;
            //调用HashMap中的方法删除当前结点
            removeNode(hash(key), key, null, false, false);
            //删除以后modCount会加1,同步到expectedModCount
            expectedModCount = modCount;
        }
    }

序列化与反序列化

先看一下clone方法,该方法是浅克隆,克隆对象的hash表的结点元素和宿主对象是共享的

 //返回一个克隆对象,共享当前hashmap中的key,value
    @Override
    public Object clone() {
    
    
        HashMap<K,V> result;
        try {
    
    
            //创建克隆对象
            result = (HashMap<K,V>)super.clone();
        } catch (CloneNotSupportedException e) {
    
    
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
        //初始化字段
        result.reinitialize();
        //将当前对象的key、value添加到克隆对象中
        result.putMapEntries(this, false);
        return result;
    }

深克隆可以通过序列化的方式实现,他的每一个结点元素都单独做序列化、反序列化。先看一下序列化方法

 //序列化方法,ObjectOutputStream#writeObject(obj)会调用到此方法
    private void writeObject(java.io.ObjectOutputStream s)
        throws IOException {
    
    
        //返回hash表的桶数,也就是hash表的长度
        int buckets = capacity();
        //将非static、非transient修饰的字段写到stream中
        s.defaultWriteObject();
        //写入桶大小
        s.writeInt(buckets);
        //写入结点个数
        s.writeInt(size);
        //依次写入每个entry
        internalWriteEntries(s);
    }
    
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
    
    
        Node<K,V>[] tab;
        if (size > 0 && (tab = table) != null) {
    
    
            //依次遍历每一个结点写入序列化流中
            for (int i = 0; i < tab.length; ++i) {
    
    
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
    
    
                    s.writeObject(e.key);
                    s.writeObject(e.value);
                }
            }
        }
    }

反序列化方法


//反序列方法,ObjectInputStream#readObject会调用到此方法
    private void readObject(java.io.ObjectInputStream s)
        throws IOException, ClassNotFoundException {
    
    
        //将非static、非transient修饰的字段反序列化到当前对象中
        s.defaultReadObject();
        //初始化某些字段,因为接下来要重新构造hash表
        reinitialize();
        //判断负载因子是否合法
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new InvalidObjectException("Illegal load factor: " +
                                             loadFactor);
        //对应于序列化方法中的s.writeInt(buckets);读取桶数
        s.readInt();
        //对应于序列化方法中的s.writeInt(size);读取结点个数
        int mappings = s.readInt();
        if (mappings < 0)
            throw new InvalidObjectException("Illegal mappings count: " +
                                             mappings);
        else if (mappings > 0) {
    
     // (if zero, use defaults)
            // 负载因子的大小范围在0.25f--4.0f
            float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
            //根据负载因子和结点个数反向计算所需的最小容量
            float fc = (float)mappings / lf + 1.0f;
            //返回一个在指定范围内,并且大小为2的n次方的cap
            int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                       DEFAULT_INITIAL_CAPACITY :
                       (fc >= MAXIMUM_CAPACITY) ?
                       MAXIMUM_CAPACITY :
                       tableSizeFor((int)fc));
            //计算下一次扩容的阈值
            float ft = (float)cap * lf;
            //扩容阈值范围检查并赋值
            threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                         (int)ft : Integer.MAX_VALUE);

            // Check Map.Entry[].class since it's the nearest public type to
            // what we're actually creating.
            SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
            @SuppressWarnings({
    
    "rawtypes","unchecked"})
            //新的hash表
            Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
            table = tab;

            //依次反序列化每一个key、value,并把他们添加到hash表中
            for (int i = 0; i < mappings; i++) {
    
    
                @SuppressWarnings("unchecked")
                    K key = (K) s.readObject();
                @SuppressWarnings("unchecked")
                    V value = (V) s.readObject();
                putVal(hash(key), key, value, false, false);
            }
        }
    }

LinkedHashMap源码分析

  我们知道元素插入hash表是根据hash函数随机散落在hash表上的,我们无法根据插入的先后顺序或访问的先后顺序去遍历它。那有没有办法实现按顺序遍历呢。LinkedHashMap就实现了这种功能,它本质上还是一个hash表,结点还是按照hash值散落在数组上,但是每个结点内部多了两个指针before、after用来记录在它前后的两个结点,也就是说每个结点还通过链表的形式按照添加的先后顺序串连了起来,如下图所示。有了这个顺序链表之后我们就可以实现一些HashMap无法实现的功能,比如,按照添加顺序遍历访问,按照FIFO的策略淘汰最先进来的结点,还可以实现LRU的缓存淘汰策略。
在这里插入图片描述

  因为LinkedHashMap是继承自HashMap,所以很多基本的方法在上面HashMap中都已经分析了,我们只看LinkedHashMap自己扩展的方法和属性。因为LinkedHashMap比HashMap多了一层链表的数据结构,所以它的属性和方法都是在维护和操作链表。

属性字段

  Entry继承自HashMap.Node,多了两个字段用来指向前后结点,accessOrder是组织链表顺序的方式,true的话按照元素的访问顺序排列,即如果某个结点元素被访问,就把它搬移到链表末尾,头结点就是最近最少使用的结点。false的话就是按照元素的插入顺序组织链表。

//继承自HashMap的Node,多了两个指针用来将所有结点串起来,实现按顺序遍历
    static class Entry<K,V> extends HashMap.Node<K,V> {
    
    
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
    
    
            super(hash, key, value, next);
        }
    }
    
    //头结点
    transient LinkedHashMap.Entry<K,V> head;

    //尾结点
    transient LinkedHashMap.Entry<K,V> tail;

    //组织链表时的顺序方式
    //true 按照元素的访问顺序,false 按照元素的插入顺序
    final boolean accessOrder;

构造方法

  可以看到构造方法基本都调用了父类的方法,除非特别指定,否则就是取元素的插入顺序组织链表结构

    //构造方法,默认按照元素的插入顺序维护链表结构
    public LinkedHashMap(int initialCapacity, float loadFactor) {
    
    
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }
    public LinkedHashMap(int initialCapacity) {
    
    
        super(initialCapacity);
        accessOrder = false;
    }

    public LinkedHashMap() {
    
    
        super();
        accessOrder = false;
    }

    public LinkedHashMap(Map<? extends K, ? extends V> m) {
    
    
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }

    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
    
    
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

特性方法

  在继承HashMap的基础上,LinkedHashMap有他自己的方法来操作链表,先来看一下创建结点的方法。在创建完新的结点后,会自动把它添加到当前链表的末尾

    //创建新的结点
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    
    
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        //添加到链表末尾
        linkNodeLast(p);
        return p;
    }

    //创建TreeNode的,TreeNode继承自Entry
    TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
    
    
        TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
        //同样也添加到链表尾部
        linkNodeLast(p);
        return p;
    }

afterNodeRemoval方法,该方法在HashMap#removeNode方法最后调用,表示删除hash表中的结点e之后,该结点在链表中还存在,也要将它删除

//hash表中删除该结点之后的操作
    //同样的将该结点从当前链表中删除
    void afterNodeRemoval(Node<K,V> e) {
    
     
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.before = p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

afterNodeInsertion方法,该方法在HashMap#putVal方法最后被调用,表示元素e添加到hash表之后可能需要淘汰链表上的头结点。

//将当前结点添加到hash表之后的操作
    void afterNodeInsertion(boolean evict) {
    
     // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        //如果evict为true,并且removeEldestEntry方法返回true,说明可以删除链表头结点
        //默认情况下removeEldestEntry返回false,即不会淘汰头结点
        if (evict && (first = head) != null && removeEldestEntry(first)) {
    
    
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

afterNodeAccess方法,在HashMap的查询、更新等操作方法中会调用到该方法,会把相应的被访问到的结点搬到链表尾部

//访问某个结点e之后的操作
    void afterNodeAccess(Node<K,V> e) {
    
     // move node to last
        LinkedHashMap.Entry<K,V> last;
        //accessOrder为true,并且当前结点不在链表尾部
        //则将该结点移到链表末尾
        //默认情况下是不会搬移的,链表的顺序按照插入顺序
        if (accessOrder && (last = tail) != e) {
    
    
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
    
    
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

所以我们可以通过LinkedHashMap来实现一个基于LRU缓存淘汰策略的hash表,只需设置accessOrder为true,并重写removeEldestEntry允许淘汰最近最少使用的头结点

迭代器

  该迭代器可以按照元素的顺序遍历访问,只需按照链表顺序迭代即可

//迭代器
    abstract class LinkedHashIterator {
    
    
        //下一个结点
        LinkedHashMap.Entry<K,V> next;
        //当前结点
        LinkedHashMap.Entry<K,V> current;
        //更新标志
        int expectedModCount;

        LinkedHashIterator() {
    
    
            //从头结点开始遍历
            next = head;
            expectedModCount = modCount;
            current = null;
        }

        public final boolean hasNext() {
    
    
            return next != null;
        }

        final LinkedHashMap.Entry<K,V> nextNode() {
    
    
            //要返回的当前结点e
            LinkedHashMap.Entry<K,V> e = next;
            //如果发生了结点的变动,抛异常
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            //当前结点不存在,抛异常
            if (e == null)
                throw new NoSuchElementException();
            //当前结点
            current = e;
            //下一个结点
            next = e.after;
            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;
            //删除当前结点p
            removeNode(hash(key), key, null, false, false);
            //更新expectedModCount,可以继续迭代下一个结点
            expectedModCount = modCount;
        }
    }

    //key迭代器,返回结点的key
    final class LinkedKeyIterator extends LinkedHashIterator
        implements Iterator<K> {
    
    
        public final K next() {
    
     return nextNode().getKey(); }
    }

    //value迭代器,返回结点的value
    final class LinkedValueIterator extends LinkedHashIterator
        implements Iterator<V> {
    
    
        public final V next() {
    
     return nextNode().value; }
    }

    //Entry迭代器,返回整个结点
    final class LinkedEntryIterator extends LinkedHashIterator
        implements Iterator<Map.Entry<K,V>> {
    
    
        public final Map.Entry<K,V> next() {
    
     return nextNode(); }
    }

序列化

  LInkedHashMap的序列化、反序列化方法使用了HashMap的相关方法,只不过自己实现了结点的序列化策略,即按照链表的顺序序列化,这样反序列化的时候链表的顺序维持不变

//序列化hash表结点Entry时的操作
    //按照结点所在链表的顺序序列化
    //这样反序列化时的顺序也是按照原链表的顺序
    void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
    
    
        for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
    
    
            s.writeObject(e.key);
            s.writeObject(e.value);
        }
    }

猜你喜欢

转载自blog.csdn.net/ddsssdfsdsdshenji/article/details/108817020
今日推荐