Java基础快速入门: Set集合与TreeSet详解

本文纲要

  1. Set 集合概述
  2. Set 集合的基本使用
  3. TreeSet 基本使用
  4. TreeSet 自然排序
    自然排序步骤
    主要条件与次要条件
    compareTo 方法原理
  5. 比较器排序
  6. 两种排序方式对比与练习
  7. 综合练习:按总分排序

Set 集合概述

在 Java 集合框架中,单列集合的顶层接口是 Collection,它派生出两大类:

  • List集合:元素可以重复,有索引。常见实现类:ArrayList(底层数组,查询快增删慢)、LinkedList(底层链表,查询慢增删快)。
  • Set集合:元素不可重复,无索引 。常见实现类:HashSetTreeSet

本文主要学习 Set 接口及其实现类 TreeSet 的用法,包括自然排序与比较器排序。

项目代码结构如下(包:com.wb):

t 复制代码
src 
 └─ com.wb 
     ├─ myset 
     │    └─ MySet1.java 
     ├─ mytreeset 
     │    ├─ Student.java 
     │    ├─ Teacher.java 
     │    ├─ MyTreeSet1.java 
     │    ├─ MyTreeSet2.java 
     │    ├─ MyTreeSet3.java 
     │    ├─ MyTreeSet4.java 
     │    └─ MyTreeSet5.java 
     └─ treesettest 
          ├─ Student.java 
          └─ TreeSetTest.java 

Set 集合的基本使用

Set 集合的特点:

  1. 不可重复:添加重复元素时只会保留一个。
  2. 存取顺序不一致:存入顺序与遍历顺序不一定相同。
  3. 没有带索引的方法 :不能使用普通 for 循环按索引遍历,也不支持通过索引获取/删除元素。

遍历方式只能使用迭代器增强 for 循环

代码示例:存储字符串并遍历

java 复制代码
package com.wb.myset;
 
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;
 
/*
Set集合的基本使用 
 */
public class MySet1 {
    public static void main(String[] args) {
        Set<String> set = new TreeSet<>();
        set.add("ccc");
        set.add("aaa");
        set.add("aaa");   // 重复元素,只会保留一个 
        set.add("bbb");
 
        // 不能使用普通 for 循环,因为 Set 没有索引 
        // for (int i = 0; i < set.size(); i++) {
        //     // Set集合是没有索引的,所以不能使用通过索引获取元素的方法 
        // }
 
        // 迭代器遍历 
        Iterator<String> it = set.iterator();
        while (it.hasNext()) {
            String s = it.next();
            System.out.println(s);
        }
        System.out.println("-----------------------------------");
 
        // 增强 for 遍历 
        for (String s : set) {
            System.out.println(s);
        }
    }
}

运行结果:

log 复制代码
aaa 
bbb 
ccc 
-----------------------------------
aaa 
bbb 
ccc 

可以看到:重复的 "aaa" 只保留了一个,且最终顺序是按字典序排列的,与原插入顺序 "ccc","aaa","bbb" 不一致,体现了 Set 的特点

TreeSet 基本使用

TreeSetSet 接口的一个实现类,其特点:

  • 不包含重复元素(Set 基本特性)。
  • 没有带索引的方法。
  • 可以对元素进行排序(自然顺序或自定义规则)。
  • 存取顺序不一定一致,但最终按排序规则输出。

示例:存储 Integer 类型

java 复制代码
package com.wb.mytreeset;
 
import java.util.TreeSet;
 
/
TreeSet集合来存储Integer类型 
 */
public class MyTreeSet1 {
    public static void main(String[] args) {
        TreeSet<Integer> ts = new TreeSet<>();
        ts.add(5);
        ts.add(3);
        ts.add(4);
        ts.add(1);
        ts.add(2);
 
        System.out.println(ts);
    }
}

输出:[1, 2, 3, 4, 5]

虽然插入顺序是 5,3,4,1,2,但 TreeSet 自动按升序排序。

存储自定义对象报错的原因

如果直接存储未实现排序规则的自定义类,会抛出异常。

例如直接使用以下 Student 类:

java 复制代码
// 未实现Comparable接口的Student 
public class Student {
    private String name;
    private int age;
    // ... 构造器、getter、setter、toString 
}
java 复制代码
TreeSet<Student> ts = new TreeSet<>();
ts.add(new Student("xiaohua", 27));
// ❌ 运行时抛出 ClassCastException 

原因是 TreeSet 需要知道按什么规则排序,而自定义类没有指定排序规则,因此必须显式指定。

TreeSet 自然排序

