Java常用集合之Set

目录

1、Set集合概述

2、HashSet

(1)HashSet概述

(2)HashSet源码分析

(3)HashSet存储自定义对象

3、LinkedHashSet

4、TreeSet


1、Set集合概述

Set继承自Collection接口,没有特殊的方法

Set集合的特点

  • Set中不包含重复元素(只能存储一个null)
  • 不维护存储顺序
  • Set没有索引,不能通过索引访问元素,所以不能使用普通for循环遍历
  • 只能通过元素本身来访问

2、HashSet

(1)HashSet概述

  • 此类实现Set接口
  • 底层数据结构为HashMap哈希表
  • 对集合的迭代次序不作任何保证
  • 允许一个null元素
  • 有快速失败机制
  • 线程不安全

元素放入时是无序的,指的是内存空间不连续,因为元素在set中的位置是由该元素的HashCode决定的。

而且,HashMap每次扩容都可能导致元素重新排布,所以元素的位置是不确定的。

(2)HashSet源码分析

(1)继承结构

HashSet继承了AbstractSet类,实现了Set、克隆、序列化接口。

AbstractSet有三个方法,提供了Set接口的基本实现

(2)成员变量

map:HashMap类型,初始没有赋值

PRESENT:Object类型,用来占位。HashSet只用HashMap的key存储数据,对应的value都是用同一个PRESENT占位。

(3)构造函数

有三个有参构造,一个无参构造

无参构造通过无参构造,创建了一个HashMap对象。

通过集合对象创建HashSet

 也可以指定hashMap的指定初始容量和负载因子

总结

不管使用哪种构造方式,都会立刻创建一个HashMap对象。

(4)add()

直接调用hashMap的put()方法。

hashMap的put()方法,如果添加成功,会返回null。如果key值重复,会更新key对应的value,并返回旧的value值。

此处的add()方法,如果添加成功就会返回true。

(5)remove()

直接调用hashMap的remove()方法。

hashMap的remove()方法,如果删除成功,会返回被删除的value。

(3)HashSet存储自定义对象

在元素类中重写hashCode方法和equals方法,依此来确保元素的唯一性

使用HashSet时,如果没有重写两个方法,则元素还是会重复录入,因为HashSet不知道判断两个对象相等的条件

重写时可以添加条件,比如name和age相等时,对象相等,则只会保留一个该数据的对象

如果存入的是字符串或整数等数据,则HashSet会自动判断唯一性

如果存入的是用户对象,则HashSet根据该对象的类中重写的hashCode方法和equals方法,来判断唯一性

示例 HashSet集合存储学生对象并遍历

学生类

//定义学生类
public class Student05 {
    private String name;
    private int age;

    public Student05() {
    }

    public Student05(String name, int age) {
        this.name = name;
        this.age = age;
    }

