Giao Diện Serializable
(Interface Serializable)

Serializable là một interface trong Java, thuộc gói java.io, cho phép các đối tượng của lớp thực hiện nó có thể được tuần tự hóa (serialization).

Tuần tự hóa (Serialization) trong Java là quá trình chuyển đổi trạng thái của một đối tượng thành một luồng byte. Nghĩa là, chúng ta lấy một đối tượng phức tạp, bao gồm các trường và phương thức, và biến nó thành một chuỗi các byte đơn giản. Chuỗi byte này có thể được lưu trữ vào file, truyền qua mạng hoặc lưu trữ trong cơ sở dữ liệu. Khi cần, đối tượng có thể được "khôi phục" (deserialization) từ dòng byte này.

Quá trình tuần tự hóa (serialization) và khôi phục (deserialization) là độc lập với nền tảng, nghĩa là bạn có thể tuần tự hóa (serialization) một đối tượng trên một nền tảng và khôi phục (deserialization) nó trên một nền tảng khác.

Kích hoạt khả năng tuần tự hóa

Để có thể thực hiện tuần tự hóa (serializable) cho một đối tượng, lớp của đối tượng đó phải thực hiện giao diện java.io.Serializable.

Để tuần tự hóa (serializable) đối tượng, ta gọi phương thức writeObject() của lớp ObjectOutputStream và để khôi phục (deserialization) ta gọi phương thức readObject() của lớp ObjectInputStream.

Quá trình tuần tự hóa (serialization) và khôi phục (deserialization) trong Java - minh họa
Ảnh mô tả quá trình tuần tự hóa (serialization) và khôi phục (deserialization).

Đặc điểm của interface Serializable

Tất cả các lớp con của một lớp tuần tự hóa đều tự động trở thành tuần tự hóa. Nghĩa là nếu lớp cha triển khai Serializable, thì các lớp con của nó cũng có khả năng tuần tự hóa.

Giao diện Serializable không chứa bất kỳ phương thức hay trường dữ liệu nào. Nó chỉ đóng vai trò đánh dấu để xác định một lớp có thể tuần tự hóa hay không.

Quy tắc tuần tự hóa các lớp con

Đối với lớp con của một lớp không implements Serializable, lớp con có thể tự chịu trách nhiệm lưu và khôi phục trạng thái của các trường public, protected và package (nếu truy cập được) của lớp cha.

Điều kiện để lớp con có thể đảm nhận trách nhiệm này là lớp cha phải có một constructor không tham số (no-arg constructor) để khởi tạo trạng thái của nó. Nếu lớp cha không có constructor không tham số hoặc không thể truy cập được, thì việc khai báo lớp con là Serializable sẽ gây ra lỗi, và lỗi này sẽ được phát hiện tại thời gian chạy (runtime).

Khởi tạo đối tượng trong quá trình giải tuần tự hóa

Trong quá trình giải tuần tự hóa (deserialization), các trường của các lớp không implements Serializable sẽ được khởi tạo bằng constructor không tham số public hoặc protected của lớp đó.

Constructor không tham số này phải truy cập được đối với lớp con implements Serializable.

Các trường của các lớp con implements Serializable sẽ được khôi phục từ luồng dữ liệu.

Ngoại lệ NotSerializableException

Khi tuần tự hóa một biểu đồ đối tượng (object graph), nếu gặp phải một đối tượng không implements Serializable, một ngoại lệ NotSerializableException sẽ được ném ra, chỉ ra lớp của đối tượng không tuần tự hóa đó.

Phương thức xử lý đặc biệt

Các lớp yêu cầu xử lý riêng biệt trong quá trình tuần tự hóa và giải tuần tự hóa cần implements các phương thức đặc biệt với định nghĩa chính xác như sau:

Ví dụ

private void writeObject(java.io.ObjectOutputStream out)
throws IOException;

private void readObject(java.io.ObjectInputStream in)
    throws IOException, ClassNotFoundException;

private void readObjectNoData()
    throws ObjectStreamException;

Số phiên bản lớp (serialVersionUID)

serialVersionUID là một số nguyên 64 bit được sử dụng để xác định số phiên bản (version number) tương thích của một lớp có thể tuần tự hóa. Nó được sử dụng để đảm bảo rằng đối tượng được tuần tự hóa và giải tuần tự hóa trên các nền tảng khác nhau hoặc các phiên bản khác nhau của cùng một lớp vẫn tương thích.

Nếu giá trị serialVersionUID của lớp được giải tuần tự hóa khác với giá trị của lớp ban đầu, sẽ xảy ra lỗi InvalidClassException. Điều này cho thấy hai phiên bản của lớp không tương thích.

Khai Báo serialVersionUID với Từ Khóa private: Nên sử dụng từ khóa private để khai báo serialVersionUID khi có thể, vì giá trị này chỉ áp dụng cho lớp khai báo ngay lập tức mà không được thừa kế. Các trường serialVersionUID không có tác dụng khi được thừa kế, do đó khai báo chúng với từ khóa private là cách tốt nhất để đảm bảo tính an toàn.

Một lớp tuần tự hóa có thể tự khai báo serialVersionUID của riêng mình bằng cách khai báo một trường có tên là serialVersionUID, và trường này phải là static, final, và kiểu long:

Ví dụ

ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;

Quá trình tuần tự hóa bao gồm các bước sau:

(1) Tạo đối tượng:

Tạo một đối tượng của lớp mà bạn muốn tuần tự hóa. Đối tượng này phải thuộc một lớp thực hiện interface Serializable.

(2) Tạo luồng:

