在上一篇文章中我们讲解了单列集合的顶层Collection的第一种单列集合List,那下面我们就来学习一下第二种Set系列的单列集合吧,本文将详细的介绍Set集合。
Set集合
Set集合特点
- 无序:存取顺序不一致。
- 无索引:没有带索引的方法,所以不能使用普通for遍历,也不能通过索引来获取元素。
- 不可重复:可以去除重复。
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底层原理
-
HashSet集合底层采取哈希表存储数据。
-
哈希表是一种对于增删改查数据性能都较好的结构。
-
哈希表组成:
- JDK8以前:数组+链表
- JDK8以后:数组+链表+红黑树。
HashSet存入原理
-
创建应该默认长度16,默认加载因此0.75的数组。数组名:teble。
-
根据元素的哈希值跟数组的长度计算出应存入的位置。
- 存入公式:
int index = (数组长度 - 1) & 哈希值
- 存入公式:
-
判断当前位置是否为null,如果是null直接存入。
-
如果位置不为null,表示有元素,则调用equals方法比较属性值。
- 一样:舍弃不存。不一样:存入数组,新元素直接挂在老元素下面,形成链表。
-
当链表长度大于8而且数组长度大于等于64的时候,会自动转成红黑树。
哈希值
哈希值:对象的整数表现形式
-
根据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());
可以看到在默认情况下用地址值去计算哈希值,是不是就不一样了呀!
- 我们来看第二种:重写hashCode方法
那怎么重写hashCode方法呢?其实不用我们自己重写的,idea可以帮我们重写,我们可以按快捷键:FN+ALT+INSERT
选择重写equals方法,后面有个hashCode方法。
是不是已经帮我们重写好了equals和hashCode方法呀。这个时候我们代码不变,重新运行来看一下:
这个时候属性值一样,是不是计算出来的哈希值也是一样了呀!
- 在小部分情况下,不同的属性值或者不同的地址值计算出来的哈希值也有可能一样。会叫(哈希碰撞)
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);
这个时候我们来运行看一下:
是不是发现只存进去了一个张三呀,另外一个张三去重了没有存进。
好啦,到这里HashSet就学习完毕啦,我们下面继续来学习他的儿子:LinkedHashSet吧!
LinkedHashSet集合
LinkedHashSet他的辈分比较低,刚刚我们学习了他爹HashSet。所以在学习他的时候就很轻松,我们来看下面这张图吧:
可以直接使用他们上面的方法就可以啦
LinkedHashSet底层原理
-
有序、不重复、无索引
- 这里的有序是保证存储和取出的元素顺序是一致的。
-
原理:底层数据结构依然是哈希表,只是使用双链表记录添加顺序。
下面我们也来实操一下吧:
代码演示
// 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);
这个时候我们来运行看一下:
是不是把重复的去掉了,然后按照添加的顺序打印出来的呀,那我这个时候把顺序打乱一下看看:
stu.add(stu3);
stu.add(stu1);
stu.add(stu2);
stu.add(stu4);
是不是也是一样的呀,按照添加的先后顺序打印呀!
那关于HasgSet和LinkedHashSet我什么时候用哪个比较合适呢?我们下面来总结一下吧:
小结
- 以后如果要数据去重,我们使用哪个?
- 默认使用HashSet(效率高)
- 如果要求去重且存取有序,才使用LinkedHashSet
好啦,到这里HashSet和LinkedHashSet就学习完毕啦!我们下面来学习最后一个TreeSet吧!
TreeSet集合
TreeSet底层原理
- 可排序、不重复、无索引
- 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);
这个时候我们来打印看一下:
他直接给你干了个报错回来,这是为什么呀?
其实是因为student类型是我们自己写的,我们并没有给他去添加一个比较规则,所以他就不知道该怎么比,所以在添加元素的时候就报错了。那我们该怎么给他添加比较规则呢?我们接着往下看:
TreeSet的两种比较规则
方式一:默认排序/自然排序:JavaBean类实现Comparable接口指定比较规则。
利用Student实现Comparable接口,重写里面的抽象方法,在指定比较规则
这个时候JavaBean类中就多了个compareTo的方法
然后就可以在方法里面重写指定排序的规则:
@Override
public int compareTo(stu1 o) {
// 指定排序的规则
// 假设只看年龄,按照年龄的升序进行排序
return this.getAge() - o.getAge();
}
那这里的this、o、返回值是什么意思呢?
- this:表示当前要添加的元素
- o:表示已经在红黑树存在的元素
- 返回值:
- 负数:认为要添加的元素是小的,存在左边
- 正数:认为要添加的元素是大的,存在右边
- 0:认为要添加的元素已经存在,舍弃
这个时候我们再来运行一次看一下:
这个时候是不是就按照从小到大进行排序了呀!那有时候默认排序不能满足我的要求了怎么办呢?下面我们来学习第二种方式吧:
方式二:比较器排序:创建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);
我们这个时候来打印看一下:
是不是默认按照首字母进行排序呀?那不行呀,我要按照长度排序,长度一样再按照字母排序呀,那怎么办呢?那我们可以使用第二种比较方式来指定排序规则哟,我们一起来看一下:
那这里的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;
}
这个时候我们来运行看一下:
是不是就按照长度进行排序了呀,长度一样的话就按照默认的对字母进行排序方法排序。
下面我们来做个案例练一下吧:
案例演示
- 需求:创建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 + " ");
}
最后我们打印看一下吧:
是不是按照我们的规则来进行排序了呀!好啦,到这里我们就学完TreeSet集合啦,下面来做个总结吧:
TreeSet总结
-
Treeset集合的特点是怎么样的
- 可排序、不重复、无索引
- 底层基于红黑树实现排序,,增删改查性能较好
-
Treeset集合自定义排序规则有几种方式
- 方式一:Javabean类实现Comparable接口,指定比较规则
- 方式二:1创建集合时,自定义Comparator比较器对象,指定比较规则
-
Treeset方法返回值的特点
- 负数:表示当前要添加的元素是小的,存左边
- 正数:表示当前要添加的元素是大的,存右边
- 0:表示当前要添加的元素已经存在,舍弃
到这里我们的单列List、Set集合就全部学习完毕啦,那这么多集合,我什么时候用哪种呢?下面也来做个大总结吧
单列集合应用场景
- 如果想要集合中的元素可重复
- 用ArrayList集合,基于数组的。(用的最多)
- 如果想要集合中的元素可重复,而且当前的增删操作明显多于查询
- 用LinkedList集合,基于链表的。
- 如果想对集合中的元素去重
- 用Hashset集合,基于哈希表的。(用的最多)
- 如果想对集合中的元素去重,而且保证存取顺序
- 用LinkedHashSet集合,基于哈希表和双链表,效率低于Hashset.
- 如果想对集合中的元素进行排序
- 用Treeset集合,基于红黑树。后续也可以用List集合实现排序
HashSet和TreeSet易混点
- HashSet底层基于哈希表实现的,需要重写hashCode和equals方法。
- TreeSet底层基于红黑树实现的,不用重写hashCode和equals方法,但是要指定排序规则
好啦,到这里单列集合的全部单列List、Set集合就学习完毕啦,有什么不懂的可以在评论区互相探讨哟,我们下期不见不散!!!
==最后非常感谢您的阅读,也希望能得到您的反馈 ==