	//重写两个方法,如果学生对象的成员变量值相同,就认为是同一个对象
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student05 student05 = (Student05) o;
        return age == student05.age && Objects.equals(name, student05.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

 集合


import java.util.HashSet;

public class HashSetDemo01 {
    public static void main(String[] args) {
        HashSet<Student05> hs = new HashSet<Student05>();

        Student05 s1 = new Student05("张三", 20);
        Student05 s2 = new Student05("李四", 21);
        Student05 s3 = new Student05("王五", 23);

        Student05 s4 = new Student05("李四", 21);

        hs.add(s1);
        hs.add(s2);
        hs.add(s3);
        hs.add(s4);

        for (Student05 s : hs) {
            System.out.println(s.getName() + "," + s.getAge());
        }

    }
}

3、LinkedHashSet

LinkedHashSet概述

LinkedHashSet底层数据结构是哈希表和链表,根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的,也就是说当遍历集合LinkedHashSet集合里的元素时,集合将会按元素的添加顺序来访问集合里的元素。

输出集合里的元素时,元素顺序总是与添加顺序一致。但是LinkedHashSet依然是HashSet,因此它不允许集合重复。

4、TreeSet

TreeSet概述

TreeSet是SortedSet接口的实现类,TreeSet可以确保集合元素处于排序状态。

TreeSet内部实现的是红黑树,默认整形排序为从小到大。

不允许放入null值

TreeSet排序

存入TreeSet集合中的元素要具备比较性

比较性要实现Comparable接口,重写该接口的compareTo方法

TreeSet根据构造方法不同,分为自然排序(无参构造)和比较器排序(有参构造)

  • 自然排序要求元素必须实现Compareable接口,并重写里面的compareTo()方法,元素通过比较返回的int值来判断排序序列,返回0说明两个对象相同,不需要存储
  • 比较器排需要在TreeSet初始化是时候传入一个实现Comparator接口的比较器对象,或者采用匿名内部类的方式new一个Comparator对象,重写里面的compare()方法;

TreeSet两种构造方法:

方法名说明
TreeSet()根据其元素的自然排序进行排序
TreeSet(Comparator comparator)根据指定的比较器进行排序

排序详细做法

需求:比如要对Student对象排序,按年龄从小到大排,如果年龄一样,则按名字的自然顺序排

分析:对Student对象排序,有两种方式,第一种需要对Student类进行改造,即使用自然排序,第二种则需要另外写一个比较器类

自然排序

自然排序概述

用TreeSet集合存储对象,无参构造方法使用的是自然排序对元素进行排序

  • 这个方法,是让元素自身具备比较性
  • 自然排序,需要让元素所属的类实现Comparable接口,添加泛型,重写compareTo(T o)方法
  • 当调用一个一个对象调用该方法与另一个对象进行比较时,obj1.compareTo(obj2)如果返回0表示两个对象相等;如果返回正整数则表明obj1大于obj2,如果是负整数则相反。

自然排序的使用

1.在Student类中,需要继承Comparable接口,并添加泛型,如果只是Student类之间的比较,则泛型为Student类型就可以

public class Student implements Comparable<Student>{}

 2.在Student类中,重写compareTo方法,比较的类型还是它自己,Student类型

public int compareTo(Student06 s) {}

3.根据需要,添加比较器中的内容,有两种写法

  • 因为是先按年龄排,年龄相等时再按姓名排,所以年龄为首要条件,姓名为次要条件

    先写两个if一个return,如果大于返回1,等于返回0,小于直接返回-1

    在符合的首要条件里覆盖掉返回0,写次要条件

@Override
public int compareTo(Student06 s) {
    //判断首要条件
    //大于返回1
    if(this.age>s.age){
        return 1;
    }
    //等于,判断次要条件
    if (this.age==s.age){
        return this.name.compareTo(s.name);
    }
    //小于返回-1
    return -1;
}

这种写法把每种情况罗列且区分出来了,一目了然,而且可以很方便地修改比较条件。

比如原本是年龄升序排列,我要改成年龄降序,只需要把判断年龄条件之后执行的操作改成return -1即可

  • 第二种,首要条件和次要条件一致,但这次我们换一种写法,不写这么多if,直接返回表达式的值
@Override
public int compareTo(Student06 s) {
    int num = this.age - s.age;
    int num2 = (num == 0) ? this.name.compareTo(s.name) : num;
    return num2;
}

第二种写法显然更简洁,与第一种写法做的是同样的事情,但需要写出涉及比较的内容,有几层判断,定义几个int类型的返回值即可

自然排序的总结

  • 不符合面向对象的思维

    自然排序是对要排序的类直接操作,比较器的内容在被比较的类中,要更改条件,只能进入被比较的类中进行修改原理

  • 这里的this,指的是当前被比较的对象,s则是指同类型的其他对象

  • TreeSet是红黑树(二叉树),比较器用来给元素定位。如果比较器返回0,则元素重复,不添加,如果返回1或-1,则元素存储在二叉树的左或右孩子

自制排序

自制排序概述

TreeSet集合有参构造方法使用比较器排序对元素进行排序

  • 这个方法,是让容器具有比较性
  • 当元素自身不具备比较性,或者自身具备的比较性不是所需要的。那么此时可以让容器自身具备。需要定义一个类实现接口Comparator,重写compare方法,并将该接口的子类实例对象作为参数传递给TreeMap集合的构造方法。
  • 注意:当Comparable比较方式和Comparator比较方式同时存在时,以Comparator的比较方式为主;

    注意:在重写compareTo或者compare方法时,必须要明确比较的主要条件相等时要比较次要条件。(假设姓名和年龄一直的人为相同的人,如果想要对人按照年龄的大小来排序,如果年龄相同的人,需要如何处理?不能直接return 0,因为可能姓名不同(年龄相同姓名不同的人是不同的人)。此时就需要进行次要条件判断(需要判断姓名),只有姓名和年龄同时相等的才可以返回0.)

    通过return 0来判断唯一性。

  • 由于Comparator是一个函数式接口,因此还可以使用Lambda表达式来代替Comparator子类对象。

自制排序的使用

1.首先需要写一个比较器类

public class MyComparator implements Comparator<Student> {}

2.在比较器类中重写compare方法

public class MyComparator implements Comparator<Student> {

    @Override
    public int compare(Student s1, Student s2) {
    }

}

3.通过已知的首要、次要方法,添加比较内容

public class MyComparator implements Comparator<Student> {

    @Override
    public int compare(Student s1, Student s2) {
        //主要条件:学生年龄按从大到小排序
        int num = s2.getAge() - s1.getAge();

        //学生年龄相同,比较姓名是否一样
        int num2 = (num == 0) ? (s1.getName().compareTo(s2.getName())) : num;
        return num2;
    }

}

4.创建TreeSet集合时,需要指明使用了哪个自制比较器类

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

自制排序的总结

  • 符合面向对象思想

    无需改动被比较的类,只需要单独写一个比较器类即可,可以单独写,也可以在创建TreeSet时使用匿名内部类的方式添加

  • 比较器类中可以调用被比较对象的所有参数,灵活比较各种东西

注意

  • 大部分类在实现CompareTo(Object o)方法时,都需要将被比较对象obj强制类型转换成相同类型,因为只有相同的两个实例才会比较大小。

  • 加入集合的类都必须实现Comparable接口,否则会引发ClassCastException异常。

  • 不要修改已经存入集合的实例变量,这将导致它与其他对象的大小顺序发生改变,但TreeSet集合不会再次调整它们的顺序,这点和HashSet一样。

  • 总结:如果希望TreeSet能正常工作,TreeSet只能添加同一种类型的对象

TreeSet如何确保唯一性

对于TreeSet集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过compareTo(Object obj)方法比较是否返回0,如果是0则认为对象相等,否则认为不相等。

Set小结

Set具有与Collection完全一样的接口,因此没有任何额外的功能,不像前面有两个不同的List。实际上Set就是Collection,只 是行为不同。(这是继承与多态思想的典型应用:表现不同的行为。)Set不保存重复的元素。

TreeSet不允许放入null值,HashSet允许放入一个null值

适用场景分析:

HashSet是基于Hash算法实现的,其性能通常都优于TreeSet。为快速查找而设计的Set,我们通常都应该使用HashSet,在我们需要排序的功能时,我们才使用TreeSet。

List与Set的对比

Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。 List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。

如何挑选合适的Collection集合

看到array,就要想到索引。

看到link,就要想到first,last。

看到hash,就要想到hashCode,equals.

看到tree,就要想到两个接口。Comparable,Comparator。

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值