Set集合知识点

本文详细介绍了Java中Set接口的实现类HashSet和TreeSet的原理与使用。HashSet基于哈希表,利用元素的hashCode和equals进行去重,JDK1.8后引入红黑树提高性能。而TreeSet基于红黑树,能自动排序。文章还探讨了如何处理自定义类在Set中的比较规则,包括Comparable接口和Comparator接口的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

(一)概述

java.util.Set接口继承自Collection接口,它与Collection接口中的方法基本一致,并没有对Collection接口进行功能上的扩充,只是比Collection接口更加严格了。Set接口中元素无序,并且都会以某种规则保证存入的元素不重复

主要包含的实现类有:HashSet、TreeSet、LinkedHashSet ...

  • HashSet
  1. 原理

用Hash技术实现的Set结构。

由于Set集合是不能存入重复元素的集合。那么HashSet也是具备这一特性的。

HashSet如何检查重复?HashSet会通过元素的hashcode()和equals()方法进行判断元素师否重复。

当你试图把对象加入HashSet时,HashSet会使用对象的hashCode来判断对象加入的位置。同时也会与其他已经加入的对象的hashCode进行比较,如果没有相等的hashCode,HashSet就会假设对象没有重复出现。

简单一句话,如果对象的hashCode值是不同的,那么HashSet会认为对象是不可能相等的。

因此我们自定义类的时候需要重写hashCode,来确保对象具有相同的hashCode值。

如果元素(对象)的hashCode值相同,是不是就无法存入HashSet中了? 当然不是,会继续使用equals 进行比较.如果 equals为true 那么HashSet认为新加入的对象重复了,所以加入失败。如果equals 为false那么HashSet 认为新加入的对象没有重复.新元素可以存入。

  1. 散列技术原理

把对象的主键直接用一个固定的公式计算,得出存储位置的方法。

优点是:可以快速命中搜索的目标。

在JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

JDK1.8引入红黑树大程度优化了性能。

  1. 使用

1 基本数据类型存入HashSet中

HashSet set = new HashSet();

set.add(100);

set.add(200);

set.add(300);

set.add(400);

set.add(500);

set.add(400);  // 不会存入重复数据

 

2 字符串类型数据存入HashSet中

HashSet set = new HashSet();

set.add("aaa");

set.add("bbb");

set.add("ccc");

set.add("ddd");

set.add("aaa");// 不会存入重复数据

3 自定义类型存入HashSet中

class Student{

private String name;

private int age;

public Student(){

}

public Student(String name, int age) {

super();

this.name = name;

this.age = age;

}

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 class TestHashSet {

public static void main(String[] args) {

HashSet hs = new HashSet();

Student s1 = new Student("张三",20);

Student s2 = new Student("李四",21);

Student s3 = new Student("王五",23);

Student s4 = new Student("张三",20);

Student s5 = s1;

hs.add(s1);

hs.add(s2);

hs.add(s3);

hs.add(s4);

hs.add(s5);  //s5和s1 指向同一个对象,所以hashCode相同,不会被存入

System.out.println(hs.size());

System.out.println(hs);

}

}

修改需求。只要Student的name和age值相同,就认为是重复数据,不能加入到HashSet中,如何处理? 重写Student类的hashCode和equals方法

class Student{

private String name;

private int age;

public Student(){

}

public Student(String name, int age) {

this.name = name;

this.age = age;

}

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;

}

@Override

public int hashCode() {

final int prime = 31;

int result = 1;

result = prime * result + age;

result = prime * result + ((name == null) ? 0 : name.hashCode());

return result;

}

public boolean equals(Object obj) {

if (this == obj)

return true;

if (obj == null)

return false;

if (getClass() != obj.getClass())

return false;

Student other = (Student) obj;

if (age != other.age)

return false;

if (name == null) {

if (other.name != null)

return false;

} else if (!name.equals(other.name))

return false;

return true;

}

/* public boolean equals(Object obj){

Student s = (Student)obj;

if(this.name.equals(s.getName()) && this.age==s.getAge()){

return true;

}else{

return false;

}

}

public int hashCode(){

return this.name.hashCode()+age;

}

*/

}

public class TestHashSet {

public static void main(String[] args) {

HashSet hs = new HashSet();

Student s1 = new Student("张三",20);

Student s2 = new Student("李四",21);

Student s3 = new Student("王五",23);

Student s4 = new Student("张三",20);

System.out.println(s1.hashCode());

System.out.println(s4.hashCode());

System.out.println(s1.equals(s4));

//Student s5 = s1;

hs.add(s1);

hs.add(s2);

hs.add(s3);

hs.add(s4);

//hs.add(s5);  //s5和s1 指向同一个对象,所以hashCode相同,不会被存入

System.out.println(hs.size());

System.out.println(hs);

}

}

  1. 案例

