秋招准备之——深入理解HashMap(JDK1.8)

秋招复习笔记系列目录(不断更新中):

  • 1.数据结构全系列
  • 2.计算机网络知识整理(一)
  • 3.计算机网络知识整理(二)
  • 4. Java虚拟机知识整理
  • 5.计算机操作系统
  • 6.深入理解HashMap
  • 7.深入理解ConcurrentHashMap
  • 8.MySQL

一、介绍

1.1 前言

最近在研究Java集合的内容,本来想像前面那样将整个集合都放在一篇博客里面,但发现HashMap、ConcurrentHashMap还有LinkedConcurrentHashMap里面的知识太多太多了,而且拜读了源码以后,才真正地感受到了集合设计者的厉害,所以还是把这三个集合单独列出来写博客来理解吧。在看的过程中,看了很多博客,感觉有些东西其他博客还是没有讲清楚,这里我将我遇到的每个问题都详细的查了,在这里做一个记录,希望能帮助到有需要的同学。

1.2 基础知识

1. 红黑树

红黑树的性质

  • 每个节点或者是红色的,或者是黑色的;
  • 根节点是黑色的;
  • 每个叶子结点(红黑树中叶子节点为最后的空节点)是黑色的;
  • 如果一个节点是红色的,那么他的孩子都是黑色的
  • 从任意一个节点到叶子节点经过的黑色节点是一样的。

红黑树的插入过程:
红黑树的插入遵循以下规则

  • 1.如果根节点为空,直接插入,并设置根节点为黑色

  • 2.根节点不为空,将新插入的节点cur标记为红色。如果cur的父节点不为黑色,在则需要分情况讨论

    • cur为红,parent为红,pParent为黑,uncle存在且为红,则将parent,uncle改为黑,pParent改为红,然后把pParent当成cur,继续向上调整。
      秋招准备之——深入理解HashMap(JDK1.8)_第1张图片

    • 左左: cur为红且为parent的左孩子,parent为红,pParent为黑,uncle不存在或uncle为黑,且parent为为pParent的左孩子,则进行右旋
      秋招准备之——深入理解HashMap(JDK1.8)_第2张图片

    • 右右: cur为红且为parent的右孩子,parent为红,pParent为黑,uncle不存在或uncle为黑,且parent为pParent的右孩子,则进行左旋秋招准备之——深入理解HashMap(JDK1.8)_第3张图片

    • 左右: cur为parent的右孩子,parent为pParent的左孩子,则先左旋转换成左左情况,再右旋
      秋招准备之——深入理解HashMap(JDK1.8)_第4张图片

    • 右左: cur为parent的左孩子,parent为pParent的右孩子,则先右旋转换成右右情况,再左旋
      秋招准备之——深入理解HashMap(JDK1.8)_第5张图片

2. 散列的相关知识

1.常见的几种哈希函数

  • ①直接寻址法: 以关键字的某个线性函数值为哈希地址,可以表示为 hash(K)=aK+C。优点是不会冲突,缺点是空间复杂度很高
  • ②数字分析法: 该方法是取数据元素关键字中某些取值较均匀的数字来作为哈希地址的方法,这样可以尽量避免冲突。缺点是只适合于能预估出全体关键字的每一位数字出现的频率
  • ③除留取余法: 最为常用。是由数据元素关键字除以某个常数所留的余数为哈希地址,可以表示为:hash(K)=K mod C。C通常取哈希表的长度。
  • ④平法取中法: 对关键字计算平方,然后根据可使用空间的大小取中间分布较均匀的几位,散列到相应的位置。
    这样计算的原因是因为关键字的大多数位或所有位对结果都有贡献,并且通过取平方扩大差别,平方值的中间几位(位数可用lgN计算)和这个数的每一位都相关,则对不同的关键字得到的哈希函数值不易产生冲突,由此产生的哈希地址也较为均匀。

