對象序列化機制和作用
對象序列化的目標是將對象保存到磁盤中,或允許在網絡中直接傳輸對象,對象序列化機制允許把內存中的Java對象轉換成平臺無關的二進制流,從而允許吧這種二進制流持久的保存在磁盤上,通過網絡將這種二進制流傳輸到另一個網絡節點,其他程序一旦獲得了這種二進制流,無論是從磁盤中獲取還是從網絡中獲取,都可以將這種二進制流回複稱原來的Java對象
- 序列化機制允許將實現序列化的Java對象轉換成字節序列,這些字節序列可以保存在磁盤上,或通過網絡傳輸,以備以後重新恢復成原來的對象,序列化機制使得對象可以脫離程序的運行而獨立存在
- 對象的序列化(Serialize)是指將一個Java對象寫入IO流中,與此對應的是,對象的反序列化(Deserialize)則指從IO流中恢復該Java對象
- Java9之後增強了對象序列化機制,它允許對讀入的序列化數據進行過濾,這種過濾可在反序列化之前對數據執行校驗,從而提高安全性和健壯性
- 如果要讓某個對象支持序列化機制,則必須讓它的類是可序列化的(serializable)的,要讓這個類是可序列化的,該類必須實現如下兩個接口之一
- Serializable
- Externalizable
- Java的很多類已經實現了Serializable,該接口是一個標記接口,實現該接口無需實現任何方法,它只是表明該類的實例是可序列化的
- 所有可能在網絡上傳輸的對象的類都應該是可序列化的,否則程序將會出現異常,例如RMI(Remote Method Invoke,遠程方法調用) 過程中的參數和返回值,所有需要保存到磁盤裏的對象的類都必須可序列化,例如Web應用中需要保存到HttpSession或ServletContext屬性的Java對象
- 序列化是RMI過程的參數和返回值都必須實現的機制,而所有分佈式應用常常需要跨平臺、跨網絡,所以要求所有傳遞的參數、返回值必須實現序列化,通常情況下程序創建的每個JavaBean類都應該實現Serializable
使用對象流實現序列化
如果需要將某個對象保存到磁盤上或者通過網絡傳輸,則這個類應該實現Serializable接口或者Externalizable接口,而使用Serializable來實現序列化非常簡單,只要讓目標淚實現Serializable接口即可,無需實現任何方法。
當一個類實現了Serializable接口後,其對象就是可序列化的,程序可以通過如下兩個步驟來序列化該對象
- 創建一個ObjectOutputStream,這個輸出流是一個處理流,所以必須建立在其他節點流的基礎之上
// 創建個ObjectOutputStream輸出流
ObjectOutputStream oos = ObjectOutputStream(new FileOutputStream("object.txt"));
- 調用ObjectOutputStream對象的writeObject()方法輸出可序列化對象
//將一個Person對象輸出到輸出流中
oos.writeObject(per);
public class Person
implements java.io.Serializable
{
private String name;
private int age;
// 注意此處沒有提供無參數的構造器!
public Person(String name, int age)
{
System.out.println("有參數的構造器");
this.name = name;
this.age = age;
}
// 省略name與age的setter和getter方法
// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}
}
定義了一個Person類,且實現了Serializable接口,於是Person類可序列化,然後使用ObjectOutputStream將一個Person對象寫入磁盤文件
import java.io.*;
public class WriteObject
{
public static void main(String[] args)
{
try (
// 創建一個ObjectOutputStream輸出流
var oos = new ObjectOutputStream(new FileOutputStream("object.txt")))
{
var per = new Person("孫悟空", 500);
// 將per對象寫入輸出流
oos.writeObject(per);
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
如果要在二進制流中恢復Java對象,則需要使用反序列化,反序列化的實現分爲兩步:
- 創建一個ObjectInputStream輸入流,這個輸入流是一個處理流,所以必須建立在其他節點流的基礎上
// 創建一個ObjectInputStream輸入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
- 調用ObjectInputStream對象的readObject()方法讀取流中的對象,該方法返回一個Object類型的Java對象,如果程序知道該Java對象的類型,則可以將該對象強制類型轉換成其真實的類型
// 從輸入流中讀取一個Java對象,並將其強制類型轉換爲Person類
Person p = (Person)ois.readObject();
import java.io.*;
public class ReadObject
{
public static void main(String[] args)
{
try (
// 創建一個ObjectInputStream輸入流
var ois = new ObjectInputStream(new FileInputStream("object.txt")))
{
// 從輸入流中讀取一個Java對象,並將其強制類型轉換爲Person類
var p = (Person) ois.readObject();
System.out.println("名字爲:" + p.getName()
+ "\n年齡爲:" + p.getAge());
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
反序列化讀取的僅僅是Java對象的數據,而不是Java類,因此採用反序列化恢復Java對象時,必須提供該Java對象所屬類的class文件,否則將會拋ClassNotFoundException異常
- 如果使用序列化機制向文件中寫入了多個Java對象,使用反序列化機制恢復對象時必須按實際寫入的順序讀取
- 當一個可序列化類有多個父類時(包括直接父類和間接父類),這些父類要麼有無參數的構造器,要麼也是可序列化的,否則反序列化時將拋出InvalidClassException,並且如果父類是不可序列化的,只是帶有無參數的構造器,則該父類中定義的成員變量值不會序列化到二進制流中
對象引用的序列化
前面的Person類的兩個成員變量分別是String類型和int類型,如果某個類的成員變量的類型不是基本類型或者String類型,而是另一個引用類型,那麼這個引用類型必須是可序列化的,否則擁有該類型成員變量的類也不不可序列化的
如下代碼所示,Teacher類持有一個Person類的引用,只有Person類是可序列化的,Teacher類纔是可序列化的,如果Person類不可序列化,則無論Teacher類是否實現Serilizable、Externalizable接口,Teacher類都是不可序列化的
public class Teacher
implements java.io.Serializable
{
private String name;
private Person student;
public Teacher(String name, Person student)
{
this.name = name;
this.student = student;
}
// 此處省略了name和student的setter和getter方法
// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
// student的setter和getter方法
public void setStudent(Person student)
{
this.student = student;
}
public Person getStudent()
{
return this.student;
}
}
Person per = new Person("孫子", 500);
Teacher t1 = new Teacher("孫賊", per);
Teacher t2 = new Teacher("孫砸", per);
創建了兩個Teacher對象和一個Person對象,這3個對象在內存中如圖所示:
那麼問題就來了:
- 如果先序列化t1對象,則系統將該t1對象所引用的Person對象一起序列化
- 如果先序列化t2對象,則系統將該t2對象所引用的Person對象一起序列化
- 如果先序列化per對象,則系統將再次序列化該Person對象
如果系統向輸出流中寫入了三個Person對象,那麼當程序從輸入流中反序列化這些對象時,將會得到3個Person對象,這顯然違背了Java序列化機制的初衷
解決問題(Java序列化機制採用了一種特殊的序列化算法):
- 所有保存到磁盤中的對象都有一個序列化編號
- 當程序視圖序列化一個對象時,程序將先檢查該對象是否已經被序列化過,只有該對象從未(在當前JVM中)被序列化過,系統纔會將該對象轉換成字節序列並輸出
- 如果某個對象已經序列化過,程序將只是直接輸出一個序列化編號,而不是再次重新序列化該對象
假設用如下順序的序列化代碼:
oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(per);
則序列化後磁盤文件的存儲示意圖如圖所示
import java.io.*;
public class WriteTeacher
{
public static void main(String[] args)
{
try (
// 創建一個ObjectOutputStream輸出流
var oos = new ObjectOutputStream(new FileOutputStream("teacher.txt")))
{
var per = new Person("孫悟空", 500);
var t1 = new Teacher("唐僧", per);
var t2 = new Teacher("菩提祖師", per);
// 依次將四個對象寫入輸出流
oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(per);
oos.writeObject(t2);
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
import java.io.*;
public class ReadTeacher
{
public static void main(String[] args)
{
try (
// 創建一個ObjectInputStream輸出流
var ois = new ObjectInputStream(new FileInputStream("teacher.txt")))
{
// 依次讀取ObjectInputStream輸入流中的四個對象
var t1 = (Teacher) ois.readObject();
var t2 = (Teacher) ois.readObject();
var p = (Person) ois.readObject();
var t3 = (Teacher) ois.readObject();
// 輸出true
System.out.println("t1的student引用和p是否相同:"
+ (t1.getStudent() == p));
// 輸出true
System.out.println("t2的student引用和p是否相同:"
+ (t2.getStudent() == p));
// 輸出true
System.out.println("t2和t3是否是同一個對象:"
+ (t2 == t3));
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
那麼問題又來了:只有第一次使用writeObject()方法輸出時纔會將該對象轉換成字節序列,之後再調用只是返回序列化編號,那麼如果這個對象是可變對象,當他在被序列化後發生了變化的情況下,就不會再次序列化輸出
import java.io.*;
public class SerializeMutable
{
public static void main(String[] args)
{
try (
// 創建一個ObjectOutputStream輸入流
var oos = new ObjectOutputStream(new FileOutputStream("mutable.txt"));
// 創建一個ObjectInputStream輸入流
var ois = new ObjectInputStream(new FileInputStream("mutable.txt")))
{
var per = new Person("孫悟空", 500);
// 系統會per對象轉換字節序列並輸出
oos.writeObject(per);
// 改變per對象的name實例變量
per.setName("豬八戒");
// 系統只是輸出序列化編號,所以改變後的name不會被序列化
oos.writeObject(per);
var p1 = (Person) ois.readObject(); // ①
var p2 = (Person) ois.readObject(); // ②
// 下面輸出true,即反序列化後p1等於p2
System.out.println(p1 == p2);
// 下面依然看到輸出"孫悟空",即改變後的實例變量沒有被序列化
System.out.println(p2.getName());
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}