Java中LinkedHashSet的源码分析

 更新时间:2023年09月05日 09:52:04   作者:_雨_  
这篇文章主要介绍了Java中LinkedHashSet的源码分析,LinkedHashSet 是 Java 中的一个集合类,它是 HashSet 的子类,同时也实现了 Set 接口,与 HashSet 不同的是,LinkedHashSet 保留了元素插入的顺序,并且具有 HashSet 的快速查找特性,需要的朋友可以参考下

LinkedHashSet的基本介绍

LinkedHashSet 是 Java 中的一个集合类,它是 HashSet 的子类,同时也实现了 Set 接口。与 HashSet 不同的是,LinkedHashSet 保留了元素插入的顺序,并且具有 HashSet 的快速查找特性。

LinkedHashSet 继承了 HashSet,所以它是在 HashSet 的基础上维护了元素添加顺序的功能。

它的构造方法是 LinkedHashSet()。 LinkedHashSet 是一个基于 LinkedHashMap 实现的有序去重集合列表。

它中的元素没有重复,有顺序,并且可以存储 null 值。

需要注意的是,LinkedHashSet 是一个线程不安全的容器。

  1. LinkedHashSet是HashSet的子类
  2. LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双向链表
  3. LinkedHashSet 根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的。
  4. LinkedHashSet不允许添重复元素

LinkedHashSet源码分析

1.在LinkedHastSet 中维护了一个hash表和双向链表(LinkedHashSet有head和tail)

2.每一个节点有pre和next属性,这样可以形成双向链表

3.在添加一个元素时,先求hash值,在求索引.,确定该元素在hashtable的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加[原则和hashset一样])

tail.next=newElement//简单指定
newElement.pre=tail
tail = newEelment;

 4)这样的话,我们遍历LinkedHashSet也能确保插入顺序和遍历顺序一致

源码分析

因为LinkedHashSet是HashSet的子类,因此在底层使用的方法,还是HashSet的方法,因为HashSet的底层是HashMap,所以最终走的还是HashMap的putVal方法

可以参考putVal方法,我们在将HashSet的时候已经详细的解释过了