Tạo một luồng đầu ra (ví dụ: FileOutputStream) để xác định nơi bạn muốn ghi dữ liệu tuần tự hóa, như một file hoặc một kết nối mạng.

(3) Tạo đối tượng ObjectOutputStream:

Tạo một đối tượng ObjectOutputStream liên kết với luồng đầu ra. ObjectOutputStream sẽ chuyển đổi đối tượng thành một chuỗi byte.

(4) Ghi đối tượng:

Sử dụng phương thức writeObject của ObjectOutputStream để ghi đối tượng vào luồng. Phương thức này sẽ tuần tự hóa đối tượng và ghi nó vào luồng đầu ra dưới dạng một chuỗi byte.

(5) Đóng luồng:

Sau khi ghi xong đối tượng, đóng luồng đầu ra để giải phóng tài nguyên.

Dưới đây là một ví dụ về quá trình tuần tự hóa trong Java, tất cả được thực hiện trong cùng một lớp. Ví dụ này bao gồm các bước: tạo đối tượng, tạo luồng, tạo đối tượng ObjectOutputStream, ghi đối tượng vào luồng, và đóng luồng.

Ví dụ: Example.java

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Example {

    public static void main(String[] args) {
        // 1. Tạo đối tượng
        Person person = new Person("Duong", 30);

        // 2. Tạo luồng và ghi đối tượng vào luồng
        try (FileOutputStream fileOut = new FileOutputStream("person.ser");
             ObjectOutputStream out = new ObjectOutputStream(fileOut)) {

            // 3. Ghi đối tượng
            out.writeObject(person);
            System.out.println("Đối tượng đã được tuần tự hóa và lưu vào file person.ser");

        } catch (IOException e) {
            System.err.println("IOException occurred: " + e.getMessage());
        }
    }

    // Lớp Person phải thực hiện interface Serializable
    static class Person implements Serializable {
        private static final long serialVersionUID = 1L; // UID để đảm bảo tính tương thích
        private String name;
        private transient int age; // Trường transient sẽ không được tuần tự hóa

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        @Override
        public String toString() {
            return "Person{Tên='" + name + "', Tuổi=" + age + "}";
        }
    }
}

Chạy chương trình này sẽ tạo ra một file person.ser chứa dữ liệu tuần tự hóa của đối tượng Person:

���sr�Example$Person��������L�
namet�Ljava/lang/String;xpt�Duong

Quá trình giải tuần tự hóa (deserialization) bao gồm các bước sau:

(1) Tạo luồng:

Tạo một luồng đầu vào (ví dụ: FileInputStream) để đọc dữ liệu đã tuần tự hóa từ một file hoặc một nguồn dữ liệu khác.

(2) Tạo đối tượng ObjectInputStream:

Tạo một đối tượng ObjectInputStream liên kết với luồng đầu vào. ObjectInputStream sẽ chuyển đổi chuỗi byte thành đối tượng.

(3) Đọc đối tượng:

Sử dụng phương thức readObject của ObjectInputStream để đọc đối tượng từ luồng. Phương thức này sẽ tái tạo đối tượng từ chuỗi byte đã được lưu trữ trước đó.

(4) Đóng luồng:

Sau khi đọc xong đối tượng, đóng luồng đầu vào để giải phóng tài nguyên.

Dưới đây là một ví dụ về quá trình giải tuần tự hóa (deserialization) trong Java, tất cả được thực hiện trong cùng một lớp. Ví dụ này bao gồm các bước: tạo luồng, tạo đối tượng ObjectInputStream, đọc đối tượng từ luồng, và đóng luồng.

Ví dụ: Example.java

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class Example {

    public static void main(String[] args) {
        // 1. Tạo luồng đầu vào và đọc đối tượng từ luồng
        try (FileInputStream fileIn = new FileInputStream("person.ser");
             ObjectInputStream in = new ObjectInputStream(fileIn)) {

            // 2. Đọc đối tượng
            Person person = (Person) in.readObject();
            System.out.println("Đối tượng đã được giải tuần tự hóa: " + person);

        } catch (IOException e) {
            System.err.println("IOException đã xảy ra: " + e.getMessage());
        } catch (ClassNotFoundException e) {
            System.err.println("ClassNotFoundException đã xảy ra: " + e.getMessage());
        }
    }

    // Lớp Person phải thực hiện interface Serializable
    static class Person implements Serializable {
        private static final long serialVersionUID = 1L; // UID để đảm bảo tính tương thích
        private String name;
        private transient int age; // Trường transient sẽ không được tuần tự hóa

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        @Override
        public String toString() {
            return "\nPerson{Tên='" + name + "', Tuổi=" + age + "}";
        }
    }
}

Chạy chương trình này sẽ đọc đối tượng Person từ file person.ser và in ra thông tin của đối tượng đó. Chú ý rằng bạn cần phải đảm bảo rằng file person.ser đã được tạo trước đó bằng cách thực hiện quá trình tuần tự hóa. Kết quả của chương trình là

Đối tượng đã được giải tuần tự hóa:
Person{tên='Duong', Tuổi=30}

Hy vọng rằng qua hai ví dụ trên, bạn sẽ hiểu rõ hơn về quá trình tuần tự hóa (serialization) và khôi phục (deserialization) trong Java.

Câu Nói Truyền Cảm Hứng

“Bắt đầu ở đâu không quan trọng, quan trọng là bạn sẵn sàng bắt đầu.” – W. Clement Stone

Không Gian Tích Cực

“Chúc bạn luôn giữ vững niềm tin và sức mạnh để vượt qua mọi thử thách trong cuộc sống.”