1. 程式人生 > >JAVA之輸入輸出(三)

JAVA之輸入輸出(三)

物件序列化

物件序列化的含義和意義

物件序列化的目標是將物件儲存到磁碟中,或允許在網路中直接傳輸物件。兌現序列化機制允許把記憶體中的JAVA物件轉換成平臺無關的二進位制流,從而把這種二進位制流永久地儲存在磁碟上。通過網路可以將這種二進位制流傳輸到另一個網路節點。其它程式一旦獲得了這種二進位制流,都可以講這種二進位制流回覆成原來的JAVA物件。 序列化機制使得物件可以脫離程式的執行單獨存在。 物件的序列化是指將一個JAVA物件寫入IO流,其反序列化則指從IO流中恢復該物件。 如果想讓某個物件支援序列化機制,則必須讓它的類是可序列化的,也就是說這個類必須實現Serializable或是Externalizable介面中的一個。
其中Serializable介面是一個標記介面,實現該介面無需實現任何抽象方法,它只是表面這個類是可序列化的。 所有可能在網路上傳輸的物件都應該是可序列化的,否則程式將會出現異常。

使用物件流實現序列化

一旦某個類實現了Serializable介面,那麼就可以通過以下兩步實現序列化: 1.建立一個ObjectOutputStream,該輸出流是一個處理流,所以必須建立在其它節點流的基礎上 2.呼叫ObjectOutputStream物件的writeObject()方法輸出可序列化物件 舉個例子
package 物件序列化;

public class Person implements java.io.Serializable{
		private String name;
		private int age;
		public Person(String name,int age)
		{
			this.name=name;
			this.age=age;
		}
		public void setName(String name)
		{
			this.name=name;
		}
		public String getName()
		{
			return this.name;
		}
		public void setAge(int age)
		{
			this.age=age;
		}
		public int getAge()
		{
			return this.age ;
		}
}
package 物件序列化;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class WriteObject {
public static void main(String []args)
{
	try(ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("object.txt")))
	{
		Person p=new Person("孫悟空",500);
		oos.writeObject(p);
	}
	catch(IOException e)
	{
		e.printStackTrace();
	}
	}
}

如果需要從二進位制流中恢復JAVA物件,則需要使用反序列化。反序列化的步驟如下: 1.建立一個ObjectInputStream 2.呼叫ObjectInputStream物件的readObject()方法讀取流中的物件,該方法返回了一個Object型物件,如果知道該物件的型別,可以使用強制型別轉換,將其轉換為真實的型別。
package 物件序列化;

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class ReadObject {
	public static void main(String []args)
	{
		try(ObjectInputStream ois=new ObjectInputStream(new FileInputStream("object.txt")))
		{
			Person p=(Person)ois.readObject();
			System.out.println("名字 "+p.getName()+" 年齡 "+p.getAge());
		}
		catch(Exception e)
		{
			e.printStackTrace();
		}
	}

}
輸出結果為: 反序列化讀取的僅僅是JAVA物件,而不是JAVA類,因此採用反序列化恢復JAVA物件時,必須給出該JAVA物件所屬類的class檔案,否則將引發ClassNotFoundException異常。 如果使用序列化機制向檔案中寫入多個JAVA物件,使用反序列化機制恢復物件時必須按照實際寫入的順序讀取。 當一個可序列化類有多個父類的時候(包括直接和間接父類),這些父類要麼有無參構造器,要麼也是可以序列化的——否則反序列化時會排出InvalidClassException。如果父類只是帶有無參構造器而不是可序列化的,那麼該父類中定義的成員變數值不會序列化到二進位制中。

物件引用的序列化

如果某個類的成員變數的型別不是基本型別或String型別,而是另一個引用型別,那麼這個引用類必須是可序列化的,否則擁有該型別成員變數的類也是不可序列化的。 為了避免當一個物件作為多個類的成員變數在反序列化後被認為是多個不同的物件,JAVA序列化機制採取了一種特殊的演算法: 所有保留在磁盤裡的物件都有一個序列化編號 當程式試圖序列化一個物件時,程式將先檢查該物件是否已經被序列化過,只有未被序列化,才會將其轉化成位元組序列並輸出。 如果某個物件已經被序列化過,程式將至少直接輸出一個序列化編號,而不是重新序列化該物件。 也就是說,當多次序列化同一個JAVA物件時,只有第一次序列化才會把該JAVA物件轉化成位元組序列並輸出,這就引發了一個問題——當程式序列化一個可變物件時,只有第一次使用writeObject()方法輸出時才會把物件轉化為位元組碼序列並輸出,之後即使該可變物件已改變,再呼叫writeObject()方法程式只是輸出序列化編號,並不會再次輸出例項。

自定義序列化

當某個物件進行序列化時,系統會自動把該物件的所有例項變數一次進行序列化,如果某個例項變數引用到另一個物件,則被引用的物件也會被序列化;如果被引用的物件的例項變數也引用了其它物件,那麼依舊會被序列化,這就是所謂的遞迴序列化。 通過在例項變數前面使用transient關鍵字修飾,可以知道Java序列化時無需理會該例項變數。 不過這會導致某些副作用,比如:
package 自定義序列化;


public class Person implements java.io.Serializable{
		private String name;
		private transient int age;
		public Person(String name,int age)
		{
			this.name=name;
			this.age=age;
		}
		public void setName(String name)
		{
			this.name=name;
		}
		public String getName()
		{
			return this.name;
		}
		public void setAge(int age)
		{
			this.age=age;
		}
		public int getAge()
		{
			return this.age ;
		}
}