自然排序步骤

  1. 使用 TreeSet 的空参构造创建集合。
  2. 自定义类实现 Comparable 接口,泛型与集合元素类型一致。
  3. 重写 compareTo 方法,在方法内定义排序逻辑。

代码实现:让学生按年龄排序

Student 类实现 Comparable

java 复制代码
package com.wb.mytreeset;
 
public class Student implements Comparable<Student> {
    private String name;
    private int age;
 
    public Student() {}
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // getter、setter、toString 省略 ...
 
    @Override 
    public int compareTo(Student o) {
        // 按照对象的年龄进行排序 
        // 主要判断条件 
        int result = this.age - o.age;
        // 次要判断条件:年龄相同时按姓名比较 
        result = result == 0 ? this.name.compareTo(o.getName()) : result;
        return result;
    }
}

测试类:

java 复制代码
package com.wb.mytreeset;
 
import java.util.TreeSet;
 
/*
TreeSet集合来存储Student类型 
 */
public class MyTreeSet2 {
    public static void main(String[] args) {
        TreeSet<Student> ts = new TreeSet<>();
 
        Student s1 = new Student("zhangsan", 28);
        Student s2 = new Student("lisi", 27);
        Student s3 = new Student("wangwu", 29);
        Student s4 = new Student("zhaoliu", 28);
        Student s5 = new Student("qianqi", 30);
 
        ts.add(s1);
        ts.add(s2);
        ts.add(s3);
        ts.add(s4);
        ts.add(s5);
 
        System.out.println(ts);
    }
}

输出:

log 复制代码
[Student{name='lisi', age=27}, Student{name='zhangsan', age=28}, 
 Student{name='zhaoliu', age=28}, Student{name='wangwu', age=29}, 
 Student{name='qianqi', age=30}]

年龄相同的 zhangsanzhaoliu,按姓名字典序排列(zhangsanzhaoliu 前)

主要条件与次要条件

compareTo 方法中:

  • 主要条件:优先比较的属性,如年龄。
  • 次要条件:当主要条件相同时才使用的比较,如姓名。
    • 次要条件通常写在 result == 0 的判断体内,避免无效比较。

compareTo 方法原理

TreeSet 底层在添加元素时,会调用 compareTo 方法将新元素与已有元素比较。

  • 返回负数:当前元素较小,放在左子树。
  • 返回零:视为重复,不存入。
  • 返回正数:当前元素较大,放在右子树。

插入过程示意(以年龄为例):
#mermaid-svg-XhmnN9XwyrgLnVXh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XhmnN9XwyrgLnVXh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XhmnN9XwyrgLnVXh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XhmnN9XwyrgLnVXh .error-icon{fill:#552222;}#mermaid-svg-XhmnN9XwyrgLnVXh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XhmnN9XwyrgLnVXh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XhmnN9XwyrgLnVXh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XhmnN9XwyrgLnVXh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XhmnN9XwyrgLnVXh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XhmnN9XwyrgLnVXh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XhmnN9XwyrgLnVXh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XhmnN9XwyrgLnVXh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XhmnN9XwyrgLnVXh .marker.cross{stroke:#333333;}#mermaid-svg-XhmnN9XwyrgLnVXh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XhmnN9XwyrgLnVXh p{margin:0;}#mermaid-svg-XhmnN9XwyrgLnVXh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XhmnN9XwyrgLnVXh .cluster-label text{fill:#333;}#mermaid-svg-XhmnN9XwyrgLnVXh .cluster-label span{color:#333;}#mermaid-svg-XhmnN9XwyrgLnVXh .cluster-label span p{background-color:transparent;}#mermaid-svg-XhmnN9XwyrgLnVXh .label text,#mermaid-svg-XhmnN9XwyrgLnVXh span{fill:#333;color:#333;}#mermaid-svg-XhmnN9XwyrgLnVXh .node rect,#mermaid-svg-XhmnN9XwyrgLnVXh .node circle,#mermaid-svg-XhmnN9XwyrgLnVXh .node ellipse,#mermaid-svg-XhmnN9XwyrgLnVXh .node polygon,#mermaid-svg-XhmnN9XwyrgLnVXh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XhmnN9XwyrgLnVXh .rough-node .label text,#mermaid-svg-XhmnN9XwyrgLnVXh .node .label text,#mermaid-svg-XhmnN9XwyrgLnVXh .image-shape .label,#mermaid-svg-XhmnN9XwyrgLnVXh .icon-shape .label{text-anchor:middle;}#mermaid-svg-XhmnN9XwyrgLnVXh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XhmnN9XwyrgLnVXh .rough-node .label,#mermaid-svg-XhmnN9XwyrgLnVXh .node .label,#mermaid-svg-XhmnN9XwyrgLnVXh .image-shape .label,#mermaid-svg-XhmnN9XwyrgLnVXh .icon-shape .label{text-align:center;}#mermaid-svg-XhmnN9XwyrgLnVXh .node.clickable{cursor:pointer;}#mermaid-svg-XhmnN9XwyrgLnVXh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XhmnN9XwyrgLnVXh .arrowheadPath{fill:#333333;}#mermaid-svg-XhmnN9XwyrgLnVXh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XhmnN9XwyrgLnVXh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XhmnN9XwyrgLnVXh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XhmnN9XwyrgLnVXh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XhmnN9XwyrgLnVXh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XhmnN9XwyrgLnVXh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XhmnN9XwyrgLnVXh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XhmnN9XwyrgLnVXh .cluster text{fill:#333;}#mermaid-svg-XhmnN9XwyrgLnVXh .cluster span{color:#333;}#mermaid-svg-XhmnN9XwyrgLnVXh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XhmnN9XwyrgLnVXh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XhmnN9XwyrgLnVXh rect.text{fill:none;stroke-width:0;}#mermaid-svg-XhmnN9XwyrgLnVXh .icon-shape,#mermaid-svg-XhmnN9XwyrgLnVXh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XhmnN9XwyrgLnVXh .icon-shape p,#mermaid-svg-XhmnN9XwyrgLnVXh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XhmnN9XwyrgLnVXh .icon-shape .label rect,#mermaid-svg-XhmnN9XwyrgLnVXh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XhmnN9XwyrgLnVXh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XhmnN9XwyrgLnVXh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XhmnN9XwyrgLnVXh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} <0 负数
=0 零
>0 正数
插入新元素: this, 已有元素: o
this.age - o.age
当前元素较小,放左子树
元素重复,不存入
当前元素较大,放右子树

