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