Java集合Set与Collections案例详解

 更新时间:2026年05月13日 11:06:53   作者:Kiling_0704  
文章介绍了Set集合的特点和几种常见的Set集合,包括HashSet和LinkedHashSet,重点讲解了HashSet的底层原理,包括哈希值的计算、数组+链表/红黑树的实现方式、equals和hashCode方法的作用,感兴趣的朋友一起看看吧

集合进阶(Set集合、Collections)

一、Set系列集合

1.1 认识Set集合的特点

Set集合是属于Collection体系下的另一个分支,它的特点如下图所示

下面我们用代码简单演示一下,每一种Set集合的特点。

public class SetTest1 {
    public static void main(String[] args) {
        // 创建一个Set集合的对象
        /**
         * HashSet的集合, 是一行经典代码(常用) 特点: 无序不重复 无索引
         * 针对无序不是每次都是随机的, 第一次无序排好之后, 以后都是这个顺序(面试题)
         */
        // Set<Integer> set = new HashSet<>();
        // 特点: 有序、无索引、不重复
        // Set<Integer> set = new LinkedHashSet<>();
        // 可排序(默认升序)、无索引、不重复
        Set<Integer> set = new TreeSet<>();
        set.add(666);
        set.add(555);
        set.add(555);
        set.add(888);
        set.add(888);
        set.add(777);
        set.add(777);
        System.out.println(set);
    }
}

1.2 HashSet集合底层原理

接下来,为了让同学们更加透彻的理解HashSet为什么可以去重,我们来看一下它的底层原理。

HashSet集合底层是基于哈希表实现的,所以在正式了解HashSet集合的底层原理前,我们需要先搞清楚一个前置知识:哈希值!:

演示哈希值的相同与不相同

public class Student {
    private String name;
    private int age;
    private double height;
​
    public Student() {
    }
​
    public Student(String name, int age, double height) {
        this.name = name;
        this.age = age;
        this.height = height;
    }
​
    // 自己提供Getter 和 Setter方法 以及toString方法
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Double.compare(height, student.height) == 0 && Objects.equals(name, student.name);
    }
​
    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }
}
​
public class SetTest2 {
    public static void main(String[] args) {
        // 哈希值不相同
        Student s1 = new Student("柳岩", 18, 163);
        Student s2 = new Student("宝强", 18, 163);
        System.out.println(s1.hashCode());
        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());
​
        System.out.println("----------------------");
​
        // 哈希值相同
        String str1 = new String("abc");
        String str2 = new String("acD");
        System.out.println(str1.hashCode());
        System.out.println(str2.hashCode());
    }
}

HashSet集合的底层原理

  • 基于哈希表实现。
  • 哈希表是一种增删改查数据,性能都较好的数据结构。

哈希表根据JDK版本的不同,也是有点区别的

  • JDK8以前:哈希表 = 数组+链表
  • JDK8以后:哈希表 = 数组+链表+红黑树

我们发现往HashSet集合中存储元素时,底层调用了元素的两个方法:一个是hashCode方法获取元素的hashCode值(哈希值);另一个是调用了元素的equals方法,用来比较新添加的元素和集合中已有的元素是否相同。

  • 只有新添加元素的hashCode值和集合中以后元素的hashCode值相同、新添加的元素调用equals方法和集合中已有元素比较结果为true, 才认为元素重复。
  • 如果hashCode值相同,equals比较不同,则以链表的形式连接在数组的同一个索引为位置(如上图所示)

在JDK8开始后,为了提高性能,当链表的长度超过8时,就会把链表转换为红黑树,如下图所示:

总结:
1. 什么是哈希值?对象的哈希值有什么特点?
    所谓的哈希值就是JDK根据对象的地址或者属性值算出来的int类型整数。
特点:
    同一个对象多次调用hashCode()方法的哈希值相同
    不同对象调用的hashCode()方法,哈希值不同,但是可以根据子类重写hashCode()方法让其相同。
