Java中Set集合的使用和底层原理

Posted 学全栈的灌汤包

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中Set集合的使用和底层原理相关的知识,希望对你有一定的参考价值。

文章目录

Set系列集合介绍

Set集合概述

Set系列集合特点:

无序:存取数据的顺序是不一定的, 当数据存入后, 集合的顺序就固定下来了

不重复:可以去除重复

无索引:没有带索引的方法,所以不能使用普通for循环遍历,也不能通过索引来获取元素。

Set集合实现类特点:

HashSet : 无序、不重复、无索引。

public static void main(String[] args) 
  	// 无序, 不重复, 无索引
    Set<String> sets = new HashSet<>();
    sets.add("mysql");
    sets.add("MySQL");
    sets.add("JAVA");
    sets.add("JAVA");
    sets.add("html");
    sets.add("HTML");
    sets.add("Vue");
    sets.add("Vue");
    System.out.println(sets); // [JAVA, MySQL, Vue, HTML]

LinkedHashSet:有序、不重复、无索引。

public static void main(String[] args) 
  	// 有序、不重复、无索引
    Set<String> sets = new LinkedHashSet<>();
    sets.add("MySQL");
    sets.add("MySQL");
    sets.add("JAVA");
    sets.add("JAVA");
    sets.add("HTML");
    sets.add("HTML");
    sets.add("Vue");
    sets.add("Vue");
    System.out.println(sets); // [MySQL, JAVA, HTML, Vue]

TreeSet:排序: 默认升序、不重复、无索引。

public static void main(String[] args) 
    // 排序、不重复、无索引
    Set<Integer> sets = new TreeSet<>();
    sets.add(10);
    sets.add(10);
    sets.add(20);
    sets.add(20);
    sets.add(30);
    sets.add(30);
    sets.add(40);
    sets.add(40);
    sets.add(50);
    sets.add(50);
    System.out.println(sets); // [10, 20, 30, 40, 50]

Set集合的功能上基本上与Collection的API一致, Set集合没有扩展额外的API

HashSet无序原理

HashSet集合底层采取哈希表存储的数据。

哈希表是一种对于增删改查数据性能都较好的结构。

哈希表的组成:

JDK8之前的,底层使用数组+链表组成

JDK8开始后,底层采用数组+链表+红黑树组成。

哈希表是一种对于增删改查数据性能都较好的结构。

在了解哈希表之前需要先理解哈希值的概念

哈希值:

是JDK根据对象的地址,按照某种规则算出来的int类型的数值。

获取哈希值: 通过Object类的API:

public int hashCode():返回对象的哈希值

对象的哈希值特点:

同一个对象多次调用hashCode()方法返回的哈希值是相同的

public static void main(String[] args) 
    String address = "成都市";
    System.out.println(address.hashCode()); // 25299637
    System.out.println(address.hashCode()); // 25299637
    System.out.println(address.hashCode()); // 25299637

默认情况下,不同对象的哈希值是不同的。

public static void main(String[] args) 
    String address = "成都市";
    System.out.println(address.hashCode()); // 25299637
    System.out.println(address.hashCode()); // 25299637
    System.out.println(address.hashCode()); // 25299637

    String address2 = "重庆市";
    System.out.println(address2.hashCode()); // 36643529

JDK8之前的版本HashSet原理解析:数组 + 链表 +(结合哈希算法), 详细流程如下:

  1. 底层会默认创建一个默认长度16的数组,数组名table

  1. 根据元素的哈希值数组的长度求余计算出应存入的位置(哈希算法)

例如数组长度是16, 哈希值与16取余, 得出的结果一定是0到15之间的数字

  1. 判断当前位置是否为null,如果是null直接存入
  2. 如果位置不为null,表示有元素,则调用equals方法比较
  3. 如果一样,则不存,如果不一样,则存入数组

在JDK 7中, 新元素占老元素位置,并且新元素会指向老元素

在JDK 8中, 新元素挂在老元素下面

JDK8之后的版本HashSet原理解析:

底层结构:哈希表(数组、链表、红黑树的结合体)

当挂在元素下面的数据过多时,查询性能降低,从JDK8开始后,当链表长度超过8的时候,自动转换为红黑树。

JDK8开始后,哈希表对于红黑树的引入进一步提高了操作数据的性能。

Set集合对象去重

HashSet去重注意点:

Set集合在比较两个对象时, 默认比较的是两个对象的地址是否一致, 若地址不同则认为是两个不同的对象;

而如果希望Set集合认为2个内容一样的对象是重复的,则需要自己重写对象的hashCode()和equals()方法

