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的源码内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • SpringBoot Web开发之请求响应、分层解耦问题记录

    SpringBoot Web开发之请求响应、分层解耦问题记录

    在 Spring Boot 的 Web 请求响应处理中,Servlet 起着关键的作用,Servlet 是 Java Web 开发中的基本组件,主要负责处理客户端的请求并生成响应,这篇文章主要介绍了SpringBoot Web开发之请求响应,分层解耦,需要的朋友可以参考下
    2024-08-08
  • Spring Bean生命周期之Bean的实例化详解

    Spring Bean生命周期之Bean的实例化详解

    这篇文章主要为大家详细介绍了Spring Bean生命周期之Bean的实例化,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-03-03
  • MyBatis-Plus 批量保存的操作方法

    MyBatis-Plus 批量保存的操作方法

    在项目开发中,需要插入批量插入20多万条数据,通过日志观察,发现在调用MyBatis-Plus中的saveBatch()方法性能非常的差,本篇文章主要分享一下saveBatch()的原理以及使用的注意事项,感兴趣的朋友跟随小编一起看看吧
    2024-01-01
  • MybatisPlus使用代码生成器遇到的小问题(推荐)

    MybatisPlus使用代码生成器遇到的小问题(推荐)

    这篇文章主要介绍了MybatisPlus使用代码生成器遇到的小问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-08-08
  • Spring Cloud Feign的使用案例详解

    Spring Cloud Feign的使用案例详解

    Feign是Netflix开发的⼀个轻量级RESTful的HTTP服务客户端(⽤它来发起请求,远程调⽤的),是以Java接⼝注解的⽅式调⽤Http请求,Feign被⼴泛应⽤在Spring Cloud 的解决⽅案中,本文给大家介绍Spring Cloud Feign的使用,感兴趣的朋友一起看看吧
    2023-02-02
  • SpringCloud网关Gateway架构解析

    SpringCloud网关Gateway架构解析

    这篇文章主要介绍了SpringCloud网关Gateway架构解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-03-03
  • Java中间的接口用法详解

    Java中间的接口用法详解

    Java 程序员都知道要面向接口编程,那 Java​ 中的接口除了定义接口方法之外还能怎么用你知道吗,今天小编就来带大家看一下 Java 中间的接口还可以有哪些用法,需要的朋友可以参考下
    2023-07-07
  • java实现ftp文件上传下载功能

    java实现ftp文件上传下载功能

    这篇文章主要为大家详细介绍了java实现ftp文件上传下载功能的相关资料,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-04-04
  • 详解Spring Boot 部署jar和war的区别

    详解Spring Boot 部署jar和war的区别

    本篇文章主要介绍了详解Spring Boot 部署jar和war的区别,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-09-09
  • Java中的Map集合根据key值排序的实现

    Java中的Map集合根据key值排序的实现

    本文主要介绍了Java中的Map集合如何根据key值排序,包含使用TreeMap和使用lambda表达式和Stream流两种方法,具有一定的参考价值,感兴趣的可以了解一下
    2024-03-03

最新评论