2. HashSet集合的底层原理是什么样的? 
    基于哈希表实现的。
        JDK8之前的,哈希表:底层使用数组+链表组成
        JDK8开始后,哈希表:底层采用数组+链表+红黑树组成。
3. HashSet集合利用哈希表操作数据的详细流程是咋回事?
    HashSet底层采用了哈希表数据结构
    哈希表又叫做散列表,哈希表底层是一个数组,这个数组中每一个元素是一个单向链表,每个单向链表都有一个独一无二的hash值,代表数组的下标。在某个单向链表中的每一个节点上的hash值是相同的。hash值实际上是key调用hashCode方法,再通过"hash function"转换成的值。
如何向哈希表中添加元素?
    先调用被存储的key的hashCode方法,经过某个算法得出hash值,如果在这个哈希表中不存在这个hash值,则直接加入元素。如果该hash值已经存在,继续调用Key之间的equals方法,如果equals方法返回false,则将该元素添加。如果equals方法返回true,则放弃添加该元素
    HashSet初始化容量是16,默认加载因子是0.75
4. 哈希表的详细流程(面试题)
    ①.创建一个默认长度16,默认加载因为0.75的数组,数组名table
    ②.根据元素的哈希值跟数组的长度计算出应存入的位置
    ③.判断当前位置是否为null,如果是null直接存入,如果位置不为null,表示有元素,则调用equals方法比较属性值,如果一样,则不存,如果不一样,则存入数组。
    ④.当数组存满到16*0.75=12时,就自动扩容,每次扩容原先的两倍

1.3 HashSet去重原理

前面我们学习了HashSet存储元素的原理,依赖于两个方法:一个是hashCode方法用来确定在底层数组中存储的位置,另一个是用equals方法判断新添加的元素是否和集合中已有的元素相同。

要想保证在HashSet集合中没有重复元素,我们需要重写元素类的hashCode和equals方法。

比如以下面的Student类为例,假设把Student类的对象作为HashSet集合的元素,想要让学生的姓名和年龄相同,就认为元素重复。

public class Student{
    private String name; //姓名
    private int age; //年龄
    private double height; //身高
    //无参数构造方法
    public Student(){}
    //全参数构造方法
    public Student(String name, int age, double height){
        this.name=name;
        this.age=age;
        this.height=height;
    }
    //...get、set、toString()方法自己补上..
    // 按快捷键生成hashCode和equals方法
    // alt+insert 选择 hashCode and equals
    // 只要两个对象的内容一样就会返回true
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
​
        Student student = (Student) o;
​
        if (age != student.age) return false;
        if (Double.compare(student.height, height) != 0) return false;
        return name != null ? name.equals(student.name) : student.name == null;
    }
​
    // 只要两个对象内容一样, 返回的哈希值就是一样的
    @Override
    public int hashCode() {
        // 根据姓名  年龄  身高计算哈希值
        return Objects.hash(name, age, height);
    }
}

接着,写一个测试类,往HashSet集合中存储Student对象。

public class SetTest3 {
    public static void main(String[] args) {
        Set<Student> students = new HashSet<>();
        Student s1 = new Student("至尊宝",20, 169.6);
        Student s2 = new Student("蜘蛛精",23, 169.6);
        Student s3 = new Student("蜘蛛精",23, 169.6);
        Student s4 = new Student("牛魔王",48, 169.6);
        System.out.println(s2.hashCode());
        System.out.println(s3.hashCode());
​
        students.add(s1);
        students.add(s2);
        students.add(s3);
        students.add(s4);
​
        for(Student s : students){
            System.out.println(s);
        }
    }
}

打印结果如下,我们发现存了两个蜘蛛精,当时实际打印出来只有一个,而且是无序的。

Student{name='牛魔王', age=48, height=169.6}
Student{name='至尊宝', age=20, height=169.6}
Student{name='蜘蛛精', age=23, height=169.6}

总结:
如果希望Set集合认为2个内容相同的对象是重复的应该怎么办?
    需要重写元素类的hashCode和equals方法。

1.4 LinkedHashSet底层原理

