Java序列化
Java允許我們在內存中創建可複用的Java對象,但一般情況下,只有當JVM處於運行時,這些對象纔可能存在;也即,這些對象的生命週期不會比JVM的生命週期更長。但在實際應用中,就可能要求在JVM停止運行之後能夠持久化指定的對象,並在將來重新讀取被保存的對象。
使用Java對象序列化,在保存對象時,會把其狀態(只是對象狀態,不包括類變量)保存爲一組字節,以便再將這些字節組裝成對象。
除了在持久化對象時會用到對象序列化之外,當使用RMI,或在網絡中傳遞對象時,都會用到對象序列化。
1、 Serializable接口
在Java中,只要一個類實現了Java.io.Serializable接口,那麼它就可以被序列化,如下
/*枚舉默認繼承java.lang.Enum,而該類實現了Serializable接口
* */
public enum Gender
{
FEMALE,
MALE,
OTHER
}
public class Person implements Serializable
{
private static final long serialVersionUID = 1L;
private int id;
private Stringname;
private int age;
private Gendergender;
private Stringinfo;
public Person()
{
}
/*省略setter和getter方法
* */
}
爲什麼一個類實現了Serializable接口,就可以被序列化呢?其實底層採用的是ObjectOutputStream類來進行序列化的,其中ObjectOutputStream類中最重要的一個方法爲writeObject0,部分代碼人如下:
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
// …
// remaining cases
if (objinstanceof String)
{
writeString((String)obj, unshared);
}
else if (cl.isArray())
{
writeArray(obj,desc, unshared);
}
else if (obj instanceof Enum)
{
writeEnum((Enum)obj, desc, unshared);
}
else if (obj instanceof Serializable)
{
writeOrdinaryObject(obj,desc, unshared);
}
else
{
// …
}
}
2、 持久化對象
對於實現Serializable接口的類的實例,可以將對象持久化到磁盤中,在持久化時需要用到ObjectOutputStream流,如下
/*持久化*/
File file=new File("/Users/ssl/person.out");
OutputStream outputStream=new FileOutputStream(file);
ObjectOutputStream objectOutputStream=newObjectOutputStream(outputStream);
Person person=new Person();
person.setId(1);
person.setName("ssl");
person.setAge(18);
person.setGender(Gender.MALE);
person.setInfo("ssl");
objectOutputStream.writeObject(person);
objectOutputStream.close();
/*反序列化*/
ObjectInputStream objectInputStream=newObjectInputStream(new FileInputStream(file));
Person p=(Person)objectInputStream.readObject();
objectInputStream.close();
System.out.println(p);
3、 默認序列化機制
如果僅僅只是讓某個實現類實現Serializable接口,而沒有其他任何處理的話,則就是使用默認序列化機制。使用默認序列化機制,在序列化對象時,不僅會序列化當前對象本身,還會對該對象引用的其他對象也進行序列化(若屬性對象沒有實現Serializable接口,將會報錯),同樣地,若引用對象也引用其他的對象,這些對象都會被序列化。所以,如果一個對象包含的成員變量是容器類對象,而這些容器所含的元素也是容器類,那麼序列化的過程就會複雜,開銷也大。
此外,如果父類實現了Serializable接口,其子類都可以被序列化;若子類實現了Serializable接口,而父類沒有,則父類中的屬性不能被序列化,子類中的屬性仍能夠被序列化,結果導致父類的信息丟失。在純Java環境下,Java序列換能夠很好的工作,但是在跨平臺的系統中,最好採用通用存儲數據結構,如JSON或XML等。
3.1、transient
若是某個屬性不想被序列化,則可以加上transient關鍵字。默認序列化機制就會忽略該字段。
3.2、writeObject、readObject
在序列化和反序列化過程中會調用到writeObject和readObject方法,這兩個方法爲私有方法,在序列化和反序列化過程中會通過反射機制來調用。所以在序列化類中添加這些方法,可以改變默認的序列化機制。
public class Person implements Serializable
{
private static final long serialVersionUID = 1L;
private int id;
private Stringname;
transient private int age;
private Gendergender;
private Stringinfo;
public Person()
{
}
/*添加writeObject、readObject方法會影響默認的序列化機制*/
private void writeObject(ObjectOutputStream out)throws IOException
{
/*defaultWriteObject會執行默認的序列化機制*/
out.defaultWriteObject();
out.writeInt(age);
}
private void readObject(ObjectInputStream in)throwsClassNotFoundException, IOException
{
in.defaultReadObject();
age=in.readInt();
}
}
3.3、Externalizable接口
無論是使用transient關鍵字,還是使用writeObject和readObject方法,其實都是基於Serializable接口的序列化。JDK還提供了另一個序列化接口-Externalizable,使用該接口之後,之前基於Serializable接口的序列化機制就將失效。
Externalizable繼承Serializable,當使用該接口時,序列化的細節需要由程序員顯示完成,重寫
public void writeExternal(ObjectOutput out)throws IOException
public void readExternal(ObjectInput in)throws IOException,
ClassNotFoundException
等方法來完成序列化操作。
當使用Externalizable進行序列化時,讀取對象時,會調用序列化類無參的構造函數來創建一個新的對象,然後再將被保存對象的字段的值填充到新對象中,所以序列化類最好要提供無參的構造函數(默認的構造函數)。
3.4、readResolve
當我們使用單例模式時,應該是期望某個類的實例是唯一的,但如果該類是可序列化的,那麼情況可能會略有不同。
當從文件中反序列化得到的對象與原單例對象並不是一個對象,爲了能在序列化過程中仍保持單例的特性,可以在單例類中添加一個readResolve()方法,在該方法中直接返回Person的單例對象。
private Object readResolve();
無論是實現Serializable接口,或者Externalizable接口,當從I/O流中讀取對象時,readResolve()方法都會被調用到,實際上就是用readResolve()方法返回的對象直接替換在反序列化過程中創建的對象。
4、 高級認識
將Java對象序列化爲二進制文件,是Java序列化的本質。在不部分情況下,開發人員只需要瞭解被序列化的類實現Serializable接口,使用ObjectInputStream和ObjectOutputStream進行對象的讀寫。然而,在某些情況下,知道這些是遠遠不夠的,下面列舉一些Java序列化中的高級知識。
4.1、序列化ID
序列化可以使Java對象在網絡中傳輸,在A端序列化的對象經過網絡傳向B端,在B端反序列化爲對象,此時要求A段和B段都有被序列化的類或.class文件。若此時,被序列化的類中沒有顯示指定序列化ID會出現什麼情況呢?
虛擬機是否允許序列化,不僅取決於類路徑和功能代碼是否一致,一個非常重要的一點的是兩個類的序列化ID是否一致。
private static final long serialVersionUID =1L;
若兩個類的序列化ID不一致,他們之間是無法相互序列化和反序列化的。簡單來說,Java的序列化機制是通過在運行時判斷類的序列化ID來驗證版本一致性的。在進行反序列化時,JVM會把傳來字節流中的序列化ID與本地對應的類中序列化ID進行比較。如果相同就認爲是一致的,可以進行反序列化;否則就會出現序列化版本不一致的異常。
4.2、靜態變量
在序列化時,並不持久化靜態變量。那麼,在本地序列化和反序列化時,如何獲取靜態變量?在網絡傳輸時,又如何獲取靜態變量。
4.3、敏感字段加密
在序列化過程中,虛擬機會試圖調用類對象中的writeObject和readObject方法,進行用戶自定義的序列化和反序列化。如果沒有這些方法,則默認會調用ObjectOutputStream的defaultWriteObject方法以及ObjectInputStream的defaultReadObject方法。基於該過程,我們自定義writeObject和readObject方法,來改變序列化的數值,如用於敏感字段的加密工作。
4.4、序列化存儲規則
Java序列化機制爲了節省磁盤空間,具有特定的存儲規則。當寫入文件的爲同一個對象時,並不會再將對象的內容進行存儲,而只是再次存儲一份引用。反序列化時,恢復引用關係。