/*
对HashSet 的源码解读
1. 执行 HashSet()
    public HashSet() {
        map = new HashMap<>();
    }
2. 执行 add()
   public boolean add(E e) {//e = "java"
        //这里的PRESENT 是一个空对象数组,起到占位符作用
        return map.put(e, PRESENT)==null;//(static) PRESENT = new Object();
   }
 3.执行 put() , 该方法会执行 hash(key) 得到key对应的hash值 算法h = key.hashCode()) ^ (h >>> 16)
   根据我们传入进来的值,去计算hash值,在Table中存放的位置
     public V put(K key, V value) {//key = "java" value = PRESENT 共享
        return putVal(hash(key), key, value, false, true);
    }
 4.执行 putVal
 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 就是 HashMap 的一个数组,类型是 Node[]
        //if 语句表示如果当前table 是null, 或者 大小=0
        //就会进行第一次扩容,到16个空间.
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //(1)根据key,得到hash 去计算该key应该存放到table表的哪个索引位置
        //并把这个位置的对象,赋给 p
        //(2)判断p 是否为null
        //(2.1) 如果p 为null, 表示还没有存放元素, 就创建一个Node (key="java",value=PRESENT)
        //(2.2) 就放在该位置 tab[i] = newNode(hash, key, value, null) 就是直接存放进去
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //一个开发技巧提示: 在需要局部变量(辅助变量)时候,在创建
            Node<K,V> e; K k; //
            //当进入else的时候,就说明我们当前计算出来的Hash值在数组中的位置已经存在了 ,那么就先进行判断
            //如果当前索引位置对应的链表的第一个元素的hash值和准备添加的key的hash值一样
            //并且满足 下面两个条件之一:
            //(1) 准备加入的keyhash值 和 p 指向的Node 结点的hash值相同,那就说明是是同一个对象
            //(2)  当前的key对象 或者和我们传入对象的地址相同,因为==在判断引用类型的时候,判断的是地址是否相同,如果地址相同,或者他们的内容相同
            //就不能加入     如果不能加入就把p赋给e
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //再判断 p 是不是一颗红黑树,
            //如果是一颗红黑树,就调用 putTreeVal , 来进行添加
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                //如果上面的情况都不是的话,那么就说明,此时这个索引对应的位置是一个链表了
            else {//如果table对应索引位置,已经是一个链表, 就使用for循环比较
                  //(1) 依次和该链表的每一个元素比较后,都不相同, 则加入到该链表的最后
                  //    注意在把元素添加到链表后,立即判断 该链表是否已经达到8个结点
                  //    , 就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树)
                  //    注意,在转成红黑树时,要进行判断, 判断条件
                  //    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
                  //            resize();
                  //    如果上面条件成立,先table扩容.
                  //    只有上面条件不成立时,才进行转成红黑树
                  //(2) 依次和该链表的每一个元素比较过程中,如果有相同情况,就直接break
                //这是一个死循环,会一直进行 比较,只有两种情况,才会退出循环
                //第一种:当数组中的其中一条列表的长度到达了7,准备进行树化的时候
                //第二种:就是发现我们加入的元素,在这个列表中发现了重复的,也会直接跳出循环
                for (int binCount = 0; ; ++binCount) {
                        这里e=p.next 因为我们在最开始上面的时候,已经对第一个元素进行了判断,所以这里直接从下一个元素开始判断
                        如果下一个元素为空,那么就直接加入
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        加入完一个元素之后,马上的进行判断,当前列表的个数有几个,是否进行树化
                        if (binCount >= TREEIFY_THRESHOLD(8) - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //这里就是,在循环比较的过程中,如果发现有相同的内容,那么会直接break
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                       //这里就是让p 指向下一个
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //size 就是我们每加入一个结点Node(k,v,h,next), size++
        if (++size > threshold)
            resize();//扩容
        afterNodeInsertion(evict);
        return null;
    }
 */
package idea.chapter14.set_;
import java.util.LinkedHashSet;
import java.util.Set;
@SuppressWarnings({"all"})
public class LinkedHashSetSource {
    public static void main(String[] args) {
        //分析一下LinkedHashSet的底层机制
        Set set = new LinkedHashSet();
        set.add(new String("AA"));
        set.add(456);
        set.add(456);
        set.add(new Customer("刘", 1001));
        set.add(123);
        System.out.println("set=" + set);
        //源码分析
        //1. LinkedHashSet 加入顺序和取出元素/数据的顺序一致,因为LinkedHashSet在底层维护了一个双向链表+数组,因此可以保证数据取出的顺序保持一致
        //2. LinkedHashSet 底层维护的是一个LinkedHashMap(是HashMap的子类)
        //3. LinkedHashSet 底层结构 (数组table+双向链表)
        //4. 添加第一次时,直接将 数组table 扩容到 16 ,存放的结点类型是 LinkedHashMap$Entry
        //5. 数组是 HashMap$Node[] 存放的元素/数据是 LinkedHashMap$Entry类型
        /*
                //继承关系是在内部类完成.
                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);
                    }
                }
         */
    }
}
class Customer {
    private String name;
    private int no;
    public Customer(String name, int no) {
        this.name = name;
        this.no = no;
    }
}

LinkedHashset练习

代码演示:

在没有重写equals方法和hashcode方法的时候都是可以加入进去的,因为都是创建出来新的对象

package idea.chapter14.set_;
import java.util.LinkedHashSet;
import java.util.Objects;
/*
Car类(属性:name,price), 如果name和price一样,
。则认为是相同元素,就不能添加。5min. I
 */
