从零开始学Java-单列集合:Set

110 阅读14分钟

在上一篇文章中我们讲解了单列集合的顶层Collection的第一种单列集合List,那下面我们就来学习一下第二种Set系列的单列集合吧,本文将详细的介绍Set集合。

Set集合

Set集合特点

  1. 无序:存取顺序不一致。
  2. 无索引:没有带索引的方法,所以不能使用普通for遍历,也不能通过索引来获取元素。
  3. 不可重复:可以去除重复。

Set集合的实现类特点

  • HashSet:无序、不重复、无索引。
  • LinkedHashSet:有序、不重复、无索引。
  • TreeSet:可排序、不重复、无索引。

Set接口中的方法基本上与Collecton的API一致,所以没有什么额外的方法来学习,直接使用Collecton中常见方法就可以了。

Set方法

常用的方法如下:

方法名说明
public boolean add(Ee)把给定的对象添加到当前集合中
public void clear()清空集合中所有的元素
public boolean remove(Ee)把给定的对象在当前集合中删除
public boolean contains(object obj)判断当前集合中是否包含给定的对象
public boolean isEmpty()判断当前集合是否为空
public int size()返回集合中元素的个数/集合的长度

下面我们也来实操一下吧:

代码演示

  • 常见Set集合的对象
// 1.创建一个Set集合的对象
Set<String> s = new HashSet<>();
  • 添加元素
// 2.添加元素
boolean s1 = s.add("张三");
boolean s2 = s.add("张三");
System.out.println(s1);     // true
System.out.println(s2);     // false

这边有个小小细节需要注意一下:

如果当前元素是第一次添加,那么添加成功返回true

如果当前元素是第二次添加,那么添加失败返回false

  • 遍历集合
// 3.打印集合
// 迭代器遍历
Iterator<String> it = s.iterator();
while (it.hasNext()){
    String str = it.next();
    System.out.print(str + " ");
}

// 增强for遍历
for (String str : s) {
    System.out.print(str + " ");
}

//Lambda表达式遍历
s.forEach((str)-> System.out.print(str + " "));

到这里Set集合就学完啦,我们下面来学习他每个实现类的方法吧!

HashSet集合

HashSet底层原理

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

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

  3. 哈希表组成:

    1. JDK8以前:数组+链表
    2. JDK8以后:数组+链表+红黑树。

HashSet存入原理

  1. 创建应该默认长度16,默认加载因此0.75的数组。数组名:teble

  2. 根据元素的哈希值跟数组的长度计算出应存入的位置。

    1. 存入公式:int index = (数组长度 - 1) & 哈希值
  3. 判断当前位置是否为null,如果是null直接存入。

  4. 如果位置不为null,表示有元素,则调用equals方法比较属性值。

    1. 一样:舍弃不存。不一样:存入数组,新元素直接挂在老元素下面,形成链表。

    image.png

  5. 当链表长度大于8而且数组长度大于等于64的时候,会自动转成红黑树

    image.png

哈希值

哈希值:对象的整数表现形式

  • 根据HashCode方法算出来的int类型的整数。

  • 该方法定义在Object类中,所有对象都可以调用,默认使用地址值进行计算。

  • 一般情况下会重写hashCode方法,利用对象内部的属性值计算哈希值

  • 对象的哈希值特点

    • 如果没有重写hashCode方法,不同对象计算出的哈希值是不同的。
    • 如果已经重写hashCode方法,不同的对象只要属性值相同,计算出的哈希值就是一样的。
    • 在小部分情况下,不同的属性值或者不同的地址值计算出来的哈希值也有可能一样。会叫(哈希碰撞)

下面我们也来实操一下吧:

代码演示

  • 创建有个学生对象的JavaBean
private String name;
private int age;
  • 创建两个学生对象信息
// 1. 创建学生对象
Student stu1 = new Student("张三",20);
Student stu2 = new Student("张三",20);
  • 我们先来看第一种:没有重写hashCode方法
System.out.println(stu1.hashCode());
System.out.println(stu2.hashCode());

image.png

可以看到在默认情况下用地址值去计算哈希值,是不是就不一样了呀!

  • 我们来看第二种:重写hashCode方法

那怎么重写hashCode方法呢?其实不用我们自己重写的,idea可以帮我们重写,我们可以按快捷键:FN+ALT+INSERT