接下来,我们再学习一个HashSet的子类LinkedHashSet类。LinkedHashSet它底层采用的是也是哈希表结构,只不过额外新增了一个双向链表来维护元素的存取顺序。如下下图所示:

每次添加元素,就和上一个元素用双向链表连接一下。第一个添加的元素是双向链表的头节点,最后一个添加的元素是双向链表的尾节点。

把上个案例中的集合改成LinkedList集合,我们观察效果怎样

public class SetTest4 {
    public static void main(String[] args) {
        Set<Student> students = new LinkedHashSet<>();
        Student s1 = new Student("至尊宝",20, 169.6);
        Student s2 = new Student("蜘蛛精",23, 169.6);
        Student s3 = new Student("蜘蛛精",23, 169.6);
        Student s4 = new Student("牛魔王",48, 169.6);
​
        students.add(s1);
        students.add(s2);
        students.add(s3);
        students.add(s4);
​
        for(Student s : students){
            System.out.println(s);
        }
    }
}

打印结果如下

Student{name='至尊宝', age=20, height=169.6}
Student{name='蜘蛛精', age=23, height=169.6}
Student{name='牛魔王', age=48, height=169.6}

总结:
LinkedHashSet集合的特点和原理是怎么样的?
    特点: 有序、不重复、无索引
    底层原理: 基于哈希表,使用链表记录添加顺序。

到此这篇关于Java集合Set与Collections案例详解的文章就介绍到这了,更多相关Java集合Set与Collections内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 启动Tomcat报错Unsupported major.minor version xxx的解决方法

    启动Tomcat报错Unsupported major.minor version xxx的解决方法

    这篇文章主要为大家详细介绍了启动Tomcat报错Unsupported major.minor version xxx的解决方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-11-11
  • java8 计算时间差的方法示例

    java8 计算时间差的方法示例

    这篇文章主要介绍了java8 计算时间差的方法示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-10-10
  • 微服务springcloud 03.Eureka实现高可用的过程

    微服务springcloud 03.Eureka实现高可用的过程

    这篇文章主要介绍了微服务springcloud 03.Eureka实现高可用的相关资料,本文给大家介绍的非常详细,需要的朋友可以参考下
    2024-07-07
  • 详解Java读取本地文件并显示在JSP文件中

    详解Java读取本地文件并显示在JSP文件中

    这篇文章主要介绍了详解Java读取本地文件并显示在JSP文件中的相关资料,这里提供实例帮助大家实现这样的功能,希望能帮助到大家,需要的朋友可以参考下
    2017-08-08
  • 浅谈hashmap为什么查询时间复杂度为O(1)

    浅谈hashmap为什么查询时间复杂度为O(1)

    这篇文章主要介绍了hashmap为什么查询时间复杂度为O(1),具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08
  • 全面理解java中的异常处理机制

    全面理解java中的异常处理机制

    下面小编就为大家带来一篇全面理解java中的异常处理机制。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-06-06
  • 基于SpringBoot的Dubbo泛化调用的实现代码

    基于SpringBoot的Dubbo泛化调用的实现代码

    这篇文章主要介绍了基于SpringBoot的Dubbo泛化调用的实现,从泛化调用实现的过程来看,我们可以对自己提供所有服务进行测试,不需要引入调用的接口,减少代码的侵入,需要的朋友可以参考下
    2022-04-04
  • mybatis的ParamNameResolver参数名称解析

    mybatis的ParamNameResolver参数名称解析

    这篇文章主要为大家介绍了mybatis的ParamNameResolver参数名称解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08
  • SpringBoot一个接口多个实现类的调用方式总结

    SpringBoot一个接口多个实现类的调用方式总结

    这篇文章主要介绍了SpringBoot一个接口多个实现类的调用方式,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2024-01-01
  • 全面解析Java中的注解与注释

    全面解析Java中的注解与注释

    这篇文章主要介绍了Java中的注解与注释,简单来说注解以@符号开头而注释被包含在/***/符号中,各自具体的作用则来看本文详解,需要的朋友可以参考下
    2016-05-05

最新评论