序列化簡介
Java序列化,一個日常開發中比較少用到的技術。正常情況下,JVM啟動后,我們可以創建對象,JVM關閉后,我們創建過的對象都隨之銷毀,資源釋放。但有些時候可能要求在JVM停止之后,某些對象需要保存起來,以便將來再重新讀取它們。
舉個例子,應用服務器的HttpSession對象,Session是指瀏覽器與服務器之間的一次會話,對應的是服務器中的一個Session對象,而客戶端中保存一個jsessionid。那么當某種情況下,我們不得不重啟服務器的時候,就需要把之前所有的Session對象保存起來,服務器重啟之后,將這些Session對象再重新加載過來,這樣避免了之前瀏覽器與服務器建立的會話失效,在瀏覽器那看來,就好象服務器沒有關閉過一樣(假設服務器重啟期間用戶沒有操作)。這就用到了Java序列化技術,關于這個例子,我們可以拿Tomcat來測試一下,注意要用正常的手段來關閉服務器(shutdown.bat),而非強制關閉,強制關閉沒有序列化的過程。
Java序列化
首先創建一個可序列化的JavaBean類:Name.java
import java.io.Serializable;
/**
* 可序列化的類,需要實現Serializable接口
*/
public class Name implements Serializable {
private String firstname;
private String lastname;
public Name() {
System.out.println("無參構造器");
}
public Name(String firstname, String lastname) {
System.out.println("全參構造器");
this.firstname = firstname;
this.lastname = lastname;
}
//省略getter和setter
@Override
public String toString() {
return "我的名字是" + firstname + "," + lastname;
}
}
再實現一個序列化的工具類:Serializations.java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
/**
* 序列化工具類
*/
public class Serializations {
/**
* 序列化對象到指定路徑文件
* @param outPath 文件路徑
* @param outObj 需要序列化的對象
* @throws IOException 當I/O發生異常時
*/
public static void serialize(String outPath, Object outObj)
throws IOException {
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream(outPath));
oos.writeObject(outObj);
} finally {
if(oos != null) oos.close();
}
}
/**
* 從文件中逆序列化出對象
* @param inPath 文件路徑
* @return 你序列化出的對象
* @throws IOException 當I/O發生異常時
* @throws ClassNotFoundException 當文件中不存在序列化的對象時
*/
public static Object deserialize(String inPath)
throws IOException, ClassNotFoundException {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(inPath));
return ois.readObject();
} finally {
if(ois != null) ois.close();
}
}
}
最后創建兩個個測試類,來使用一下序列化方法和逆序列化方法:WriteObject.java。
import java.io.IOException;
public class WriteObject {
public static void main(String[] args) throws IOException {
Name name = new Name("科比", "布萊恩特");
Serializations.serialize(args[0], name);
}
}
運行后,指定目錄下會生成相應文件,其內包含了name對象信息。
ReadObject.java:
import java.io.IOException;
public class ReadObject {
public static void main(String[] args)
throws ClassNotFoundException, IOException {
Object obj = Serializations.deserialize(args[0]);
System.out.println(obj);
}
運行后,輸出:我的名字是科比,布萊恩特
我們成功的將name對象序列化到了指定文件中,并且通過逆序列化得到一個和原對象屬性相同的對象。注意,逆序列化出的對象沒有使用該對象的構造器(由輸出結果可以證明),并且和原對象不相等。
對象的默認序列化機制
序列化時,對象的類、類的簽名,以及類及其所有超類型的非瞬態(non-transient)和非靜態(non-static)字段的值都將被寫入。逆序列化時,對象的類、類的簽名,以及類及其所有超類型的非瞬態(non-transient)和非靜態(non-static)字段的值都將被讀取。如果我們想某個成員變量不被序列化,可以在其前面加入transient關鍵字。如:private transient String lastname;
序列化版本號
如果對象所屬類在對象序列化之后做了修改,比如修改屬性名稱、類型、修飾符等等,再次逆序列化就會發生異常,如我們將lastname前加入transient,使用ReadObject.java進行逆序列化, 將會拋出如下異常:
Exception in thread "main" java.io.InvalidClassException: Name; local class incompatible: stream classdesc serialVersionUID = 3999552307707967101, local class serialVersionUID = -4860856635192050881
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:604)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1601)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1514)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1750)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1347)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:369)
at Serializations.deserialize(Serializations.java:43)
at ReadObject.main(SerializationsTest.java:14)
異常的大概描述是說流中的類的版本號和本地類的版本號不一致,這里要引入一個序列化版本號(serialVersionUID)的概念,serialVersionUID是一個64位的值,在類中需要聲明為private static final long,它可以人為來維護,也可以通過JVM實現的算法來生成,安裝JDK后,可以過%JAVA_HOME%/bin/serialver.exe來生成serialVersionUID。在逆序列化時,會將從對象流中讀取的類信息和當前classpath下的相應類的類信息(Name.class)進行比對,比對的媒介就是serialVersionUID,如果對象中沒有聲明serialVersionUID,那么該值就會通過默認的算法生成,兩端不一致時,就會拋出上面的異常,逆序列化失敗。
當編寫一個可序列化的類時(Name.java),可以給serialVersionUID賦一個即簡單又易理解的值,如:
private static final long serialVersionUID = 1L;
如果對該類進行了更改,可能需要同時更新serialVersionUID,如:
private static final long serialVersionUID = 2L;
但有時我們可能即使更改了類之后,仍然要保持之前序列化的可逆性,也就是對之前的序列化文件做個兼容,那么就不能更新serialVersionUID的值,這時更改前生成的序列化文件依然可逆序列化,那么其更新的字段會以字段類型的預設值逆序列化,避開不兼容的問題。
復合類序列化
上文中實現了序列化和逆序列化一個簡單的Name對象,下面來看一個稍復雜的情況,Name類中復合了其它類。
import java.io.Serializable;
/**
* 可序列化的類,需要實現Serializable接口
*/
public class Name implements Serializable {
private static final long serialVersionUID = 1L;
private String firstname;
private String lastname;
private Nickname nickname;
public Name() {}
public Name(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
public Name(String firstname, String lastname, Nickname nickname) {
this.firstname = firstname;
this.lastname = lastname;
this.nickname = nickname;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public Nickname getNickname() {
return nickname;
}
public void setNickname(Nickname nickname) {
this.nickname = nickname;
}
@Override
public String toString() {
return "我的名字是" + firstname + "," + lastname + "\n我的昵稱是" + nickname;
}
}
Nickname.java:
import java.io.Serializable;
/**
* 昵稱類
*/
public class Nickname implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
public Nickname() {}
//省略getter和setter
@Override
public String toString() {
return name;
}
}
WriteObject.java:
import java.io.IOException;
import com.runqianapp.test.bean.Name;
import com.runqianapp.test.bean.Nickname;
public class WriteObject {
public static void main(String[] args) throws IOException {
Nickname nickname = new Nickname("黑曼巴");
Name name = new Name("科比", "布萊恩特", nickname);
Serializations.serialize(args[0], name);
}
}
運行后,指定目錄下會生成相應文件,再次運行ReadObject.java,會得到如下輸出信息:
我的名字是科比,布萊恩特
我的昵稱是黑曼巴
在序列化對象時,不僅會序列化當前對象本身,還會對該對象引用的其它對象也進行序列化,同樣地,這些其它對象引用的另外對象也將被序列化,以此類推。在序列化過程中,可能會遇到不支持可序列化接口的對象,在此情況下,將拋出 NotSerializableException,并將標識不可序列化對象的類。如將Nickname.java去掉Serializable接口,再次運行WriteObject.java,會拋出如下異常:
Exception in thread "main" java.io.NotSerializableException: Nickname
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1180)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1528)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1493)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1416)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1174)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:346)
at Serializations.serialize(Serializations.java:26)
at WriteObject.main(WriteObject.java:13)
我們可以用transient來修飾nickname屬性,這樣該類就可以正常序列化了,但是nickname中的屬性也就無法序列化了,那我們如何讓不能序列化的類NickName中的name屬性可以序列化和反序列化呢?在序列化和反序列化過程中需要特殊處理的類必須使用下列準確簽名來實現特殊方法:
public class Name implements Serializable {
...
transient private Nickname nickname;
...
private void writeObject(ObjectOutputStream out) throws IOException {
// 默認序列化機制
out.defaultWriteObject();
// 序列化nickname中的name屬性
out.writeObject(nickname.getName());
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 默認逆序列化機制
in.defaultReadObject();
// 逆序列化一個nickname對象
nickname = new Nickname(in.readObject().toString());
}
}
這樣就可以處理其不可序列化的復合類Nickname中的name屬性序列化及反序列化。運行WriteObject和ReadObject,序列化和反序列化成功。這兩個方法如何實現取決于最終的需求。