Serializable
最近在閱讀JDK源碼中的集合,看到很多集合類實現了Serializable介面,Cloneable介面。在閱讀了很多關於Serializable介面的部落格後,淺談下我對Serializable介面的理解。
序列化
查看 官方文檔 就會發現 Serializable介面中一個成員函數或者成員變數也沒有。那麼這個介面的作用是什麼呢。網上找了一些部落格看過之後,知道這個介面的作用是實現序列化。
序列化:對象的壽命通常隨著生成該對象的程式的終止而終止,有時候需要把在記憶體中的各種對象的狀態(也就是實例變數,不是方法)保存下來,並且可以在需要時再將對象恢復。雖然你可以用你自己的各種各樣的方法來保存對象的狀態,但是Java給你提供一種應該比你自己的好的保存對象狀態的機制,那就是序列化。
總結:Java 序列化技術可以使你將一個對象的狀態寫入一個Byte 流里(系列化),並且可以從其它地方把該Byte 流里的數據讀出來(反序列化)。
系列化的用途
想把的記憶體中的對象狀態保存到一個文件中或者資料庫中時候
想把對象通過網路進行傳播的時候
如何序列化
只要一個類實現Serializable介面,那麼這個類就可以序列化了。
例如有一個 Person類,實現了Serializable介面,那麼這個類就可以被序列化了。
class Person implements Serializable{
private static final long serialVersionUID = 1L; //一會就說這個是做什麼的
String name;
int age;
public Person(String name,int age){
this.name = name;
this.age = age;
}
public String toString(){
return "name:"+name+"\tage:"+age;
}
}
通過ObjectOutputStream 的writeObject()方法把這個類的對象寫到一個地方(文件),再通過ObjectInputStream 的readObject()方法把這個對象讀出來。
File file = new File("file"+File.separator+"out.txt");
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file);
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(fos);
Person person = new Person("tom", 22);
System.out.println(person);
oos.writeObject(person); //寫入對象
oos.flush();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
oos.close();
} catch (IOException e) {
System.out.println("oos關閉失敗:"+e.getMessage());
}
}
} catch (FileNotFoundException e) {
System.out.println("找不到文件:"+e.getMessage());
} finally{
try {
fos.close();
} catch (IOException e) {
System.out.println("fos關閉失敗:"+e.getMessage());
}
}
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(fis);
try {
Person person = (Person)ois.readObject(); //讀出對象
System.out.println(person);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
ois.close();
} catch (IOException e) {
System.out.println("ois關閉失敗:"+e.getMessage());
}
}
} catch (FileNotFoundException e) {
System.out.println("找不到文件:"+e.getMessage());
} finally{
try {
fis.close();
} catch (IOException e) {
System.out.println("fis關閉失敗:"+e.getMessage());
}
}
輸出結果為:
name:tom age:22
name:tom age:22
結果完全一樣。如果我把Person類中的implements Serializable 去掉,Person類就不能序列化了。此時再運行上述程式,就會報java.io.NotSerializableException異常。
serialVersionUID
注意到上面程式中有一個 serialVersionUID ,實現了Serializable介面之後,Eclipse就會提示你增加一個 serialVersionUID,雖然不加的話上述程式依然能夠正常運行。
序列化 ID 在 Eclipse 下提供了兩種生成策略
一個是固定的 1L
一個是隨機生成一個不重複的 long 類型數據(實際上是使用 JDK 工具,根據類名、介面名、成員方法及屬性等來生成)
上面程式中,輸出對象和讀入對象使用的是同一個Person類。
如果是通過網路傳輸的話,如果Person類的serialVersionUID不一致,那麼反序列化就不能正常進行。例如在客戶端A中Person類的serialVersionUID=1L,而在客戶端B中Person類的serialVersionUID=2L 那麼就不能重構這個Person對象。
客戶端A中的Person類:
class Person implements Serializable{
private static final long serialVersionUID = 1L;
String name;
int age;
public Person(String name,int age){
this.name = name;
this.age = age;
}
public String toString(){
return "name:"+name+"\tage:"+age;
}
}
客戶端B中的Person類:
class Person implements Serializable{
private static final long serialVersionUID = 2L;
String name;
int age;
public Person(String name,int age){
this.name = name;
this.age = age;
}
public String toString(){
return "name:"+name+"\tage:"+age;
}
}
試圖重構就會報java.io.InvalidClassException異常,因為這兩個類的版本不一致,local class incompatible,重構就會出現錯誤。
如果沒有特殊需求的話,使用用默認的 1L 就可以,這樣可以確保程式碼一致時反序列化成功。那麼隨機生成的序列化 ID 有什麼作用呢,有些時候,通過改變序列化 ID 可以用來限制某些用戶的使用。
靜態變數序列化
串列化只能保存對象的非靜態成員交量,不能保存任何的成員方法和靜態的成員變數,而且串列化保存的只是變數的值,對於變數的任何修飾符都不能保存。
如果把Person類中的name定義為static類型的話,試圖重構,就不能得到原來的值,只能得到null。說明對靜態成員變數值是不保存的。這其實比較容易理解,序列化保存的是對象的狀態,靜態變數屬於類的狀態,因此 序列化並不保存靜態變數。
transient關鍵字
經常在實現了 Serializable介面的類中能看見transient關鍵字。這個關鍵字並不常見。 transient關鍵字的作用是:阻止實例中那些用此關鍵字聲明的變數持久化;當對象被反序列化時(從源文件讀取位元組序列進行重構),這樣的實例變數值不會被持久化和恢復。
當某些變數不想被序列化,同是又不適合使用static關鍵字聲明,那麼此時就需要用transient關鍵字來聲明該變數。
例如用 transient關鍵字 修飾name變數
class Person implements Serializable{
private static final long serialVersionUID = 1L;
transient String name;
int age;
public Person(String name,int age){
this.name = name;
this.age = age;
}
public String toString(){
return "name:"+name+"\tage:"+age;
}
}
在反序列化視圖重構對象的時候,作用與static變數一樣: 輸出結果為:
name:null age:22
在被反序列化後,transient 變數的值被設為初始值,如 int 型的是 0,對象型的是 null。
註:對於某些類型的屬性,其狀態是瞬時的,這樣的屬性是無法保存其狀態的。例如一個執行緒屬性或需要訪問IO、本地資源、網路資源等的屬性,對於這些欄位,我們必須用transient關鍵字標明,否則編譯器將報措。
序列化中的繼承問題
當一個父類實現序列化,子類自動實現序列化,不需要顯式實現Serializable介面。
一個子類實現了 Serializable 介面,它的父類都沒有實現 Serializable 介面,要想將父類對象也序列化,就需要讓父類也實現Serializable 介面。
第二種情況中:如果父類不實現 Serializable介面的話,就需要有默認的無參的構造函數。這是因為一個 Java 對象的構造必須先有父對象,才有子對象,反序列化也不例外。在反序列化時,為了構造父對象,只能調用父類的無參構造函數作為默認的父對象。因此當我們取父對象的變數值時,它的值是調用父類無參構造函數後的值。在這種情況下,在序列化時根據需要在父類無參構造函數中對變數進行初始化,否則的話,父類變數值都是默認聲明的值,如 int 型的默認是 0,string 型的默認是 null。
例如:
class People{
int num;
public People(){} //默認的無參構造函數,沒有進行初始化
public People(int num){ //有參構造函數
this.num = num;
}
public String toString(){
return "num:"+num;
}
}
class Person extends People implements Serializable{
private static final long serialVersionUID = 1L;
String name;
int age;
public Person(int num,String name,int age){
super(num); //調用父類中的構造函數
this.name = name;
this.age = age;
}
public String toString(){
return super.toString()+"\tname:"+name+"\tage:"+age;
}
}
在一端寫出對象的時候
Person person = new Person(10,"tom", 22); //調用帶參數的構造函數num=10,name = "tim",age =22
System.out.println(person);
oos.writeObject(person); //寫出對象
在另一端讀出對象的時候
Person person = (Person)ois.readObject(); //反序列化,調用父類中的無參構函數。
System.out.println(person);
輸出為
num:0 name:tom age:22
發現由於父類中無參構造函數並沒有對num初始化,所以num使用默認值為0。
總結
序列化給我們提供了一種技術,用於保存對象的變數。以便於傳輸。雖然也可以使用別的一些方法實現同樣的功能,但是java給我們提供的方法使用起來是非常方便的。以上僅僅是我的一些理解,由於本人水平有限,不足之處還請指正。下篇學習Cloneable介面的用途。
原文地址 //blademastercoder.github.io/2015/01/29/java-Serializable.html