image.png

选择重写equals方法,后面有个hashCode方法。

image.png

是不是已经帮我们重写好了equals和hashCode方法呀。这个时候我们代码不变,重新运行来看一下:

image.png

这个时候属性值一样,是不是计算出来的哈希值也是一样了呀!

  • 在小部分情况下,不同的属性值或者不同的地址值计算出来的哈希值也有可能一样。会叫(哈希碰撞)
System.out.println("abc".hashCode());   // 96354
System.out.println("acD".hashCode());   // 96354

这个时候是不是属性值不一样,但是计算出来的哈希值就一样了呀,这也叫哈希碰撞,当然这也是小概率的事件。

HashSet的疑惑

  • HashSet为什么存和取的顺序不一样

    HashSet在遍历的时候从0的值开始遍历的,然后遍历1的值的时候如果有链表会继续遍历链表,遍历完链表才会继续遍历2的值,如果2上面有红黑树的话会继续遍历完才会继续遍历3的值。同时这也说明了在前面的元素有可能不是一开始就存进去的,所以存和取的顺序就不一样。

  • HashSet为什么没有索引

    HashSet底层是由数组、链表、红黑树组成的,所以取消了索引机制。

  • HashSet是利用什么机制保证数据去重的

    底层是利用HashCode方法equals方法比较去重的。HashCode方法是可以计算出哈希值,而哈希值可以计算出应存入的位置,然后在去调用equals方法比较对象内部属性值是否相等。

下面我们来看个案例吧:

简单案例

  • 利用HashSet集合去除重复元素

    • 需求:创建一个存储学生对象的集合,存储多个学生对象。
    • 要求:学生对象的成员变量值相同,我们就认为是同一个对象
  • 先来创建一个学生对象的JavaBean类

private String name;
private int age;
// 1.创建三个学生对象
student stu1 = new student("张三",19);
student stu2 = new student("张三",19);
student stu3 = new student("李四",18);
student stu4 = new student("王五",20);
// 2.创建集合添加学生
HashSet<student> stu = new HashSet<>();
// 3.添加元素
stu.add(stu1);
stu.add(stu2);
stu.add(stu3);
stu.add(stu4);
// 4.打印集合
System.out.println(stu);

这个时候我们来运行看一下:

image.png

是不是发现只存进去了一个张三呀,另外一个张三去重了没有存进。

好啦,到这里HashSet就学习完毕啦,我们下面继续来学习他的儿子:LinkedHashSet吧!

LinkedHashSet集合

LinkedHashSet他的辈分比较低,刚刚我们学习了他爹HashSet。所以在学习他的时候就很轻松,我们来看下面这张图吧:

image.png

可以直接使用他们上面的方法就可以啦

LinkedHashSet底层原理

  1. 有序、不重复、无索引

    1. 这里的有序是保证存储和取出的元素顺序是一致的。
  2. 原理:底层数据结构依然是哈希表,只是使用双链表记录添加顺序。 image.png

下面我们也来实操一下吧:

代码演示

// 1.创建4个学生对象
student stu1 = new student("张三",23);
student stu2 = new student("李四",24);
student stu3 = new student("王五",25);
student stu4 = new student("张三",23);
// 2.创建集合添加学生对象
LinkedHashSet<student> stu = new LinkedHashSet<>();
// 3.添加学生对象到集合
stu.add(stu1);
stu.add(stu2);
stu.add(stu3);
stu.add(stu4);
System.out.println(stu);

这个时候我们来运行看一下:

image.png 是不是把重复的去掉了,然后按照添加的顺序打印出来的呀,那我这个时候把顺序打乱一下看看:

stu.add(stu3);
stu.add(stu1);
stu.add(stu2);
stu.add(stu4);

image.png

是不是也是一样的呀,按照添加的先后顺序打印呀!

那关于HasgSet和LinkedHashSet我什么时候用哪个比较合适呢?我们下面来总结一下吧:

小结

  • 以后如果要数据去重,我们使用哪个?
    • 默认使用HashSet(效率高)
    • 如果要求去重且存取有序,才使用LinkedHashSet

好啦,到这里HashSet和LinkedHashSet就学习完毕啦!我们下面来学习最后一个TreeSet吧!

TreeSet集合