2.冲突的解决

  • 开发地址法(再散列法): 主要有以下几种再散列的方式:

    • 线性探测法:从产生冲突的位置开始,一个一个往下探测,看是否存有数据,没有则存入
    • 二次探测法:从产生冲突的位置开始,在左右两边进行跳跃式探测
    • 伪随机探测法:建立一个伪随机数生成器,生成探测位置
  • ②链地址法: HashMap的实现方式,用数组当桶,发生冲突时,在桶中以链表链接

二、HashMap

2.1 HashMap的结构

在JDK1.8中,HashMap使用链地址法的方式来实现,其先用一个数组作为桶,然后桶中存放的要么是链表(链表的节点个数小于等于8),要么是红黑树,要么为空。整个数据结构如下所示:秋招准备之——深入理解HashMap(JDK1.8)_第6张图片

2.2 扩容方法

HashMap每次扩容,容量都是2的幂次方下面的方法是获得大于cap且最近的2的整数次幂的数。如输入10,则返回16。其中cap-1的的目的是另找到的目标值大于或等于原值。例如二进制1000,十进制数值为8。如果不对它减1而直接操作,将得到答案10000,即16。显然不是结果。减1后二进制为111,再进行操作则会得到原来的数值1000,即8。
秋招准备之——深入理解HashMap(JDK1.8)_第7张图片
举个栗子,如果传入参数10,那计算的过程如下:
秋招准备之——深入理解HashMap(JDK1.8)_第8张图片

2.3 对象hash值和在桶中位置的计算方法

秋招准备之——深入理解HashMap(JDK1.8)_第9张图片
举个栗子:
在这里插入图片描述
这样做的好处是,可以将hashcode高位和低位的值进行混合做异或运算,而且混合后,低位的信息中加入了高位的信息,这样高位的信息被变相的保留了下来。掺杂的元素多了,那么生成的hash值的随机性会增大。

知道hash值后,根据hash值来计算其在数组中的位置,计算公式为:

pos = (n - 1) & hash非常重要),其中n为数组的长度。由于null对象不能计算hash值,所以null对象都放在桶中的固定位置,HashMap中使用第0个桶来存放键为null的键值对。

2.4 查找元素的过程

先根据keyhashCode计算得到hash值,然后利用pos = (n - 1) & hash计算其在数组中的位置,并在该位置中看,该位置存的是链表还是红黑树,然后再在链表/红黑树中进行查找。
秋招准备之——深入理解HashMap(JDK1.8)_第10张图片

2.5 添加元素的过程(put操作)

主要通过putVal()函数实现,putVal的过程中,如果没有达到链表的阈值长度(默认是8),则直接加入到链表的尾部。如果链表长度超过8,则将链表变化为红黑树。代码如下,必要的地方都做了详细的注释:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            HashMap.Node<K,V> e; K k;
            if (p.hash == hash &&  //这种情况下,说明放入了重复元素,需要根据key更新value
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof HashMap.TreeNode)//这种情况下,在红黑树中插入节点
                e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {//这种情况下,在链表中插入节点
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {//将新节点插入到链表的末尾
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) //这里链表长度大于阈值,需要将链表转为红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&//链表中已经存在key了,需要根据key更新value
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // 链表中存在key的值的更新
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;//增加修改次数
        if (++size > threshold)
            resize();//如果容量大于阈值了,就去扩容
        afterNodeInsertion(evict);
        return null;
    }

2.6 扩容的过程

