当前位置: 首页 > news >正文

【Java集合】LinkedHashSet源码深度分析

参考笔记:java LinkedHashSet 源码分析(深度讲解)-CSDN博客


目录

一、前言

二、LinkedHashSet简介

三、LinkedHashSet底层实现

四、LinkedHashSet的源码解读

        0. 准备工作

        1. 向集合中添加第一个元素

                ① 跳入无参构造

                ② 跳入resize方法

                ③ 跳出resize方法,回到 putVal 方法

                ④ 回到演示类

        2. 继续向集合添加元素

                ① 向集合中添加重复元素

                ② 向集合中添加第二个元素

                ③ 向集合中添加第三个元素

                ④ 向集合中添加第四个元素

                ⑤ 向集合中添加第五个元素(重要)

五、完结


一、前言

        本篇博文是对集合篇章——单列集合 Set 的内容补充。 Set 集合常见的实现类有两个——HashSet、TreeSet。在我的另一篇博文中已经分析了 HashSet 的源码,知道了  HashSet 的底层其实就是 HashMap 。链接如下:

【Java集合】HashSet源码深度分析-CSDN博客https://blog.csdn.net/m0_55908255/article/details/146999979?spm=1011.2415.3001.5331       本文要解读的是 HashSet 的一个子类——LinkedHashSet,非常建议先阅读一下HashSet 源码分析,因为 LinkedHashSet、HashSet 底层调用的方法几乎一致,只是有略微的差别

        注意:本文对 HashSet 源码的解读基于主流的 JDK 8.0 的版本

二、LinkedHashSet简介

LinkedHashSetHashSet 的子类,而由于 HashSet 实现了 Set 接口,因此 LinkedHashSet 也间接 implements Set 接口。LinkedHashSet 类位于 java.util.LinkedHashSet 下,其类定义和继承关系图如下:

三、LinkedHashSet底层实现

LinkedHashSet 在底层会用到一个 HashMap$Node[ ] 类型的 table 表( Node 类是 HashMap 中维护的一个静态内部类),该 table 表即用来存储元素,这一点和 HashSet 是一样的。(实际上在通过 add 方法添加元素时,LinkedHashSet、HashSet 底层都是走的 HashMapput 方法) table 属性的定义如下:

由于 table 属性是由 HashMap 类维护的,所以,无论是 HashSet 还是 LinkedHashSet ,都需要先成功访问到 HashMap 。以 HashSet 为例,HashSet 中维护了一个 HashMap<E,Object> 类型的 map 属性,而 HashSet 的构造器中对该 map 属性进行了初始化。

如此一来,HashSet 可以借助该 map 对象即可访问到 HashMap 中维护的 table 属性。如下图所示:

LinkedHashSet 的父类是 HashSet ,因此该 map 属性自然可以继承给 LinkedHashSet ,所以LinkedHashSet、HashSet 都是通过 private transient HashMap<E, Object> map 来间接调用 HashMap 中的内容

只不过 HashSet 的构造器中是直接将 map 置为了一个 HashMap 类型的对象,而在 LinkedHashSet 的构造器中,却是使用多态的方式,将 map 置为了一个 LinkedHashMap 类型的对象( LinkedHashMap 继承自 HashMap ,如此一来,亦可借助 map 对象访问到 HashMap 中维护的 table 数组,因为 table 数组是非私有的),如下图所示 : 

② LinkedHashSet 通过 headtail 维护了一个双向链表,head、tailLinkedHashMap 中的两个属性

head:指向双向链表头结点的指针

tail:指向双向链表尾结点的指针

此处的 EntryLinkedHashMap 的一个静态内部类,它继承了 HashMap 的一个静态内部类 NodeEntry、Node 的定义如下:

如上图所示,每个 Entry 结点中维护了 before,after 两个属性,其中通过 before 指向前一个结点,通过 after 指向后一个结点

LinkedHashMap$Entry 类又继承自 HashMap$Node 类。在 Entry 类的构造器中,通过super(hash,key,value,next) 调用 Node 类的构造器。由此可知,与 HashSet 集合一致,在使用 LinkedHashSet 集合时:

  • key:存放加入到 LinkedHashSet 集合中的元素
  • hash:存放元素的哈希值
  • next:指向挂载在同一链表下的后面一个结点,如果没有,则 next = null
  • value:存储 PRESENT占位符,无实际意义,PRESENT 占位符是 HashSet 类的一个属性,如下:

④ LinkedHashSet 的底层其实就是 LinkedHashMap,关于这一点,可以类比 HashSet 的底层是 HashMap