@SuppressWarnings({"all"})
public class LinkedHashSetExercise {
    public static void main(String[] args) {
        LinkedHashSet linkedHashSet = new LinkedHashSet();
        linkedHashSet.add(new Car("奥拓", 1000));//OK
        linkedHashSet.add(new Car("奥迪", 300000));//OK
        linkedHashSet.add(new Car("法拉利", 10000000));//OK
        linkedHashSet.add(new Car("奥迪", 300000));//加入不了
        linkedHashSet.add(new Car("保时捷", 70000000));//OK
        linkedHashSet.add(new Car("奥迪", 300000));//加入不了
        System.out.println(linkedHashSet);
    }
}
class Car {
    private String name;
    private double price;
    public Car(String name, double price) {
        this.name = name;
        this.price = price;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Car car = (Car) o;
        return Double.compare(car.price, price) == 0 && Objects.equals(name, car.name);
    }
    @Override
    public int hashCode() {
        return Objects.hash(name, price);
    }
    @Override
    public String toString() {
        return "\nCar{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}

到此这篇关于Java中LinkedHashSet的源码分析的文章就介绍到这了,更多相关LinkedHashSet的源码内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 基于Nacos实现SpringBoot动态定时任务调度

    基于Nacos实现SpringBoot动态定时任务调度

    本文主要介绍了在Spring Boot项目中使用Spring Scheduling实现定时任务,并通过Nacos动态配置Cron表达式实现任务的动态变更,通过实现SchedulingConfigurer接口和订阅Nacos配置变更事件,成功实现了任务的动态调度和暂停
    2025-11-11
  • Spring Security获取用户认证信息的实现流程

    Spring Security获取用户认证信息的实现流程

    Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI和AOP功能,为应用系统提供声明式的安全访问控制功能
    2022-12-12
  • Java SpringMVC的@RequestMapping注解使用及说明

    Java SpringMVC的@RequestMapping注解使用及说明

    这篇文章主要介绍了Java SpringMVC的@RequestMapping注解使用及说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-01-01
  • Spring中的两种代理JDK和CGLIB的区别浅谈

    Spring中的两种代理JDK和CGLIB的区别浅谈

    本篇文章中主要介绍了Spring中的两种代理JDK和CGLIB的区别浅谈,详解的介绍了JDK和CGLIB的原理和方法,有需要的朋友可以了解一下
    2017-04-04
  • spring 重复注解和aop拦截的实现示例

    spring 重复注解和aop拦截的实现示例

    本文主要介绍了spring 重复注解和aop拦截的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-08-08
  • Java IO流之StringWriter和StringReader用法分析

    Java IO流之StringWriter和StringReader用法分析

    这篇文章主要介绍了Java IO流之StringWriter和StringReader用法分析,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • MyBatis中映射文件的使用案例代码

    MyBatis中映射文件的使用案例代码

    这篇文章主要介绍了MyBatis中映射文件的使用,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-02-02
  • java有序二叉树的删除节点方式

    java有序二叉树的删除节点方式

    文章描述了在二叉树中删除节点的三种情况及其对应的操作步骤,通过递归找到节点及其父节点,并根据节点的子树情况(无子树、单子树、双子树)进行相应的删除操作,文章还提供了一个测试类来验证删除操作的正确性
    2024-12-12
  • SpringMVC实现自定义类型转换器

    SpringMVC实现自定义类型转换器

    本篇文章主要介绍了SpringMVC实现自定义类型转换器 ,详细的介绍了自定义类型转换器的用法和好处,有兴趣的可以了解一下。
    2017-04-04
  • Java多线程编程:线程的原理和使用安全

    Java多线程编程:线程的原理和使用安全

    文章主要介绍了线程的基本概念、线程和进程的区别、线程的创建方法、线程的常见方法与属性、线程的安全问题等,线程是程序执行流的最小单位,可以并发执行并共享数据,线程的生命周期状态包括新建、就绪、运行、阻塞、死亡等
    2026-05-05

最新评论