我们来看下面这样一个案例:

需求: 创建一个存储学生对象的集合,存储多个学生对象,使用程序实现在控制台遍历该集合,要求:学生对象的成员变量值相同,我们就认为是同一个对象

分析:

  1. 定义学生类,创建Set集合对象, 创建学生对象
  2. 把学生添加到集合
  3. 在学生类中重写两个方法,hashCode()和equals(),自动生成即可
  • 步骤一: 定义学生类
public class Student 
    private String name;
    private int age;
    private int id;

    // 构造器
    public Student() ;

    public Student(String name, int age, int id) 
        this.name = name;
        this.age = age;
        this.id = id;
    

    // getter和setter方法
    public String getName() 
        return name;
    

    public void setName(String name) 
        this.name = name;
    

    public int getAge() 
        return age;
    

    public void setAge(int age) 
        this.age = age;
    

    public int getId() 
        return id;
    

    public void setId(int id) 
        this.id = id;
    
  
    // 重写toString方法
    @Override
    public String toString() 
        return "Student" +
                "name='" + name + '\\'' +
                ", age=" + age +
                ", id=" + id +
                '';
    

  • 步骤二: 创建学生对象和HashSet集合, 并将学生对象存入HashSet集合中, 如下代码:
public class Test 
    public static void main(String[] args) 
        // 创建集合存储学生对象
        Set<Student> students = new HashSet<>();
        // 创建学生对象
        Student stu1 = new Student("小明", 18, 101);
        Student stu2 = new Student("小明", 18, 101);
        Student stu3 = new Student("小王", 20, 102);
        // 将学生对象添加到集合中
        students.add(stu1);
        students.add(stu2);
        students.add(stu3);

        System.out.println(students);
        // 打印结果如下: 
        //  [Studentname='小明', age=18, id=101, 
        //  Studentname='小明', age=18, id=101, 
        //  Studentname='小王', age=20, id=102]
    

  • 步骤三: 我们发现步骤二代码中, stu1和stu2对象的内容完全一样, 但是由于对象的地址不一样, 会被当成两个不同的对象存入到集合中; 因此我们需要在学生类中重写两个方法,hashCode()和equals(),自动生成即可
@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 && id == student.id && Objects.equals(name, student.name);


@Override
public int hashCode() 
  	// 该方法传入的参数相同, 就会返回相同的哈希值
    return Objects.hash(name, age, id);

  • 步骤四: 此时再将stu1和stu2对象存入集合, 由于内容一样就会被去掉重复的
public class Test 
    public static void main(String[] args) 
        // 创建集合存储学生对象
        Set<Student> students = new HashSet<>();
        // 创建学生对象
        Student stu1 = new Student("小明", 18, 101);
        Student stu2 = new Student("小明", 18, 101);
        Student stu3 = new Student("小王", 20, 102);
        // 将学生对象添加到集合中
        students.add(stu1);
        students.add(stu2);
        students.add(stu3);

        System.out.println(students);
        //  [Studentname='小王', age=20, id=102, Studentname='小明', age=18, id=101]
    

LinkedHashSet

LinkedHashSet集合概述和特点:

有序、不重复、无索引。

这里的有序指的是保证存储和取出的元素顺序一致

原理

底层数据结构是依然哈希表,只是每个元素又额外的多了一个双链表的机制记录存储的顺序。

TreeSet排序规则

TreeSet集合特点:

不重复、无索引、可排序

可排序:按照元素的大小默认升序(有小到大)排序。

TreeSet集合底层是基于红黑树的数据结构实现排序的,增删改查性能都较好。

注意:TreeSet集合是一定要排序的,可以将元素按照指定的规则进行排序

TreeSet默认排序规则:

对于数值类型:Integer , Double,官方默认按照大小进行升序排序。

public static void main(String[] args) 
    Set<Integer> sets1 = new TreeSet<>();
    sets1.add(50);
    sets1.add(10);
    sets1.add(30);
    sets1.add(20);
    System.out.println(sets1); // [10, 20, 30, 50]

    Set<Double> sets2 = new TreeSet<>();
    sets2.add(10.11);
    sets2.add(20.22);
    sets2.add(43.22);
    sets2.add(8.22);
    System.out.println(sets2); // [8.22, 10.11, 20.22, 43.22]

对于字符串类型:默认按照首字符的编号升序排序。

public static void main(String[] args) 
    Set<String> sets = new TreeSet<>();
    sets.add("bbb");
    sets.add("eee");
    sets.add("aaa");
    sets.add("ccc");
    System.out.println(sets); // [aaa, bbb, ccc, eee]