通过resize函数将数组扩容为原来容量的2倍后,需要重新将原有的元素映射到数组中,映射时,分以下三种情况:

  • 如果原来的位置只有一个节点,直接通过上面的pos = (n - 1) & hash计算其在数组中 的位置。

  • 如果是链表,进行链表的rehash时,根据hash & oldCap的结果是0还是1,将链表拆分成两部分。
    举个栗子(栗子来自于别人的博客,我找不到出处了):如果原数组的容量为16,那n-1=15,然后有两个Entry,第一个Entry的key的hashCode值为1111 1111 1111 1111 0000 1111 0000 0101,第二个Entry的key的hash值为1111 1111 1111 1111 0000 1111 0001 0101,那在扩容之前,通过pos = (n - 1) & hash这个公式计算得到,他们在桶中的位置索引都是5(二进制为00101),当扩容后,数组长度变成原来的两倍即32,那n-1=31,用二进制表示就是1 1111,然后通过pos = (n - 1) & hash这个公式,就能把上面两个节点分成两个部分。具体的看第二张图的红字部分。
    秋招准备之——深入理解HashMap(JDK1.8)_第11张图片
    秋招准备之——深入理解HashMap(JDK1.8)_第12张图片

  • 如果是红黑树,就调用红黑树的split()方法,此处,会对红黑树也和链表一样分为高位和低位两个部分,如果树中元素的个数小于6个了,就将红黑树转换成链表

  • 扩容过程的代码如下:

    final HashMap.Node<K,V>[] resize() {
            HashMap.Node<K,V>[] oldTab = table;
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            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; // 新的threshold也变成原来的2倍(装载因子没变,容量扩大二倍,阈值自然也扩大二倍)
            }
            else if (oldThr > 0) //容量在threshold之内
                newCap = oldThr;
            else {               // 使用默认容量
                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"})
            HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];//开辟新的数组空间
            table = newTab;
            if (oldTab != null) {
                for (int j = 0; j < oldCap; ++j) {
                    HashMap.Node<K,V> e;
                    if ((e = oldTab[j]) != null) {
                        oldTab[j] = null;
                        if (e.next == null)//原来桶中只有一个元素,直接利用pos = (n - 1) & hash的公式重新计算在数组中的位置
                            newTab[e.hash & (newCap - 1)] = e;
                        else if (e instanceof HashMap.TreeNode)//原来桶中是红黑树,分裂红黑树
                            ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        else { // 原来桶中是链表,进行链表的rehash
                            HashMap.Node<K,V> loHead = null, loTail = null;
                            HashMap.Node<K,V> hiHead = null, hiTail = null;
                            HashMap.Node<K,V> next;
                            // 将同一桶中的元素根据(e.hash & oldCap)是否为0进行分割,分成两个不同的链表,完成rehash
                            do {
                                next = e.next;
                                //(e.hash & oldCap) == 0分为一个链表部分
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail == null)
                                        loHead = e;
                                    else
                                        loTail.next = e;
                                    loTail = e;
                                }
                                //(e.hash & oldCap) == 1分为一个链表部分
                                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) {//在数组中的位置后移oldCap个位置
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            return newTab;
        }
    

    什么时候扩容: 当前容器中元素的个数(是HashMap中的size,而不是使用桶的个数)超过阈值(当前数组长度乘以加载因子的值)的时候,就要自动扩容。

其中,上面说的链表的方法很好理解,再来看看红黑树的split方法:

final void split(HashMap<K,V> map, HashMap.Node<K,V>[] tab, int index, int bit) {
    HashMap.TreeNode<K,V> b = this;
    // Relink into lo and hi lists, preserving order
    HashMap.TreeNode<K,V> loHead = null, loTail = null;
    HashMap.TreeNode<K,V> hiHead = null, hiTail = null;
    int lc = 0, hc = 0;
    //在这里,根据与bit(即oldCap)进行&操作的结果是否为1,将树修剪为两部分
    for (HashMap.TreeNode<K,V> e = b, next; e != null; e = next) {
        next = (HashMap.TreeNode<K,V>)e.next;
        e.next = null;
        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) {
        //低位树的元素个数小于阈值(6个),将红黑树转换成链表
        if (lc <= UNTREEIFY_THRESHOLD)
            tab[index] = loHead.untreeify(map);
        else {
            //低位的红黑树不变
            tab[index] = loHead;
            if (hiHead != null) //高位不为空,说明低位的树结构已经破坏了,需要对loHead重新树化
                loHead.treeify(tab);
        }
    }
    //同上
    if (hiHead != null) {
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
                hiHead.treeify(tab);
        }
    }
}

2.7 链表转红黑树的过程

在桶中的链表长度超过阈值(默认是8)时,就会将其转换成红黑树。转换过程分为两步:

  • 1.将链表节点转换成树节点表示的链表

    final void treeifyBin(HashMap.Node<K,V>[] tab, int hash) {
            int n, index; HashMap.Node<K,V> e;
            //在转换为红黑树时,需要判断一下当前数组的长度是否小于最小的树化容量(64),小于的话先去扩容
            if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
                resize();
            else if ((e = tab[index = (n - 1) & hash]) != null) {
    
                HashMap.TreeNode<K,V> hd = null, tl = null;
                do {//遍历链表,将链表的节点全部转换成树节点,形成一个新的前驱链表(prev指针)
                    HashMap.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);
            }
        }
    
  • 2.树节点表示的链表转为红黑树

    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);
            }
            final void treeify(Node<K,V>[] tab) {//将链表转换成红黑树
                TreeNode<K,V> root = null;
                for (TreeNode<K,V> x = this, next; x != null; x = next) {//遍历链表中的每一个TreeNode,当前结点为x
                    next = (TreeNode<K,V>)x.next;
                    x.left = x.right = null;
                    if (root == null) {         //对于第一个树结点,当前红黑树的root == null,所以第一个结点是树的根,设置为黑色
                        x.parent = null;
                        x.red = false;
                        root = x;
                    }
                    else { //对于余下的结点:
                        K k = x.key;
                        int h = x.hash;
                        Class<?> kc = null;
                        for (TreeNode<K,V> p = root; ; ) {//从根结点开始遍历,寻找当前结点x的插入位置
                            int dir, ph;
                            K pk = p.key;
                            if ((ph = p.hash) > h)   //如果当前结点的hash值小于根结点的hash值,方向dir = -1;
                                dir = -1;
                            else if (ph < h)                //如果当前结点的hash值大于根结点的hash值,方向dir = 1;
                                dir = 1;
                            else if ((kc == null &&         //如果x结点的key没有实现comparable接口,或者其key和根结点的key相等(k.compareTo(x) == 0)仲裁插入规则
                                      (kc = comparableClassFor(k)) == null) ||      //只有k的类型K直接实现了Comparable接口,才返回K的class,否则返回null,间接实现也不行。
                                     (dir = compareComparables(kc, k, pk)) == 0)
                                dir = tieBreakOrder(k, pk);         //仲裁插入规则
    
                            TreeNode<K,V> xp = p;
                            if ((p = (dir <= 0) ? p.left : p.right) == null) {      //如果p的左右结点都不为null,继续for循环,否则执行插入
                                x.parent = xp;
                                if (dir <= 0)           //dir <= 0,插入到左儿子
                                    xp.left = x;
                                else            //否则插入到右结点
                                    xp.right = x;
                                root = balanceInsertion(root, x);   //插入后进行树的调整,使之符合红黑树的性质
                                break;
                            }
                        }
                    }
                }
                moveRootToFront(tab, root);         //Ensures that the given root is the first node of its bin.
            }
    }
    

    其中,仲裁插入规则的过程如下: 先比较两个对象的类名,类名是字符串对象,就按字符串的比较规则。如果两个对象是同一个类型,那么调用本地方法为两个对象生成hashCode值,再进行比较,hashCode相等的话返回-1

    static int tieBreakOrder(Object a, Object b) {
      int d;
      if (a == null || b == null ||
          (d = a.getClass().getName().
           compareTo(b.getClass().getName())) == 0)
        d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
             -1 : 1);
      return d;
    }
    

