JAVA之輸入輸出(三)
阿新 • • 發佈:2019-01-06
物件序列化
物件序列化的含義和意義
物件序列化的目標是將物件儲存到磁碟中,或允許在網路中直接傳輸物件。兌現序列化機制允許把記憶體中的JAVA物件轉換成平臺無關的二進位制流,從而把這種二進位制流永久地儲存在磁碟上。通過網路可以將這種二進位制流傳輸到另一個網路節點。其它程式一旦獲得了這種二進位制流,都可以講這種二進位制流回覆成原來的JAVA物件。 序列化機制使得物件可以脫離程式的執行單獨存在。 物件的序列化是指將一個JAVA物件寫入IO流,其反序列化則指從IO流中恢復該物件。 如果想讓某個物件支援序列化機制,則必須讓它的類是可序列化的,也就是說這個類必須實現Serializable或是Externalizable介面中的一個。使用物件流實現序列化
一旦某個類實現了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修飾。