1. 接口 Comparable<T>
你是否排序过数字或字符串列表?当然!现在想象你有自己定义的对象列表——例如学生、商品或猫咪。Java 怎么知道按什么顺序排序它们?这正是接口 Comparable<T> 的用武之地。
该接口定义了对象的“自然顺序”——即对该数据类型而言合理的顺序。比如数字按升序,字符串按字母序,学生可以按姓氏或年龄(由你决定)。
Comparable 的结构
这个接口很简单:它只有一个方法:
public interface Comparable<T> {
int compareTo(T o);
}
方法 compareTo 需要返回:
- 负数:如果当前对象“小于”另一个对象;
- 0:如果“相等”;
- 正数:如果“大于”。
示例:按年龄对学生排序
我们添加一个 Student 类,并为其实现 Comparable<Student>:
public class Student implements Comparable<Student> {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
// 示例的 getter
public String getName() { return name; }
public int getAge() { return age; }
@Override
public int compareTo(Student other) {
// 按年龄升序排序
return Integer.compare(this.age, other.age);
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
现在我们可以轻松地对学生的数组或列表进行排序:
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("Vasya", 20));
students.add(new Student("Petya", 18));
students.add(new Student("Masha", 22));
Collections.sort(students); // 能工作是因为实现了 Comparable!
System.out.println("已排序的学生:");
for (Student s : students) {
System.out.println(s);
}
}
}
结果:
Petya (18)
Vasya (20)
Masha (22)
重要细节
如果你实现了 Comparable,尽量让 compareTo 与 equals 保持一致。也就是说,如果 a.compareTo(b) == 0,那么 a.equals(b) 应为 true。否则,排序和集合可能会表现得不可预测,你也许会开始思考程序员生命的意义。
2. 接口 Serializable
序列化是指对象能够转换为字节序列(例如保存到文件或通过网络发送),随后再还原回来。想保存游戏状态或把对象发送到服务器?离不开序列化。
在 Java 中有一个标记型接口 Serializable 用于此目的。标记型意味着它不包含任何方法,只是“标记”该类可被序列化。
import java.io.Serializable;
public class Student implements Serializable {
private String name;
private int age;
// 其余代码
}
如何序列化对象
用于序列化和反序列化的类是 ObjectOutputStream 和 ObjectInputStream。示例——将对象保存到文件并读回:
import java.io.*;
public class Main {
public static void main(String[] args) throws Exception {
Student s = new Student("Katya", 19);
// 将对象保存到文件
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("student.dat"))) {
out.writeObject(s);
}
// 从文件读取对象
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("student.dat"))) {
Student loaded = (Student) in.readObject();
System.out.println("已加载: " + loaded);
}
}
}
注意:对象的所有字段(以及嵌套对象)也必须是可序列化的,否则会报错。
为何需要标记型接口
接口 Serializable 不要求实现任何方法——它只是告诉 JVM:“这个对象可以被序列化”。如果你忘了实现它,尝试序列化时会抛出 NotSerializableException。
3. 标准库中的其他重要接口
接口 Cloneable
又一个标记型接口。它的作用是让 JVM 明白,可以通过 Object.clone() 对象进行克隆。否则调用 clone() 会抛出异常。
不过,在 Java 中克隆是个有坑的话题。默认克隆是浅拷贝(shallow copy),很多情况下更好的做法是编写自己的拷贝方法。
public class Student implements Cloneable {
private String name;
private int age;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
接口 AutoCloseable
该接口只包含一个方法 close()。任何实现它的类都可以用于 try-with-resources 结构——自动关闭资源(例如文件、流):
public class MyResource implements AutoCloseable {
@Override
public void close() {
System.out.println("资源已关闭!");
}
}
public class Main {
public static void main(String[] args) {
try (MyResource res = new MyResource()) {
System.out.println("正在使用资源");
}
// 此处将自动调用 res.close()
}
}
接口 Iterable<T>
该接口使你的对象可以在 for-each 循环中被“遍历”。它只包含一个方法 iterator(),返回一个 Iterator<T> 对象。
public class MyList implements Iterable<String> {
// … 内部存储
@Override
public java.util.Iterator<String> iterator() {
// 返回用于遍历元素的迭代器
return ...;
}
}
所有标准集合(ArrayList、HashSet 等)都实现了 Iterable,因此可以在 for-each 中遍历。
接口 Comparator<T>
该接口允许在不同规则下比较对象,而无需修改对象本身。例如按姓名而不是年龄对学生排序。
import java.util.Comparator;
Comparator<Student> byName = new Comparator<Student>() {
@Override
public int compare(Student a, Student b) {
return a.getName().compareTo(b.getName());
}
};
在现代 Java 中通常通过 lambda 表达式完成:
Comparator<Student> byName = (a, b) -> a.getName().compareTo(b.getName());
Observer, EventListener
这些接口用于构建“观察者”和“事件监听器”模式——当一个对象对另一个对象中发生的事件作出反应时。例如在图形界面(Swing、JavaFX)中,按钮处理器实现 ActionListener 接口。
4. 实践:实现 Comparable 并序列化对象
示例 1. 自定义类的 Comparable
我们编写一个可以按出版年份排序的 Book 类:
public class Book implements Comparable<Book> {
private String title;
private int year;
public Book(String title, int year) {
this.title = title;
this.year = year;
}
@Override
public int compareTo(Book other) {
return Integer.compare(this.year, other.year);
}
@Override
public String toString() {
return title + " (" + year + ")";
}
}
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Book> books = Arrays.asList(
new Book("Java 入门", 2018),
new Book("战争与和平", 1869),
new Book("哈利·波特", 1997)
);
Collections.sort(books);
System.out.println(books);
}
}
结果:
[战争与和平 (1869), 哈利·波特 (1997), Java 入门 (2018)]
示例 2. 对象序列化
import java.io.*;
public class Book implements Serializable {
private String title;
private int year;
// 构造器、getter、toString
public Book(String title, int year) {
this.title = title;
this.year = year;
}
@Override
public String toString() {
return title + " (" + year + ")";
}
}
public class Main {
public static void main(String[] args) throws Exception {
Book book = new Book("Java 入门", 2018);
// 将对象保存到文件
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("book.dat"))) {
out.writeObject(book);
}
// 从文件读取对象
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("book.dat"))) {
Book loaded = (Book) in.readObject();
System.out.println("已加载: " + loaded);
}
}
}
5. 表格:标准库中的主要接口
| 接口 | 用途 | 关键方法 | 使用示例 |
|---|---|---|---|
|
对象的自然顺序 | |
列表排序 |
|
自定义对象比较 | |
按不同规则排序 |
|
对象序列化 | —(标记型) | 对象保存/加载 |
|
对象克隆 | —(标记型) | 创建对象副本 |
|
资源自动关闭 | |
try-with-resources |
|
集合元素遍历 | |
for-each 循环 |
| Observer / EventListener | 对事件作出响应 | |
在 UI 中处理事件、相关模式 |
6. 使用标准接口时的常见错误
错误 1:未实现所需接口却使用其功能。
例如忘了实现 Serializable 却尝试序列化对象——会抛出 NotSerializableException。对 Cloneable 与调用 clone() 也同理。
错误 2:违反 Comparable 与 equals 的契约。
如果 a.compareTo(b) == 0,却不满足 a.equals(b),集合可能会出现奇怪行为。例如 TreeSet 可能会“丢”对象。
错误 3:克隆时仅做浅拷贝。
clone() 默认只拷贝对象的“表层”。如果有字段是对其他对象的引用,它们不会被深拷贝。这可能导致难以察觉的 bug。
错误 4:未使用 try-with-resources。
如果类实现了 AutoCloseable,却不在 try-with-resources 中使用,你可能会忘记关闭资源——从而造成内存泄漏或文件被占用。
错误 5:错误实现 compareTo 或 compare。
如果只返回 0 或 1,而不是负/零/正数,排序将无法正确工作。
GO TO FULL VERSION