对于自定义类型如Student对象,TreeSet无法直接排序, 需要制定排序规则; 例如下面代码中向集合中添加学生类, TreeSet是无法进行排序的, 会崩溃报错

// 错误演示
public static void main(String[] args) 
    // 创建学生对象
    Student stu1 = new Student("小明", 18, 101);
    Student stu2 = new Student("小赵", 18, 102);
    Student stu3 = new Student("小王", 18, 103);
    // 创建集合
    Set<Student> students = new TreeSet<>();
    students.add(stu1);
    students.add(stu2);
    students.add(stu3);
    System.out.println(students);

自定义排序规则: TreeSet集合存储对象的的时候有2种方式可以设计自定义比较规则

方式一: 让自定义的类(如学生类)实现Comparable接口, 并重写compareTo方法来定制比较规则。

// 实现Comparable接口
public class Student implements Comparable<Student> 
  	// 其他代码...
  
  	// 重写compareTo方法
    @Override
    public int compareTo(Student o) 
        // 例如按照id进行排序
        return this.id - o.id;
    

public static void main(String[] args) 
    // 创建学生对象
    Student stu1 = new Student("小明", 18, 101);
    Student stu2 = new Student("小赵", 18, 102);
    Student stu3 = new Student("小王", 18, 103);
    // 创建集合
    Set<Student> students = new TreeSet<>();
    students.add(stu1);
    students.add(stu2);
    students.add(stu3);
    System.out.println(students);
    // 打印结果: 按照id升序
    // [Studentname='小明', age=18, id=101, 
    // Studentname='小赵', age=18, id=102, 
    // Studentname='小王', age=18, id=103]

方式二: TreeSet集合有参数构造器自带比较器对象,来进行定制比较规则, 并且该方法如果和方式一同时出现, 会优先使用此方法的比较规则。

public class SetDemo 
    public static void main(String[] args) 
        // 创建学生对象
        Student stu1 = new Student("小明", 18, 101);
        Student stu2 = new Student("小赵", 18, 102);
        Student stu3 = new Student("小王", 18, 103);
        // 创建集合
        // 方式二: 使用构造器自带的比较器对象
        Set<Student> students = new TreeSet<>(new Comparator<Student>() 
            @Override
            public int compare(Student o1, Student o2) 
                return o2.getId() - o1.getId();
            
        );
        students.add(stu1);
        students.add(stu2);
        students.add(stu3);
        System.out.println(students);
        // 打印结果: 按照id降序
        // [Studentname='小王', age=18, id=103,
        // Studentname='小赵', age=18, id=102,
        // Studentname='小明', age=18, id=101]
    

  • 并且可以使用Lambda表达式简化代码
public class SetDemo 
    public static void main(String[] args) 
        // 创建学生对象
        Student stu1 = 第一讲 Set

一 概述

Set:1. 元素存储无下标,所以元素是无序(存入和取出的顺序不一定一致

  2. 元素不可以重复

|--HashSet:底层数据结构是哈希表。线程不同步。 保证元素唯一性的原理:判断元素的hashCode值是否相同。如果相同,还会继续判断元素的equals方法,是否为true。

|--TreeSet:可以对Set集合中的元素进行排序。默认按照字母的自然排序。底层数据结构是二叉树。保证元素唯一性的依据:compareTo方法return 0。       

Set集合没有特有的功能,Set集合的功能和Collection是一致的

二 HashSet

HashSet:线程不安全,存取速度快。

HashSet的原理: HashSet中不能存入重复的值,如果add()存入重复值,返回false,那么HashSet如何保证数据是不是唯一的呢?

如果在存储的时候逐个equals()比较, 效率较低,哈希算法提高了去重复的效率, 降低了使用equals()方法的次数

可以通过元素的两个方法,hashCode和equals来完成保证元素唯一性。如果元素的HashCode值相同,才会判断equals是否为true。如果元素的hashCode值不同,不会调用equals。

2. 知道了HashSet原理之后所以我们在存储自定义类的时候必须重写hashCode()和equals()方法

* hashCode(): 属性相同的对象返回值必须相同, 属性不同的返回值尽量不同(提高效率)

* equals(): 属性相同返回true, 属性不同返回false,返回false的时候存储 

 

以上是关于Java中Set集合的使用和底层原理的主要内容,如果未能解决你的问题,请参考以下文章

深入Java集合:HashSet实现原理

Java 集合深入理解 (十五) :HashSet实现原理研究

Java集合框架

Java集合底层原理

Java的集合

Java集合-HashSet