JavaSE学习进阶day06_02 Set集合和Set接口

第二章 Set系列集合和Set接口

Set集合概述:前面学习了Collection集合下的List集合,现在继续学习它的另一个分支,Set集合。

set系列集合的特点:

Set接口:

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

Set集合有多个子类,这里我们介绍其中的java.util.HashSetjava.util.LinkedHashSetjava.util.TreeSet这两个集合。

tips:Set集合取出元素的方式可以采用:迭代器、增强for。

2.1 HashSet集合介绍

java.util.HashSetSet接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序不能保证不一致)java.util.HashSet底层的实现其实是一个java.util.HashMap支持,由于我们暂时还未学习,先做了解。

HashSet是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存储和查找性能。保证元素唯一性的方式依赖于:hashCodeequals方法

我们先来使用一下Set集合存储,看下现象,再进行原理的讲解:

public class HashSetDemo {
    public static void main(String[] args) {
        //创建 Set集合
        HashSet<String>  set = new HashSet<String>();
​
        //添加元素
        set.add(new String("cba"));
        set.add("abc");
        set.add("bac"); 
        set.add("cba");  
        //遍历
        for (String name : set) {
            System.out.println(name);
        }
    }
}

输出结果如下,说明集合中不能存储重复元素:

cba
abc
bac

tips:根据结果我们发现字符串"cba"只存储了一个,也就是说重复的元素set集合不存储。 

为了方便理解,我们再给出一个例子:

我们发现重复的的第二个“111”字符串没有被添加进去,说明HashSet的特点就是像上面所说的,有去重复的功能,或者说不能添加重复的数据。因此打印只打印了一个字符串:“111”

再看:

添加的顺序是"111"、"333"、"222",但是打印的顺序是"111"、"222"、"333",说明打印顺序和添加顺序没有关系,也就是存的顺序和取的顺序不一致。也就是说HashSet是无序的。其实在底层是用HashMap实现的,后面会细说。

至于无索引怎么体现呢,可以到JDK参考文档里查看,HashSet集合里没有获取索引的方法。因此这也是它的一个特点:无索引。

HashSet在开发中的使用场景:

可以利用HashSet的特性给数据去重

代码示例:

//练习需求:给ArrayList进行去重
ArrayList<String> list = new ArrayList<>();
list.add("aaa");
list.add("aaa");
list.add("bbb");
list.add("bbb");
list.add("ccc");
list.add("ccc");
System.out.println(list);
​
​
//可以利用HashSet的特性给数据去重
HashSet<String> hs = new HashSet<>();
//把list集合中所有的元素全部添加到hs当中
hs.addAll(list);
​
//无序
System.out.println(hs);

运行结果:

2.2 HashSet集合存储数据的结构(哈希表)

什么是哈希表呢?在了解哈希表之前先得知道哈希值:

JDK1.7的哈希值:

关于更多哈希值的知识点请查看课件PPT的内容。

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

简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下图所示。

看到这张图就有人要问了,这个是怎么存储的呢?

为了方便大家的理解我们结合一个存储流程图来说明一下:

总而言之,JDK1.8引入红黑树大程度优化了HashMap的性能,那么对于我们来讲保证HashSet集合元素的唯一,其实就是根据对象的hashCode和equals方法来决定的如果我们往集合中存放自定义的对象,那么保证其唯一,就必须重写hashCode和equals方法建立属于当前对象的比较方式

哈希表详细流程:

2.3 HashSet存储自定义类型元素

给HashSet中存放自定义类型元素时,比如学生类型,需要重写对象中的hashCode和equals方法,建立自己的比较方式,才能保证HashSet集合中的对象唯一.

创建自定义Student类:

public class Student {
    private String name;
    private int age;
​
​
    public Student() {
    }
​
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
​
    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }
​
    /**
     * 设置
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }
​
    /**
     * 获取
     * @return age
     */
    public int getAge() {
        return age;
    }
​
​
​
    /**
     * 设置
     * @param age
     */
    public void setAge(int age) {
        this.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);
    }
​
    public String toString() {
        return "Student{name = " + name + ", age = " + age + "}";
    }
}
​

创建测试类:

public class HashSetDemo1 {
    public static void main(String[] args) {
        //创建集合
        HashSet<Student> hs = new HashSet<>();
​
        //如果在Student类中,没有重写hashCode和equals方法
        //都是操作的都是地址值。
        //如果我们认为,属性相同,就是同一个对象,那么就需要重写hashCode和equals方法
        //添加元素
        hs.add(new Student("zhangsan",23));
        hs.add(new Student("zhangsan",23));
        hs.add(new Student("zhangsan",23));
​
        System.out.println(hs);
    }
}

这段代码可能理解不透彻,我细说一下,现在就是创建一个学生类Student类,学生属性有姓名跟年龄,我们用快捷键生成标准javabean类后,如下:

注意啊,现在没有重写equals方法和hashCode方法,我们在测试类写入如下代码并运行:

发现重复的元素也添加进去了,这是为什么??不是说HashSet不是有不重复的特点吗?为啥上面那个String就不能添加重复的元素,而自定义的Student类不能。这就是我们要讨论的问题。当添加的元素是自己自定义的类型后,就需要重写equals方法和hashCode方法。快捷键:alt+insert

 

原来这个快捷键每一项都是有用的,现在学了很多了。重写后我们再运行上面那个代码:

 

发现现在不能添加重复的元素了。总结:再用HashSet集合添加自定义元素(除了java提供的都叫自定义元素)时,都要重写equals和hashCode方法才能保证它的不重复性。前面那个String之所以不能添加重复元素,是因为在String类已经重写equals和hashCode方法了。其他包装类也是如此

2.4 LinkedHashSet

我们知道HashSet保证元素唯一,可是元素存放进去是没有顺序的,那么我们要保证有序,怎么办呢

在HashSet下面有一个子类java.util.LinkedHashSet,它是链表和哈希表组合的一个数据存储结构。它能使得元素存放时是有序的。但是在以后的开发中,LinkedHashSet用的不多,因为它的底层多了一个双向链表,效率有点低。以后用hashSet的比较多。

先看一个代码:

发现添加元素的顺序和打印的顺序是一致的。相比hashSet,LinkHashSet就多了一个有序性

演示代码如下:

public class LinkedHashSetDemo {
    public static void main(String[] args) {
        Set<String> set = new LinkedHashSet<String>();
        set.add("bbb");
        set.add("aaa");
        set.add("abc");
        set.add("bbc");
        Iterator<String> it = set.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
}
结果:
  bbb
  aaa
  abc
  bbc

2.5 TreeSet集合

1. 特点

TreeSet集合是Set接口的一个实现类,底层依赖于TreeMap,是一种基于红黑树的实现,其特点为:

  1. 元素唯一(不能添加重复的元素)

  2. 元素没有索引

  3. 使用元素的自然顺序对元素进行排序,或者根据创建 TreeSet 时提供的 Comparator比较器 进行排序,具体取决于使用的构造方法:

先看一个案例:

发现Integer打印的是默认升序排序,且不能添加重复的元素。

2.排序方式一:默认方式

默认的排序方式,也叫做自然排序

是让Javabean类实现Comparable接口,重写里面的compareTo方法来实现的。

注意点:

1.Java已经写好的String,Integer已经定义好了默认规则。

String:按照首字母的字典顺序排序,如果首字母一样,则比较第二个字母。

Integer:升序

2.我们自己定义了Javabean类需要指定默认排序规则,否则会报错。

代码示例1(存自定义对象):

package com.itheima.a04treesetdemo;
​
public class Student implements Comparable<Student> {
    private String name;
    private int age;
​
    //...空参...
    //...有参...
    //...get和set方法...
    //...toString方法...
​
    @Override
    public int compareTo(Student o) {
        //按照年龄进行排序
        //this表示当前要添加的元素
        //o:已经在树里面存在的元素
        //如果结果是负数,那么就存左边(降序)
        //如果结果是正数,那么就存右边(升序)
        //如果结果是0,认为现在要添加的元素跟当前元素一直,就不存
​
        //System.out.println("this:" + this);
        //System.out.println("o:" + o);
        return this.age - o.age;
    }
}

代码示例2(存Integer):

案例演示自然排序(20,18,23,22,17,24,19):

public static void main(String[] args) {
    //无参构造,默认使用元素的自然顺序进行排序
    TreeSet<Integer> set = new TreeSet<Integer>();
    set.add(20);
    set.add(18);
    set.add(23);
    set.add(22);
    set.add(17);
    set.add(24);
    set.add(19);
    System.out.println(set);
}
​
控制台的输出结果为:
[17, 18, 19, 20, 22, 23, 24]

3.排序方式二:比较器排序

创建集合对象时,传递Comparator实现类的对象,并重写compare方法。

并在使用的时候,默认用第一种,当第一种不能满足要求的时候,可以用第二种排序方式。

比如,Integer默认升序,如果我想降序排列,就需要用第二种了。

比如,String默认按照字典的顺序排列,如果我想按照字符串的长度排列,就需要用第二种了

案例:

演示比较器排序(20,18,23,22,17,24,19):

public static void main(String[] args) {
    //有参构造,传入比较器,使用比较器对元素进行排序
    TreeSet<Integer> set = new TreeSet<Integer>(new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            //元素前 - 元素后 : 升序
            //元素后 - 元素前 : 降序(C语言库的快速排序算法就是这个原理)
            return o2 - o1;
        }
    });
    set.add(20);
    set.add(18);
    set.add(23);
    set.add(22);
    set.add(17);
    set.add(24);
    set.add(19);
    System.out.println(set);
}
​
控制台的输出结果为:
[24, 23, 22, 20, 19, 18, 17]