TreeSet底层原理

  1. 可排序、不重复、无索引
  2. TreeSet集合底层基于红黑树的数据结构实现排序的,增删改查性能都比较好。

TreeSet默认排序规则

  • 对于数值类型:Integer,Double,默认按照从小到大的顺序进行排序
  • 对于字符、字符串类型:按照字符在ASCII码表中的数字升序进行排序。

我们下面来看一个案例吧:

  • 需求:创建TreeSet集合,并添加3个学生对象
    • 学生对象属性:姓名,年龄。
    • 要求按照学生的年龄进行排序:同年龄按照姓名字母排列(暂不考虑中文)同姓名,同年龄认为是同一个人
// 1.创建4个学生对象
student stu1 = new student("zhangsan",23);
student stu2 = new student("lisi",24);
student stu3 = new student("wangwu",25);
// 2.创建集合添加学生对象
TreeSet<student> stu = new TreeSet<>();
// 3.添加学生对象到集合
stu.add(stu3);
stu.add(stu1);
stu.add(stu2);
System.out.println(stu);

这个时候我们来打印看一下:

image.png

他直接给你干了个报错回来,这是为什么呀?

其实是因为student类型是我们自己写的,我们并没有给他去添加一个比较规则,所以他就不知道该怎么比,所以在添加元素的时候就报错了。那我们该怎么给他添加比较规则呢?我们接着往下看:

TreeSet的两种比较规则

方式一:默认排序/自然排序:JavaBean类实现Comparable接口指定比较规则。

利用Student实现Comparable接口,重写里面的抽象方法,在指定比较规则

image.png

这个时候JavaBean类中就多了个compareTo的方法

image.png

然后就可以在方法里面重写指定排序的规则:

@Override
public int compareTo(stu1 o) {
    // 指定排序的规则
    // 假设只看年龄,按照年龄的升序进行排序
    return this.getAge() - o.getAge();
}

那这里的this、o、返回值是什么意思呢?

  • this:表示当前要添加的元素
  • o:表示已经在红黑树存在的元素
  • 返回值:
    • 负数:认为要添加的元素是小的,存在左边
    • 正数:认为要添加的元素是大的,存在右边
    • 0:认为要添加的元素已经存在,舍弃

这个时候我们再来运行一次看一下:

image.png

这个时候是不是就按照从小到大进行排序了呀!那有时候默认排序不能满足我的要求了怎么办呢?下面我们来学习第二种方式吧:

方式二:比较器排序:创建TreeSet对象时候,传递比较器Comparator指定规则

  • 我们也来看一个案例吧:
    • 需求:请自行选择比较器排序和自然排序两种方式
    • 要求:存入四个字符串,按照长度排序,如果一样长则按照首字母排序
// 1.创建集合
TreeSet<String> str = new TreeSet<>();
// 2.添加元素
str.add("ccc");
str.add("bb");
str.add("daab");
str.add("baac");
str.add("accc");
// 3.打印集合
System.out.println(str);

我们这个时候来打印看一下:

image.png

是不是默认按照首字母进行排序呀?那不行呀,我要按照长度排序,长度一样再按照字母排序呀,那怎么办呢?那我们可以使用第二种比较方式来指定排序规则哟,我们一起来看一下:

image.png

image.png

那这里的o1和o2又是什么意思呢?

  • o1:表示当前要添加的元素
  • o2:表示已经在红黑树存在的元素
  • 返回值:
    • 负数:认为要添加的元素是小的,存在左边
    • 正数:认为要添加的元素是大的,存在右边
    • 0:认为要添加的元素已经存在,舍弃

既然我们知道了参数,那下面重写里面的compare方法就可以啦:

public int compare(String o1, String o2) {
    // 按照长度排序
    int i = o1.length() - o2.length();
    // 如果长度一样则按照首字母排序
    i = i == 0 ? o1.compareTo(o2) : i;
    return i;
}

这个时候我们来运行看一下:

image.png

是不是就按照长度进行排序了呀,长度一样的话就按照默认的对字母进行排序方法排序。

下面我们来做个案例练一下吧:

案例演示

  • 需求:创建5个学生对象,属性:(姓名,年龄,语文成绩,数学成绩,英语成绩)
    • 按照总分从高到低输出到控制台,如果总分一样,按照语文成绩排
    • 如果语文一样,按照数学成绩排,如果数学成绩一样,按照英语成绩排
    • 如果英文成绩一样,按照年龄排,如果年龄一样,按照姓名的字母顺序排
    • 如果都一样,认为是同一个学生,不存。 先来创建一个学生类的JavaBean
