1. Giao diện Comparable<T>
Bạn đã từng sắp xếp danh sách số hay chuỗi chưa? Chắc chắn là rồi! Bây giờ, hãy tưởng tượng bạn có danh sách các đối tượng “của riêng mình” — ví dụ danh sách sinh viên, sản phẩm hoặc mèo. Java sẽ hiểu theo thứ tự nào để sắp xếp chúng? Đó chính là lý do cần giao diện Comparable<T>.
Giao diện này xác định “thứ tự tự nhiên” của các đối tượng — tức thứ tự hợp lý cho kiểu dữ liệu đó. Ví dụ, với số — tăng dần; với chuỗi — theo bảng chữ cái; với sinh viên — theo họ hoặc theo tuổi (tùy bạn chọn).
Comparable hoạt động thế nào
Giao diện rất đơn giản: chỉ có một phương thức:
public interface Comparable<T> {
int compareTo(T o);
}
Phương thức compareTo phải trả về:
- số âm nếu đối tượng hiện tại “nhỏ hơn” đối tượng kia;
- 0 nếu “bằng nhau”;
- số dương nếu “lớn hơn”.
Ví dụ: sắp xếp sinh viên theo tuổi
Hãy thêm lớp Student và cài đặt cho nó giao diện 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 cho ví dụ
public String getName() { return name; }
public int getAge() { return age; }
@Override
public int compareTo(Student other) {
// Sắp xếp theo tuổi (tăng dần)
return Integer.compare(this.age, other.age);
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
Bây giờ ta có thể dễ dàng sắp xếp mảng hay danh sách sinh viên:
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); // Hoạt động nhờ Comparable!
System.out.println("Danh sách sinh viên đã được sắp xếp:");
for (Student s : students) {
System.out.println(s);
}
}
}
Kết quả:
Petya (18)
Vasya (20)
Masha (22)
Lưu ý quan trọng
Nếu bạn cài đặt Comparable, hãy cố gắng để compareTo vận hành nhất quán với equals. Tức là nếu a.compareTo(b) == 0, thì a.equals(b) phải true. Nếu không, sắp xếp và các collection có thể cư xử khó lường, và bạn sẽ có cớ để suy tư về ý nghĩa cuộc đời của lập trình viên.
2. Giao diện Serializable
Tuần tự hóa (serialization) — là khả năng một đối tượng biến thành dãy byte (ví dụ để tự lưu vào tệp hoặc gửi qua mạng), rồi khôi phục lại. Hãy tưởng tượng bạn muốn lưu trạng thái trò chơi hoặc gửi đối tượng lên server — thiếu tuần tự hóa là không được.
Trong Java có giao diện đánh dấu (marker) Serializable. “Đánh dấu” — nghĩa là nó không chứa phương thức, mà chỉ “đánh dấu” lớp là có thể tuần tự hóa.
import java.io.Serializable;
public class Student implements Serializable {
private String name;
private int age;
// ... phần mã còn lại
}
Cách tuần tự hóa một đối tượng
Để tuần tự hóa và giải tuần tự sử dụng các lớp ObjectOutputStream và ObjectInputStream. Ví dụ — lưu đối tượng vào tệp và đọc lại:
import java.io.*;
public class Main {
public static void main(String[] args) throws Exception {
Student s = new Student("Katya", 19);
// Lưu đối tượng vào tệp
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("student.dat"))) {
out.writeObject(s);
}
// Đọc đối tượng từ tệp
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("student.dat"))) {
Student loaded = (Student) in.readObject();
System.out.println("Đã tải: " + loaded);
}
}
}
Lưu ý: Tất cả các trường của đối tượng (và các đối tượng lồng bên trong) cũng phải có thể tuần tự hóa, nếu không sẽ xảy ra lỗi.
Tại sao cần marker interface
Giao diện Serializable không yêu cầu cài đặt phương thức nào — nó chỉ thông báo cho JVM: “đối tượng này có thể tuần tự hóa”. Nếu quên không cài đặt, nỗ lực tuần tự hóa sẽ dẫn đến ngoại lệ NotSerializableException.
3. Các giao diện quan trọng khác của thư viện chuẩn
Giao diện Cloneable
Lại là một marker interface. Nhiệm vụ của nó — cho JVM biết rằng đối tượng có thể nhân bản (clone) bằng phương thức Object.clone(). Nếu không có nó, gọi clone() sẽ ném ngoại lệ.
Tuy nhiên, cloning trong Java — là chủ đề có “cạm bẫy”. Việc clone mặc định là nông (shallow copy), và thường thay vì thế bạn nên viết các phương thức sao chép của riêng mình.
public class Student implements Cloneable {
private String name;
private int age;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
Giao diện AutoCloseable
Giao diện này chỉ có một phương thức close(). Bất kỳ lớp nào cài đặt nó đều có thể dùng trong cấu trúc try-with-resources — tự động đóng tài nguyên (ví dụ tệp, stream):
public class MyResource implements AutoCloseable {
@Override
public void close() {
System.out.println("Tài nguyên đã được đóng!");
}
}
public class Main {
public static void main(String[] args) {
try (MyResource res = new MyResource()) {
System.out.println("Đang làm việc với tài nguyên");
}
// Ở đây res.close() sẽ được gọi tự động
}
}
Giao diện Iterable<T>
Giao diện này cho phép đối tượng của bạn được “duyệt” trong vòng lặp for-each. Chỉ có một phương thức iterator(), trả về đối tượng Iterator<T>.
public class MyList implements Iterable<String> {
// ... kho lưu trữ nội bộ
@Override
public java.util.Iterator<String> iterator() {
// Trả về iterator để duyệt các phần tử
return ...;
}
}
Tất cả các collection chuẩn (ArrayList, HashSet, v.v.) đều cài đặt Iterable, vì vậy có thể duyệt bằng for-each.
Giao diện Comparator<T>
Giao diện này cho phép so sánh các đối tượng theo nhiều quy tắc khác nhau mà không cần sửa chính đối tượng. Ví dụ, sắp xếp sinh viên theo tên thay vì theo tuổi.
import java.util.Comparator;
Comparator<Student> byName = new Comparator<Student>() {
@Override
public int compare(Student a, Student b) {
return a.getName().compareTo(b.getName());
}
};
Trong Java hiện đại, việc này thường làm qua lambda:
Comparator<Student> byName = (a, b) -> a.getName().compareTo(b.getName());
Observer, EventListener
Các giao diện này dùng để xây dựng các pattern “observer” và “event listener” — khi một đối tượng phản ứng với sự kiện xảy ra ở đối tượng khác. Ví dụ, trong giao diện đồ họa (Swing, JavaFX) các handler của nút bấm cài đặt giao diện ActionListener.
4. Thực hành: cài đặt Comparable và tuần tự hóa đối tượng
Ví dụ 1. Comparable cho lớp tự định nghĩa
Hãy viết lớp Book, có thể sắp xếp theo năm xuất bản:
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 dlya chaynikov", 2018),
new Book("Voyna i mir", 1869),
new Book("Garri Potter", 1997)
);
Collections.sort(books);
System.out.println(books);
}
}
Kết quả:
[Voyna i mir (1869), Garri Potter (1997), Java dlya chaynikov (2018)]
Ví dụ 2. Tuần tự hóa đối tượng
import java.io.*;
public class Book implements Serializable {
private String title;
private int year;
// ... constructor, 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 dlya chaynikov", 2018);
// Lưu đối tượng vào tệp
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("book.dat"))) {
out.writeObject(book);
}
// Đọc đối tượng từ tệp
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("book.dat"))) {
Book loaded = (Book) in.readObject();
System.out.println("Đã tải: " + loaded);
}
}
}
5. Bảng: các giao diện chính của thư viện chuẩn
| Giao diện | Mục đích | Phương thức chính | Ví dụ sử dụng |
|---|---|---|---|
|
Thứ tự tự nhiên của đối tượng | |
Sắp xếp danh sách |
|
So sánh đối tượng theo quy tắc tùy chỉnh | |
Sắp xếp theo nhiều tiêu chí khác nhau |
|
Tuần tự hóa đối tượng | — (marker) | Lưu/tải đối tượng |
|
Nhân bản (clone) đối tượng | — (marker) | Tạo bản sao đối tượng |
|
Tự động đóng tài nguyên | |
try-with-resources |
|
Duyệt phần tử trong collection | |
Vòng lặp for-each |
| Observer / EventListener | Phản ứng với sự kiện | |
Xử lý sự kiện trong UI, các pattern |
6. Các lỗi thường gặp khi làm việc với các giao diện chuẩn
Lỗi 1: Chưa cài đặt giao diện nhưng lại cần chức năng.
Ví dụ, quên cài đặt Serializable mà lại cố tuần tự hóa đối tượng — sẽ nhận NotSerializableException. Tương tự với Cloneable và việc gọi clone().
Lỗi 2: Vi phạm hợp đồng giữa Comparable và equals.
Nếu a.compareTo(b) == 0 nhưng không thỏa a.equals(b), các collection có thể cư xử kỳ lạ. Ví dụ, TreeSet có thể “mất” đối tượng.
Lỗi 3: Sao chép nông khi clone.
Phương thức clone() mặc định chỉ sao chép “lớp bên trên” của đối tượng. Nếu bạn có các trường tham chiếu đến đối tượng khác, chúng sẽ không được sao chép sâu. Điều này có thể dẫn đến những bug khó hiểu.
Lỗi 4: Không dùng try-with-resources.
Nếu lớp cài đặt AutoCloseable nhưng bạn không dùng trong try-with-resources, bạn có nguy cơ quên đóng tài nguyên — và gặp rò rỉ bộ nhớ hoặc khóa tệp.
Lỗi 5: Cài đặt sai compareTo hoặc compare.
Nếu chỉ trả về 0 hoặc 1, thay vì số âm/0/dương, việc sắp xếp sẽ không chính xác.
GO TO FULL VERSION