String[] names = {"A","B","C","D","E","F","G"};

String[] scores = {"A,数学,89", "A,语文,86","B,数学,88","B,英语,98","C,英语,67","G,数学,60"};

求names中,谁全部缺考?

提示:使用removeAll方法。

String[] names = {"A","B","C","D","E","F","G"};

String[] scores = {"A,数学,89", "A,语文,86","B,数学,88","B,英语,98","C,英语,67","G,数学,60"};

Set<String> namesSet = new HashSet<>();

for(String name:names){

namesSet.add(name);

}

Set<String> scoresSet = new HashSet<>();

for(String str:scores){

scoresSet.add(str.split(",")[0]);

}

namesSet.removeAll(scoresSet);

Iterator<String> it = namesSet.iterator();

while(it.hasNext()){

System.out.println(it.next());

}

  • TreeSet

TreeSet是一个可排序集合,基于红黑树(自平衡二叉树),默认按元素的自然顺序升序排列。

public static void main(String[] args) {

Set<Integer> set = new TreeSet<>(); //会对存入的数据自动排序(升序)

set.add(2);

set.add(5);

set.add(3);

set.add(1);

set.add(4);

set.add(5);

System.out.println("集合中元素个数:"+set.size());

for(int x:set){

System.out.println(x);

}

Set<String> s = new TreeSet<>();

s.add("bbb");

s.add("aaa");

s.add("ccc");

s.add("abc");

s.add("cba");

for(String str:s){

System.out.println(str);

}

Set<Student> stuSet = new TreeSet<>();

Student s1 = new Student(4,"aaa",20);

Student s2 = new Student(2,"bbb",30);

Student s3 = new Student(3,"ccc",40);

Student s4 = new Student(4,"cba",50);

stuSet.add(s1);

stuSet.add(s2);

stuSet.add(s3);

stuSet.add(s4);

for(Student stu:stuSet){

System.out.println(stu);

}

Set<Teacher> teacherSet = new TreeSet<>(new TeacherCaiPan());

Teacher t1 = new Teacher(1,"aaa",20);

Teacher t2 = new Teacher(2,"bbb",30);

Teacher t3 = new Teacher(3,"ccc",40);

Teacher t4 = new Teacher(4,"cba",20);

teacherSet.add(t1);

teacherSet.add(t2);

teacherSet.add(t3);

teacherSet.add(t4);

for(Teacher t:teacherSet){

System.out.println(t);

}

}

}

以上案例可以看出,当存入的是基本数据类型和字符串时,TreeSet有自动升序排列的能力。而当存入的是自定义引用类型,遍历时会抛出异常。因为TreeSet并不知道如何排序。

(四)比较器

1、Comparable接口

TreeSet集合排序的两种方式:

第一种方式让元素自身具备比较性。也就是元素需要实现Comparable接口,重写compareTo 方法。

这种方式也作为元素的自然排序,也可称为默认排序。

compareTo 方法:this和参数的比较。

  1. 返回整数  this比参数大 (this排在参数后面)
  2. 返回负数  this比参数小 (this排在参数前面)
  3. 返回零    this和参数相等(认为是重复数据)

实现步骤:

  1. 使用空参构造创建TreeSet集合,用TreeSet集合存储自定义对象,无参构造方法使用的是自然排序对元素进行排序的
  2. 自定义的Student类实现Comparable接口自然排序,就是让元素所属的类实现Comparable接口,重写compareTo(T o)方法
  3. 重写接口中的compareTo方法重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写

案例: Student类中有年龄和姓名两个属性, 将几个Student对象存储在集合中, 要求按照年龄从小到大进行排序, 年龄相同时, 按照姓名的字母顺序排序

代码:

package com.offcn.comparable;

import java.util.Objects;

/*

 *  要想让我们自定义的数据存到 TreeSet集合里面, 我们需要实现Comparable接口

 *   需要把 接口里面的所有抽象方法全部重写

 * */

public class Student implements Comparable<Student> {

    private String name;

    private int age;

    public Student() {

    }

