TreeSet排序原理(比较器)

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 如果添加的是基本数据类型,不需要定义比较器。

内容如有错误欢迎指正,谢谢

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值