package 自定義序列化;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class TransientTest {
	public static void main(String []args)
	{
		try
		(ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("transient.txt"));
		ObjectInputStream ois=new ObjectInputStream(new FileInputStream("transient.txt")))
		{
			Person p=new Person("孫悟空",500);
			oos.writeObject(p);
			Person ps=(Person)ois.readObject();
			System.out.println("姓名 "+ps.getName()+" 年齡 "+ps.getAge());
			
		}
		catch(Exception e)
		{
			e.printStackTrace();
		}
	}

}
結果如下: 如圖所示,由於age變數被transient關鍵字修飾,所以實際輸出為0. 使用transient關鍵字修飾例項變數雖然簡單,但被transient修飾的例項變數將被完全隔離在序列化機制之外,這樣導致在反序列化恢復Java物件時無法取得該例項變數的值。

在序列化和反序列化的過程中,可以通過重寫writeObject和readObject方法來實現對序列化機制的完全控制。(P693) 在預設條件下,該writeObject()方法會呼叫out.defaultWriteObject來儲存JAVA物件的非瞬態例項變數。 而readObject()方法會呼叫in.defaultReadObject來恢復JAVA物件的非瞬態例項變數。 當序列化流不完整時(接收方使用的反序列化類的版本不同於傳送方,或序列化流被篡改等),readObjectNoData()方法可以用來正確地初始化反序列化的物件。 還有一種更為徹底的自定義機制,它甚至可以在序列化該物件時將該物件替換成其它物件,如果需要替換,那個要重寫Object writeReplace()throws ObjectStreamException方法。
package 自定義序列化替換;

import java.io.ObjectStreamException;
import java.util.ArrayList;

public class Person implements java.io.Serializable{
	private String name;
	private  int age;
	public Person(String name,int age)
	{
		this.name=name;
		this.age=age;
	}
	public void setName(String name)
	{
		this.name=name;
	}
	public String getName()
	{
		return this.name;
	}
	public void setAge(int age)
	{
		this.age=age;
	}
	public int getAge()
	{
		return this.age ;
	}
	private Object writeReplace()throws ObjectStreamException
	{
		ArrayList<Object> list=new ArrayList<Object>();
		list.add(name);
		list.add(age);
		return list;
	}
}
package 自定義序列化替換;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;

public class ReplaceTest {
	public static void main(String []args)
	{
		try(
			ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("replace.txt"));
			ObjectInputStream ois=new ObjectInputStream(new FileInputStream("replace.txt")))
		{
			Person p=new Person("孫悟空",500);
			oos.writeObject(p);
			ArrayList l=(ArrayList)ois.readObject();
			System.out.println(l);
		}
		catch(Exception e)
		{
			e.printStackTrace();
		}
	}
}
輸出結果為: 可以看到,確實是以List的形式輸出的 在系統序列化某個物件之前,會先呼叫該物件的writeReplace()方法和writeObject()方法。系統總會先呼叫writeReplace()方法,如果該方法返回另一個物件,那麼再呼叫另一個物件的writeReplace()方法。。。直到不返回另一個物件,程式會呼叫該物件的writeObject()方法來儲存該物件的狀態 與writeReplace()方法相對,序列化機制中還有一個特殊的方法,它可以實現保護性複製整個物件。 Object readResolve()throws ObjectStreamException 該方法在序列化單例類、列舉類時特別有用

如果父類包含一個protected或public的readResolve()方法且子類沒有重寫該方法,將會使子類反序列化時得到一個父類的物件,這顯然不正確,且這種錯誤河南發現, 對於final類重寫readResolve()方法不會有任何問題,否則,重寫readResolve()方法時應儘量使用private修飾。

另一種自定義序列化機制

P697 通過繼承Externalizable介面,該介面定義瞭如下兩個方法 void readExteranl(ObjectInput in):需要序列化的類實現readExternal()方法來實現反序列化 void writeExternal(ObjectOutput out):需要序列化的類實現writeExternal()方法來儲存物件的現狀 當使用Externalizable機制反序列化物件時,程式會先使用public的無引數構造器建立例項,然後才執行readExternal()方法進行反序列化,因此實現Externalizable的序列化必須實現public的無參構造器。 雖然實現Externalizable介面可以帶來效能上的提升,但是這樣會導致程式設計複雜度增加,所有大部分都是採用實現Serializable介面的方式來實現序列化。 關於物件序列化,需要注意以下幾點: 物件的類名、例項變數(包括基本型別、陣列、對其它物件的引用)都會被序列化;方法、類變數、transient例項變數(也成為瞬態例項變數)都不會被序列化。 實現Serializable介面的類如果需要讓某個例項變數不被序列化,則可在該例項變數前加transient,而不是static。雖然static也能達到相同效果,但static不是這麼用的。 要保證序列化物件的例項變數也是可序列化的,否則要用transient關鍵字來修飾該例項變數。否則該物件不可序列化。 反序列化物件時必須有序列化物件的class檔案。 當檔案、網路來讀取序列化後的物件時,必須按實際寫入的順序讀寫。 如果修改類時修改了非瞬態的例項變數,則可能導致序列化版本不見人。如果物件流中的物件和新類中包含同名的例項變數,而例項變數型別不同,則反序列化失敗。