1. 程式人生 > >Java 物件序列化機制詳解

Java 物件序列化機制詳解

物件序列化的目標:將物件儲存到磁碟中,或允許在網路中直接傳輸物件。

物件序列化機制允許把記憶體中的Java物件轉換成平臺無關的二進位制流,從而允許把這種二進位制流持久的儲存在磁碟上,通過網路將這種二進位制流傳輸到另一個網路節點。其他程式一旦獲得了這種二進位制流,都可以講這種二進位制流恢復成原來的Java物件。

如果需要讓某個物件支援序列化機制,則必須讓它的類是可序列化的,則這個類必須實現如下兩個介面之一:

· Serializable

· Externalizable

Serializable是一個標誌介面,它只是表明該類的例項是可序列化的。

一、 使用物件流實現序列化

一旦某個類實現了Serializable介面,則該類的物件就是可序列化的,程式通過如下步驟建立可序列化物件:

1) 建立一個ObjectOutStream,這個輸出流是一個處理流:

ObjectOutputStream oos = new ObjectOutputStream("object.txt");

2) 呼叫ObjectOutputStream物件的writeObject()方法輸出可序列化物件:
public class Person implements java.io.Serializable
{
   public String name;
 
   public int age;
 
   // 構造方法
 
   // setter和getter方法
}

使用ObjectOutputStream
將一個Person物件寫入磁碟檔案:
public class WriteObject 
{
	public static void main(String[] args) 	throws Exception
	{
		ObjectOutputStream oos = new ObjectOutputStream("object.txt");
		Person per = new Person("沉緣",25);
		oos.writeObject(per);
	}
}

通過ObjectOutputStream,我們將Person物件儲存到了檔案中(硬碟),我們可以看到在當前目錄中已經有了

object.txt檔案。

如果希望從二進位制流中恢復物件,則可以通過反序列化機制,步驟如下:

1) 建立一個ObjectInputStream輸入流,這個輸入流是一個處理流,所以必須建立在其他節點流的基礎上。

 FileInputStream fis = new FileInputStream("object.txt");
 ObjectInputStream ois = new ObjectInputStream(fis);

2) 呼叫ObjectInputStream物件的readObject()方法讀取流中的物件,該方法返回一個Object型別的Java物件,可對該物件進行強制轉換:
Person per= (Person) ois.readObject();
ois.close();

例項:
public class ReadObject
{
	public static void main(String[] args)
	{
		try(
			// 建立一個ObjectInputStream輸入流
			ObjectInputStream ois = new ObjectInputStream(
				new FileInputStream("object.txt")))
		{
			// 從輸入流中讀取一個Java物件,並將其強制型別轉換為Person類
			Person p = (Person)ois.readObject();
			System.out.println("名字為:" + p.getName()
				+ "\n年齡為:" + p.getAge());
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
	}
}

反序列化讀取的僅僅是Java物件的資料,而不是Java類,因此採用反序列化恢復Java物件時,必須提供該物件所屬類的class檔案,否則將會引發ClassNotFoundException異常

反序列化機制無須通過構造器來初始化Java物件。

二、 物件引用的序列化

在物件的巢狀過程中,比如Teacher類中有Person物件,如果希望Teacher物件是可序列化的,則Person物件也必須是可序列化的。

class Teacher implements java.io.Serializable
{
	private String name;
	
	private Person student;

	//構造方法