2.8 移除元素的过程

移除元素时,先通过key找到对应的结点,然后再分为链表、红黑树两种情况去处理。需要注意的是,红黑树转换成链表的操作,在resize中和remove中是不同的,resize中是当红黑树中的节点少于6个时就将红黑树转为链表,而remove函数中,是当红黑树的根节点的左孩子或右孩子为空时,才将红黑树转换成链表的,此处的代码如下:
秋招准备之——深入理解HashMap(JDK1.8)_第13张图片
整个移除元素的过程如下:

final HashMap.Node<K,V> removeNode(int hash, Object key, Object value,
                                   boolean matchValue, boolean movable) {
    HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
        HashMap.Node<K,V> node = null, e; K k; V v;
        //在HashMap中查找要移除的节点
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            if (p instanceof HashMap.TreeNode)
                node = ((HashMap.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);
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value ||
                (value != null && value.equals(v)))) {
            //红黑树中删除节点
            if (node instanceof HashMap.TreeNode)
                ((HashMap.TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            //删除的节点是链表中的第一个节点
            else if (node == p)
                tab[index] = node.next;
            //删除的节点是链表中的中间节点
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

2.9 HashMap为什么线程不安全

  • put时导致数据覆盖: 在往HashMap中插入数据时,如果桶中的数据为空,则直接new一个元素,放入桶中,但如果在并发条件下,第一个线程判断桶中无元素,则new一个新节点,但此时该线程被挂起。然后另一个线程直接在此处放了一个新节点进去,然后线程1被唤醒后,不知道此位置已经有新元素了,会直接放数据进去,此时会将原来线程放的数据覆盖。此外,在put的时候涉及到size++的操作,可能会导致HashMap的size出错

2.10 和HashTable的区别

HashTable通过加synchronized关键字,实现线程安全。

三、LinkedHashMap

3.1 介绍

LinkedHashMap继承自HashMap,其在HashMap的基础上,增加了一条双向链表,使得可以保持键值对的插入顺序。如下所示:
秋招准备之——深入理解HashMap(JDK1.8)_第14张图片
需要注意的是,HashMap中的Entry,继承自原来HashMap的Node,增加了beforafter两个字段,用于维护双向链表。

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

除此之外,为了记录插入顺序,在LinkedHashMap中,增加了头结点和尾节点两个变量,每当有新键值对节点插入,新节点最终会接在 tail 引用指向的节点后面。而 tail 引用则会移动到新的节点上,这样一个双向链表就建立起来了。
秋招准备之——深入理解HashMap(JDK1.8)_第15张图片

2. 链表的建立过程

插入数据时,使用的是HashMap的put方法,但是重写了newNode方法,在这里建立链表的关系。
秋招准备之——深入理解HashMap(JDK1.8)_第16张图片

3. 删除元素的过程

删除元素时,使用的是HashMap的remove方法,但是重写了afterNodeRemoval方法,在这里维护链表的关系。

// LinkedHashMap 中覆写
void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    // 将 p 节点的前驱后后继引用置空
    p.before = p.after = null;
    // b 为 null,表明 p 是头节点
    if (b == null)
        head = a;
    else
        b.after = a;
    // a 为 null,表明 p 是尾节点
    if (a == null)
        tail = b;
    else
        a.before = b;
}

4. 访问元素的过程

在访问元素的时候,有一个accessOrder属性,当其为true时,每次访问一个元素,都会将这个访问的元素放到尾部,保证链表尾部是最常访问的数据,可用在缓存等场景中。LinkedHashMap使用父类的HashMap来实现元素的访问,当时重写了afterNodeAccess方法,这样如果accessOrdertrue时,就会调用afterNodeAccess方法,来按照访问来维护链表的顺序

// LinkedHashMap 中覆写
void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        // 如果 b 为 null,表明 p 为头节点
        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 接在链表的最后
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}

5.基于LinkedHashMap的缓存实现

基于LinkedHashMap,可以实现LRU缓存。在HashMap的putVal函数最后,会调用afterInsertion函数。而LinkedHashMap重写了这个函数

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    // 根据条件判断是否移除最近最少被访问的节点
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

// 移除最近最少被访问条件之一,通过覆盖此方法可实现不同策略的缓存
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

如果想用LinkedHashMap来实现LRU缓存,就直接继承LinkedHashMap,然后重写removeEldestEntry函数,设置在某些条件下让其返回true(比如当前集合的元素数量大于某一阈值,就返回true,否则返回false)。这样,当条件满足时,就会删除头节点。

你可能感兴趣的:(复习)