Như đã nói ở trên, khi lưu một đối tượng, toàn bộ các đối tượng trong đồ thị tham chiếu của nó cũng được lưu. Do đó, tất cả các lớp đó đều phải thuộc loại Serializable. Như trong ví dụ Hình 12.2 thì các lớp ContactList, Contact, PhoneNumber, String đều phải thuộc loại chuỗi hóa được nếu không muốn xảy ra ngoại lệ NonSerializableException khi chương trình chạy.
Ta đi đến tình huống khi trong một đối tượng cần lưu lại có một biến thực thể là tham chiếu tới đối tượng thuộc lớp không chuỗi hóa được. Và ta không thể sửa cài đặt lớp để cho nó chuỗi hóa được, chẳng hạn khi lớp đó do người khác viết. Giải pháp là khai báo biến thực thể đó với từ khóa transient. Từ khóa này có tác dụng tuyên bố rằng "hãy bỏ qua biến này khi chuỗi hóa".
Bên cạnh tình huống biến thực thể thuộc loại không thể chuỗi hóa, ta còn cần đến khai báo transient trong những trường hợp khác. Chẳng hạn như khi người thiết kế lớp đó quên không cho lớp đó khả năng chuỗi hóa. Hoặc vì đối tượng đó phụ thuộc vào thông tin đặc thù cho từng lần chạy chương trình mà thông tin đó không thể lưu được. Ví dụ về dạng đối tượng đó là các đối tượng luồng (thread), kết nối mạng, hoặc file trong thư viện Java. Chúng thay đổi tùy theo từng lần chạy của chương
trình, từng platform cụ thể, từng máy ảo Java cụ thể. Một khi chương trình tắt, không có cách gì khôi phục chúng một cách hữu ích, chúng phải được tạo lại từ đầu mỗi lần cần dùng đến.
12.3. KHÔI PHỤC ĐỐI TƯỢNG
Có thể bạn quan tâm!
- Java - ĐH Công Nghệ - 23
- Java - ĐH Công Nghệ - 24
- Java - ĐH Công Nghệ - 25
- Java - ĐH Công Nghệ - 27
- Java - ĐH Công Nghệ - 28
- Java - ĐH Công Nghệ - 29
Xem toàn bộ 251 trang tài liệu này.
Mục đích của việc chuỗi hóa một đối tượng là để ta có thể khôi phục nó về trạng thái cũ vào một thời điểm khác, tại một lần chạy khác của máy ảo Java (thậm chí tại máy ảo khác). Việc khôi phục đối tượng (deserialization) gần như là quá trình ngược lại của chuỗi hóa.
Bước 1 tạo một dòng vào dạng file, FileInputStream, đối tượng dòng vào này kết nối với file có tên 'game.dat', nếu không tìm thấy file với tên đó thì ta sẽ nhận được một ngoại lệ. Bước 2 tạo một đối tượng dòng vào dạng đối tượng, ObjectInputStream. Nó cho phép đọc đối tượng nhưng nó lại không thể kết nối trực tiếp với một file. Nó cần được nối với một đối tượng kết nối, ở đây là FileInputStream, để có thể ghi ra file. Bước 3, mỗi lần gọi readObject(), ta sẽ lấy được đối tượng tiếp theo từ trong dòng ObjectInputStream. Do đó, ta sẽ đọc các đối tượng theo đúng thứ tự mà chúng đã được ghi. Ta sẽ nhận được ngoại lệ nếu cố đọc nhiều hơn số đối tượng đã được ghi vào file. Bước 4, giá trị trả về của readObject() là tham chiếu kiểu Object, do đó ta cần ép kiểu cho nó trở lại kiểu thực sự của đối tượng mà ta biết. Bước 4 đóng ObjectInputStream. Khi đóng một dòng vào, các dòng mà nó nối tới, ở đây là FileInputStream, sẽ được đóng tự động. Việc đọc dữ liệu đến đây kết thúc.
Quá trình khôi phục đối tượng diễn ra theo các bước như sau:
1. Đối tượng được đọc từ dòng vào dưới dạng một chuỗi byte.
2. Máy ảo Java xác định xem đối tượng thuộc lớp gì, qua thông tin lưu trữ tại đối tượng được chuỗi hóa.
3. Máy ảo tìm và nạp lớp đó. Nếu không tìm thấy hoặc không nạp được, máy ảo sẽ ném một ngoại lệ và quá trình khôi phục thất bại.
4. Một đối tượng mới được cấp phát bộ nhớ tại heap, nhưng hàm khởi tạo của đối tượng đó không chạy. Nếu chạy thì nó sẽ khởi tạo về trạng thái ban đầu như kết quả của lệnh new. Ta muốn đối tượng được khôi phục về trạng thái khi nó được chuỗi hóa, chứ không phải trạng thái khi nó mới được sinh ra.
5. Nếu đối tượng có một lớp tổ tiên thuộc loại không chuỗi hóa được, hàm khởi tạo cho lớp đó sẽ được chạy cùng với các hàm khởi tạo của các lớp bên trên nó trên cây phả hệ.
6. Các biến thực thể của đối tượng được gán giá trị từ trạng thái đã được chuỗi hóa. Các biến transient được gán giá trị mặc định: null cho tham chiếu và 0/false/… cho kiểu cơ bản.
Hình 12.3: Ghi đối tượng vào file và đọc từ file.
Hình 12.4: Cài đặt các lớp chuỗi hóa được.
Tổng kết lại, ta cài đặt hoàn chỉnh ví dụ ghi và đọc các đối tượng nhân vật trò chơi trong Hình 12.3. Phiên bản cài đặt tối thiểu của GameCharacter và các lớp cần thiết được cho trong Hình 12.4. Lưu ý rằng đó chỉ là nội dung cơ bản phục vụ mục đích thử nghiệm đọc và ghi đối tượng chứ không phải dành cho một chương trình trò chơi thực sự.
12.4. GHI CHUỖI KÍ TỰ RA TỆP VĂN BẢN
Sử dụng cơ chế chuỗi hóa cho việc lưu trữ đối tượng là cách dễ dàng nhất để lưu trữ và khôi phục dữ liệu giữa các lần chạy của một chương trình Java. Nhưng đôi khi, ta cũng cần lưu dữ liệu vào một file văn bản, chẳng hạn khi file đó để cho một chương trình khác (có thể không viết bằng Java) đọc.
Việc ghi một chuỗi kí tự ra file văn bản tương tự với việc ghi một đối tượng, chỉ khác ở chỗ ta ghi một đối tượng String thay vì một đối tượng chung chung, và ta dùng các dòng khác thay cho FileOutputStream và ObjectOutputStream.
Hình 12.5: Ghi file văn bản.
Hình 12.5 là ví dụ cơ bản nhất minh họa việc ghi file văn bản. Java cho ta nhiều cách để tinh chỉnh chuỗi các dòng ra dùng cho việc ghi file.
12.4.1. Lớp File
Đối tượng thuộc lớp java.io.File đại diện cho một file hoặc một thư mục. Lớp này không có các tiện ích ghi đọc file, nhưng nó là đại diện an toàn cho file hơn là chuỗi kí tự tên file. Hầu hết các lớp lấy tên file làm tham số cho hàm khởi tạo, chẳng hạn FileWriter hay FileInputStream, cũng cung cấp hàm khởi tạo lấy một đối tượng File. Ta có thể tạo một đối tượng File, kiểm tra xem đường dẫn có hợp lệ hay không, v.v.. rồi chuyển đối tượng File đó cho FileWriter hay FileInputStream.
Với một đối tượng File, ta có thể làm một số việc hữu ích như:
1. Tạo một đối tượng File đại diện cho một file đang tồn tại:
File f = new File("foo.txt");
2. Tạo một thư mục mới:
File dir = new File("Books"); dir.mkdir();
3. Liệt kê nội dung của một thư mục:
if (dir.isDirectory()) {
String[] dirContents = dir.list(); for (int i = 0; i < dirContents; i++)
System.out.println(dirContents[i]);
}
4. Lấy đường dẫn tuyệt đối của file hoặc thư mục:
System.out.println(dir.getAbsolutePath());
5. Xóa file hoặc thư mục (trả về true nếu thành công):
boolean isDeleted = f.delete();
12.4.2. Bộ nhớ đệm
Bộ nhớ đệm (buffer) cho ta một nơi lưu trữ tạm thời để tăng hiệu quả của thao tác đọc/ghi dữ liệu. Cách sử dụng BufferWriter như sau:
BufferWriter writer = new BufferWriter(new FileWriter(aFile);
Sau lệnh trên thì ta chỉ cần làm việc với BufferWriter mà không cần để ý đến đối tượng FileWriter vừa tạo nữa.
Lợi ích của việc sử dụng BufferWriter được giải thích như sau: Nếu chỉ dùng FileWriter, mỗi lần ta yêu cầu FileWriter ghi một chuỗi dữ liệu nào đó, chuỗi đó lập tức được đổ vào file. Chi phí về thời gian xử lý cho mỗi lần ghi file là rất lớn so với chi phí cho các thao tác trong bộ nhớ. Khi nối một dòng BufferWriter với một FileWriter, BufferWriter sẽ giữ những gì ta ghi vào nó cho đến khi đầy. Chỉ khi bộ nhớ đệm BufferWriter đầy thì FileWriter mới được lệnh ghi dữ liệu ra đĩa. Như vậy, ta tăng được hiệu quả về mặt thời gian của việc ghi dữ liệu do giảm số lần ghi đĩa cứng. Nếu ta muốn đổ dữ liệu ra đĩa trước khi bộ nhớ đệm đầy, ta có thể gọi writer.flush() để lập tức xả toàn bộ nội dung trong bộ nhớ đệm.
12.5. ĐỌC TỆP VĂN BẢN
Đọc từ file văn bản là công việc có quy trình tương tự ghi file, chỉ khác là giờ ta dùng một đối tượng FileReader để trực tiếp thực hiện công việc đọc file và một đối tượng BufferReader nối với nó để tăng hiệu quả đọc.
Hình 12.6 là ví dụ đơn giản về việc đọc một file văn bản. Trong đó, một đối tượng FileReader – một dòng kết nối cho dạng kí tự – được nối với một file để đọc trực tiếp. Tiếp theo là một đối tượng BufferReader được nối với FileReader để tăng hiệu quả đọc. Vòng while lặp đi lặp lại việc đọc một dòng từ BufferReader cho đến khi dòng đọc được là rỗng (tham chiếu null), đó là khi không còn gì để đọc nữa - đã chạm đến cuối file.
Hình 12.6: Đọc file văn bản.
Như vậy với cách đọc này, ta đọc được dữ liệu dưới dạng các dòng văn bản. Để tách các giá trị dữ liệu tại mỗi dòng, ta cần xử lý chuỗi theo định dạng mà dữ liệu gốc đã được ghi. Chẳng hạn, nếu dữ liệu là các chuỗi kí tự cách nhau bởi dấu phảy thì ta sẽ phải tìm vị trí của các dấu phảy để tách các giá trị dữ liệu ra. Phương thức split của lớp String cho phép ta làm điều này. Ví dụ sử dụng phương thức split được cho trong Hình 12.7. Có thể tra cứu chi tiết về phương thức này tại tài liệu Java API.