	//setter、getter方法
}
序列化機制的演算法:

· 所有儲存到磁碟中的物件都有一個序列化編號。

· 當程式試圖序列化一個物件時,程式將先檢查該物件是否已經被序列化過,只有該物件從未被序列化過,系統才會將該物件轉換成位元組序列輸出。

· 如果某個物件已經被序列化過,程式將只是直接輸出一個序列化編號,而不是再次重新序列化該物件。

下面程式序列化兩個Teacher物件,兩個Teacher物件都持有一個引用到同一個Person物件的引用,而且程式兩次呼叫writeObject()方法輸出同一個Teacher物件。

public class WriteTeacher
{
	public static void main(String[] args) throws Exception{
		ObjectOutputStream oos = new ObjectOutputStream("object.txt");
		Person per = new Person("沉緣",25);
		Teacher t1 = new Teacher("無情",<span style="font-family: SimSun;">per</span>);
		Teacher t2 = new Teacher("無緣",per);

		oos.writeObject(t1);
		oos.writeObject(t2);
		oos.writeObject(per);
		oos.writeObject(t2);

		oos.close();
	}
}
上述程式,我們看著是序列化了四個物件,實際上只有三個,而且序列中的兩個Teacher物件的student引用實際上時用一個Person物件。

接下來,我們讀取引用:

public class ReadTeacher
{
	public static void main(String[] args) 
	{
		try(
			// 建立一個ObjectInputStream輸出流
			ObjectInputStream ois = new ObjectInputStream(
				new FileInputStream("teacher.txt")))
		{
			// 依次讀取ObjectInputStream輸入流中的四個物件
			Teacher t1 = (Teacher)ois.readObject();
			Teacher t2 = (Teacher)ois.readObject();
			Person p = (Person)ois.readObject();
			Teacher 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()方法輸出時才會將該物件轉換成位元組序列並輸出,當程式再次呼叫writeObject()方法時,程式只是輸出前面的序列化編號,及時後面對象的Field值已改變,改變的Field值也不會被輸出。
public class SerializeMutable
{
	public static void main(String[] args) 
	{
		
		try(
			// 建立一個ObjectOutputStream輸入流
			ObjectOutputStream oos = new ObjectOutputStream(
				new FileOutputStream("mutable.txt"));
			// 建立一個ObjectInputStream輸入流
			ObjectInputStream ois = new ObjectInputStream(
				new FileInputStream("mutable.txt")))
		{		
			Person per = new Person("孫悟空", 500);
			// 系統會per物件轉換位元組序列並輸出
			oos.writeObject(per);
			// 改變per物件的name Field
			per.setName("豬八戒");
			// 系統只是輸出序列化編號,所以改變後的name不會被序列化
			oos.writeObject(per);
			Person p1 = (Person)ois.readObject();    //①
			Person p2 = (Person)ois.readObject();    //②
			// 下面輸出true,即反序列化後p1等於p2
			System.out.println(p1 == p2);
			// 下面依然看到輸出"孫悟空",即改變後的Field沒有被序列化
			System.out.println(p2.getName());
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
	}
}

三、 自定義序列化

通過在Field(屬性)前使用transient關鍵字,可以指定Java序列化時無須理會該Field。

public class Person
	implements java.io.Serializable
{
	private String name;
	private transient int age;
	// 注意此處沒有提供無引數的構造器!
	public Person(String name , int age)
	{
		System.out.println("有引數的構造器");
		this.name = name;
		this.age = age;
	}
	// 省略name與age的setter和getter方法

}
測試該Person物件:
public class TransientTest
{
	public static void main(String[] args) 
	{
		try(
			// 建立一個ObjectOutputStream輸出流
			ObjectOutputStream oos = new ObjectOutputStream(
				new FileOutputStream("transient.txt"));
			// 建立一個ObjectInputStream輸入流
			ObjectInputStream ois = new ObjectInputStream(
				new FileInputStream("transient.txt")))
		{
			Person per = new Person("孫悟空", 500);
			// 系統會per物件轉換位元組序列並輸出
			oos.writeObject(per);
			Person p = (Person)ois.readObject();
			System.out.println(p.getName());
			System.out.println(p.getAge());
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
	}
}


輸出:

有引數的構造器

孫悟空0

觀察輸出,獲取的age值為0, 說明在序列化時,該age屬性並未被序列化。


四、 Externalizable介面

該介面提供的序列化機制,完全由程式設計師決定儲存和恢復物件資料。Externalizable介面中兩個需實現的方法。

void The object implements the readExternal method to restore its contents by calling the methods of DataInput for primitive types and readObject for objects, strings and arrays.
void The object implements the writeExternal method to save its contents by calling the methods of DataOutput for its primitive values or calling the writeObject method of ObjectOutput for objects, strings, and arrays.

我們舉個例子,看下如何使用該介面來序列化物件。

public class Person
	implements java.io.Externalizable
{
	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;
	}

	public void writeExternal(java.io.ObjectOutput out)
		throws IOException
	{
		// 將name Field的值反轉後寫入二進位制流
		out.writeObject(new StringBuffer(name).reverse());
		out.writeInt(age);
	}
	public void readExternal(java.io.ObjectInput in)
		throws IOException, ClassNotFoundException
	{
		// 將讀取的字串反轉後賦給name Field
		this.name = ((StringBuffer)in.readObject()).reverse().toString();
		this.age = in.readInt();
	}
}

兩種序列化機制對比:


物件序列化需要注意:

1.  物件的類名、Field都會被序列化; 方法、static Field、transient Field都不會被序列化。

2. 實現Serializable介面的類如果需要讓某個Field不被序列化,則可以在該Field前新增transient私事符。

3. 保證序列化物件的Field型別也是可序列化的。

4. 反序列化物件時必須有序列化物件的class檔案。

5. 當通過檔案、網路來讀取序列化後的物件時,必須按實際寫入的順序讀取。

五、 版本

在物件進行序列化或者反序列化操作的時候,要考慮JDK版本問題。如果序列化的JDK版本和反序列化的版本不一致,則可能出現異常。

因此,可以在序列化操作中引入一個serialVersionUID的長了,通過此常量驗證版本的一致性。

import java.io.Serializable ;
public class Person implements Serializable{
	private static final long serialVersionUID = 1L;
	private String name ;	// 宣告name屬性,但是此屬性不被序列化
	private int age ;		// 宣告age屬性
	public Person(String name,int age){	// 通過構造設定內容
		this.name = name ;
		this.age = age ;
	}
	public String toString(){	// 覆寫toString()方法
		return "姓名:" + this.name + ";年齡:" + this.age ;
	}
};