private String name;    // 姓名
private int age;        // 年龄
private int chinese; // 语文
private int math;    // 数学
private int english; // 英语

接着来创建学生对象并添加进集合中

// 1.创建学生对象
Student1 stu1 = new Student1("zhangsan", 23, 90, 99, 50);
Student1 stu2 = new Student1("lisi", 24, 90, 98, 50);
Student1 stu3 = new Student1("wangwu", 25, 95, 100, 30);
Student1 stu4 = new Student1("zhaoliu", 26, 60, 99, 70);
Student1 stu5 = new Student1("qianqi", 26, 70, 80, 70);
// 2.创建集合
TreeSet<Student1> stu = new TreeSet<>();
// 3.添加元素
stu.add(stu1);
stu.add(stu2);
stu.add(stu3);
stu.add(stu4);
stu.add(stu5);

由于默认的排序不能满足我们的题目要求,所以这边需要重写一下他的排序方法:

@Override
public int compareTo(Student1 o) {
    int sum1 = this.getChinese() + this.getMath() + this.getEnglish();
    int sum2 = o.getChinese() + o.getMath() + o.getEnglish();
    // 比较两者的总分
    int sum = sum2 - sum1;
    // 如果总分一样,就按照语文成绩排序
    sum = sum == 0 ? this.getChinese() - o.getChinese() : sum;
    // 如果语文成绩一样,就按照数学成绩排序
    sum = sum == 0 ? this.getMath() - o.getMath() : sum;
    // 如果数学成绩一样,按照英语成绩排序
    sum = sum == 0 ? this.getEnglish() - o.getEnglish() : sum;
    // 如果英文成绩一样,按照年龄排序
    sum = sum == 0 ? this.getAge() - o.getAge() : sum;
    // 如果年龄一样,按照姓名的字母顺序排序
    sum = sum == 0 ? this.getName().compareTo(o.getName()) : sum;
    return sum;
}

遍历集合:

// 4.遍历集合
for (Student1 str : stu) {
    System.out.println(str + " ");
}

最后我们打印看一下吧:

image.png

是不是按照我们的规则来进行排序了呀!好啦,到这里我们就学完TreeSet集合啦,下面来做个总结吧:

TreeSet总结

  • Treeset集合的特点是怎么样的

    • 可排序、不重复、无索引
    • 底层基于红黑树实现排序,,增删改查性能较好
  • Treeset集合自定义排序规则有几种方式

    • 方式一:Javabean类实现Comparable接口,指定比较规则
    • 方式二:1创建集合时,自定义Comparator比较器对象,指定比较规则
  • Treeset方法返回值的特点

    • 负数:表示当前要添加的元素是小的,存左边
    • 正数:表示当前要添加的元素是大的,存右边
    • 0:表示当前要添加的元素已经存在,舍弃

到这里我们的单列List、Set集合就全部学习完毕啦,那这么多集合,我什么时候用哪种呢?下面也来做个大总结吧

单列集合应用场景

  1. 如果想要集合中的元素可重复
    • 用ArrayList集合,基于数组的。(用的最多)
  2. 如果想要集合中的元素可重复,而且当前的增删操作明显多于查询
    • 用LinkedList集合,基于链表的。
  3. 如果想对集合中的元素去重
    • 用Hashset集合,基于哈希表的。(用的最多)
  4. 如果想对集合中的元素去重,而且保证存取顺序
    • 用LinkedHashSet集合,基于哈希表和双链表,效率低于Hashset.
  5. 如果想对集合中的元素进行排序
    • 用Treeset集合,基于红黑树。后续也可以用List集合实现排序

HashSet和TreeSet易混点

  • HashSet底层基于哈希表实现的,需要重写hashCode和equals方法。
  • TreeSet底层基于红黑树实现的,不用重写hashCode和equals方法,但是要指定排序规则

好啦,到这里单列集合的全部单列List、Set集合就学习完毕啦,有什么不懂的可以在评论区互相探讨哟,我们下期不见不散!!!

==最后非常感谢您的阅读,也希望能得到您的反馈  ==