TreeSet排序原理
TreeSet简介:
\qquad 1、TreeSet 是一个有序集合,可以以任意顺序将元素插入到集合中,在对集合进行遍历的时候,每个元素将自动按照排序后的顺序呈现。
\qquad 2、TreeSet底层使用的是红黑树实现,对于元素之间排序,如果不指定自定义的外部比较器 ——Comparator,那么插入的对象必须实现内部比较器——Comparable 接口,元素按照实现此接口的 compareTo() 方法去排序
具有如下特点:
- 对插入的元素进行排序,是一个有序的集合(主要与HashSet的区别;
- 底层使用红黑树结构,而不是哈希表结构;
- 允许插入Null值;
- 不允许插入重复元素;
- 线程不安全;
但具体为什么元素有序,按照什么规则排序呢?底层怎么实现的?
一、首先我们来看以下代码
public class treeSetDemo {
public static void main(String[] args) {
//创建TreeSet集合
TreeSet treeSet = new TreeSet();
//添加进集合
treeSet.add("bb");
treeSet.add("dd");
treeSet.add("cc");
treeSet.add("bb");
treeSet.add("aa");
//结果输出
System.out.println(treeSet);
}
}
结果:
总结
- 元素不重复这是因为是Set集合,底层是由哈希表实现,则不重复
- 顺序依次从小到大,这是默认自然排序的结果
二、异常产生
这里我们尝试将Student自定义对象存入TreeSet集合中
public class treeSetDemo {
public static void main(String[] args) {
//创建TreeSet集合
TreeSet treeSet = new TreeSet();
//分别存储学生实体对象
treeSet.add(new Student("小明",22));
treeSet.add(new Student("小红",18));
treeSet.add(new Student("小军",16));
treeSet.add(new Student("小华",20));
//打印输出
System.out.println(treeSet);
}
}
Student类结构
public class Student {
private String name;
private int age;
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;
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
运行结果: 类转换异常
三、异常分析及解决
1、TreeSet底层插入元素的过程:
前面说过,TreeSet 底层是二叉树实现的,当存储元素的时候,会调用比较规则方法,将新元素和二叉树上已有的元素进行一一比较。
如果要插入的元素比当前元素小、就到左子树去比较,
如果比当前元素大,就到右子树去比较,直到当前元素的左或者右子树为空,就插入此元素,
如果在比较过程中,出现当前元素等于要插入的元素,那么此元素不插入。
例如上例中最后一个元素 3 被过滤掉了,这样也就保证了 Set 的元素唯一性。
常见的Java的API中定义的类,基本上都已经实现了内部比较器——Comparable 接口
查看 API 我们发现:基本上所有 API 定义好的类都实现了 Comparable 接口
所以我们在向 TreeSet 集合中存储这些类的时候可以直接存储,没必要再自定义比较器。
当然如果有特别的需求,也可以自定义比较器覆盖原有的比较规则。
2、异常分析:
异常提示我们自定义的Student对象不能转换成一个java.lang.Comparable,如果没有自定义比较器,TreeSet 集合存储的对象元素必须实现 Comparable 接口。这里我们的 Student 类既没有定义比较器,没有实现 Comparable 接口,所以会报 java.lang.ClassCastException 异常,意思就是这里存的元素不是 Comparable 类型的。
3、异常解决
方案一:实现 内部比较器——Comparable 接口
实现 Comparable 接口的类必须实现 compareTo(Object obj) 方法,两个对象通过 compareTo 方法的返回值来比较大小 :
- ① 如果当前对象 this 大于形参对象 obj 则返回正整数;
- ② 如果当前对象 this 小于 形参对象 obj则返回 负整数;
- ③ 如果当前对象 this 等于 形参对象 obj 则返回零 。
修改Student类结构
//实现Comparable接口重写compareTo方法
public class Student implements Comparable {
private String name;
private int age;
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;
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
//比较写法与equals方法相似
//编写比较规则
@Override
public int compareTo(Object obj) {
//按照学生年龄排序
if(((Student) obj).age == this.age){
//相等返回0
return 0;
}else {
if(((Student) obj).age > this.age){
//大于返回1 (正数)
return 1;
}else {
//小于返回-1 (负数)
return -1;
}
}
}
}
测试:
public class treeSetDemo {
public static void main(String[] args) {
//创建TreeSet集合
TreeSet treeSet = new TreeSet();
//分别存储学生实体对象
treeSet.add(new Student("小明",22));
treeSet.add(new Student("小红",18));
treeSet.add(new Student("小军",16));
treeSet.add(new Student("小华",20));
//打印输出
System.out.println(treeSet);
}
}
结果:
结论:
这里我们的 Student 类实现了 Comparable 接口,并且实现了此接口的唯一抽象方法compareTo(),这里我们是按照学生年龄排序(降序),比较规则相反编写就是升序。
方案二 :自定义外部比较器——Comparator接口(推荐)
通过new一个匿名内部类的方式(也可以写一个类然后实现接口然后调用但不推荐,太繁琐了),定义一个外部比较器Comparator接口对象,重写 compare(Object o1,Object o2) 方法,比较 o1 和 o2 的大小:
- ① 如果方法返回正整数,则表示 o1 大于 o2 ;
- ② 如果方法返回 0 ,表示相等;
- ③ 如果方法返回负整数,表示o1 小于 o2 。
修改Student类结构
public class Student {
private String name;
private int age;
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;
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
测试:
public class treeSetDemo {
public static void main(String[] args) {
//创建TreeSet集合
//创建匿名内部类使用外部比较器
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//按照学生年龄排序
if(((Student) o1).getAge() == ((Student) o2).getAge()){
//相等返回0
return 0;
}else {
if(((Student) o1).getAge() > ((Student) o2).getAge()){
//大于返回1 (正数)
return 1;
}else {
//小于返回-1 (负数)
return -1;
}
}
}
});
//分别存储学生实体对象
treeSet.add(new Student("小明",22));
treeSet.add(new Student("小红",18));
treeSet.add(new Student("小军",16));
treeSet.add(new Student("小华",20));
//打印输出
System.out.println(treeSet);
}
}
结果:
疑问:那为什么前面测试字符串的时候可以正常排序呢????
\qquad
上面也说了treeSet要想实现排序必须要实现比较器接口并且重写方法也就是Comparable和Comparator接口,那为什么对String字符串排序时不需要实现接口那套操作呢,其实并不是没有那套流程,而是String类帮我们实现了,我们可以去分析下String类的源码
并且重写了compareTo方法
四、总结
1、比较器有两种,内部比较器和外部比较器;
关于内部比较器Comparable与外部比较器Comparator,更多详情请见:
Comparable和Comparator详解
2、内部比较器:定义在要比较的类元素中。
- 接口:Comparable
- 方法:compareTo(Object o)
3、外部比较器:定义在新建的TreeSet集合的构造函数中。
\qquad
接口:Comparator
\qquad
方法:compare(T o1, T o2)
4、当同时有内部比较器和外部比较器时,外部比较器起作用。
5、推荐使用外部比较器;
\qquad 内部比较器只有在存储当前那对象的时候才可以使用;
\qquad 外部比较器可以定义为一个工具类,此时所有需要比较的规则如果一致的话,可以复用。推荐使用外部比较器。
6、基本数据类型,不需要定义比较器;
\qquad 只有当以树作为存储结构时,而且添加的是引用对象时,才需要定义比较器。
\qquad 此篇文章举例用TreeSet,TreeSet用TreeMap实现,TreeMap用红黑树实现。而且添加的是自定义的类,所以需要定义比较器。
\qquad 如果添加的是基本数据类型,不需要定义比较器。
内容如有错误欢迎指正,谢谢