Serialization
1. Giới thiệu
"Tuần tự hóa (Serialization) 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; giải tuần tự hóa (deserialization) thực hiện điều ngược lại. Nói cách khác, tuần tự hóa là quá trình chuyển đổi một đối tượng Java thành một luồng tĩnh (chuỗi) các byte, mà chúng ta có thể lưu trữ vào cơ sở dữ liệu hoặc truyền qua mạng."
2. Serialization and Deserialization
Quá trình tuần tự hóa (serialization) không phụ thuộc vào phiên bản (instance-independent); ví dụ, chúng ta có thể tuần tự hóa các đối tượng trên một nền tảng và giải tuần tự hóa chúng trên một nền tảng khác. Các lớp đủ điều kiện để tuần tự hóa cần triển khai một giao diện đánh dấu đặc biệt, Serializable.
Cả ObjectInputStream và ObjectOutputStream đều là các lớp cấp cao mở rộng từ java.io.InputStream và java.io.OutputStream, tương ứng. ObjectOutputStream có thể ghi các kiểu dữ liệu nguyên thủy và đồ thị của các đối tượng vào một OutputStream dưới dạng một luồng byte. Sau đó, chúng ta có thể đọc các luồng này bằng ObjectInputStream.
Phương thức quan trọng nhất trong ObjectOutputStream là:
public final void writeObject(Object o) throws IOException;
Phương thức này nhận một đối tượng có thể tuần tự hóa (serializable) và chuyển đổi nó thành một chuỗi (luồng) các byte.
Tương tự, phương thức quan trọng nhất trong ObjectInputStream là:
public final Object readObject()
throws IOException, ClassNotFoundException;
Phương thức này có thể đọc một luồng byte và chuyển đổi nó trở lại thành một đối tượng Java. Sau đó, đối tượng này có thể được ép kiểu về đối tượng gốc.
Dưới đây là minh họa quá trình tuần tự hóa với một lớp Person.
Lưu ý rằng các trường tĩnh (static fields) thuộc về lớp (thay vì một đối tượng) và sẽ không được tuần tự hóa. Ngoài ra, chúng ta có thể sử dụng từ khóa transient để bỏ qua các trường của lớp trong quá trình tuần tự hóa:
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
static String country = "ITALY";
private int age;
private String name;
transient int height;
// getters and setters
}
Test bên dưới cho thấy một ví dụ minh hoạc về lưu một đối tượng thuộc loại Person vào một local file, và sau đó đọc lại giá trị:
@Test
public void whenSerializingAndDeserializing_ThenObjectIsTheSame() ()
throws IOException, ClassNotFoundException {
Person person = new Person();
person.setAge(20);
person.setName("Joe");
FileOutputStream fileOutputStream = new FileOutputStream("yourfile.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(person);
objectOutputStream.flush();
objectOutputStream.close();
FileInputStream fileInputStream = new FileInputStream("yourfile.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Person p2 = (Person) objectInputStream.readObject();
objectInputStream.close();
assertTrue(p2.getAge() == person.getAge());
assertTrue(p2.getName().equals(person.getName()));
}
Mình đã sử dụng ObjectOutputStream để lưu trạng thái của đối tượng này vào một tệp bằng cách sử dụng FileOutputStream. Tệp “yourfile.txt” được tạo trong thư mục dự án. Tệp này sau đó được tải lên bằng FileInputStream. ObjectInputStream sẽ đọc luồng này và chuyển đổi nó thành một đối tượng mới gọi là p2.
Cuối cùng, ta sẽ kiểm tra trạng thái của đối tượng đã được tải lên và đảm bảo rằng nó khớp với trạng thái của đối tượng ban đầu.
Lưu ý: chúng ta cần phải ép kiểu tường minh đối tượng đã được tải lên thành kiểu Person.
3. Java Serialization Caveats (Lưu ý)
Có một số lưu ý cần quan tâm liên quan đến quá trình tuần tự hóa (serialization) trong Java.
3.1 Inheritance and Composition
Khi một lớp triển khai giao diện java.io.Serializable, tất cả các lớp con của nó cũng sẽ tự động có khả năng tuần tự hóa. Ngược lại, nếu một đối tượng có tham chiếu đến một đối tượng khác, thì các đối tượng này phải triển khai giao diện Serializable riêng biệt, nếu không sẽ xảy ra ngoại lệ NotSerializableException.
public class Person implements Serializable {
private int age;
private String name;
private Address country; // must be serializable too
}
Nếu một trong các trường trong đối tượng có thể tuần tự hóa chứa một mảng các đối tượng, thì tất cả các đối tượng trong mảng đó cũng phải có khả năng tuần tự hóa, nếu không sẽ xảy ra ngoại lệ NotSerializableException.
3.2 Serial Version UID
JVM gán một số phiên bản (long) cho mỗi lớp có khả năng tuần tự hóa. Số này được sử dụng để xác minh rằng các đối tượng được lưu và tải có cùng thuộc tính, đảm bảo tương thích trong quá trình tuần tự hóa.
Hầu hết các IDE có thể tự động tạo số này, dựa trên tên lớp, các thuộc tính và các bộ sửa đổi truy cập liên quan. Bất kỳ thay đổi nào cũng sẽ dẫn đến một số khác, và có thể gây ra ngoại lệ InvalidClassException.
Nếu một lớp có khả năng tuần tự hóa không khai báo serialVersionUID, JVM sẽ tự động tạo số này trong thời gian chạy (runtime). Tuy nhiên, rất khuyến khích rằng mỗi lớp tự khai báo serialVersionUID, vì số được tạo tự động phụ thuộc vào trình biên dịch, do đó có thể dẫn đến ngoại lệ InvalidClassException không mong muốn.
3.3 Custom Serialization in Java
Java cung cấp một cách mặc định để tuần tự hóa các đối tượng, nhưng các lớp Java có thể ghi đè hành vi mặc định này. Tuần tự hóa tùy chỉnh đặc biệt hữu ích khi muốn tuần tự hóa một đối tượng có một số thuộc tính không thể tuần tự hóa. Chúng ta có thể thực hiện điều này bằng cách định nghĩa hai phương thức trong lớp mà chúng ta muốn tuần tự hóa:
private void writeObject(ObjectOutputStream out) throws IOException;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
Với các phương thức này, chúng ta có thể chuyển đổi các thuộc tính không thể tuần tự hóa sang các dạng khác có thể tuần tự hóa được.
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private transient Address address;
private Person person;
// setters and getters
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
oos.writeObject(address.getHouseNumber());
}
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
ois.defaultReadObject();
Integer houseNumber = (Integer) ois.readObject();
Address a = new Address();
a.setHouseNumber(houseNumber);
this.setAddress(a);
}
}
public class Address {
private int houseNumber;
// setters and getters
}
Chúng ta có thể chạy bài kiểm thử đơn vị (unit test) sau đây để kiểm tra quá trình tuần tự hóa tùy chỉnh này:
@Test
public void whenCustomSerializingAndDeserializing_ThenObjectIsTheSame() throws IOException, ClassNotFoundException {
Person p = new Person();
p.setAge(20);
p.setName("Joe");
Address a = new Address();
a.setHouseNumber(1);
Employee e = new Employee();
e.setPerson(p);
e.setAddress(a);
FileOutputStream fileOutputStream = new FileOutputStream("yourfile2.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(e);
objectOutputStream.flush();
objectOutputStream.close();
FileInputStream fileInputStream = new FileInputStream("yourfile2.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Employee e2 = (Employee) objectInputStream.readObject();
objectInputStream.close();
assertTrue(e2.getPerson().getAge() == e.getPerson().getAge());
assertTrue(e2.getAddress().getHouseNumber() == e.getAddress().getHouseNumber());
}
Trong đoạn mã này, chúng ta có thể thấy cách lưu một số thuộc tính không thể tuần tự hóa bằng cách tuần tự hóa Address với cơ chế tuần tự hóa tùy chỉnh. Lưu ý rằng chúng ta phải đánh dấu các thuộc tính không thể tuần tự hóa là transient để tránh ngoại lệ NotSerializableException.
All rights reserved