官方文檔理解
要使類的成員變量可以序列化和反序列化,必須實現Serializable接口。任何可序列化類的子類都是可序列化的。Serializable接口沒有提供任何方法和字段,只是標記可以序列化。
為了允許不可序列化類的子類可序列化,子類要承擔父類的public,protected和包內可訪問(default)修飾的字段。該父類必須有一個子類可訪問的無參構造器,去初始化它的屬性。
在反序列化過程中,不可序列化類的字段通過public或protected修飾的無參構造器初始化。這個構造器必須是子類可訪問的。而可序列化子類的字段從流中恢復。
這里沒有提到使用friendly修飾的構造器,應該是不確定子類和父類屬于同一包中。如果是一個包,應該也可以。因為friendly修飾的構造器也可以被同一個包下的子類訪問。
如果對一個不可序列化對象進行序列化操作時,會拋出NotSerializableException標記該類不可序列化。
如果一個類在序列化和反序列化中需要制定一些特殊操作,必須實現一下方法簽名的特殊方法:
private void writeObject(java.io.ObjectOutputStream out)
throws IOException
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException;
private void readObjectNoData()
throws ObjectStreamException;
writeObject()
方法職責是將特定類的對象屬性輸出,這樣,相應的readObject()
可以恢復。可以調用out.defaultWriteObject()
使用默認保存對象屬性的機制。out.defaultWriteObject()
自身不必考慮使用的變量是屬于父類還是子類。通過使用writeObject
或通過使用DataOutput支持的基本數據類型的方法將各個字段寫入ObjectOutputStream來保存狀態。
readObject()
主要責任是從流中讀取并恢復類的字段。它可以通過調用in.defaultReadObject()
采用默認機制恢復非靜態和non-transient字段。in.defaultReadObject()
使用流中的信息來將流中保存的對象的字段分配給當前對象中相應命名的字段。當類添加了新字段,它依舊可以使用。in.defaultReadObject()
自身不必考慮使用的變量是屬于父類還是子類。通過使用writeObject
或通過使用DataOutput支持的基本數據類型的方法將各個字段寫入ObjectOutputStream來保存狀態。
readObjectNoData()
:Serializable對象反序列化時,由于序列化與反序列化提供的class版本不同,序列化的class的super class不同于序列化時的class的super class;或者收到有敵意的流;或接收不完整;都會對初始化對象字段值時造成影響。針對這些情況可以在在該方法中實現這些字段的初始化。如果沒有定義readObjectNoData()
,這些字段會初始化成JVM默認值。
序列化類在將對象寫入流中時,可以指定一個替代對象寫入。但是必須實現精確的方法簽名:
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
如果writeReplace()
存在,序列化時會被調用。它的權限修飾符可以是任何一個,子類遵循Java權限訪問規則。
當一個類的對像從流中讀取時,指派另一個類作為返回值。必須實現精確的方法簽名:
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
該方法有著與writeReplace()
類似的調用和訪問規則。
Java允許為序列化的類提供一個serialVersionUID的常量標識該類的版本。只要serialVersionUID的值不變,Java就會把它們當作相同的序列化版本。例如,一個類升級后,它的serialVersionUID類變量值保持不變,序列化機制也會把它們當成同一個類版本。
ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;
如果反序列時,發送方的類(指序列化時使用的類文件)與接受方的(指反序列化時使用的類文件)類的各自serialVersionUID不同,那么會拋出InvalidClassException異常。
JVM就會根據類的各個方面計算出一個serialVersionUID的值。不同的編譯器下會產生不同的serialVersionUID值。serialVersionUID值不同則會導致反序列化程序編譯失敗。解決辦法是顯示指定一個serialVersionUID。這樣,即使在某個對象被序列化后,它所對應的類被修改了,該對象也依然可以被正確的反序列化。
實踐
可序列化子類默認實現序列化
這應該是毋庸置疑的。因為父類實現了Serializable接口,那么子類必然可序列化。
不可序列化父類的子類可否序列化
從文檔中得知,只要父類有一個子類可訪問的無參構造器能夠初始化父類自身的字段,就可行。那么沒有訪問權限修飾符號修飾的無參構造器可行嗎?
public class FriendlyConstructorFather {
private int number;
FriendlyConstructorFather() {
this.number = 12;
}
public int getNumber() {
return this.number;
}
public void setNumber(int num) {
this.number = num;
}
}
public class FriendlyConstructorSon extends FriendlyConstructorFather
implements Serializable{
private int sonNum;
public FriendlyConstructorSon() {
}
public int getSonNum() {
return sonNum;
}
public void setSonNum(int number) {
sonNum = number;
}
}
public class TestFriendlyConstructor {
public static void main(String[] agrs) {
FriendlyConstructorSon son = new FriendlyConstructorSon();
son.setSonNum(2);
FileOutputStream fileOut = null;
ObjectOutputStream objectOut = null;
File file = new File("../file/TestFriendlyConstructor.txt");
try {
try {
fileOut = new FileOutputStream(file);
objectOut = new ObjectOutputStream(fileOut);
objectOut.writeObject(son);
}finally {
objectOut.close();
}
}catch(IOException e) {
e.printStackTrace();
}
FileInputStream fileIn = null;
ObjectInputStream objectIn = null;
try {
try {
fileIn = new FileInputStream(file);
objectIn = new ObjectInputStream(fileIn);
FriendlyConstructorSon resultSon =
(FriendlyConstructorSon) objectIn.readObject();
System.out.println(
"the father's number is " + resultSon.getNumber());
System.out.println(
"the son's num is " + resultSon.getSonNum());
}finally{
objectIn.close();
}
}catch(IOException e) {
e.printStackTrace();
}catch(ClassNotFoundException e) {
e.printStackTrace();
}
}
}
執行后效果:
the father's number is 12
the son's num is 2
發現是可行的,但是要求子類必須和父類在同一個包中。
readObjectNoDate方法使用情況
原始Person.java
public class Person implements Serializable{
private int age;
public Person() {
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
}
并且通過序列化ObjectOutputStream輸出到test.txt文件中進行保存。然后升級Person類:
public class Animal implements Serializable {
private String name;
public Animal() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private void readObjectNoData() throws ObjectStreamException{
this.name = "zhangsan";
}
}
public class Person extends Animal implements Serializable {
private int age;
public Person() {}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
編譯新的Person類后,使用新的Person.class文件從test.txt反序列化加載Person對象。并且使用getName()
獲取字段name
值,執行輸出如下:
the age is 25
the name is zhangsan
可以看到從信息不完整的序列化流中得到了完整的Person類,這要歸功于readObjectNoData()
。它初始化了name
字段。如果在這種Person發生升級的情況下,沒有定義readObjectNoData()
那么name
字段會初始化它們的默認值。readObjectNoData()
一般用于序列化對象和反序列化對象父類不同的情況,還有就是為了防止信息不完整,可以使用它來進一步保證初始化。如果了類中有自定義的readObject()
,出現上述情況時,會用readObjectNoData()
替代它。
這里可以注意下,雖然升級了Person,且沒有顯示指定SerializableUID。序列化機制依舊認為升級前后的Person是同一個版本。這是因為:
- 只是修改了類的方法,不會影響反序列化。
- 只是修改了類的static Field或transient Field,不會影響反序列化。
- 修改了類的非static和非transient Field,會影響序列化。
如果此時升級Person時,繼承的Animal中有與原始Person相同的字段。那么readObjectNoDate()
的初始化無效果,會使用它們的默認值初始化。而且不顯示指定SerializableUID會拋出InvalidClassException異常。唯一的解決辦法就是顯示指定SerializableUID,即可執行。
自定義序列化
case one
在一些特殊情況下,類中某些實例變量是敏感信息不希望被序列化,或者這些實例變量的類型不可序列化為避免發生NotSerializableException異常。可以通過關鍵詞transient修飾這些實例變量,指定類在序列化時無需理會它們。這樣一來,反序列化后得到的對象中這些字段會被初始化為默認值。
序列化注意事項:
- 對象的類名、Field(包括基本類型、數組及對其他對象的引用)都會被序列化,對象的static Field,transient Field及方法不會被序列化;
- 實現Serializable接口的類,如不想某個Field被序列化,可以使用transient關鍵字進行修飾;
- 保證序列化對象的引用類型Filed的類也是可序列化的,如不可序列化,可以使用transient關鍵字進行修飾,否則會序列化失敗;
- 反序列化時必須要有序列化對象的類的class文件,而且方法不會被序列化;
- 當通過文件網絡讀取序列化對象的時候,必需按寫入的順序來讀取。
使用transient關鍵字修飾實例變量避免序列化非常便捷,但該變量將被完全隔離在序列化機制之外,這樣導致在反序列化恢復的對象無法取得該實例變量值。Java還提供了一種自定義序列化機制,可以讓程序控制如何序列化各實例變量,甚至完全不序列化某些實例變量(與使用transient關鍵字的效果相同)。
public class CustomSerializable implements Serializable {
private String account;
private transient String password;
private int passwordCount;
public CustomSerializable(String name, String password)
throws Exception{
passwordCount = password.length();
for(int i = 0; i < passwordCount; i++) {
char c = password.charAt(i);
if(c < '0' && '9' < c) {
throw new Exception("the password is not correct!");
}
}
this.account = name;
this.password = password;
}
private String changePassword() {
byte[] bArray = new byte[passwordCount];
for(int i = 0; i < passwordCount; i++) {
bArray[i] = '*';
}
return new String(bArray);
}
private void writeObject(ObjectOutputStream out) throws IOException {
System.out.println("custom writeObject method execute!");
out.defaultWriteObject();
out.writeObject(changePassword());
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
System.out.println("custom readObject method execute!");
in.defaultReadObject();
this.password = (String) in.readObject();
}
@Override
public String toString() {
return "Account: " + account + "\nPassword: " + password +
"\nPasswrod Count: " + passwordCount;
}
}
在代碼中,使用了defaultWrite/ReadObject()
去執行默認的序列化機制。
在序列化中(自定義的writeObject()),由于password是一個敏感信息,所以使用transient修飾,將其排除在默認序列化機制外。然后針對敏感信息自定義一套序列化操作,保護信息安全。
在反序列化中(自定義的readObject()),首先使用默認反序列化機制去初始化可序列化字段,然后針對自定義序列化中的password字段,使用相應的readObject()
讀取,并且賦值給反序列化對象中的同名字段。
執行效果,無自定義序列化和反序列化,transient修飾的password字段讀寫:
自定義序列化和反序列化后,transient修飾的password字段讀寫:
在代碼中還看到writeObject()
,readObject()
,readObjectNoDate()
都是private修飾的,但是序列化過程中一樣可以被外部的ObjectOut/InputStream調用。
ObjectOutputStream在執行自己的writeObject方法前會先通過反射在要被序列化的對象的類中查找有無自定義的writeObject方法,如有的話,則會優先調用自定義的writeObject方法。因為查找反射方法時使用的是getPrivateMethod,所以自定以的writeObject方法的作用域要被設置為private。通過自定義writeObject和readObject方法可以完全控制對象的序列化與反序列化。(詳情可見ObjectOutputStream中的writeSerialData方法,以及ObjectInputStream中的readSerialData方法。)
case two
Java序列化機制提供一種更徹底的序列化方式,writeReplace()
和readResolve()
。前者是在序列化過程中替換成其他對象,后者是在反序列化中替換掉readObject()
返回的實例。
注意兩者一般不同時使用,因為同時存在時,只會去執行前者,而且不會調用自定義的writeObject()
和readobject()
。而后者執行之前回去執行writeObject()
和readobject()
,然后替換掉readObject()
返回的實例,并且拋起它。
在CustomSerializable.java中添加下列代碼:
private Object writeReplace() throws ObjectStreamException {
System.out.println("custom writeReplace method execute!");
return "No Permission to access the account'password!";
}
執行序列化讀取:
在CustomSerializable.java中添加下列代碼:
private Object readResolve() throws ObjectStreamException {
System.out.println("custom readResolve method execute!");
return "The account's password is a private!";
}
執行序列化讀取:
注意,writeReplace()
和readResolve()
可以被任何權限修飾符修飾,且子類訪問這兩個方法遵循Java的權限訪問規則。這樣做的目的子類可以使用父類中已有的序列化操作,也可以覆寫它們。
一般readResolve()
用于單例模式
一般類實現單例模式的用途是保證該類的實例只有一個。但是對象序列化后,通過反序列化得到的對象是重構的,也就是在堆內存中重新創建的。所以無法保障唯一實例。
public class SingletonSer implements Serializable {
private static SingletonSer instance;
private String name;
private int age;
private SingletonSer(String name, int age) {
this.name = name;
this.age = age;
}
public static SingletonSer getInstance() {
if(instance == null) {
instance = new SingletonSer("FoolishDev", 25);
}
return instance;
}
private Object readResolve() throws ObjectStreamException {
return getInstance();
}
}
執行序列化讀取,在代碼中對序列化前后實例引用進行比較(==),得到
they are same? true
總結
Serializable反序列化時,不會去調用類的構造器,除非類的父類不可序列化,會去調用子類可訪問的無參構造器來初始化父類字段。如果父類和子類都可序列化,且各自自定義了序列化和反序列化方法,在整個序列化過程中父類和子類自定義的操作互不影響,各自執行。
Externalizable
官方文檔理解
Externalizable實例類可以實現序列化,而且承擔了自身內容存儲和恢復的責任。Externalizable子類需要實現兩個方法,writeExternal()
和readExternal()
,通過這兩個方法可以完全控制它的對象和它父類的流的內容和格式。這兩個方法必須明確的和父類協調,處理它的狀態,同時替代了自定義的writeObject()
和readObject()
。
類的序列化可以使用Serializable和Externalizable接口。對象的持久化可以使用這兩個接口。任何一個對象要存儲,首先回去判斷有無實現Externalizable接口,如果有那么執行writeExternal()
,如果沒有但是實現了Serializable接口,那么使用ObjectOutputStream。
一旦類實現了Externalizable就會替代了Serializable機制。
當一個Externalizable對象重建時,使用public no-arg 構造器進行創建實例,然后調用readExternal()
。Serializable對象創建是從流中讀取信息。Externalizable類一樣可以使用writeReplace()
和readResolve()
替換對象。
實踐
Externalizable父類構造器必須是公共的?
public class Supertype implements Externalizable{
private String className;
private Date date;
Supertype() {
System.out.println("super no arg constructor executed!");
className = "Super";
date = new Date();
}
public void setClassName(String name) {
this.className = name;
}
@Override
public String toString() {
return "Class name is " + className + ". The date is " + date.toString();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(className);
out.writeObject(date);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
className = (String) in.readObject();
date = (Date) in.readObject();
}
}
public class Subtype extends Supertype{
private String nickName;
public Subtype() {
System.out.println("sub no arg constructor executed!");
nickName = "Sub";
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
super.writeExternal(out);
out.writeObject(nickName);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
super.readExternal(in);
nickName = (String) in.readObject();
}
public String getNickName() {
return nickName;
}
public void setNickName(String name) {
this.nickName = name;
}
@Override
public String toString() {
return super.toString() + ". The nick name is " + nickName;
}
}
執行序列化操作后,輸出:
super no arg constructor executed!
sub no arg constructor executed!
super no arg constructor executed!
sub no arg constructor executed!
Class name is Supertype. The date is Sun Feb 26 16:10:45 CST 2017. The nick name is Subtype
發現父類的構造器可以被子類訪問即可,并沒有強制規定為public權限。如果不可被子類訪問,創建子類對象時會拋出異常java.lang.IllegalAccessError。如果子類的構造器不是public修飾,反序列時會拋出異常java.io.InvalidClassException。
而且還需要注意,即使在反序列化時調用了Externalizable類的無參構造器去初始化了字段,最后依舊會使用序列化流中的信息賦值給同名字段。
最后代碼中子類并沒有覆寫父類中的writeExternal()
和readExternal()
,而是繼承了。這樣保證了父類字段信息不丟失。如果子類覆寫了這兩個方法,那么父類字段會使用構造器去初始化(實際上就是調用了子類的公共無參構造器,然后執行內部引用的父類構造器去初始化)。
這樣聯想到Serializable文檔中提到
writeObject()
和readObject()
無需考慮字段屬于父類還是子類。因為它們權限是private,所以是各自執行字段的初始化,互不影響。
不可序列化父類的子類實現可序列化
如何去初始化父類中的私有字段,以及子類可訪問的字段?
public class NoExternalSuper {
private String className;
private Date date;
protected NoExternalSuper() {
System.out.println("no-externalizable super no arg constructor executed!");
className = "Super";
date = new Date();
}
public void setClassName(String name) {
this.className = name;
}
@Override
public String toString() {
return "Class name is " + className + ",date is " + date.toString();
}
}
public class NoExternalSub
extends NoExternalSuper implements Externalizable{
private String nickName;
public NoExternalSub() {
System.out.println("no-externalizable sub no arg constructor executed!");
nickName = "Sub";
}
@Override
public void writeExternal(ObjectOutput out)
throws IOException {
out.writeObject(nickName);
}
@Override
public void readExternal(ObjectInput in)
throws IOException, ClassNotFoundException {
nickName = (String) in.readObject();
}
@Override
public String toString() {
return super.toString() + ".The nick name is " + nickName;
}
}
執行序列化操作后輸出結果:
no-externalizable super no arg constructor executed!
no-externalizable sub no arg constructor executed!
no-externalizable super no arg constructor executed!
no-externalizable sub no arg constructor executed!
Class name is Super,date is Sun Feb 26 16:36:34 CST 2017.The nick name is Sub
可以看出,父類使用自身構造器初始化自身字段(就是子類公共無參構造器引用的父類構造器)。而子類可訪問的字段當然可以在子類中做出相應序列化和反序列化操作。
總結
一個實現Externalizable接口的序列化類,必須有一個public-no-arg constructor,且會完全替代Serializable機制,讓開發者完全控制和自定義序列化和反序列化。所以需要注意協調處理父類的屬性狀態。它雖然使用起來復雜,但是性能比Serializable好。它實現持久化的方法是通過ObjectOutput和ObjectInput。