文章目录
一、介绍
TreeSet
也是 Set集合体系
中的一员,在 TreeSet
中,也没有新的方法需要学习,直接使用上面 Colleciton
里面的方法就行了。
在 TreeSet
中延续了 Set集合
的两个特点:不重复、无索引,但是它是可以给元素进行排序的。
可排序
:默认情况下,是按照从小到大的顺序进行排列的,因为在底层,TreeSet
是基于红黑树的数据结构实现排序的,增删改查性能都比较好。
二、练习:TreeSet
对象排序
需求:存储整数并进行排序
首先来创建 TreeSet集合
的对象,注意泛型不能直接写 int
,而是需要写对应的包装类。
TreeSet<Integer> ts = new TreeSet<>();
然后添加元素,并打印 ts
。
//2.添加元素
ts.add(4);
ts.add(5);
ts.add(1);
ts.add(3);
ts.add(2);
程序运行完毕,可以发现已经排序成功,并且默认排序规则:从小到大
//3.打印集合
System.out.println(ts); // [1, 2, 3, 4, 5]
同样遍历也是可以的。
由于 TreeSet
是 Set体系
中的一员,因此三种遍历方式它都是可以的。
//迭代器
Iterator<Integer> it = ts.iterator();
while(it.hasNext()){
int i = it.next();
System.out.println(i);
}
System.out.println("--------------------------");
//增强for
for (int t : ts) {
System.out.println(t);
}
这里的 forEach()
在底层是通过 增强for
来进行遍历的。
this
表示的是当前方法的调用者,而我们方法的调用者就是 ts
,即集合
//lambda
ts.forEach( i-> System.out.println(i));
三、字符串比较规则
- 对于数值类型:
Integer
、Double
,默认按照从小到大的顺序进行排序。 - 对于字符、字符串类型:按照字符在ASCII码表中的数字升序进行排序。
很多课程中都会这么去解释:字符和字符串默认的排序是按照字典的顺序进行排列的。
这句话表示的结果是对的,因为最终的排序结果跟字典里面的是一样的。
但是如果细细琢磨源码的话,这句话就不对了,因为在源码中,它是按照 ASCII码表
的数字来进行排列的,只不过刚好 ASCII码表
中字母的顺序跟字典上的顺序是一样的。
因此在以后,建议大家还是按照标准的方式来进行理解:按照字符在ASCII码表中的数字升序进行排序。
那如果字符串里面的内容比较多,那它是怎么排列的呢?
如果字符串里的字符比较多,那么它就是从首字母开始,挨个比较的,要注意的是,此时跟字符串的长度是没有什么关系的。
首先来看 "aaa"
和 "ab"
,在比较的时候,首先比第一个字母,发现第一个字母都是 a
;
继续往后,来比第二个字母,第二个字母就不一样了,这个时候就已经能确定大小关系了,'a'
比 b
大,此时后面的就不会再看了。
因此 "aaa"
要在 "ab"
的前面。
接下来看 "ab"
和 "aba"
。
按照刚刚的规则,先比较第一个字母,发现是一样的,继续比,直到比到第三个字母,"ab"
没有第三个字母,但是 "aba"
有第三个字母,因此它就会认为 "aba"
要更大,因此 "aba"
排在后面。
接下来看 "aba"
和 "cd"
,此时第一个字母就能确定大小关系了,后面就不会再去看了,因此 "aba"
要排在前面。
四、自定义对象比较规则
1)引出问题
需求:创建TreeSet集合,并添加3个学生对象
学生对象属性:
姓名,年龄。
要求按照学生的年龄进行排序
同年龄按照姓名字母排列(暂不考虑中文)
同姓名,同年龄认为是同一个人
首先得写一个学生类
public class Student {
//姓名
private String name;
//年龄
private int age;
// 无参构造、有参构造、get、set方法
}
首先来创建三个学生对象
PS:我们在起名字的时候暂时不用中文,因为中文在码表中对应的数字我们不知道。
Student s1 = new Student("zhangsan",23);
Student s2 = new Student("lisi",24);
Student s3 = new Student("wangwu",25);
接下来就创建 TreeSet集合
对象
TreeSet<Student> ts = new TreeSet<>();
添加元素
ts.add(s3);
ts.add(s2);
ts.add(s1);
ts.add(s4);
打印集合看看效果
System.out.println(ts);
程序运行完毕发现,我们没有看见想要的结果,而是一个报错:main方法
的 33行
报错。
报错原因其实也很简单,刚刚的 Student
是我们自己写的,我们并没有给它去添加一个默认的比较规则,此时 TreeSet
就比知道如何比较,因此在添加元素的时候就报错了。
那我们如何添加默认的比较规则呢?
2)TreeSet
的两种比较方式
方式一:默认排序,它也叫做 自然排序
,让JavaBean类实现 Comparable接口
指定比较规则。
方式二:比较器排序,创建 TreeSet对象
的时候,传递比较器 Comparator
,然后再去指定比较的规则
使用原则:默认使用第一种,若干第一种不能满足当前需求,就使用第二种。
例如字符串里面的排序规则Java已经定义好了,它默认是从首字母开始,按照ASCII码表的值来进行排列的。
但如果我不想要按照这个规则排序,我想要按照长度排序,你能去修改字符串里面的源码吗?不能吧,或者是非常的麻烦,几乎是不可能实现的,此时我们就可以使用第二种方式来进行排序。
方式一和方式二如果同时存在,以方式二为准。
例如String中Java已经定义好了默认的排序规则,此时我再给它定义一个比较器,这种情况下两种方式都存在,我们实际运行代码的时候,就是以方式二比较器的规则为准。
3)方式一:默认的排序规则 / 自然排序
让 Student类
实现 Comparable接口
,重写里面的抽象方法,再指定比较规则。
注意,这个接口前面是有泛型的
由于类型确定,因此泛型的地方直接写 Student
就好,就不需要延续泛型了。
表示我们要比较排序的就是 Student
,然后重写里面的抽象方法。
此时就可以在 compareTo()
中指定排序的规则。
需求:只看年龄,我想要按照年龄的升序进行排列
@Override
public int compareTo(Student o) {
return this.getAge() - o.getAge();
}
此时再去运行我们刚刚写的测试类,可以发现它在打印的时候就已经按照年龄的升序进行排序了。
此时有同学心里肯定有两个小疑问?
此时我们还需要去重写 hashCode()
和 equals()
呢?
不需要,在之前我们讲解的 hashCode()
和 equals()
跟哈希表有关的,而现在我们讲解的是 TreeSet集合
, TreeSet集合
底层不是哈希表,而是红黑树。
因此当我们把 Student
添加到 TreeSet集合
中的时候, Student
中不需要去重写 hashCode()
和 equals()
,但是我们需要去指定它的排序规则。
4)理解规则
建议结合视频观看:集合进阶-14-TreeSet第一种排序方式超详细讲解_哔哩哔哩_bilibili,定位到 21:47
在添加元素的时候,它会反复不断去调用 compareTo()
直到确认自己的位置
我们在 compareTo()
中打印 this
跟 o
,就能很明确的感受到 this
跟 o
到底指的是是什么了
@Override
//this:表示当前要添加的元素
//o:表示已经在红黑树存在的元素
//返回值:
//负数:表示当前要添加的元素是小的,存左边
//正数:表示当前要添加的元素是大的,存右边
//0 :表示当前要添加的元素已经存在,舍弃
public int compareTo(Student o) {
System.out.println("--------------");
System.out.println("this:" + this);
System.out.println("o:" + o);
//指定排序的规则
//只看年龄,我想要按照年龄的升序进行排列
return this.getAge() - o.getAge();
}
第一次插入 wangwu、25
的时候,一开始树中是没有节点的,因此第一次是 wangwu、25
自己跟自己比较了一下,将它当做根节点。
我们真正要看的是从第二次开始,第二次添加的是 s2
,也就是 lisi、24
,因此 this
就是 lisi、24
。
它需要跟根节点 wangwu、25
比较,因此 o
就是 wangwu、25
,比完后用 24 - 25
发现得到一个负数,所以 lisi、24
就要存到左边去。
其他的同理。
5)方式二:比较器排序
需求:请自行选择比较器排序和自然排序两种方式;
要求:存入四个字符串, “c”, “ab”, “df”, “qwer”
按照长度排序,如果一样长则按照首字母排序
第一步创建集合
TreeSet<String> ts = new TreeSet<>();
添加元素
ts.add("c");
ts.add("ab");
ts.add("df");
ts.add("qwer");
打印集合,如果我什么操作都没做的话,它就是采取默认的方式进行排序的:从首字母开始比较,跟长度无关。
System.out.println(ts);
那为什么是这样的呢?其实这个代码就是 String
里面已经写好的代码。
选中 String
ctrl + b,它在源码中也实现了 Comparable接口
并且它也重写了 compareTo()
,在这个方法里面就是字符串默认的排序规则。
但由于现在这个规则不满足我们的要求了,又不能去修改源码,此时就可以采取第二种排序方式:比较器排序。
即我们在创建集合对象的时候,传递一个比较器的对象就行了。
那这个比较器叫什么名字呢?打开 API帮助文档
查看,找到 TreeSet
的构造方法,之前我们采取的是空参构造,空参构造采用的就是默认的比较规则,但是现在默认的比较规则不满足我的要求了,此时我们就可以用下面的第三个构造。
在第三个构造中,需要传入 Comparator
,这个 Comparator
其实就是一个比较器,但是它也是以接口的形式来体现的,因此我们真正传的还是这个接口的实现类对象。
//o1:表示当前要添加的元素
//o2:表示已经在红黑树存在的元素
//返回值规则跟之前是一样的
TreeSet<String> ts = new TreeSet<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
// 按照长度排序
int i = o1.length() - o2.length();
//如果一样长则按照首字母排序
i = i == 0 ? o1.compareTo(o2) : i;
return i;
}
});
重新运行程序,可以发现运行结果已经是我们想要的了
System.out.println(ts); // [c, ab, df, qwer]
那这里能不能改成 Lambda呢?改成 Lambda有个前提,那就是这里的接口必须为函数式接口。
选中 Comparator
ctrl + b,可以发现这个接口就是一个函数式接口,因此我们可以改成Lambda表达式。
TreeSet<String> ts = new TreeSet<>((o1, o2)->{
// 按照长度排序
int i = o1.length() - o2.length();
//如果一样长则按照首字母排序
i = i == 0 ? o1.compareTo(o2) : i;
return i;
});
五、练习
需求:创建5个学生对象
属性:(姓名,年龄,语文成绩,数学成绩,英语成绩),
按照总分从高到低输出到控制台
如果总分一样,按照语文成绩排
如果语文一样,按照数学成绩排
如果数学成绩一样,按照英语成绩排
如果英文成绩一样,按照年龄排
如果年龄一样,按照姓名的字母顺序排
如果都一样,认为是同一个学生,不存。
第一种:默认排序/自然排序
第二种:比较器排序
默认情况下,用第一种排序方式,如果第一种不能满足当前的需求,采取第二种方式。
课堂练习:
要求:在遍历集合的时候,我想看到总分。
Student.java
第一种:默认排序/自然排序
第二种:比较器排序
默认情况下,用第一种排序方式,如果第一种不能满足当前的需求,采取第二种方式。
public class Student2 implements Comparable<Student2> {
//姓名
private String name;
//年龄
private int age;
//语文成绩
private int chinese;
//数学成绩
private int math;
//英语成绩
private int english;
// 无参构造、有参构造、get、set方法
public String toString() {
return "Student2{name = " + name + ", age = " + age + ", chinese = " + chinese + ", math = " + math + ", english = " + english + "}";
}
/* 按照总分从高到低输出到控制台
如果总分一样,按照语文成绩排
如果语文一样,按照数学成绩排
如果数学成绩一样,按照英语成绩排
如果英文成绩一样,按照年龄排
如果年龄一样,按照姓名的字母顺序排
如果都一样,认为是同一个学生,不存。*/
@Override
public int compareTo(Student2 o) {
int sum1 = this.getChinese() + this.getMath() + this.getEnglish();
int sum2 = o.getChinese() + o.getMath() + o.getEnglish();
//比较两者的总分
int i = sum1 - sum2;
//如果总分一样,就按照语文成绩排序
i = i == 0 ? this.getChinese() - o.getChinese() : i;
//如果语文成绩一样,就按照数学成绩排序
i = i == 0 ? this.getMath() - o.getMath() : i;
//如果数学成绩一样,按照英语成绩排序(可以省略不写,因为如果总分、英文都一样,那么英语肯定也一样)
i = i == 0 ? this.getEnglish() - o.getEnglish() : i;
//如果英文成绩一样,按照年龄排序
i = i == 0 ? this.getAge() - o.getAge() : i;
//如果年龄一样,按照姓名的字母顺序排序,此时需要调用字符串默认的排序方式
i = i == 0 ? this.getName().compareTo(o.getName()) : i;
return i;
}
}
接下来在测试类中创建学生对象
//1.创建学生对象
Student2 s1 = new Student2("zhangsan",23,90,99,50);
Student2 s2 = new Student2("lisi",24,90,98,50);
Student2 s3 = new Student2("wangwu",25,95,100,30);
Student2 s4 = new Student2("zhaoliu",26,60,99,70);
Student2 s5 = new Student2("qianqi",26,70,80,70);
创建集合的时候,默认情况下会使用 ArrayList
会比较多,因为这个集合最常见。
如果想要数据唯一,就可以在 Set集合
中选择,Set集合
默认情况下可以使用 HashSet
。
但如果又要唯一,又要排序,就可以使用 TreeSet
。
//2.创建集合
TreeSet<Student> ts = new TreeSet<>();
//3.添加元素
ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
ts.add(s5);
//4.打印集合
//System.out.println(ts);
for (Student2 t : ts) {
System.out.println(t);
}
六、总结
1、TreeSet集合
的特点是什么样的?
- 可排序、不重复、无索引
- 底层基于红黑树实现排序,增删改查性能较好
2、TreeSet集合
自定义排序规则有几种方式
- 方式一:默认排序,JavaBean类实现Comparable接口,制定比较规则
- 方式二:比较器排序,创建
TreeSet集合
时,自定义Comparator比较器,指定比较规则
使用原则:默认使用第一种,若干第一种不能满足当前需求,就使用第二种。
例如字符串里面的排序规则Java已经定义好了,它默认是从首字母开始,按照ASCII码表的值来进行排列的。
但如果我不想要按照这个规则排序,我想要按照长度排序,你能去修改字符串里面的源码吗?不能吧,或者是非常的麻烦,几乎是不可能实现的,此时我们就可以使用第二种方式来进行排序。
方式一和方式二如果同时存在,以方式二为准。
例如String中Java已经定义好了默认的排序规则,此时我再给它定义一个比较器,这种情况下两种方式都存在,我们实际运行代码的时候,就是以方式二比较器的规则为准。
不管是方式一还是方式二,都是跟接口有关的,当我们重写了接口之后,方法的返回值有什么特点?
3、方法返回值的特点
- 负数:表示当前要添加的元素是小的,存左边
- 整数:表示当前要添加的元素是大的,存右边
- 0:表示当前要添加的元素已经存在,舍弃