⑤  LinkedHashSet 在添加元素时的底层规则和 HashSet 高度一致,在后续的源码解读部分可以看到。仍然是先求出添加元素的 hash 值,然后根据特定算法将其转换一个索引值这个索引值决定该元素在集合中应该存放的位置

⑥  得到元素 hash 值,将其转换为索引值后,添加元素的规则:

  • 当索引值对应的位置没有元素存在时:直接将当前元素加入集合

  • 当索引值对应的位置有元素存在时,调用 equals 方法判断当前添加元素与该位置处的元素是否相等

    • 相等:放弃添加该元素(因为 LinkedHashSet 不允许重复)

    • 不相等:将当前元素添加到(挂到)该位置处对应的链表的最后。这便实现了  "数组+链表" 的结构。如下图所示:

说明:LinkedHashSet 集合如何添加元素如何判断重复元素扩容机制链表转换为红黑树HashSet 是完全一致的 

四、LinkedHashSet的源码解读

        0. 准备工作

        用以下代码作为演示类,一步一步 Debug :

import java.util.LinkedHashSet;

public class demo {
    public static void main(String[] args) {
        LinkedHashSet linkedHashSet = new LinkedHashSet();

        linkedHashSet.add(141);
        linkedHashSet.add(141);//重复元素,放弃添加
        linkedHashSet.add("CSDN");
        linkedHashSet.add(11);
        linkedHashSet.add(new Apple("红富士1"));
        linkedHashSet.add(new Apple("红富士2"));
    }
}

class Apple {
    private String name;

    public Apple(String name) {
        this.name = name;
    }
        
    //所有Apple对象实例都返回相同的哈希码值
    @Override
    public int hashCode() {
        return 100;
    }
}

        1. 向集合中添加第一个元素

                ① 跳入无参构造

                首先跳入 LinkedHashSet 的无参构造,由于内部嵌套的构造器比较器多,所以我以流程图展示,如下图所示 :

                可以看到,调用 LinkedHashSet 的无参构造,最终是走到了 HashMap 的构造器 public HashMap(int initialCapacity,float loadFactor) 中,最后两句赋值语句中的 loadFactor 即默认增长因子threshold 即临界值,看过 HashSet 集合的源码分析都知道这两个属性,这里就不再赘述了

        最后一行给临界值 threshold 赋值是调用了 tableSizeFor(intitialCapacity) 方法,我们追进去看看,其源码如下所示:

                源码中的 n |= n >>> 1 相当于 n = n | ( n >>> 1 )  ,这里大家可以自己计算一下,经过中间这几行 n 还是为 15,没有任何改变。我们只需要关注最后一行的 return 语句即可 。|  和 >>> 不懂的可以看我写的一篇博客的 "5.2位运算符" 部分,链接如下:

【Java SE】基础知识1-CSDN博客https://blog.csdn.net/m0_55908255/article/details/145900460?spm=1011.2415.3001.5331                return 语句的返回值是一个双重复合的三目运算符。什么意思呢?就是如果前面三目运算符的判断条件 (n < 0) 成立,就返回 1 ,否则返回后面三目运算符的结果。显然前面运算符的判断条件 n < 0 显然不成立,所以要返回后面三目运算符的结果;

                后面的三目运算符的判断条件 n >= MAXIMU_CAPACITY = 1073741824,显然不成立。所以 return 语句最后返回的值就是 n + 1 = 16

                跳出 tableSizeFor 方法,如下 :

                🆗,接下来我们逐层返回,跳出无参构造器,回到演示类中,查看此时集合的状态,如下图所示:

                可以看到,此时的 map 对象是 LinkedHashMap 类型,用来存放元素的 table 数组为 null,数组元素个数 size = 0 ,临界值 threshold = 16

                ② 跳入resize方法

                准备向集合中添加第一个元素141 。注意, LinkedHashMapHashMap 在底层添加元素时,几乎完全一样,前面几步跳入 add方法 ——> 跳入put方法 ——> 跳入putVal方法二者是完全一致的。所以这里就不再赘述了。在我的另一篇博客 HashSet 源码深度分析中已经非常详细地 Debug 过了

                 我们直接跳到比较重要的地方。在 putVal 方法中,第一个 if 语句满足判断,如下 :

                可以看到,我们要进入 resize 方法,resize 方法也是 "老演员" 了,它的作用就是table 数组进行扩容resize 方法要返回一个 Node 类型( HashMap$Node )的数组给 tab 数组。我们跳入 resize 方法,其源码如下:

//table数组扩容
final Node<K,V>[] resize() {
    //oldTab记录旧数组,此时table = null ,因此 oldTab = null
    Node<K,V>[] oldTab = table;

    //记录旧数组的长度(注意,不是元素个数),oldCap = 0
    int oldCap = (oldTab == null) ? 0 : oldTab.length;

    //记录就旧数组的临界值,oldThr = threshold =  16
    int oldThr = threshold;

    //newCap记录新数组的长度,newThr记录新数组的临界值
    int newCap, newThr = 0;

    if (oldCap > 0) {//oldCap = 0,不跳入该if语句
        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
    }
    //oldThr = 16 > 0,执行该 else if 语句
    else if (oldThr > 0) // initial capacity was placed in threshold
        //newCap = oldThr = 16
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //newThr = 0,跳入该 if 结构
    if (newThr == 0) {
        //新临界值 = 新数组长度 * 增长因子 = 16 * 0.75 = 12
        float ft = (float)newCap * loadFactor;
        //这里是作一个健壮性判断,最终newThr = 12
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                (int)ft : Integer.MAX_VALUE);
    }

    //将新临界值赋值给 threshold 属性,threshold = newThr = 12
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    //创建一个容量为 newCap = 16 的新数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //将 table 数组赋值为 newTab
    table = newTab;

    /*
     后续就是如果原数组不为空,则将原数组中的内容拷贝到新数组中
     由于此时 oldTab = null ,所以不会执行跳入该 if 结构
    */
    if (oldTab != null) {
        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;
}

                 前面几步都还和 HashSet 集合添加第一个元素时一样,如下 :

               将 table 数组引用赋值给了一个 Node 类型的数组 oldTab ,即 oldTab 引用现在也是null 了。第二行又用一个三目运算符最终将 0 赋值给了 oldCap 变量

                下一步开始就要不一样了,如下:

                注意看,不知道大家还记不记得,在 HashSet 的源码分析中,第一次添加元素时,这里的 threshold = 0 。而 LinkedHashSet 第一次添加元素时,threshold = 16,原因我们在 "①跳入无参构造" 已经看到:在调用 LinkedHashSet 的无参构造时,底层会将 threshold 初始化为 16

                继续往下执行,如下:

              可以看到,首先执行 oldThr = threshold = 16,将 threshold 赋值给 oldThr 变量,然后又定义了 newCap = newCapacity,见名知意,即新数组的长度newThr = newThreShold,即新数组的临界值。第三行的 if 语句显然判断不成立,不进入它。但后面的 else if 语句的判断是成立的,如下 :

                else if 语句中,newCap = oldThr = 16,所以 LinkedHashSet 集合和 HashSet 集合一样,第一次扩容都是将 table 数组的长度扩大至 16
                继续,接下来的一个 if 语句如下:

                由于 threshold 的改变,我们并没有像 HashSet 那样进入 if --- else if --- else 中的 else 语句,而是进入了 else if 语句,所以此时 newThr 变量还是默认值 0 ,因此跳入上图的 if 语句中,先是计算 ft = 新数组容量 * 增长因子 = 16 * 0.75 = 12,再利用三目运算符作一个健壮性判断,最终新数组的临界值 newThr = 12

                 继续往下执行,如下图 :

                后面几步就都一样了。threshold = newThr = 12,即将临界值 12 赋值给 threshold 属性,关于为什么要设置临界值 threshold ,这里就不再赘述了

                接着,又是 new 一个长度为 16Node (HashMap$Node) 类型数组,然后将新数组的地址赋给了 newTable 引用,并由 newTab 引用传递给 table 。如下:

                到此, table 已经由 null 变为了长度为 16 的数组 ,如下图所示:

                再往下是一个非常大的 if 条件语句,如下 :  

                该 if 语句的作用是:如果旧数组不为空,则需要将旧数组中的元素全部拷贝到新数组中。 由于此时旧数组 oldTab = null,因此条件不成立,不执行

                OK,这下 resize 方法执行完了,返回 new 出的新数组,如下 :  

                ③ 跳出resize方法,回到 putVal 方法

                执行完 resize 函数,我们先回到 putVal 方法,如下 :

                可以看到, n = 16 ,即新数组的长度

                与 HashSet 一样,仍然是根据当前元素的 hash 值:141,通过算法 [ i = (n-1) & hash ] 获得当前欲添加元素在 table 数组中应该存放的索引位置。然后判断,如果 table 数组该索引处为空,就直接放进去;不为空的话就去下面的 else 语句,去链表中一一进行判断,如果不与链表中的元素重复,则挂载到链表尾部;如果与链表中的某个元素重复,则放弃添加

                当前欲添加元素 141 计算得到的索引位置为  i = (n-1) & hash = 13,如下图所示:

                因为 141 是集合添加的第一个元素,所以集合的对应索引处肯定为 null ,条件满足,继续执行 if 中的语句,"tab[i] = tab[13] = newNode(hash,key,value,null)",直接将该元素加入 table 数组中索引为 13 的位置

                这里需要注意,由于 LinkedHashSet 的底层实现是 LinkedHashMap,所以这里调用的 "newNode(hash,key,value,null)" 方法是 LinkedHashMap 中的 newNode ,其源码如下:

                由于 LinkedHashSettable 数组中每个结点类型是 LinkedHashMap$Entry ,所以在上图中可以看到,newNode 方法中先创建一个 LinkedHashMap$Entry 类型结点,并存储 hash 属性:141, key 属性:141, value 属性:PRESENT占位符, next 属性:null。这里我们再看一下 Entry、Node 的定义,如下:

                接着 newNode 方法中调用 linkNodeLast 方法处理 Entrybefore、after 属性,使得结点以双向链表的形式连接起来 

                OK,逐层返回到 putVal 方法, tab[i] = tab[13] = newNode (hash,key,value,null) 语句执行完毕

                继续往下执行,如下图所示:

                1° modCount 老演员了,表示修改集合的次数
                2° if 语句,判断当前集合中元素的个数 size 是否超过了临界值 threshold ,如果超过临界值就调用 resize 方法对 table 数组进行扩容
                3° afterNodeInsertion (true) ,调用的是 LinkedHashMap 中的 afterNodeInsertion 方法,该方法在插入新结点后触发,用于移除最老结点(eldest entry,即双向链表的第一个结点)。但是在默认情况下,afterNodeInsertion 内部调用的 removeEldestEntry 方法的返回值是 false ,不执行移除,所以此处的 afterNodeInsertion 方法相当于什么都没做

                到这, putVal 方法也结束,并最终返回了 null ,代表添加元素成功 

                ④ 回到演示类

                从 putVal 方法逐层返回到演示类中,此时的 LinkedHashSet 集合状态如下:

                可以看到,table 数组成功初始化为长度 = 16 的数组, 141 元素也成功添加到了集合索引为 13 的位置

                另外很重要的是,可以看到 table 数组是 Node(HashMap$Node)  类型,但是里面保存的元素却是 Entry(LinkedHashMap$Entry)  类型。一个类型的数组里面存放了另一类型的元素,请问,你想到了什么?😎!没错,多态数组!!!这里我们再看一下这两个类的定义:

                因为 Entry 继承了 Node ,所以一个父类的引用可以指向子类的对象 