2.6练习-存储学生信息按照总分排序

需求:

需求:键盘录入3个学生信息(姓名,语文成绩,数学成绩,英语成绩),按照总分从高到低输出到控制台

隐藏的排序规则:总分一致还得按照语文,数学,英语,姓名这样的顺序排序。

分析

①定义学生类

②创建TreeSet集合对象,通过进行排序

③创建学生对象

④把学生对象添加到集合

⑤遍历集合

注意:

在指定顺序的时候默认按照自然排序方式,当自然排序不能满足我们的要求时,就用比较器排序。

代码示例:

public class Student implements Comparable<Student>{
    private String name;
    private double chinese;
    private double math;
    private double english;
    
    ...空参构造...
    ...带参构造...
    ...get和set方法...
    ...toString方法...    
​
    @Override
    public int compareTo(Student o) {
        //this o
        double sum1 = this.chinese + this.math + this.english;
        double sum2 = o.chinese + o.math + o.english;
        double sum = sum1 - sum2;
​
        //总分一样,看语文
        double result = sum == 0 ? this.chinese - o.chinese : sum;
        //语文一样,看数学
        result = result == 0 ? this.math - o.math : result;
        //学生一样,看英语
        result = result == 0 ? this.english - o.english : result;
        //英语一样,看姓名
        result = result == 0 ? this.name.compareTo(o.name) : result;
        return (int)result;
    }
}
​
​
​
public class TreeSetTest1 {
    public static void main(String[] args) {
        //1.创建集合
        TreeSet<Student> ts = new TreeSet<>();
​
        //2.
        //只要使用TreeSet就一定要指定排序规则。
        //首先默认按照自然排序
        Scanner sc = new Scanner(System.in);
        for (int i = 0; i < 3; i++) {
            System.out.println("请输入学生的姓名");
            String name = sc.next();
            System.out.println("请输入语文成绩");
            double chinese = sc.nextDouble();
            System.out.println("请输入数学成绩");
            double math = sc.nextDouble();
            System.out.println("请输入英语成绩");
            double english = sc.nextDouble();
​
            Student s = new Student(name,chinese,math,english);
            ts.add(s);
        }
        //打印
        System.out.println(ts);
    }
}

这个代码含金量很高,一定要掌握,自己手动写出来!!TreeSet的内容差不多就这些了,掌握到这里就已经不错了!

单列集合总结:

以后单列集合用的最多的是,ArrayList、HashSet、TreeSet。前面两种用的最为频繁。默认用ArrayList,要去重的话用HashSet,如果需要排序用TreeSet(这是今后开发用的实战用到的哦)。

LinkedHashSet和LinkedList基本用不到。

可变参数:什么是可变参数?为什么要引入可变参数?如下:

 

如果我们按照之前的做法就是重写多个方法,这样会非常麻烦,因为你也不知道数据有几个,或者有的人直接创建数组,然后传入数组长度,这样也可以,但是还是有点小丢丢麻烦。可变参数就是可以传入任意个数的参数。

如:

测试类:可以传任意个数的数据,这就是可变参数,它的本质就是用数组实现的。

 

注意:当形参有多个时,可变参数只能写在最后面的位置!!放在前面的话后面的形参会传递不到值

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值