本文纲要
Set集合概述Set集合的基本使用TreeSet基本使用TreeSet自然排序
自然排序步骤
主要条件与次要条件
compareTo方法原理- 比较器排序
- 两种排序方式对比与练习
- 综合练习:按总分排序
Set 集合概述
在 Java 集合框架中,单列集合的顶层接口是 Collection,它派生出两大类:
List集合:元素可以重复,有索引。常见实现类:ArrayList(底层数组,查询快增删慢)、LinkedList(底层链表,查询慢增删快)。Set集合:元素不可重复,无索引 。常见实现类:HashSet、TreeSet
本文主要学习 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 集合的特点:
- 不可重复:添加重复元素时只会保留一个。
- 存取顺序不一致:存入顺序与遍历顺序不一定相同。
- 没有带索引的方法 :不能使用普通
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 基本使用
TreeSet 是 Set 接口的一个实现类,其特点:
- 不包含重复元素(
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 自然排序
自然排序步骤
- 使用
TreeSet的空参构造创建集合。 - 自定义类实现
Comparable接口,泛型与集合元素类型一致。 - 重写
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}]
年龄相同的 zhangsan 和 zhaoliu,按姓名字典序排列(zhangsan 在 zhaoliu 前)
主要条件与次要条件
在 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
当前元素较小,放左子树
元素重复,不存入
当前元素较大,放右子树
若首次插入直接作为根节点;后续插入会与根节点及子树节点依此规则比较,最终形成排序树结构。
比较器排序
当无法修改自定义类的源码(如 String、Integer 已固定实现),或默认自然排序不满足需求时,可使用比较器排序
步骤
- 创建 TreeSet 时传入
Comparator对象。 - 实现
compare方法定义排序规则。 - 比较的规则(负数/零/正数)与自然排序一致。
示例: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}]
年龄相同的 wangwu 和 zhaoliu 按首字母排序。
也可以使用 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,ab和df字典序ab在前)
补充:String 的 compareTo 示例
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集合概述:介绍了单列集合体系、Set与List的区别 Set基本使用:遍历方式(迭代器、增强for)TreeSet基本使用:Integer自动排序、自定义对象报错原因- 自然排序:
Comparable接口、compareTo方法、主要/次要条件、原理图解 - 比较器排序:
Comparator匿名内部类、Lambda表达式 - 两种方式对比与练习:字符串长度排序案例
- 综合练习:多条件总分排序