CodeGym /課程 /JAVA 25 SELF /標準程式庫中的介面:Comparable、Serializable 等

標準程式庫中的介面:Comparable、Serializable 等

JAVA 25 SELF
等級 21 , 課堂 4
開放

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,請盡量讓 compareToequals 保持一致。也就是說,如果 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;

    // ... 其餘程式碼
}

如何序列化物件

要進行序列化與反序列化,可使用 ObjectOutputStreamObjectInputStream。範例——將物件存到檔案再讀回:

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. 表格:標準程式庫中的主要介面

介面 用途 關鍵方法 使用範例
Comparable<T>
物件的自然順序
int compareTo(T o)
排序列表
Comparator<T>
自訂的物件比較
int compare(T a, T b)
依不同規則排序
Serializable
物件序列化 —(標記) 儲存/載入物件
Cloneable
物件複製 —(標記) 建立物件副本
AutoCloseable
資源自動關閉
void close()
try-with-resources
Iterable<T>
遍歷集合中的元素
Iterator<T> iterator()
for-each 迴圈
Observer / EventListener 對事件做出反應
update(), actionPerformed
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 實作。
如果只回傳 01,而非負數/零/正數三態,排序將無法正確運作。

1
問卷/小測驗
進階介面,等級 21,課堂 4
未開放
進階介面
進階介面與函式式介面
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION