什么是序列化
Java序列化其實是由Java序列接口提供框架將對象轉(zhuǎn)換成字節(jié)序列,而反序列則是將字節(jié)序列重新轉(zhuǎn)換成對象的過程。
序列化本身其實是一個“持久化”的過程,通常情況下Java的對象都存活在內(nèi)存中,而序列化后可以以字節(jié)序列的形式保存在物理存儲設(shè)備上。
如何實現(xiàn)序列化呢
通常情況下一個類只要實現(xiàn)了Serializable接口,那么它的對象就可以被序列化,生成默認的序列化格式。例如對son 類實現(xiàn)序列化的定義聲明:
public class son implements Serializable
下面是son類,只有一個變量,在默認構(gòu)造參數(shù)中被賦值,并且輸出一句類名到控制臺上:
public class son implements Serializable{
int b;
public son(){
b=2222;
System.out.println("son");
}
}
再測試的主函數(shù)中,我們創(chuàng)建要保存的文件,打開文件輸入/輸出流,然后用對象輸入/輸出流把它包裝起來,接下來就可以通過調(diào)用writeObject/readObject來序列化和反序列化對象:
public static void main(String[] args) throws Exception {
File relative= new File("demo/");
if(!relative.exists()){
relative.mkdirs();
}
File file=new File(relative,"serial.out");
if(!file.exists()){
try {
file.createNewFile();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// son son1=new son();
// ObjectOutputStream oos= new ObjectOutputStream(new FileOutputStream(file));
// oos.writeObject(son1);
// oos.flush();
// oos.close();
// System.out.println("done");
ObjectInputStream ois =new ObjectInputStream(new FileInputStream(file));
son son2=(son)ois.readObject();
System.out.println(son2);
System.out.println(son2.b);
ois.close();
}
上面就是簡單的序列化,但是序列化本身還有一些頭疼的事情。有這樣幾點:
- 序列化其實是在保存對象的狀態(tài),所以static修飾的靜態(tài)的域不會被保存,static修飾的對象是類的狀態(tài),即使在該對象中被修改過,依然不會被序列化保存。
- 在序列化時,不僅會序列化對象本身,還會對該對象引用的其他對象也進行序列化,所以要求這些對象也是可序列對象。
- 如果是一個子類實現(xiàn)了序列化接口,但是它繼承的父類沒有實現(xiàn),那么父類不會被序列化,而是在子類被反序列化時,遞歸調(diào)用父類默認的無參構(gòu)造函數(shù)。所以在子類中修改了父類引用的對象值,反序列后,該對象的值不會是修改后的,而是在父類的無參構(gòu)造函數(shù)中賦予的值,如果沒有賦值,則是初始化為默認值。而如果父類中沒有無參構(gòu)造函數(shù),將會拋出異常:
InvalidClassException - 序列化ID問題。在進行反序列化時,會對字節(jié)流中的serialVersionUID與本地的實體類的serialVersionUID進行比較,如果一致才能反序列化成功,否則會報錯。而serialVersionUID默認情況下是根據(jù)類名、接口名、成員方法及屬性等來生成一個64位的哈希字段,如果不想根據(jù)編譯來劃分版本,就可以顯示定義一個serialVersionUID來兼容以前編譯的版本:
private static final long serialVersionUID = 1L;
針對第三個問題這里我們再看一個父類沒有實現(xiàn)序列化的情況:
下面是parent類:
public class parent{
int a;
public parent(){
System.out.println("parent");
a=111;
}
}
對son類也稍作修改:
public class son extends parent implements Serializable{
int b;
transient int tmp=100;
public son(){
a=222;
b=2222;
System.out.println("son");
}
}
父類很簡單,不實現(xiàn)Serializable接口,有一個變量a,在無參構(gòu)造函數(shù)中賦值,并且輸出類名。
在son類中稍作修改,對父類的變量a賦新值。
在son類中還添加了一個tmp變量,被transient修飾,所有被transient修飾的對象都不會被默認序列化,只能自己定義序列化的細節(jié)。(再以后的文章中討論)
main函數(shù),多添加兩句打印輸出語句:
public class Main {
public static void main(String[] args) throws Exception {
ArrayList<Integer> a=new ArrayList<Integer>();
File relative= new File("demo/");
if(!relative.exists()){
relative.mkdirs();
}
File file=new File(relative,"serial.txt");
if(!file.exists()){
try {
file.createNewFile();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//
// son son1=new son();
// ObjectOutputStream oos= new ObjectOutputStream(new FileOutputStream(file));
// oos.writeObject(son1);
// oos.flush();
// oos.close();
// System.out.println("done");
ObjectInputStream ois =new ObjectInputStream(new FileInputStream(file));
son son2=(son)ois.readObject();
System.out.println(son2); //打印對象名
System.out.println(son2.a); //打印反序列后引用父類變量的值
System.out.println(son2.b);
ois.close();
}
}
測試結(jié)果
我們看在第一次序列化寫入文件的情況下控制臺的輸出:
parent
son
done
在實例化son類的過程中先遞歸調(diào)用了父類無參構(gòu)造函數(shù),然后才是son類中的構(gòu)造函數(shù),在最后序列化成功后輸出done,程序結(jié)束。
下面是反序列化時的輸出:
parent
son@5c647e05
111
2222
可以看到反序列的過程中首先調(diào)用了父類的無參構(gòu)造函數(shù),接著打印了反序列后的對象名(同程序中如果直接序列化然后立馬反序列,可以看到兩個對象哈希值是不同的,這里沒有比較),然后輸出了父類中的變量a,可以看到值為111,不是在son的構(gòu)造函數(shù)中賦值的222,證明對父類范圍內(nèi)的對象做修改沒有用。最后變量b的輸出正確。
最后
可以看到,序列化和繼承之間有一些矛盾,同時序列化將類中私有域也序列化輸出,破壞了類的封閉性。所以設(shè)計類的時候盡量不實現(xiàn)序列化,同時如果該類會被廣泛繼承,而繼承后的子類可能要序列化,那么該類要提供一個無參構(gòu)造函數(shù),并且在該構(gòu)造函數(shù)中給對象賦值。
并且,默認序列化并不能滿足所有場景和需求,后面的文章我們會認識一下定制序列化的方式。