    public Student(String name, int age) {

        this.name = name;

        this.age = age;

    }

    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;

    }

    @Override

    public String toString() {

        return "Student{" +

                "name='" + name + '\'' +

                ", age=" + age +

                '}';

    }

    @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 &&

                Objects.equals(name, student.name);

    }

    @Override

    public int hashCode() {

        return Objects.hash(name, age);

    }

    /*

     *  compareTo 重写之后,有返回值

     *    0 : 前面的对象所产产生的整数  等于 后面对象产生的整数

     *   正整数: 前面的对象产生的整数 - 后面的对象产生的整数 大于0 降序排序 从大到小

     *   负整数: 前面的对象产生的整数 - 后面的对象产生的整数 小于0 升序排序 从小到大

     *

     * */

    // 我们需要的是 年龄的从小到大的顺序排序

    @Override

    public int compareTo(Student o) {

        // 前面的对象: 称为当前的对象 this ,

        // o后面的对象

        // 当 this.age-o.age == 0的时候我们不能确定这两个对象是同一个对象

        int num =   o.age - this.age;

        // 如果 num == 0 执行 this.name.compareTo(o.name)

        // 如果num != 0 返回 num

        int num2 = num == 0 ? this.name.compareTo(o.name) : num;

        return num2;

    }

}

package com.offcn.comparable;

import java.util.TreeSet;

public class Demo01_Comparable {

    public static void main(String[] args) {

        TreeSet<Student> ts = new TreeSet<>();

        ts.add(new Student("张三",18)); // 8888

        ts.add(new Student("李四",28)); // 9999

        ts.add(new Student("王五",28)); // 10000

        ts.add(new Student("麻子",13));

        ts.add(new Student("法海",14));

        ts.add(new Student("小赵",55));

        ts.add(new Student("张三",18));

        System.out.println(ts);

    }

}

2、Comparator接口

当元素自身不具备比较性,或者元素自身具备的比较性不是所需的。

那么这时只能让容器自身具备。

compare方法比较规则:

  1. 返回整数   参数1大于参数2 (参数1排在参数2的后面)
  2. 返回负数   参数1小于参数2 (参数1排在参数2的前面)
  3. 返回零     参数1等于参数2 (认为是重复数据)
  4. 用TreeSet集合存储自定义对象,带参构造方法使用的是比较器排序对元素进行排序的
  5. 比较器排序,就是让集合构造方法接收Comparator的实现类对象,

重写compare(T o1,T o2)方法

  1. 重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写

package com.offcn.comparator;

import java.util.Collection;

import java.util.Comparator;

import java.util.TreeSet;

public class Demo01_Comparator {

    public static void main(String[] args) {

        // 我们再去使用 TreeSet(Comparator<? super E> comparator)

        //          构造一个新的空 TreeSet,它根据指定比较器进行排序。

        TreeSet<Student> ts = new TreeSet(new Comparator<Student>() {

            @Override

            public int compare(Student o1, Student o2) {

                // 前面的对象: 称为当前的对象 this ,

                // o后面的对象

                // 当 this.age-o.age == 0的时候我们不能确定这两个对象是同一个对象

                int num =   o1.getAge() - o2.getAge();

                // 如果 num == 0 执行 this.name.compareTo(o.name)

                // 如果num != 0 返回 num

                int num2 = num == 0 ? o1.getName().compareTo(o2.getName()) : num;

                return num2;

            }

        });

        ts.add(new Student("张三",18)); // 8888

        ts.add(new Student("李四",28)); // 9999

        ts.add(new Student("王五",28)); // 10000

        ts.add(new Student("麻子",13));

        ts.add(new Student("法海",14));

        ts.add(new Student("小赵",55));

        ts.add(new Student("张三",18));

        System.out.println(ts);

    }

}

3、TreeSet案例

将字符串中的数值进行排序。

例如String str="8 10 15 5 2 7";    

使用 TreeSet完成。

思路:

1、将字符串切割。

2、可以将这些对象存入TreeSet集合。          

因为TreeSet自身具备排序功能。

package com.offcn.treeset;

import java.util.Arrays;

import java.util.TreeSet;

/*

* 将字符串中的数值进行排序。

    例如String str="8 10 15 5 2 7";

    使用 TreeSet完成。

* */

public class Demo03_案例 {

    public static void main(String[] args) {

        // 我们要想法 获取到字符串中的每一个数据

        String str="8 10 15 5 2 7";

        // 通过截取,将字符串中的每一个数据存到数组里面

        String[] s = str.split(" ");

        System.out.println(Arrays.toString(s));

        //创建TreeSet集合

        TreeSet<Integer> ts = new TreeSet<>();

        //遍历数组

        for (int index = 0; index <= s.length-1; index++) {

            //数组中的每一个元素

            String s1 = s[index];

            // 将数组中每一个元素添加到set集合里面

            // 我们需要将 String类型的数字转换成 Integer

            ts.add(Integer.parseInt(s1));

        }

        System.out.println(ts);

    }

}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

手握日月摘星辰♡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值