若首次插入直接作为根节点;后续插入会与根节点及子树节点依此规则比较,最终形成排序树结构。

比较器排序

当无法修改自定义类的源码(如 StringInteger 已固定实现),或默认自然排序不满足需求时,可使用比较器排序

步骤

  1. 创建 TreeSet 时传入 Comparator 对象。
  2. 实现 compare 方法定义排序规则。
  3. 比较的规则(负数/零/正数)与自然排序一致。

示例:Teacher 类使用比较器排序

Teacher 类不必实现 Comparable

java 复制代码
package com.wb.mytreeset;
 
public class Teacher {
    private String name;
    private int age;
    // 构造器、getter、setter、toString ...
}

使用匿名内部类方式:

java 复制代码
package com.wb.mytreeset;
 
import java.util.Comparator;
import java.util.TreeSet;
 
public class MyTreeSet4 {
    public static void main(String[] args) {
        TreeSet<Teacher> ts = new TreeSet<>(new Comparator<Teacher>() {
            @Override 
            public int compare(Teacher o1, Teacher o2) {
                // o1 表示现在要存入的那个元素 
                // o2 表示已经存入到集合中的元素 
 
                // 主要条件:按年龄 
                int result = o1.getAge() - o2.getAge();
                // 次要条件:年龄相同时按姓名排序 
                result = result == 0 ? o1.getName().compareTo(o2.getName()) : result;
                return result;
            }
        });
 
        Teacher t1 = new Teacher("zhangsan", 23);
        Teacher t2 = new Teacher("lisi", 22);
        Teacher t3 = new Teacher("wangwu", 24);
        Teacher t4 = new Teacher("zhaoliu", 24);
 
        ts.add(t1);
        ts.add(t2);
        ts.add(t3);
        ts.add(t4);
 
        System.out.println(ts);
    }
}

输出:

log 复制代码
[Teacher{name='lisi', age=22}, Teacher{name='zhangsan', age=23}, 
 Teacher{name='wangwu', age=24}, Teacher{name='zhaoliu', age=24}]

年龄相同的 wangwuzhaoliu 按首字母排序。

也可以使用 Lambda 表达式简化

两种排序方式对比与练习

方式 实现 适用场景
自然排序 类实现 Comparable,重写 compareTo 可修改类源码,且排序规则唯一
比较器排序 创建 Comparator,重写 compare 不能修改类源码(如String),或需要多种排序规则

练习:按照字符串长度排序

  • 要求:存储字符串 "c""ab""df""qwer",按长度升序,长度相同则按字典序。
  • 分析:String 类已经实现了 Comparable,按字典序排序,不满足长度需求,所以必须使用比较器。

