【Java集合框架】3 - 8 TreeSet 集合

§3-8 TreeSet 集合

3-8.1 TreeSet:可排序的集合

TreeSet 本身并不具备特殊的方法,因此一般而言,直接调用 Collection 中的方法。

TreeSet 在底层使用红黑树存储数据,其增删改查性能都较好,不同于 HashSetLinkedHashSet,该集合可排序不重复无索引

默认排序规则

  • 对于数值类型 Ineger, Double,默认按照从小到大升序排序;
  • 对于字符、字符串类型,按照字符在 ASCII 码表中的编码升序排序;

自定义类可排序的实现有两种方式

  • Java Bean 类实现 Comparable 并重写 compareTo 方法;
  • 构造器中传入 Comparator 实现类对象,其中重写 compare 方法。

但无论是哪种方式,最终调用的方法所依赖的逻辑相同:将待添加的对象(this)与树中的某一结点(T o)做比较,计算所得返回值若小于零,则添加在该结点的左子结点处;若返回值大于零,则添加在右子结点处;若为零,则相同,舍弃该对象(若添加为根节点,则例外)。

这一特性使得类不需要重写 equalshashCode 方法。

方式一 - 实现 Comparable 接口:现有下述 Java Bean 类,并实现 Comparable 接口

public class Student implements Comparable<Student> {
    private String name;
    private int age;

    /*
    此处略去空参、全参构造器、Getter & Setter、toString 方法
    */

    @Override
    public int compareTo(Student o) {
        //按姓名(不含中文)排序:逐字按照 ASCII 编码顺序排序
        return this.getName().compareTo(o.getName());
        //或者按年龄升序排列
        //return this.getAge() - o.getAge();
    }
}

在测试类中,创建 TreeSet 对象,并添加以下对象:

public class Test {
    public static void main(String[] args) {
        TreeSet<Student> ts = new TreeSet<>();
        
        Student s1 = new Student("Zhang San", 23);
        Student s2 = new Student("Li Si", 24);
        Student s3 = new Student("Wang Wu", 25);
        Student s4 = new Student("Zhao Liu", 26);
        Student s5 = new Student("Zhang San", 23);

        ts.add(s1);
        ts.add(s2);
        ts.add(s3);
        ts.add(s4);
        ts.add(s5);

        for (Student t : ts) {
            System.out.println(t);
        }
    }
}

运行,得到

Student{name = Li Si, age = 24}
Student{name = Wang Wu, age = 25}
Student{name = Zhang San, age = 23}
Student{name = Zhao Liu, age = 26}

添加的结点按照姓名的 ASCII 编码顺序排序,添加过程同红黑树的节点添加过程,通过不断地调用 compareTo 方法判断添加为左/右结点。

在本例中,compareTo 实际上调用了 String 中重写过的 compareTo 方法比较两个字符串。

方式二 - 构造器中传入 Comparator 实现类对象:这种情况适用于 Compare 接口中的 compareTo 方法无法满足条件时使用(这种情况多是因为该类已经实现 Compare 接口并重写了 compareTo 方法),这时可以往构造器中传入 Comparator 实现类对象(可使用 Lambda 表达式)。若两种方式同时存在,则优先以方式二的定义进行比较判断。

以字符串为例,现自定义排序规则,使得字符串优先以长度升序排序,若长度一致,则按字符升序排序

public class Test {
    public static void main(String[] args) {
        //传入 Comparator 实现类对象(Lambda)
        TreeSet<String> tsSt = new TreeSet<>(((o1, o2) -> {
            //先按照字符串的长度升序排序
            int i = o1.length() - o2.length();
            //若长度相同,则按字符升序排序(忽略大小写)
            i = i == 0 ? o1.compareToIgnoreCase(o2) : i;

            return i;
        }));

        tsSt.add("Zhang San");
        tsSt.add("Zhang Ban");
        tsSt.add("Li si");
        tsSt.add("Wang Wu");

        for (String s : tsSt) {
            System.out.println(s);
        }
    }
}

运行,得到:

Li si
Wang Wu
Zhang Ban
Zhang San

3-8.2 两种排序方式的选择

两种排序方式背后所执行的逻辑是完全相同的,一般情况下,我们会默认选择第一种方式,即让 Java Bean 类实现 Comparable 接口并重写 compareTo 方法,作自然排序/默认排序。

compareTo 方法的实现不能够满足需求时,选择第二种方式,在构造器中传入 Comparator 接口并重写 compare 方法。

一般而言,Java 中提供的类(例如 IntegerString)都已经实现 Comparable,因此无需考虑对象是否可比。真正应当考虑可比性的是自定义的 Java Bean 类。

示例:使用 TreeSet 对整数自然排序

TreeSet<Integer> ts = new TreeSet<>();

ts.add(5);
ts.add(3);
ts.add(1);
ts.add(2);
ts.add(4);

System.out.println(ts);

得到:

[1, 2, 3, 4, 5]

3-8.3 Set 集合使用场景与源码

  • 若想要集合中元素可重复,则使用 ArrayList(使用最多,基于数组);
  • 若想要集合中元素可重复,且增删操作较查找更频繁,则使用 LinkedList(基于链表);
  • 若想要集合中元素无重复,则使用 HashSet(使用最多,基于哈希表);
  • 若想要集合中元素无重复,且保证存取有序性,则使用 LinkedHashSet(基于哈希表和双链表,效率低于 HashSet);
  • 若想要集合中元素无重复,且数据可排序,则使用 TreeSet(基于红黑树,后续也可使用 List 集合实现排序);

所有 Set 集合 HashSet, LinkedHashSetTreeSet 在底层,实际上创建的是 HashMap, LinkedHashMapTreeMap 对象,这些都是 Map 接口的实现类。因此,想要分析上述 Set 实现类源码,应当先了解 Map 及其对应实现类源码。

posted @ 2023-08-09 20:21  Zebt  阅读(43)  评论(0)    收藏  举报