Phân Tích Mã Nguồn Các Interface Core Và Cơ Chế Serialization Trong Java

Giao diện Appendable và CharSequence

Trong hệ thống lớp của Java, java.lang.Appendable đóng vai trò là một hợp đồng cho các đối tượng có khả năng nối thêm dữ liệu ký tự. Interface này thường được triển khai bởi các lớp làm việc với luồng dữ liệu hoặc bộ đệm ký tự. Các lớp phổ biến như StringBufferStringBuilder đều thực hiện giao diện này, cho phép chúng biến đổi nội dung linh hoạt khác hẳn với tính bất biến của String.

public interface Appendable {
    Appendable append(CharSequence csq) throws IOException;
    Appendable append(CharSequence csq, int start, int end) throws IOException;
    Appendable append(char c) throws IOException;
}

Liên quan mật thiết là java.lang.CharSequence, đại diện cho một chuỗi ký tự có thể đọc được. Giao diện này cung cấp các phương thức cơ bản để truy xuất độ dài, lấy ký tự tại vị trí cụ thể hoặc trích xuất một đoạn chuỗi con. Hầu hết các lớp làm việc với văn bản trong Java như String, StringBuilder đều kế thừa từ đây.

public interface CharSequence {
    int length();
    char charAt(int index);
    CharSequence subSequence(int start, int end);
    String toString();
    // Các phương thức mặc định từ Java 8
    default IntStream chars() { ... }
    default IntStream codePoints() { ... }
}

Tối ưu hóa truy cập với RandomAccess

java.util.RandomAccess là một giao diện đánh dấu (marker interface), không chứa bất kỳ phương thức nào. Mục đích duy nhất của nó là để phân biệt cách thức lưu trữ dữ liệu nền tảng của các danh sách (List). Cụ thể, nó phân biệt giữa cấu trúc dựa trên mảng (truy cập chỉ số nhanh) và cấu trúc dựa trên liên kết (truy cập chỉ số chậm nhưng chèn/xóa nhanh).

Thư viện Collections sử dụng thông tin này để chọn thuật toán phù hợp. Ví dụ, trong phương thức binarySearch, nếu danh sách implements RandomAccess, hệ thống sẽ dùng tìm kiếm nhị phân dựa trên chỉ số. Ngược lại, nó sẽ dùng iterator để duyệt tuần tự.

public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {
    if (list instanceof RandomAccess || list.size() < BINARYSEARCH_THRESHOLD)
        return Collections.indexedBinarySearch(list, key);
    else
        return Collections.iteratorBinarySearch(list, key);
}

Cơ chế lặp Iterable và Iterator

Để hỗ trợ cú pháp vòng lặp for-each, Java cung cấp giao diện java.lang.Iterable. Khi một collection implement interface này, nó cam kết cung cấp một Iterator để duyệt qua các phần tử.

public interface Iterable<T> {
    Iterator<T> iterator();
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

Giao diện Iterator đi kèm cung cấp các phương thức để kiểm tra phần tử tiếp theo và lấy dữ liệu. Tất cả các collection trong Java đều kế thừa Iterable thông qua interface Collection.

Xét ví dụ kiểm tra cách hoạt động của iterator:

@Test
public void verifyIteratorUsage(){
    List<String> dataList = new ArrayList<>();
    dataList.add("X");
    dataList.add("Y");
    dataList.add("Z");
    
    Iterator<String> iter = dataList.iterator();
    while (iter.hasNext()){
        System.out.println(iter.next());
    }
}

Cú pháp for-each thực chất là đường tắt (syntax sugar) của iterator. Khi biên dịch, mã bytecode sẽ chuyển đổi vòng lặp thành các lệnh gọi iterator(), hasNext()next() tương ứng.

Hierarchy Collection và List

java.util.Collection là gốc của khung framework collection, định nghĩa các hành vi chuẩn như thêm, xóa, kiểm tra tồn tại và chuyển đổi sang mảng. Interface này cũng tích hợp sẵn các phương thức mặc định cho stream và xóa theo điều kiện từ Java 8.

public interface Collection<E> extends Iterable<E> {
    int size();
    boolean isEmpty();
    boolean add(E e);
    boolean remove(Object o);
    // ... các phương thức khác
    default boolean removeIf(Predicate<? super E> filter) {
        // Implementation dựa trên iterator
    }
    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }
}

Các lớp trừ tượng như AbstractCollection cung cấp implementation mặc định cho nhiều phương thức dựa trên iterator, giúp việc tạo collection custom dễ dàng hơn. Tuy nhiên, để tối ưu hiệu năng, các lớp con thường ghi đè lại các phương thức này.

java.util.List mở rộng từ Collection để bổ sung tính chất thứ tự (ordered). Do đó, nó cung cấp thêm các phương thức thao tác dựa trên chỉ số index như get(int), set(int, E), add(int, E).

So sánh và Sắp xếp