匿名内部类实现:

java 复制代码
TreeSet<String> ts = new TreeSet<>(new Comparator<String>() {
    @Override 
    public int compare(String o1, String o2) {
        int result = o1.length() - o2.length();          // 主要条件:长度 
        result = result == 0 ? o1.compareTo(o2) : result;// 次要条件:字典序 
        return result;
    }
});

Lambda 表达式实现(更简洁):

java 复制代码
package com.wb.mytreeset;
 
import java.util.Comparator;
import java.util.TreeSet;
 
public class MyTreeSet5 {
    public static void main(String[] args) {
        TreeSet<String> ts = new TreeSet<>(
            (String o1, String o2) -> {
                int result = o1.length() - o2.length();
                result = result == 0 ? o1.compareTo(o2) : result;
                return result;
            }
        );
 
        ts.add("c");
        ts.add("ab");
        ts.add("df");
        ts.add("qwer");
 
        System.out.println(ts);
    }
}

输出:[c, ab, df, qwer](长度分别为1,2,2,4,abdf字典序ab在前)

补充:StringcompareTo 示例

String 类实现了 Comparable,其 compareTo 按字典顺序比较:

java 复制代码
package com.wb.mytreeset;
 
public class MyTreeSet3 {
    public static void main(String[] args) {
        String s1 = "aaa";
        String s2 = "ab";
 
        System.out.println(s1.compareTo(s2));  // 输出 -1 
        // 比较过程:先比较第一个字符 'a' vs 'a'(相等)
        // 再比较第二个字符 'a' vs 'b','a' Unicode值97,'b' 98 => 97-98 = -1 
    }
}

综合练习:按总分排序

以下练习展示自然排序的更复杂应用:按总分降序排列,若总分相同则依次比较语文、数学、英语成绩,全部相同再比较姓名。

Student 类(实现了 Comparable):

java 复制代码
package com.wb.treesettest;
 
public class Student implements Comparable<Student> {
    private String name;
    private int chinese;
    private int math;
    private int english;
 
    // 构造器、getter、setter 略 ...
 
    @Override 
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", chinese=" + chinese +
                ", math=" + math +
                ", english=" + english +
                '}' + "总分为" + getSum();
    }
 
    public int getSum() {
        return chinese + math + english;
    }
 
    @Override 
    public int compareTo(Student o) {
        // 按照总分从高到低排序(降序)
        int result = o.getSum() - this.getSum();
        // 总分一样,比较语文成绩(也降序)
        result = result == 0 ? o.getChinese() - this.getChinese() : result;
        // 语文一样,比较数学 
        result = result == 0 ? o.getMath() - this.getMath() : result;
        // 数学一样,比较英语 
        result = result == 0 ? o.getEnglish() - this.getEnglish() : result;
        // 全部相同,按姓名字典序 
        result = result == 0 ? o.getName().compareTo(this.getName()) : result;
        return result;
    }
}

测试类:

java 复制代码
package com.wb.treesettest;
 
import java.util.TreeSet;
 
/*
键盘录入3个学生信息,属性为(姓名,语文成绩,数学成绩,英语成绩),按照总分从低到高输出到控制台 
本示例直接硬编码演示 
 */
public class TreeSetTest {
    public static void main(String[] args) {
        TreeSet<Student> ts = new TreeSet<>();
 
        Student s1 = new Student("dahei", 80, 80, 80);
        Student s2 = new Student("erhei", 90, 90, 90);
        Student s3 = new Student("xiaohei", 100, 100, 100);
 
        ts.add(s1);
        ts.add(s2);
        ts.add(s3);
 
        for (Student student : ts) {
            System.out.println(student);
        }
    }
}

输出(总分从低到高):

log 复制代码
Student{name='dahei', chinese=80, math=80, english=80}总分为240 
Student{name='erhei', chinese=90, math=90, english=90}总分为270 
Student{name='xiaohei', chinese=100, math=100, english=100}总分为300 

此练习体现了多级排序条件的链式处理。

总结

我们来回顾一下上述知识结构:

  • ·Set 集合概述:介绍了单列集合体系、SetList 的区别
  • Set 基本使用:遍历方式(迭代器、增强 for
  • TreeSet 基本使用:Integer 自动排序、自定义对象报错原因
  • 自然排序:Comparable 接口、compareTo 方法、主要/次要条件、原理图解
  • 比较器排序:Comparator 匿名内部类、Lambda 表达式
  • 两种方式对比与练习:字符串长度排序案例
  • 综合练习:多条件总分排序