Luồng Đối tượng
(Object Streams)
Trong Java, luồng đối tượng (Object Streams) là một cơ chế mạnh mẽ cho phép chúng ta thực hiện các hoạt động nhập/xuất (I/O) với các đối tượng. Không chỉ hỗ trợ việc đọc và ghi các dữ liệu nguyên thủy như các luồng dữ liệu (Data Streams), luồng đối tượng còn mở rộng khả năng này để làm việc với các đối tượng phức tạp, giúp việc lưu trữ và truyền tải dữ liệu trở nên linh hoạt và hiệu quả hơn.
Các lớp luồng đối tượng bao gồm ObjectInputStream và ObjectOutputStream. Những lớp này triển khai các giao diện ObjectInput và ObjectOutput, đây là các giao diện con của DataInput và DataOutput. Điều này có nghĩa là tất cả các phương thức I/O dữ liệu nguyên thủy được đề cập trong Data Streams cũng được triển khai trong các luồng đối tượng. Vì vậy, một luồng đối tượng có thể chứa hỗn hợp các giá trị nguyên thủy và đối tượng.
Nếu readObject() không trả về kiểu đối tượng mong đợi, việc cố gắng ép kiểu nó thành kiểu chính xác có thể ném ra một ClassNotFoundException. Trong ví dụ đơn giản, điều đó không thể xảy ra, vì vậy chúng ta không cố gắng bắt ngoại lệ. Thay vào đó, chúng ta thông báo cho trình biên dịch rằng chúng ta biết về vấn đề bằng cách thêm ClassNotFoundException vào mệnh đề throws của phương thức main.
Những điểm quan trọng trong luồng đối tượng:
↳ Luồng đối tượng cho phép lưu trữ và đọc các đối tượng Java.
↳ Các lớp ObjectInputStream và ObjectOutputStream được sử dụng để thực hiện quá trình này.
↳ Đối tượng cần được tuần tự hóa phải thực hiện giao diện Serializable.
↳ Luồng đối tượng cũng hỗ trợ các kiểu dữ liệu nguyên thủy.
↳ Cần xử lý ngoại lệ ClassNotFoundException khi đọc đối tượng.
↳ Bây giờ chúng ta sẽ khám phá chi tiết từng lớp trong luồng đối tượng trong Java, bao gồm các lớp chính được sử dụng để thực hiện các hoạt động đầu vào và đầu ra liên quan đến dữ liệu đối tượng.
Lớp ObjectInputStream (Class ObjectInputStream )
Lớp ObjectInputStream kế thừa từ InputStream và triển khai các giao diện ObjectInput, ObjectStreamConstants được sử dụng để đọc các đối tượng đã được tuần tự hóa (serialized) từ một file hoặc một luồng dữ liệu khác. Khi một đối tượng được ghi vào một file bằng ObjectOutputStream, nó được lưu trữ dưới dạng một chuỗi byte. Lớp ObjectInputStream sẽ chuyển đổi lại chuỗi byte này thành đối tượng gốc khi nó được đọc từ file. Nó cho phép bạn khôi phục các đối tượng mà đã được tuần tự hóa trước đó bằng cách sử dụng ObjectOutputStream.
Một số đặc điểm chính của lớp ObjectInputStream:
↳ Đọc đối tượng: ObjectInputStream có thể đọc các đối tượng từ một dòng dữ liệu đã được ghi trước đó bởi ObjectOutputStream. Điều này được thực hiện thông qua phương thức readObject().
↳ Đọc dữ liệu nguyên thủy: Ngoài đọc các đối tượng, lớp ObjectInputStream kế thừa từ InputStream còn có thể đọc dữ liệu nguyên thủy như int, char, float, double, v.v., từ dòng dữ liệu.
↳ Khả năng khôi phục đối tượng: Các đối tượng được khôi phục bởi ObjectInputStream phải triển khai giao diện Serializable. Nếu một đối tượng trong quá trình đọc không triển khai Serializable, một NotSerializableException sẽ được ném ra.
Các điểm cần lưu ý:
↳ Chỉ các đối tượng implements giao diện java.io.Serializable hoặc java.io.Externalizable mới có thể đọc từ luồng.
↳ Các trường được khai báo là transient hoặc static sẽ bị bỏ qua trong quá trình giải tuần tự hóa.
↳ chuỗi (strings) và mảng (arrays) đều là các đối tượng và được xử lý như các đối tượng trong quá trình tuần tự hóa. Khi đọc, chúng cần được ép kiểu (cast) về kiểu mong muốn.
↳ Quá trình đọc một đối tượng tương tự như việc chạy các hàm tạo (constructors) của một đối tượng mới. Bộ nhớ được cấp phát cho đối tượng và khởi tạo về giá trị mặc định (NULL)
Tuần tự hóa (Serialization)
Khi một lớp triển khai giao diện Serializable, nó cho phép lưu trữ và khôi phục toàn bộ trạng thái của một đối tượng. Điều này rất quan trọng trong các trường hợp cần lưu đối tượng vào bộ nhớ hoặc truyền qua lại giữa các hệ thống. Serialization tự động duyệt qua các tham chiếu đối tượng, đảm bảo rằng toàn bộ đồ thị đối tượng được lưu và khôi phục chính xác.
Xử lý Serialization Tùy chỉnh
Trong một số trường hợp, các lớp có thể yêu cầu xử lý đặc biệt trong quá trình serialization và deserialization. Để thực hiện điều này, lớp nên triển khai các phương thức sau:
Ví dụ
private void writeObject(java.io.ObjectOutputStream stream)
throws IOException;
writeObject: Phương thức này tùy chỉnh quá trình serialization, cho phép ghi các trường hoặc dữ liệu cụ thể vào luồng.
Ví dụ
private void writeObject(java.io.ObjectOutputStream stream)
throws IOException;
readObject: Phương thức này đọc và khôi phục trạng thái của đối tượng bằng cách sử dụng dữ liệu được ghi bởi writeObject. Nó tập trung vào các trường của lớp mà nó được định nghĩa và không cần xử lý trạng thái của các lớp cha hoặc con.
Ví dụ
private void writeObject(java.io.ObjectOutputStream stream)
throws IOException;
readObjectNoData: Phương thức này được gọi khi luồng serialization không liệt kê lớp đó là lớp cha của đối tượng đang được deserialization. Điều này có thể xảy ra nếu lớp của đối tượng đã deserialization khác với lớp của bên gửi, hoặc nếu luồng đã bị giả mạo. Nó đảm bảo rằng đối tượng được khởi tạo đúng cách ngay cả trong những tình huống như vậy.
Khai báo lớp ObjectInputStream trong Java
Để sử dụng lớp ObjectInputStream, bạn cần import gói java.io bạn cần thêm câu lệnh import vào đầu file Java của mình. Gói này cung cấp các lớp và giao diện để thực hiện các hoạt động nhập xuất (I/O) trong Java.
Cú pháp câu lệnh import:
Cú pháp
import java.io.ObjectInputStream;
Cú pháp khai báo lớp ObjectInputStream:
Cú pháp
public class ObjectInputStream
extends InputStream
implements ObjectInput, ObjectStreamConstants
Dưới đây là giải thích chi tiết về cú pháp khai báo này:
↳ public class ObjectInputStream: Đây là khai báo một lớp công khai (public) tên là ObjectInputStream. Lớp này có thể được truy cập từ bất kỳ đâu trong dự án Java, miễn là nó được import hoặc ở cùng gói.
↳ extends InputStream: Lớp ObjectInputStream kế thừa (extends) từ lớp InputStream. Điều này có nghĩa là ObjectInputStream là một loại InputStream, vì vậy nó kế thừa tất cả các phương thức và thuộc tính của lớp InputStream. InputStream là lớp cơ sở cho tất cả các luồng đầu vào byte trong Java.
↳ implements ObjectInput: Lớp ObjectInputStream triển khai (implements) giao diện ObjectInput. ObjectInput là một giao diện trong Java, định nghĩa các phương thức liên quan đến việc đọc dữ liệu dưới dạng các đối tượng. Bằng cách triển khai giao diện này, ObjectInputStream phải cung cấp các triển khai cụ thể cho các phương thức được định nghĩa trong ObjectInput.
↳ implements ObjectStreamConstants: ObjectInputStream cũng triển khai (implements) giao diện ObjectStreamConstants. ObjectStreamConstants là một giao diện chứa các hằng số (constants) liên quan đến luồng đối tượng (object stream), chẳng hạn như các mã phiên bản (protocol version) hoặc các định nghĩa mã đặc biệt được sử dụng trong tuần tự hóa đối tượng.
Lớp lồng nhau bên trong lớp ObjectInputStream
Lớp ObjectInputStream có một lớp lồng bên trong duy nhất là:
↳ static class ObjectInputStream.GetField: Đây là một lớp lồng tĩnh (static) bên trong ObjectInputStream. Cung cấp quyền truy cập vào các trường dữ liệu bền vững (persistent fields) được đọc từ luồng đầu vào.
Các Constructor của lớp ObjectInputStream
Lớp ObjectInputStream có hai constructor:
↳ Constructor protected ObjectInputStream(): Là một constructor bảo vệ, được sử dụng nội bộ trong lớp ObjectInputStream để tạo các lớp con mà không cần phân bổ dữ liệu riêng. Không dùng trực tiếp cho các mục đích thông thường.
↳ Constructor ObjectInputStream(InputStream in): Tạo một đối tượng ObjectInputStream để đọc dữ liệu từ luồng đầu vào InputStream được chỉ định. Đây là constructor chính được sử dụng để tạo đối tượng ObjectInputStream và bắt đầu đọc dữ liệu từ luồng.
Các phương thức của lớp ObjectInputStream
Lớp ObjectInputStream cung cấp các phương thức để đọc dữ liệu từ luồng, bao gồm cả dữ liệu nguyên thủy và đối tượng. Dưới đây là danh sách tất cả các phương thức của lớp ObjectInputStream trong Java:
↳ int available(): Trả về số byte có thể đọc được mà không bị chặn.
↳ int read(): Đọc một byte dữ liệu.
↳ int read(byte[] buf, int off, int len): Đọc một mảng byte vào một mảng khác.
↳ boolean readBoolean(): Đọc một giá trị boolean.
↳ byte readByte(): Đọc một byte 8 bit.
↳ char readChar(): Đọc một ký tự 16 bit.
↳ double readDouble(): Đọc một số thực 64 bit.
↳ float readFloat(): Đọc một số thực 32 bit.
↳ void readFully(byte[] buf): Đọc đầy đủ mảng byte, chặn cho đến khi đọc xong.
↳ int readInt(): Đọc một số nguyên 32 bit từ luồng.
↳ long readLong(): Đọc một số nguyên 64 bit từ luồng.
↳ short readShort(): Đọc một số nguyên 16 bit từ luồng.
↳ int readUnsignedByte(): Đọc một byte không dấu (0-255) từ luồng.
↳ int readUnsignedShort(): Đọc một số nguyên không dấu 16 bit từ luồng.
↳ Object readObject(): Đọc một đối tượng từ luồng.
↳ ObjectInputStream.GetField readFields(): Đọc các trường dữ liệu liên quan đến đối tượng từ luồng.
↳ String readUTF(): Đọc một chuỗi được mã hóa theo định dạng UTF-8 từ luồng.
↳ Object readUnshared(): Đọc một đối tượng "không chia sẻ" từ luồng.
↳ void close(): Đóng luồng đầu vào.
↳ protected boolean enableResolveObject(boolean enable): Cho phép thay thế các đối tượng được đọc từ luồng (chủ yếu dùng cho mục đích nội bộ).
↳ protected ObjectStreamClass readClassDescriptor(): Đọc mô tả lớp từ luồng tuần tự hóa (chủ yếu dùng cho mục đích nội bộ).
↳ void readFully(byte[] buf, int off, int len): Đọc một số lượng byte cụ thể vào một mảng byte, bắt buộc phải đọc đủ số lượng byte.
↳ String readLine(): Phương thức này đã bị lỗi thời, không nên sử dụng vì không chuyển đổi byte sang ký tự đúng cách. Sử dụng DataInputStream thay thế.
↳ protected ObjectStreamClass readClassDescriptor(): Đọc mô tả lớp từ luồng tuần tự hóa (chủ yếu dùng cho mục đích nội bộ).
↳ protected Object readObjectOverride(): Phương thức cho phép các lớp con thay thế đối tượng trong quá trình giải tuần tự hóa (chủ yếu dùng cho mục đích nội bộ).
↳ protected Class<?> resolveClass(ObjectStreamClass desc): Tải lớp tương đương cục bộ từ mô tả lớp của luồng (chủ yếu dùng cho mục đích nội bộ).
↳ protected Class<?> resolveProxyClass(String[] interfaces): Trả về một lớp proxy thực hiện các giao diện được chỉ định trong mô tả lớp proxy (chủ yếu dùng cho mục đích nội bộ).
↳ int skipBytes(int len): Bỏ qua một số lượng byte trong luồng.
↳ void registerValidation(ObjectInputValidation obj, int prio): Đăng ký một đối tượng để xác thực trước khi trả về đồ thị đối tượng (chủ yếu dùng cho mục đích nội bộ).
Ví dụ
Dưới đây là một vài ví dụ về cách sử dụng ObjectInputStream trong Java để đọc đối tượng từ một file hoặc một nguồn đầu vào:
Ví dụ 1: Đọc một đối tượng từ một file.
Giả sử bạn có một tệp tin tên là objectdata.ser. File objectdata.ser là một file nhị phân chứa đối tượng MyClass đã được tuần tự hóa. Nội dung của file này sẽ không thể đọc được trực tiếp bằng các chương trình soạn thảo văn bản thông thường vì nó ở định dạng nhị phân. Khi bạn mở file này trong một trình soạn thảo văn bản, bạn có thể thấy các ký tự không thể đọc được. Nội dung của file objectdata.ser:
Tệp tin: objectdata.ser
���sr�MyClass1��k?�ti�L�messaget�Ljava/lang/String;xpt�
Hello World
Để đọc dữ liệu từ file tuần tự hóa objectdata.ser và khôi phục đối tượng MyClass, bạn cần sử dụng lớp ObjectInputStream trong Java. Dưới đây là một ví dụ về cách làm điều này:
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) {
try (FileInputStream fileInputStream = new FileInputStream("objectdata.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
// Đọc đối tượng từ file
MyClass myObject = (MyClass) objectInputStream.readObject();
// Hiển thị thông tin đối tượng
System.out.println("Đối tượng đã được hủy tuần tự hóa.");
System.out.println("Message: " + myObject.getMessage());
} catch (IOException e) {
System.err.println("IOException đã xảy ra: " + e.getMessage());
} catch (ClassNotFoundException e) {
System.err.println("ClassNotFoundException đã xảy ra: " + e.getMessage());
}
}
}
class MyClass implements Serializable {
private static final long serialVersionUID = 3602211659455034473L;
private String message;
public MyClass(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
// Các phương thức khác nếu cần
}
Kết quả của chương trình là:
Message: Hello World
Trong ví dụ này, đối tượng MyClass đã được lưu trữ trước đó trong file objectdata.ser và bây giờ được đọc lại sử dụng ObjectInputStream. Lớp MyClass phải triển khai giao diện Serializable để có thể được tuần tự hóa và giải tuần tự hóa.
Chú ý
↳ Serializable Interface: Đảm bảo lớp MyClass implements Serializable và có trường serialVersionUID để tương thích với phiên bản của đối tượng.
↳ File Path: Đảm bảo đường dẫn đến file objectdata.ser là chính xác. Nếu không ở cùng thư mục, bạn cần chỉ định đường dẫn đầy đủ.
Nếu xảy ra ngoại lê
Lỗi IOException: MyClass; local class incompatible: stream classdesc serialVersionUID = 3602211659455034473, local class serialVersionUID = 1 xảy ra khi có sự không tương thích giữa phiên bản của lớp trong mã nguồn của bạn và phiên bản lớp được lưu trữ trong file tuần tự hóa. Trong trường hợp của bạn, thông báo lỗi cho biết rằng serialVersionUID của lớp trong file tuần tự hóa là 3602211659455034473, trong khi serialVersionUID của lớp trong mã nguồn của bạn là 1. Điều này cho thấy lớp đã được thay đổi hoặc có sự khác biệt giữa hai phiên bản.
Giải pháp
Đảm Bảo serialVersionUID Khớp: Đảm bảo rằng serialVersionUID của lớp trong mã nguồn của bạn khớp với serialVersionUID được lưu trong file tuần tự hóa.
Giải pháp
// Bạn cần phải thay đôi serialVersionUID = 1L thành serialVersionUID = 3602211659455034473L
private static final long serialVersionUID = 1L;
-> private static final long serialVersionUID = 3602211659455034473L;
Khi bạn đã cập nhật serialVersionUID, thử lại việc đọc đối tượng từ file objectdata.ser. Nếu lớp không thay đổi hoặc không cần thay đổi, vấn đề tương thích sẽ được giải quyết.
Hy vọng điều này giúp bạn giải quyết vấn đề với tuần tự hóa đối tượng của bạn!
Ví dụ 2: Đọc một danh sách đối tượng từ file
Giả sử bạn có một tệp tin tên là people.ser. File people.ser là một file chứa danh sách đối tượng nhị phân chứa đối tượng MyClass đã được tuần tự hóa. Nội dung của file này sẽ không thể đọc được trực tiếp bằng các chương trình soạn thảo văn bản thông thường vì nó ở định dạng nhị phân. Khi bạn mở file này trong một trình soạn thảo văn bản, bạn có thể thấy các ký tự không thể đọc được. Nội dung của file people.ser:
Tệp tin: people.ser
���sr�java.util.ArrayListx����a� �I�
sizexp���w ���
sr�Person��������I� ageL�
namet�Ljava/lang/String;xp��� t�Duongsq�~����t�Vuongx
Để đọc dữ liệu từ file tuần tự hóa people.ser và khôi phục đối tượng Person, bạn cần sử dụng lớp ObjectInputStream trong Java. Dưới đây là một ví dụ về cách làm điều này:
Ví dụ: Example.java
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.List;
class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
public class Example{
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("people.ser");
ObjectInputStream ois = new ObjectInputStream(fis)) {
@SuppressWarnings("unchecked")
List<Person> people = (List<Person>) ois.readObject();
System.out.println("Danh sách đối tượng đã được đọc từ file:");
for (Person person : people) {
System.out.println(person);
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Kết quả của chương trình là:
Person{name='Vuong', age=25}
Lưu ý: Đảm bảo rằng lớp Person được định nghĩa trong cùng một package và có cùng serialVersionUID khi đọc và ghi dữ liệu để tránh lỗi không tương thích.
Hy vọng ví dụ này giúp bạn hiểu cách đọc và lưu danh sách đối tượng trong Java!