🆗,向 LinkedHashSet 集合添加第一个元素完毕

        2. 继续向集合添加元素

                ① 向集合中添加重复元素

                当我们重复添加 141 元素时,肯定无法加入。判断重复元素的底层逻辑 和 HashSet 是完全一样的,这里就不再演示了。大家可以 Debug 一下看看

                这里看一下执行该行代码之后的集合状态,如下:

                可以看到,此时集合中仍然只有一个元素 141size = 1

                ② 向集合中添加第二个元素

                继续向下执行,将 "CSDN" 元素加入集合中。此时集合的状态如下所示:

                可以看到,目前 table 数组中有两个元素。注意,记住这两个元素目前的标识:141 的标识是 576 ,"CSDN" 的标识是 606

                注意,重点的来了,如下:

                我们点开添加的第一个元素,可以看到,此时第一个元素 141after 属性指向了第二个元素 "CSDN" ,而第二个属性的 before 属性则指向了第一个元素 141 ;并且 141 元素的 before 属性和 "CSDN" 元素的 after 属性均为 null 。此时, table 数组中的两个元素已然形成了一个简单的双向链表

                并且,我们还可以看到 maphead、tail 属性分别指向了双向链表的第一个元素 141 和最后一个元素 "CSDN",如下图所示:

                此时,linkedHashSet 的底层 "数组+链表" 结构如下图所示:

                ③ 向集合中添加第三个元素

                继续向下执行,将元素 11 加入到集合中,此时集合的状态如下所示:

                此时,linkedHashSet 的底层 "数组+链表" 结构如下图所示:

                由于元素 11 是最后添加的,所以 tail 尾指针指向它

                ④ 向集合中添加第四个元素

                继续向下执行,向集合中添加 new Apple("红富士1"),此时集合的状态如下所示:

                可以看到,new Apple ("红富士1")  存放在 table 数组的 4 索引处,标识为 624  

                此时, linkedHashSet 的底层 "数组+链表" 结构如下图所示: 

                由于元素 new Apple("红富士1")  是最后添加的,所以 tail 尾指针指向它 

                 向集合中添加第五个元素(重要)

                继续向下执行,向集合中添加 new Apple("红富士2") 

                注意,由于我们没有在 Apple 类重写 equals 方法,因此两个 Apple 对象会被判定为不同的元素,可以加入集合;由于在 Apple 类中重写了 hashCode 方法,因此这两对象最终得到的哈希值一样,因此它们会挂载到同一链表下。在前面的 ④ 已经知道,new Apple("红富士1") 存放在 table 数组的 4 索引处,标识为 624 ,因此 new Apple("红富士2")  将挂载到 table 数组索引 4 处的链表

                挂载到同一链表下的过程与 HashSet 中是完全一致的,这里就不再赘述,我们直接看添加完该元素之后的集合状态,如下图所示:

               可以看到,新添加的元素 new Apple("红富士2") 与标识为 624 的元素 new Apple("红富士1") 确实挂载在了同一链表下

                此时, linkedHashSet 的底层 "数组+链表" 结构如下图所示: 

                看着可能有点乱,但是看准 head、tail 和每个结点的 before、after ,还是可以很轻松找到顺序的,这里添加的顺序是:141 --> "CSDN" --> 11 --> new Apple("红富士1") --> new Apple("红富士2")

                这里还要说明一下 afternext 的区别

                1° after是用于双向链表中,专门指向下一个元素,没有下一个元素则为 null 。不管某一个结点的下一个元素是在 table 数组中其他索引处的位置,还是挂载在该结点的后面,after 都会指向它

                2° next 则和我们在 HashSet 中分析的一样,如果数组某一个索引处的元素形成了链表, next 会指向链表中的下一个元素

                3° 比如上图所示的元素 new Apple("红富士1"),它的 after 指向元素 new Apple("红富士2") 。由于元素 new Apple("红富士2") 恰好挂载在了它的后面,即它们在 table 数组同一索引处的位置,所以元素 new Apple("红富士1")next 属性也指向元素 new Apple("红富士2")

                4° 简单来说after 是针对了整个双向链表,针对于所有元素,针对于全局;而 next 则是仅仅针对于同一索引位置处形成的单向链表,针对于 table 数组同一索引位置处的元素,针对于局部

                5° 从源码角度分析,next 属性由 HashMap$Node 类维护,而 afterLinkedHashMap$Entry 类维护

                所以,通过 Debug 和底层的"数组+链表"结构图看到,在 table 数组的所有元素中,只有第一个 Apple 对象元素 new Apple("红富士1")  的 next 属性有指向,且指向和它的 after 属性一样,指向了挂载在它后面的第二个 Apple 对象元素 new Apple("红富士2")

