1. Tại sao cần tuần tự hóa
Hãy hình dung đối tượng của bạn — như những món đồ bạn mang theo khi đi nghỉ. Tuần tự hóa là việc đóng gói toàn bộ nội dung va-li vào một container đặc biệt mà bạn có thể cho vào hành lý hoặc gửi qua bưu điện. Giải tuần tự, tương ứng, là mở container đó và lấy đồ ra đúng như ban đầu.
Về bản chất, tuần tự hóa biến một đối tượng thành luồng byte, có thể lưu vào tệp, truyền qua mạng hoặc chỉ giữ trong bộ nhớ. Giải tuần tự làm điều ngược lại: khôi phục đối tượng từ luồng đó. Nói thật đơn giản, tuần tự hóa giống như “đóng băng” đối tượng để sau đó “rã đông” và nhận lại nó trong cùng trạng thái.
Lưu trạng thái đối tượng giữa các lần chạy chương trình
Một trong những kịch bản phổ biến nhất là lưu trạng thái chương trình. Ví dụ, bạn có danh sách người dùng, kết quả trò chơi hoặc cài đặt ứng dụng. Tất cả những thứ này rất tiện lưu trực tiếp dưới dạng đối tượng. Để dữ liệu không bị mất giữa các lần chạy, chúng được tuần tự hóa vào tệp, và ở lần chạy tiếp theo — được giải tuần tự.
Ví dụ điển hình — bản lưu (save) trong trò chơi. Khi người chơi vượt qua một màn, tiến trình của họ được “đóng băng” và ghi vào tệp bằng tuần tự hóa. Ngày hôm sau họ mở trò chơi, và tiến trình được “rã đông”: dữ liệu từ tệp được biến trở lại thành các đối tượng, và người chơi tiếp tục từ nơi đã dừng.
Hãy tạo một bản lưu đơn giản như vậy:
import java.io.*;
// Lớp người chơi phải là Serializable
class Player implements Serializable {
String name;
int score;
Player(String name, int score) {
this.name = name;
this.score = score;
}
}
public class GameSaveExample {
public static void main(String[] args) throws Exception {
// Tạo đối tượng người chơi
Player player = new Player("Ihor", 1500);
// --- Lưu (tuần tự hóa) ---
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("save.dat"))) {
out.writeObject(player);
System.out.println("Tiến trình đã được lưu!");
}
// --- Tải (giải tuần tự) ---
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("save.dat"))) {
Player loaded = (Player) in.readObject();
System.out.println("Tiến trình đã được tải: " + loaded.name + " với số điểm " + loaded.score);
}
}
}
Lưu ý: để đoạn mã này hoạt động, lớp Player phải triển khai interface Serializable. Chi tiết về nó — ở bài giảng tiếp theo!
- Player — một lớp bình thường với các trường tên và điểm, được đánh dấu bằng interface Serializable (implements Serializable).
- ObjectOutputStream ghi đối tượng vào tệp "save.dat".
- ObjectInputStream đọc lại chính đối tượng đó.
- Kết quả là ta có một bản lưu thực sự: ở lần chạy tiếp theo, chương trình sẽ tải đối tượng người chơi với trạng thái như cũ.
Truyền đối tượng qua mạng và giữa JVM
Trong các hệ thống phân tán, thường cần truyền đối tượng giữa các chương trình khác nhau hoặc thậm chí giữa các máy khác nhau. Ví dụ, bạn có client và server cần trao đổi thông điệp. Tuần tự hóa cho phép “đóng gói” đối tượng ở một bên, gửi nó qua mạng và “mở gói” ở bên kia.
Ví dụ: Client gửi cho server một đối tượng đơn hàng (Order), server nhận, giải tuần tự và xử lý nó.
Sử dụng trong các công nghệ Java
- RMI (Remote Method Invocation): cho phép gọi phương thức của đối tượng từ xa — tuần tự hóa cần thiết để truyền đối số và giá trị trả về.
- Phiên HTTP: trong các servlet, đối tượng trong phiên được tuần tự hóa khi container khởi động lại.
- JMS (Java Message Service): thông điệp giữa các thành phần có thể được tuần tự hóa.
- Bộ nhớ đệm (cache): đối tượng có thể được tuần tự hóa để lưu trong cache (ra đĩa hoặc kho phân tán).
Bộ nhớ đệm và tính khả chuyển
Nếu bạn muốn lưu nhanh các kết quả trung gian (ví dụ, để làm bộ nhớ đệm), tuần tự hóa là công cụ tuyệt vời. Bạn tuần tự hóa đối tượng, lưu nó ra đĩa hoặc bộ nhớ, rồi khôi phục lại nhanh chóng mà không cần tính toán lại.
2. Ví dụ kịch bản sử dụng tuần tự hóa
Lưu một tập hợp người dùng vào tệp
Giả sử bạn có lớp User:
public class User {
String name;
int age;
// ... các trường khác
}
Và bạn có danh sách người dùng:
List<User> users = new ArrayList<>();
users.add(new User("Vasya", 25));
users.add(new User("Masha", 30));
// ... v.v.
Để lưu danh sách này vào tệp, bạn tuần tự hóa nó. Khi cần — giải tuần tự và nhận lại đúng danh sách với những người dùng đó. Xin nhắc lại, lớp User (và tất cả các trường của nó) phải hỗ trợ tuần tự hóa, tức là triển khai Serializable.
Truyền thông điệp giữa client và server
Ví dụ kinh điển — ứng dụng chat. Người dùng viết một thông điệp, đối tượng Message được tuần tự hóa và gửi qua mạng. Server nhận luồng byte, giải tuần tự đối tượng, xử lý nó và có thể chuyển tiếp tiếp.
import java.io.*;
import java.net.*;
// Thông điệp (message) phải là Serializable
class Message implements Serializable {
String text;
Message(String text) {
this.text = text;
}
}
// Server
class Server {
public static void main(String[] args) throws Exception {
try (ServerSocket serverSocket = new ServerSocket(5000)) {
System.out.println("Máy chủ đang chờ kết nối...");
Socket socket = serverSocket.accept();
System.out.println("Client đã kết nối!");
try (ObjectInputStream in = new ObjectInputStream(socket.getInputStream())) {
Message msg = (Message) in.readObject();
System.out.println("Đã nhận thông điệp: " + msg.text);
}
}
}
}
// Client
class Client {
public static void main(String[] args) throws Exception {
try (Socket socket = new Socket("localhost", 5000)) {
try (ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream())) {
Message msg = new Message("Xin chào, server!");
out.writeObject(msg);
System.out.println("Thông điệp đã được gửi!");
}
}
}
}
Cách hoạt động:
- Trước tiên chạy Server (nó chờ kết nối).
- Sau đó chạy Client (nó kết nối tới "localhost:5000").
- Client tuần tự hóa đối tượng Message và gửi nó qua socket.
- Server nhận luồng byte, giải tuần tự và in ra văn bản.
Ở đây chúng ta dùng socket (ServerSocket, Socket) — cơ chế giao tiếp mạng mà bạn sẽ học sau. Điều quan trọng bây giờ không phải là chi tiết mạng, mà là ý tưởng: client tạo đối tượng Message, tuần tự hóa và gửi nó; server nhận luồng byte, giải tuần tự lại thành đối tượng và in thông điệp. Như vậy, ngay cả khi bạn chưa rõ các lớp ServerSocket và Socket, ví dụ này cho thấy giá trị của tuần tự hóa: nhờ nó bạn có thể “đóng gói” một đối tượng, truyền qua mạng và ở phía bên kia mở gói mà không cần chuyển đổi rườm rà.
Bộ nhớ đệm đối tượng
Trong các ứng dụng lớn, bộ nhớ đệm thường được dùng để tăng tốc. Ví dụ, kết quả các phép tính phức tạp được tuần tự hóa và lưu vào bộ nhớ đệm (tệp, cơ sở dữ liệu, kho phân tán). Ở yêu cầu tiếp theo có thể khôi phục nhanh bằng cách giải tuần tự đối tượng.
import java.io.*;
// Kết quả tính toán mà chúng ta muốn lưu vào bộ nhớ đệm
class Result implements Serializable {
int value;
Result(int value) {
this.value = value;
}
}
public class CacheExample {
private static final String CACHE_FILE = "cache.dat";
public static void main(String[] args) throws Exception {
Result result;
// Kiểm tra xem có cache hay không
File file = new File(CACHE_FILE);
if (file.exists()) {
// Tải kết quả từ cache
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(file))) {
result = (Result) in.readObject();
System.out.println("Đã tải từ bộ nhớ đệm: " + result.value);
}
} else {
// Phép tính “nặng” (ví dụ: chỉ là bình phương số)
int x = 12345;
System.out.println("Đang tính... (mất thời gian)");
result = new Result(x * x);
// Lưu kết quả vào cache
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file))) {
out.writeObject(result);
System.out.println("Đã lưu vào bộ nhớ đệm: " + result.value);
}
}
}
}
3. Hạn chế và rủi ro của tuần tự hóa
Tuần tự hóa là công cụ mạnh mẽ, nhưng không phải không có cạm bẫy. Hãy bàn về các hạn chế và rủi ro chính.
Không phải mọi đối tượng đều có thể được tuần tự hóa
Trong Java, không phải mọi đối tượng đều có thể được tuần tự hóa “out of the box”. Ví dụ, các đối tượng gắn với tài nguyên bên ngoài (tệp, kết nối mạng, luồng I/O) không thể tuần tự hóa. Điều này hợp lý: không thể tuần tự hóa “tệp đang mở” hay “kết nối mạng đang hoạt động” — trạng thái của chúng phụ thuộc vào hệ điều hành và môi trường chạy.
Ví dụ: Không thể tuần tự hóa một lớp có trường kiểu FileInputStream — khi thử tuần tự hóa sẽ xảy ra lỗi.
Vấn đề bảo mật
Tuần tự hóa có thể là lỗ hổng bảo mật tiềm tàng. Nếu bạn giải tuần tự dữ liệu nhận từ nguồn không tin cậy (ví dụ, từ internet), kẻ tấn công có thể chèn một luồng byte độc hại dẫn đến hành vi bất ngờ của chương trình, thậm chí — thực thi mã độc.
Quy tắc: Không bao giờ giải tuần tự dữ liệu từ nguồn không tin cậy! Giống như nhận bưu kiện từ người gửi không rõ — bên trong có thể là bất cứ thứ gì.
Tương thích phiên bản
Nếu bạn thay đổi cấu trúc lớp (ví dụ, thêm hoặc xóa trường), các đối tượng đã tuần tự hóa trước đó có thể không tương thích với phiên bản lớp mới. Điều này có thể dẫn đến lỗi khi giải tuần tự. Vấn đề này sẽ được xem xét chi tiết trong các bài giảng tiếp theo.
Hiệu năng
Tuần tự hóa nhị phân trong Java khá nhanh, nhưng đôi khi không gọn nhất và không luôn thuận tiện để trao đổi với ngôn ngữ khác. Để trao đổi với hệ thống bên ngoài, người ta thường dùng định dạng văn bản (JSON, XML).
4. Lỗi thường gặp khi mới làm quen với tuần tự hóa
Lỗi số 1: cố gắng tuần tự hóa đối tượng không triển khai interface Serializable.
Kết quả bạn sẽ nhận ngoại lệ NotSerializableException. Đừng quên chỉ rõ implements Serializable trong lớp và đảm bảo tất cả các trường cũng tuần tự hóa được!
Lỗi số 2: tuần tự hóa đối tượng có các trường không tuần tự hóa được.
Nếu lớp của bạn chứa trường có kiểu không hỗ trợ tuần tự hóa (ví dụ, luồng hoặc kết nối DB), tuần tự hóa sẽ không hoạt động. Cách giải quyết — đánh dấu các trường như vậy là transient (sẽ nói sau).
Lỗi số 3: giải tuần tự dữ liệu từ nguồn không tin cậy.
Điều này có thể dẫn đến lỗ hổng bảo mật hoặc thậm chí thực thi mã độc. Chỉ tin tưởng dữ liệu được tuần tự hóa bởi chính chương trình của bạn!
Lỗi số 4: thay đổi cấu trúc lớp sau khi đã tuần tự hóa.
Nếu bạn đã lưu đối tượng, rồi thêm hoặc xóa trường trong lớp, khi giải tuần tự có thể phát sinh lỗi hoặc xuất hiện các giá trị “kỳ lạ”. Chi tiết — ở các bài giảng tiếp theo.
GO TO FULL VERSION