Serialization
Sau bài này, bạn sẽ:
- Hiểu serialization - chuyển Java objects thành byte stream để lưu/truyền
- Implement Serializable interface và sử dụng ObjectInputStream/ObjectOutputStream
- Nắm được serialVersionUID, transient keyword, và custom serialization (writeObject/readObject)
- Biết security issues với serialization và cách tránh
- So sánh Java Serialization vs JSON/XML và biết khi nào dùng cái gì
Bài trước: NIO.2: Path và Files — Đã học về Path, Files, và modern file API. Bài này sẽ tìm hiểu serialization - cách lưu trữ và truyền Java objects.
Serialization là gì?
Serialization là quá trình chuyển đổi Java object thành byte stream để:
- Lưu vào file (persistence)
- Truyền qua network (RMI, web services)
- Store trong database (BLOB)
- Cache trong memory (Redis, Memcached)
Deserialization là quá trình ngược lại: byte stream → object.
┌─────────────────────────────────────────────────┐
│ Serialization Process │
├─────────────────────────────────────────────────┤
│ │
│ Java Object │
│ ↓ │
│ ObjectOutputStream (serialize) │
│ ↓ │
│ Byte Stream (binary data) │
│ ↓ │
│ File / Network / Database │
│ ↓ │
│ ObjectInputStream (deserialize) │
│ ↓ │
│ Java Object (restored) │
│ │
└─────────────────────────────────────────────────┘
Serializable Interface - Marker Interface
Để một class có thể serialize, phải implement Serializable interface.
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private int age;
private String email;
public Person(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + ", email='" + email + "'}";
}
// Getters and setters...
}
Serializable là marker interface - không có methods nào! Chỉ đánh dấu rằng class này có thể serialize.
public interface Serializable {
// Không có method nào!
}
Nếu class không implement Serializable, serialization sẽ ném NotSerializableException!
ObjectOutputStream và ObjectInputStream
ObjectOutputStream - Serialize object
import java.io.*;
public class SerializationExample {
public static void main(String[] args) {
Person person = new Person("Nguyễn Văn A", 30, "[email protected]");
// Serialize object vào file
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("person.ser"))) {
oos.writeObject(person);
System.out.println("Object serialized: " + person);
} catch (IOException e) {
e.printStackTrace();
}
}
}
File person.ser chứa binary data (không đọc được bằng text editor):
¬í sr Person...ô...
ObjectInputStream - Deserialize object
import java.io.*;
public class DeserializationExample {
public static void main(String[] args) {
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("person.ser"))) {
// Deserialize object từ file
Person person = (Person) ois.readObject();
System.out.println("Object deserialized: " + person);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Output:
Object deserialized: Person{name='Nguyễn Văn A', age=30, email='[email protected]'}
Serialize nhiều objects
import java.io.*;
import java.util.ArrayList;
import java.util.List;
public class SerializeMultiple {
public static void main(String[] args) throws Exception {
List<Person> people = new ArrayList<>();
people.add(new Person("Alice", 25, "[email protected]"));
people.add(new Person("Bob", 30, "[email protected]"));
people.add(new Person("Charlie", 35, "[email protected]"));
// Serialize List
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("people.ser"))) {
oos.writeObject(people);
}
// Deserialize List
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("people.ser"))) {
@SuppressWarnings("unchecked")
List<Person> loaded = (List<Person>) ois.readObject();
System.out.println("Loaded " + loaded.size() + " people:");
loaded.forEach(System.out::println);
}
}
}
serialVersionUID - Version Control for Serialization
serialVersionUID là version number của class. JVM dùng nó để verify rằng serialized object và class definition tương thích với nhau.
serialVersionUID giống như số phiên bản của class:
- JVM checks: "Serialized data version == Current class version?"
- If match → deserialize OK
- If mismatch →
InvalidClassException
// Version 1 (serialVersionUID = auto-generated: 123456789)
public class Person implements Serializable {
private String name;
private int age;
}
// Serialize object → saves UID = 123456789
// Change class (add field)
public class Person implements Serializable {
private String name;
private int age;
private String email; // New field!
}
// Auto-generated UID changes to 987654321!
// Try to deserialize old object:
// ❌ InvalidClassException! UID mismatch (123456789 != 987654321)
Solution: Explicitly declare serialVersionUID:
public class Person implements Serializable {
private static final long serialVersionUID = 1L; // ← Fixed!
private String name;
private int age;
}
Now adding fields won't break deserialization!
Vấn đề không có serialVersionUID
// Version 1 của Person class
public class Person implements Serializable {
private String name;
private int age;
}
- Serialize object với Version 1
- Thay đổi class (thêm field mới):
// Version 2 của Person class
public class Person implements Serializable {
private String name;
private int age;
private String email; // Field mới!
}
- Deserialize object cũ → InvalidClassException!
java.io.InvalidClassException: Person; local class incompatible:
stream classdesc serialVersionUID = 1234567890,
local class serialVersionUID = 9876543210
serialVersionUID Mismatch Flowchart
Giải pháp: Khai báo serialVersionUID rõ ràng
import java.io.Serializable;
public class Person implements Serializable {
// LUÔN khai báo serialVersionUID!
private static final long serialVersionUID = 1L;
private String name;
private int age;
private String email;
// ... constructors, methods
}
Khi nào thay đổi serialVersionUID?
- ✅ Thêm field mới: KHÔNG thay đổi (backward compatible)
- ✅ Thêm methods: KHÔNG thay đổi
- ❌ Xóa field: CÓ thay đổi (breaking change)
- ❌ Đổi type của field: CÓ thay đổi (breaking change)
// Version 1
private static final long serialVersionUID = 1L;
private String name;
private int age;
// Version 2 - Thêm field (OK, không đổi serialVersionUID)
private static final long serialVersionUID = 1L;
private String name;
private int age;
private String email; // Mới
// Version 3 - Đổi type (BREAKING, phải đổi serialVersionUID)
private static final long serialVersionUID = 2L; // ← Changed!
private String name;
private long age; // int → long (breaking!)
Luôn luôn khai báo serialVersionUID rõ ràng! Nếu không, JVM tự generate dựa trên class structure → dễ bị thay đổi ngoài ý muốn.
transient Keyword - Loại trừ fields khỏi Serialization
Dùng transient để không serialize một field (vd: password, cache, derived data).
transient Keyword Effect Diagram
Use transient for:
-
Passwords / Sensitive data
private transient String password; // Don't serialize!
private transient String ssn; // Social Security Number -
Computed/Derived values
private double width;
private double height;
private transient double area; // Recompute after deserialization -
Non-serializable references
private transient Thread workerThread; // Thread is not Serializable!
private transient Socket socket; // Socket is not Serializable! -
Cache / Temporary data
private transient Map<String, Object> cache; // Runtime cache
private transient int requestCount; // Runtime counter
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // KHÔNG serialize password!
private String email;
private transient int loginCount; // Không cần lưu (runtime data)
public User(String username, String password, String email) {
this.username = username;
this.password = password;
this.email = email;
this.loginCount = 0;
}
@Override
public String toString() {
return "User{username='" + username +
"', password='" + password +
"', email='" + email +
"', loginCount=" + loginCount + "}";
}
}
Test:
public class TransientExample {
public static void main(String[] args) throws Exception {
User user = new User("alice", "secret123", "[email protected]");
user.loginCount = 42;
System.out.println("Original: " + user);
// Output: User{username='alice', password='secret123', email='[email protected]', loginCount=42}
// Serialize
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("user.ser"))) {
oos.writeObject(user);
}
// Deserialize
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("user.ser"))) {
User loaded = (User) ois.readObject();
System.out.println("Loaded: " + loaded);
// Output: User{username='alice', password='null', email='[email protected]', loginCount=0}
}
}
}
Kết quả:
password:null(transient → không serialize)loginCount:0(transient → default value)
Sau khi deserialize, transient fields có default value:
- Object references:
null - int/long/etc.:
0 - boolean:
false
Custom Serialization - writeObject/readObject
Đôi khi cần custom logic khi serialize/deserialize (vd: encryption, validation, derived data).
Implement writeObject và readObject
import java.io.*;
import java.util.Base64;
public class SecureUser implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // Không serialize trực tiếp
private String email;
public SecureUser(String username, String password, String email) {
this.username = username;
this.password = password;
this.email = email;
}
// Custom serialization
private void writeObject(ObjectOutputStream oos) throws IOException {
// 1. Gọi default serialization cho non-transient fields
oos.defaultWriteObject();
// 2. Encrypt password trước khi ghi
String encrypted = Base64.getEncoder().encodeToString(password.getBytes());
oos.writeObject(encrypted);
}
// Custom deserialization
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
// 1. Gọi default deserialization
ois.defaultReadObject();
// 2. Decrypt password
String encrypted = (String) ois.readObject();
this.password = new String(Base64.getDecoder().decode(encrypted));
}
@Override
public String toString() {
return "SecureUser{username='" + username +
"', password='" + password +
"', email='" + email + "'}";
}
}
Test:
public class CustomSerializationExample {
public static void main(String[] args) throws Exception {
SecureUser user = new SecureUser("alice", "secret123", "[email protected]");
// Serialize
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("secure_user.ser"))) {
oos.writeObject(user);
System.out.println("Serialized: " + user);
}
// Deserialize
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("secure_user.ser"))) {
SecureUser loaded = (SecureUser) ois.readObject();
System.out.println("Deserialized: " + loaded);
}
}
}
Use case: Computed/derived fields
import java.io.*;
public class Rectangle implements Serializable {
private static final long serialVersionUID = 1L;
private double width;
private double height;
private transient double area; // Computed field
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
this.area = width * height;
}
// Custom deserialization: recompute area
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
this.area = width * height; // Recompute!
}
@Override
public String toString() {
return "Rectangle{width=" + width + ", height=" + height + ", area=" + area + "}";
}
}
1. Deserialized object DOESN'T call constructor!
import java.io.*;
public class ConstructorBypass implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
public ConstructorBypass(String name) {
System.out.println("Constructor called: " + name);
this.name = name;
// Validation logic here
if (name == null) {
throw new IllegalArgumentException("Name cannot be null!");
}
}
public static void main(String[] args) throws Exception {
// Serialize
ConstructorBypass obj = new ConstructorBypass("Test");
// Output: Constructor called: Test
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("object.ser"))) {
oos.writeObject(obj);
}
// Deserialize
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("object.ser"))) {
ConstructorBypass loaded = (ConstructorBypass) ois.readObject();
// ❌ NO constructor call! No validation!
System.out.println("Loaded: " + loaded.name);
// Output: Loaded: Test (no "Constructor called" message!)
}
}
}
Danger: Attacker can craft malicious serialized data that bypasses constructor validation!
2. transient static is redundant
public class RedundantTransient implements Serializable {
private static final long serialVersionUID = 1L;
private static int staticField = 100;
private transient static int redundant = 200; // ❌ Redundant!
// static fields are NEVER serialized!
// transient is unnecessary for static
}
3. Serializing inner class → also serializes outer class!
public class OuterClass implements Serializable {
private String outerData = "outer";
public class InnerClass implements Serializable {
private String innerData = "inner";
}
public static void main(String[] args) throws Exception {
OuterClass outer = new OuterClass();
InnerClass inner = outer.new InnerClass();
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("inner.ser"))) {
oos.writeObject(inner);
// ⚠️ Also serializes OuterClass (implicit reference!)
}
// Can cause SecurityException if OuterClass has sensitive data!
}
}
4. Missing serialVersionUID = fragile code
// ❌ NO serialVersionUID declared
public class Fragile implements Serializable {
private String name;
// Auto-generated UID changes with ANY modification!
}
// Add a method → UID changes → deserialization fails!
public class Fragile implements Serializable {
private String name;
public String getName() { return name; } // ← Just added getter
// UID changed → InvalidClassException!
}
5. Serializing parent class without Serializable
class Parent {
protected String parentData = "parent";
public Parent() {
System.out.println("Parent constructor called");
}
}
class Child extends Parent implements Serializable {
private static final long serialVersionUID = 1L;
private String childData = "child";
}
public class InheritanceTest {
public static void main(String[] args) throws Exception {
Child child = new Child();
// Output: Parent constructor called
// Serialize
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("child.ser"))) {
oos.writeObject(child);
// parentData is NOT serialized (Parent not Serializable)
}
// Deserialize
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("child.ser"))) {
Child loaded = (Child) ois.readObject();
// Output: Parent constructor called
// ⚠️ Parent constructor IS called (to initialize non-serialized parent)
System.out.println("Parent data: " + loaded.parentData);
// Output: parent (default value from constructor)
}
}
}
Vấn đề bảo mật với Serialization
Serialization có nhiều vấn đề bảo mật nghiêm trọng:
1. Arbitrary Object Creation
Attacker có thể craft malicious serialized data để:
- Tạo bất kỳ object nào (bypass constructor!)
- Trigger code execution
- DoS attacks
Ví dụ nguy hiểm:
// NGUY HIỂM - KHÔNG làm thế này!
public class DangerousClass implements Serializable {
private String command;
// Attacker có thể set command = "rm -rf /"
private void readObject(ObjectInputStream ois) throws Exception {
ois.defaultReadObject();
Runtime.getRuntime().exec(command); // Execute arbitrary command!
}
}
2. Denial of Service (DoS)
// HashSet/HashMap deserialization có thể gây DoS
// Attacker craft data với hash collisions → O(n²) complexity
3. Information Disclosure
// Sensitive data có thể leak nếu không dùng transient
public class User implements Serializable {
private String username;
private String password; // DANGER! Sẽ được serialize!
private String ssn; // Social Security Number - rất nhạy cảm!
}
Deserialization Vulnerabilities - Java 17+ Serialization Filters
Java deserialization vulnerabilities have caused many critical CVEs:
- Apache Commons Collections exploit (CVE-2015-7501)
- Spring Framework RCE (CVE-2016-1000027)
- Jenkins RCE (CVE-2016-0792)
All exploited deserialization to execute arbitrary code!
Java 9+: Serialization Filters
import java.io.*;
import java.util.List;
public class SerializationFilters {
public static void main(String[] args) throws Exception {
// ===== Java 9+ Filter: Whitelist classes =====
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"java.lang.String;" + // Allow String
"java.util.ArrayList;" + // Allow ArrayList
"com.example.SafeClass;" + // Allow SafeClass
"!*" // Deny all others
);
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("data.ser"))) {
ois.setObjectInputFilter(filter);
Object obj = ois.readObject();
// If obj is not in whitelist → FilterException!
}
// ===== Custom filter with lambda =====
ObjectInputFilter customFilter = filterInfo -> {
Class<?> clazz = filterInfo.serialClass();
if (clazz == null) {
return ObjectInputFilter.Status.UNDECIDED;
}
// Check class name
if (clazz.getName().startsWith("java.lang")) {
return ObjectInputFilter.Status.ALLOWED;
}
// Reject dangerous classes
if (clazz.getName().contains("Runtime") ||
clazz.getName().contains("Process")) {
return ObjectInputFilter.Status.REJECTED;
}
// Check array depth (prevent DoS)
if (filterInfo.depth() > 10) {
return ObjectInputFilter.Status.REJECTED;
}
// Check object size (prevent DoS)
if (filterInfo.streamBytes() > 1_000_000) { // 1MB limit
return ObjectInputFilter.Status.REJECTED;
}
return ObjectInputFilter.Status.UNDECIDED;
};
}
}
Best Practices cho Security:
✅ Không deserialize untrusted data (từ network, user input)
✅ Dùng transient cho sensitive fields
✅ Implement readObject() với validation
✅ Dùng serialization filters (Java 9+)
✅ Set global filter via -Djdk.serialFilter JVM option
✅ Xem xét alternatives: JSON, Protocol Buffers
KHÔNG BAO GIỜ deserialize dữ liệu từ nguồn không tin cậy (untrusted sources)! Có thể dẫn đến Remote Code Execution (RCE).
Nhiều CVEs nghiêm trọng xuất phát từ deserialization vulnerabilities (vd: Apache Commons Collections exploit).
Externalizable Interface - Full Control
Externalizable cho phép full control over serialization process (không như Serializable chỉ có writeObject/readObject).
import java.io.*;
public class CustomExternal implements Externalizable {
private String name;
private int age;
private transient String password; // Transient still works!
// ⚠️ DEFAULT CONSTRUCTOR REQUIRED for Externalizable!
public CustomExternal() {
System.out.println("Default constructor called");
}
public CustomExternal(String name, int age, String password) {
this.name = name;
this.age = age;
this.password = password;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("writeExternal called");
// Must manually write ALL fields
out.writeUTF(name);
out.writeInt(age);
// password is NOT written (transient or manual control)
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
System.out.println("readExternal called");
// Must manually read ALL fields in SAME ORDER
this.name = in.readUTF();
this.age = in.readInt();
// password remains null
}
@Override
public String toString() {
return "CustomExternal{name='" + name + "', age=" + age + ", password='" + password + "'}";
}
public static void main(String[] args) throws Exception {
CustomExternal obj = new CustomExternal("Alice", 25, "secret");
// Serialize
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("external.ser"))) {
oos.writeObject(obj);
// Output: writeExternal called
}
// Deserialize
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("external.ser"))) {
CustomExternal loaded = (CustomExternal) ois.readObject();
// Output: Default constructor called
// readExternal called
System.out.println(loaded);
// Output: CustomExternal{name='Alice', age=25, password='null'}
}
}
}
Key differences:
| Serializable | Externalizable |
|---|---|
| Automatic serialization | Manual control |
| Optional default constructor | Requires default constructor |
| writeObject/readObject (optional) | writeExternal/readExternal (required) |
| SerialVersionUID | Not needed (you control format) |
public class MissingConstructor implements Externalizable {
private String data;
// ❌ NO default constructor!
public MissingConstructor(String data) {
this.data = data;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(data);
}
@Override
public void readExternal(ObjectInput in) throws IOException {
this.data = in.readUTF();
}
public static void main(String[] args) throws Exception {
MissingConstructor obj = new MissingConstructor("test");
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("test.ser"))) {
oos.writeObject(obj); // OK
}
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("test.ser"))) {
MissingConstructor loaded = (MissingConstructor) ois.readObject();
// ❌ InvalidClassException: no valid constructor!
}
}
}
Serialization Proxy Pattern - Effective Java Item 90
Serialization Proxy Pattern là cách an toàn nhất để serialize objects (Effective Java by Joshua Bloch).
import java.io.*;
public final class Period implements Serializable {
private static final long serialVersionUID = 1L;
private final long start;
private final long end;
public Period(long start, long end) {
if (start > end) {
throw new IllegalArgumentException("Start > End");
}
this.start = start;
this.end = end;
}
// ===== Serialization Proxy Pattern =====
// 1. Private static nested class
private static class SerializationProxy implements Serializable {
private static final long serialVersionUID = 1L;
private final long start;
private final long end;
SerializationProxy(Period period) {
this.start = period.start;
this.end = period.end;
}
// Deserialize to Period (not SerializationProxy!)
private Object readResolve() {
return new Period(start, end); // Uses constructor → validation!
}
}
// 2. writeReplace: serialize proxy instead of this
private Object writeReplace() {
return new SerializationProxy(this);
}
// 3. readObject: reject direct deserialization
private void readObject(ObjectInputStream ois) throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
@Override
public String toString() {
return "Period{start=" + start + ", end=" + end + "}";
}
public static void main(String[] args) throws Exception {
Period period = new Period(1000, 2000);
// Serialize
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("period.ser"))) {
oos.writeObject(period);
// Actually serializes SerializationProxy!
}
// Deserialize
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("period.ser"))) {
Period loaded = (Period) ois.readObject();
// Goes through constructor → validation enforced!
System.out.println(loaded);
// Output: Period{start=1000, end=2000}
}
}
}
Benefits:
- ✅ Enforces invariants (goes through constructor)
- ✅ Prevents attacks (cannot bypass validation)
- ✅ Cleaner separation of concerns
- ✅ Easier to maintain
Alternatives: JSON, XML, Protocol Buffers
Do các vấn đề của Java Serialization, nhiều projects chuyển sang alternatives:
Modern Alternatives Comparison
1. JSON (Jackson, Gson)
Ưu điểm:
- Human-readable
- Language-agnostic (dùng được với Python, JavaScript, etc.)
- Ít security issues hơn
- Dễ debug
Ví dụ với Jackson:
import com.fasterxml.jackson.databind.ObjectMapper;
public class JsonExample {
public static void main(String[] args) throws Exception {
Person person = new Person("Alice", 25, "[email protected]");
ObjectMapper mapper = new ObjectMapper();
// Serialize to JSON
String json = mapper.writeValueAsString(person);
System.out.println("JSON: " + json);
// Output: {"name":"Alice","age":25,"email":"[email protected]"}
// Deserialize from JSON
Person loaded = mapper.readValue(json, Person.class);
System.out.println("Loaded: " + loaded);
}
}
2. XML
Ưu điểm:
- Human-readable
- Schema validation (XSD)
- Widely supported
Nhược điểm:
- Verbose (file size lớn)
- Slower than binary formats
3. Protocol Buffers (Google)
Ưu điểm:
- Cực kỳ nhanh và compact
- Strongly typed với schema
- Backward/forward compatibility
- Language-agnostic
Nhược điểm:
- Binary format (không human-readable)
- Cần define .proto schema
So sánh
| Format | Size | Speed | Human-readable | Security | Cross-language |
|---|---|---|---|---|---|
| Java Serialization | Medium | Fast | ❌ | ⚠️ Risky | ❌ |
| JSON | Large | Medium | ✅ | ✅ Safe | ✅ |
| XML | Very Large | Slow | ✅ | ✅ Safe | ✅ |
| Protocol Buffers | Small | Very Fast | ❌ | ✅ Safe | ✅ |
- Dùng JSON cho web APIs, config files, human-readable data
- Dùng Protocol Buffers cho high-performance microservices, gRPC
- Tránh Java Serialization trừ khi absolutely necessary (legacy code)
Bài tập: Generic File Processor
Yêu cầu
Implement một generic file processor để save/load bất kỳ Serializable object nào.
import java.io.*;
public class FileProcessor {
/**
* Save object to file
*/
public static <T extends Serializable> void save(T object, String filename) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(filename))) {
oos.writeObject(object);
}
}
/**
* Load object from file
*/
@SuppressWarnings("unchecked")
public static <T extends Serializable> T load(String filename) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(filename))) {
return (T) ois.readObject();
}
}
/**
* Save list of objects
*/
public static <T extends Serializable> void saveList(
java.util.List<T> list, String filename) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(filename))) {
oos.writeObject(list);
}
}
/**
* Load list of objects
*/
@SuppressWarnings("unchecked")
public static <T extends Serializable> java.util.List<T> loadList(
String filename) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(filename))) {
return (java.util.List<T>) ois.readObject();
}
}
}
Test code
import java.util.ArrayList;
import java.util.List;
public class FileProcessorTest {
public static void main(String[] args) throws Exception {
// Test 1: Save/load single object
Person person = new Person("Alice", 25, "[email protected]");
FileProcessor.save(person, "person.ser");
Person loaded = FileProcessor.load("person.ser");
System.out.println("Loaded person: " + loaded);
// Test 2: Save/load list
List<Person> people = new ArrayList<>();
people.add(new Person("Alice", 25, "[email protected]"));
people.add(new Person("Bob", 30, "[email protected]"));
people.add(new Person("Charlie", 35, "[email protected]"));
FileProcessor.saveList(people, "people.ser");
List<Person> loadedPeople = FileProcessor.loadList("people.ser");
System.out.println("Loaded " + loadedPeople.size() + " people:");
loadedPeople.forEach(System.out::println);
}
}
Mở rộng
-
Thêm compression:
- Wrap với
GZIPOutputStream/GZIPInputStream - So sánh file size: compressed vs uncompressed
- Wrap với
-
Thêm encryption:
- Dùng
CipherOutputStream/CipherInputStream - Encrypt data trước khi lưu file
- Dùng
-
Thêm versioning:
- Lưu version number cùng với object
- Support migration từ version cũ lên mới
Serialization Lifecycle - Mermaid Diagram
Tổng kết
Serialization Basics
- Serializable interface: Marker interface để enable serialization
- ObjectOutputStream: Serialize objects → byte stream
- ObjectInputStream: Deserialize byte stream → objects
serialVersionUID
- Version number của class
- Luôn khai báo rõ ràng để tránh incompatibility issues
- Chỉ thay đổi khi có breaking changes
transient Keyword
- Loại trừ fields khỏi serialization
- Dùng cho: passwords, cache, derived data, sensitive info
- Sau deserialize: default values (null, 0, false)
Custom Serialization
writeObject(ObjectOutputStream): Custom serialization logicreadObject(ObjectInputStream): Custom deserialization logic- Use cases: encryption, validation, computed fields
Security
- NGUY HIỂM: Deserialization của untrusted data → RCE
- Best practices: transient, validation, filters
- Xem xét alternatives: JSON, Protocol Buffers
Alternatives
- JSON: Human-readable, cross-language, safer
- XML: Verbose, schema validation
- Protocol Buffers: Fast, compact, strongly typed
Bài tập thực hành:
-
Implement simple cache system:
- Serialize objects vào file
- Load từ file nếu còn valid (check timestamp)
- Auto-refresh nếu expired
-
Create game save system:
- Serialize game state (player, inventory, level, etc.)
- Support multiple save slots
- Versioning để migrate old saves
-
Compare serialization formats:
- Serialize cùng 1 object với: Java Serialization, JSON, XML
- So sánh: file size, speed, readability
- Kết luận format nào tốt cho use case nào