五、完结

        🆗,以上就是本文 LinkedHashSet 源码分析的全部内容了。本文主要针对于与 HashSet 的差异展开讲解,如果对 HashSet 的源码比较熟悉,那看本文比会比较容易。由于 LinkedHashSet 集合如何判断重复元素扩容机制链表转换为红黑树HashSet 是完全一致的,所以本文不再赘述

相关文章:

  • 理解企业内部信息集成
  • AcWing 166.数独
  • C++基础精讲-04
  • 对称加密与非对称加密与消息摘要算法保证https的数据交互的完整性和保密性
  • <C#>在 C# .NET 6 中,使用IWebHostEnvironment获取Web应用程序的运行信息。
  • 谷歌闭源Android后的生态变局与数据库国产替代的必要性——以金仓数据库为例
  • 出口商品贸易方式企业性质总值数据库
  • ReentrantLock 实现公平锁和非公平锁的原理!
  • swift菜鸟教程6-10(运算符,条件,循环,字符串,字符)
  • 2025年第十八届“认证杯”数学中国数学建模网络挑战赛【BC题】完整版+代码+结果
  • 深入剖析观察者模式:原理、实践与 Spring 源码解读
  • 深度学习总结(8)
  • CSS高级技巧
  • 使用治疗前MR图像预测脑膜瘤Ki-67的多模态深度学习模型
  • 【Qt】QxOrm:下载、安装、使用
  • 界面控件DevExpress WinForms v25.1新功能预览 - 聚焦用户体验升级
  • 如何应对“最后时刻任务堆积”(鼓包现象)
  • 《Vue Router实战教程》5.嵌套路由
  • 二叉树的应用
  • Dubbo、HTTP、RMI之间的区别
  • 怎么查网站是哪家制作公司做的/建站优化公司
  • 深圳集团网站建设服务/本地推广平台有哪些
  • 淮安哪里做网站/随州网络推广
  • 珠海响应式网站建设费用/推广软文范文
  • 室内装修设计资质/seo软件工具箱
  • 外贸企业招聘/惠州seo推广外包