Để sắp xếp đối tượng, Java cung cấp hai cơ chế: Comparable (so sánh nội tại) và Comparator (so sánh bên ngoài).

  • Comparable<T>: Yêu cầu class tự implement phương thức compareTo.
  • Comparator<T>: Là một đối tượng độc lập thực hiện logic so sánh qua phương thức compare.
public interface Comparator<T> {
    int compare(T o1, T o2);
    // Các phương thức default hỗ trợ chaining
}

Kết quả trả về là số nguyên: dương nếu o1 > o2, âm nếu o1 < o2 và 0 nếu bằng nhau.

Cơ chế Serialization trong Java

Serialization (tuần tự hóa) cho phép chuyển đổi trạng thái của đối tượng thành dãy byte để lưu trữ hoặc truyền qua mạng. Java cung cấp nhiều cấp độ kiểm soát cho quá trình này.

1. Serializable tiêu chuẩn

Cách đơn giản nhất là implement giao diện java.io.Serializable. Đây là một marker interface. JVM sẽ tự động xử lý việc ghi và đọc đối tượng thông qua ObjectOutputStreamObjectInputStream.

import java.io.*;
import java.util.*;

public class SerializationDemo {
    public static void main(String[] args) throws Exception {
        Account member = new Account("Nguyen Van A", 30);
        
        // Ghi đối tượng ra file
        try (ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("data.bin"))) {
            output.writeObject(member);
        }

        // Đọc đối tượng từ file
        try (ObjectInputStream input = new ObjectInputStream(new FileInputStream("data.bin"))) {
            Account restored = (Account) input.readObject();
            System.out.println(restored);
        }
    }
}

class Account implements Serializable {
    private String fullName;
    private int age;
    // Constructor, getter, setter...
}

2. Kiểm soát trường dữ liệu (transient)

Từ khóa transient được dùng để chỉ định các trường không nên được tuần tự hóa. Khi đọc lại, các trường này sẽ nhận giá trị mặc định (null, 0, false).

3. Quản lý phiên bản (serialVersionUID)

Trường serialVersionUID dùng để xác thực tính tương thích giữa class gửi và class nhận. Nếu ID không khớp, quá trình deserialization sẽ thất bại. Nên khai báo cố định để tránh lỗi khi refactor code.

private static final long serialVersionUID = 1L;

4. Custom Serialization (writeObject/readObject)

Để can thiệp sâu vào quá trình ghi/đọc (ví dụ: mã hóa dữ liệu nhạy cảm hoặc tối ưu dung lượng), class có thể định nghĩa riêng hai phương thức private này.

private void writeObject(ObjectOutputStream out) throws IOException {
    out.defaultWriteObject();
    // Logic custom thêm
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    // Logic custom thêm
}

Ví dụ điển hình là ArrayList, nó chỉ tuần tự hóa các phần tử thực tế chứ không tuần tự hóa toàn bộ mảng nội bộ (có thể chứa nhiều ô trống).

5. Bảo vệ Singleton (readResolve)

Một vấn đề phổ biến là serialization có thể phá vỡ tính chất Singleton vì nó tạo ra một instance mới khi đọc. Để khắc phục, class cần implement phương thức readResolve để trả về instance duy nhất đã tồn tại.

class Status implements Serializable {
    public static final Status ACTIVE = new Status(1);
    private int code;
    
    private Object readResolve() {
        if (code == 1) return ACTIVE;
        return this;
    }
}

6. Externalizable

Giao diện Externalizable kế thừa Serializable nhưng yêu cầu class tự hoàn toàn chịu trách nhiệm ghi đọc dữ liệu qua writeExternalreadExternal. Cơ chế này yêu cầu class phải có constructor public không tham số.

public class Product implements Externalizable {
    public String name;
    public double cost;
    
    public Product() {} // Bắt buộc
    
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeDouble(cost);
    }
    
    public void readExternal(ObjectInput in) throws IOException {
        name = in.readUTF();
        cost = in.readDouble();
    }
}

7. Phân tích luồng đối tượng (ObjectOutputStream)

Class ObjectOutputStream quản lý việc ghi dữ liệu. Khi khởi tạo, nó ghi một header (magic number 0xACED và version). Quá trình ghi đối tượng sử dụng bảng handle (HandleTable) để theo dõi các đối tượng đã gặp, đảm bảo nếu một đối tượng được tham chiếu nhiều lần, nó chỉ được ghi dữ liệu một lần, các lần sau chỉ ghi reference.

Phương thức writeObject0 là lõi của quá trình này, nó kiểm tra loại đối tượng (String, Array, Enum, Serializable...) và gọi phương thức xử lý tương ứng. Nếu đối tượng implement Externalizable, nó sẽ gọi writeExternal, ngược lại sẽ gọi writeSerialData để xử lý các trường của class và parent class.

Thẻ: java-collections java-serialization jdk-internals interface-design object-stream

Đăng vào ngày 19 tháng 7 lúc 01:49