前面说的ArraylList集合,是有序可重复的,那么下面介绍的TreeSet是无序不可重复的
Set:无序、不可重复、无下标
1. TreeSet
这个运用到的,就是计算机数据结构中,二叉树了,但其实底层由很多组成
TreeSet是无序不可重复的,不能储存null对象
使用TreeSet存储自定义对象,对象的类必须使用自然排序
或者在建立TreeSet对象时,通过构造方法传递迭代器
TreeSet的构造方法
集合的用法都大致相同,所以增删改查就不说了,更多的需要自己查api
对于TreeSet来说,最重要最难的点在于自然排序和选择排序
2.自然排序
存储进TreeSet的对象类必须实现Comparable接口,重写CompareTo(),在compareTo()实现比较规则,返回正数,存储的对象则在右子树,返回负数,则在左子树,放回0,则不存储
首先注意几点
- 第一,TreeSet放入的引用类型是要实现了Comparable接口的,像上面的int型数字,其实是Integer里面已经实现了Comparable接口,类似的还有String、Double等,我们熟知的类型都能直接放入进TreeSet里,因为他们的底层都实现了Comparable接口。
- 第二,对于实例化Comparable,一定要加上泛型固定它的存储类型,不能像ArrayList一样什么都放进去,不然在排序时候会报错。
例如,在Integer类型中突然多了一个String,那么就排不了序,报错 - 第三,像二叉树一样,如果一个值小于上面的值,那么就放到左子树,如果大于上面的值,就放到右子树。在CompareTo里面,就是根据这个判断的,两个数对比,如a1和a2,如果a1-a2<0,那么就是a1比a2小,放左边,反之放右边,通过这些判断结果,由底层代码进行排序【重点】
public class Demo01 {
public static void main(String[] args) {
TreeSet<Integer> treeSet = new TreeSet<>();
treeSet.add(1);
treeSet.add(2);
treeSet.add(3);
treeSet.add(4);
Iterator it = treeSet.iterator();
while (it.hasNext()){
System.out.println(it.next());
}
}
}
结果:
1
2
3
4
2.1 TreeSet 底层源码
想要弄懂他是如何进行排序的,就要弄懂它的底层源码
add为我们插入数据的方法
因为自然排序里,没有传入选择器,所以左下角的if语句就直接去掉了,执行最右边的else语句
最右边的代码就是二叉树运行的逻辑,中间的Entry是二叉树的元素,和右边结合起来看
中间的compare方法是add元素必经之地,它需要带有Compareable的接口的类才能接收,而且不能为空,这就是为什么TreeSet一定要传入带有Compareable且非空的元素的原因(由选择器的除外,下面说)
在中间最下面的CompareTo方法,这解释了为什么TreeSet是不可重复,且如何排序的原因:
- 在最右边代码里,负责每个结点的通过CompareTo进行对比,如果旁边的节点小,那么返回负数(里面是-1);如果相等,那么返回0,然后去除掉,如果大于,那么返回正数。通过这个方法进行排序的。
如果不想看代码,就看上面这个原因就好了,后面自己实现的Compareable接口也要根据这个定理写。
2.2类的排序
如果建立了一个学生类,我们想把类放入进去,并且按照 id号进行排序,如果两个类的属性都相同,那么就删除一个
引入这个例子更好地了解Compareable接口
Student类(为了简单点,属性就不封装了)
// 注意,Student实现了Compareable接口,且接口泛型为Student
public class Student implements Comparable<Student>{
private int id;
private String name;
private int age;
public Student(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compareTo(Student o) {
return this.id-o.id; //按照从小到大排序
}
}
this.id就是本类的id,o.id是上一个类的id
测试的main
public class TextStudent {
public static void main(String[] args) {
TreeSet<Student> set = new TreeSet<>();
set.add(new Student(1,"zs",18));
set.add(new Student(5,"ls",54));
set.add(new Student(2,"ww",7));
set.add(new Student(2,"ww",7));
set.add(new Student(7,"lili",32));
set.add(new Student(58,"jie",70));
for (Student stu:set) {
System.out.println(stu);
}
}
}
结果:
Student{id=1, name='zs', age=18}
Student{id=2, name='ww', age=7}
Student{id=5, name='ls', age=54}
Student{id=7, name='lili', age=32}
Student{id=58, name='jie', age=70}
我们可以看到,实现了从小到大的排序,且id相同的都去掉了
那么如果我想要,如果id相同了,再根据年龄来进行排序呢?
把上面Student类代码改为
@Override
public int compareTo(Student o) {
//如果id不同
if(this.id!=o.id){
return this.id-o.id;
}else{
//如果id相同
return this.age - o.age;
}
}
当然,只要你设计得当,也能根据字符串名字进行排序
3选择器
选择器,也就是选择排序了,选择排序的和自然排序的逻辑是一样的
再上面的底层代码图里,左下角的被划掉的if语句,如果添加了选择器,那么就是运行那if里面的代码了(运行被划掉的代码),逻辑其实是一样的
Student类,把Compareable去掉了,由于选择器需要外部来调用Student的属性,所以我把它属性进行封装了(当然,平时也要这么写)
public class Student{
private int id;
private String name;
private int age;
public Student(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
选择器
//和上面最开始案例一样
public class Selector implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.getId() - o2.getId();
}
}
其中o1代表后面的Student,o2代表前面的Student
测试main
public class TextStudent {
public static void main(String[] args) {
TreeSet<Student> set = new TreeSet<>(new Selector());
set.add(new Student(1,"zs",18));
set.add(new Student(5,"ls",54));
set.add(new Student(2,"ww",7));
set.add(new Student(2,"ww",7));
set.add(new Student(7,"lili",32));
set.add(new Student(58,"jie",70));
for (Student stu:set) {
System.out.println(stu);
}
}
}
结果:
Student{id=1, name='zs', age=18}
Student{id=2, name='ww', age=7}
Student{id=5, name='ls', age=54}
Student{id=7, name='lili', age=32}
Student{id=58, name='jie', age=70}
也可以用匿名内部类来写选择器,这样可以看到匿名内部类的作用的–可以少些一个类
TreeSet<Student> set = new TreeSet<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getId()-o2.getId();
}
});
4迭代器
集合的迭代器都是一样的 for(set集合没有), foreach, iterator
因为set集合是无序的,所以用不了for循环
上面用到的foreach是简便的,但作者也是希望能多用熟用 iterator作为迭代器
Iterator<Student> it = set.iterator();
while (it.hasNext()){
System.out.println(it.next());
}