簡介
序列化 (Serialization)是將對象的狀態(tài)信息轉換為可以存儲或傳輸的形式的過程。在序列化期間,對象將其當前狀態(tài)寫入到臨時或持久性存儲區(qū)。以后,可以通過從存儲區(qū)中讀取或反序列化對象的狀態(tài),重新創(chuàng)建該對象?!俣劝倏?。
在Android中序列化最常見的使用場景就是緩存數據了?,F在的App中基本需要緩存數據,例如緩存用戶登錄信息。
// 用來保存用戶信息
public class User {
private String name;
private int age;
// getter/setter
}
// 用戶信息
User user = new User("Eon Liu", 18);
ObjectOutputStream oos = null;
try {
// 緩存路徑(需要開啟存儲權限)
File cache = new File(Environment.getExternalStorageDirectory(), "cache.txt");
oos = new ObjectOutputStream(new FileOutputStream(cache));
// 將用戶信息寫到本地文件中
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
} finally {
// 關閉流
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
通常在登錄成功之后我們將用戶的信息解析成一個類似User
的對象,然后將其保存在SDCard
中。這一過程就需要用到序列化。上面代碼我們并沒有對User
進行可序列化的處理,所以在保存過程中就會拋出java.io.NotSerializableException: com.eonliu.sample.serialization.User
這樣的Java異常。因為在writeObject
方法中對需要存儲的類進行了校驗,如果沒有實現Serializable
接口就會拋出這個異常信息。處理這種異常也很簡單,只要使User
類實現Serializable
接口就可以了。
Serializable
Serializable是Java中提供的序列化接口。
package java.io;
public interface Serializable {
}
Serializable
是一個空接口,它僅僅是用來標識一個對象是可序列化的。
如果想要使User
可被序列化只要實現Serializable
接口即可。
public class User implements Serializable {
private static final long serialVersionUID = 8279379322154244252L;
private String name;
private int age;
// getter/setter
}
可以看到User類實現了Serializable
接口,這時User
就可以被序列化了。并且還多了一個serialVersionUID
字段。那么這個字段是干什么用的呢?
serialVersionUID的作用及注意事項
serialVersionUID
是用來標記User
類版本用的。其聲明的格式是任意訪問權限修飾符 static final long serialVersionUID = longValue;
因為其作用是標識每個類的版本,所以最好使用private
控制serialVersionUID
的訪問權限僅在當前類有用,不會被其他子類繼承使用。
如果不顯示聲明serialVersionUID
那么JVM會根據類的信息生成一個版本號,由于不同的JVM生成的版本號的能不一致,類的結構也可能發(fā)生變化等這些因素都可能導致序列化時候的版本號和反序列化時的版本號不止一次導致運行時拋出InvalidClassException異常
。所以最佳實踐還是在序列化時顯示的指定serialVersionUID
字段。其值是一個long
類型的數值。這個值在Android Studio
中默認是不能自動生成的,可以打開Perferences-Editor-Code Style-Inspections-Serialization issues-Serializable class without serialVersionUID
,這樣在實現Serializable
接口是如果沒有聲明serialVersionUID
字段編譯器就會給出警告??,根據警告提示就可以自動生成serialVersionUID
字段了。
總結:
- 盡量顯示聲明
serialVersionUID
字段。 - 最好使用
private
修飾serialVersionUID
字段。 - 盡量使用
Android Studio
或者其他工具生成serialVersionUID
的值。 - 不同版本的類的
serialVersionUID
值盡量保持一致,不要隨意修改,否則反序列化時會拋出InvalidClassException
異常,反序列化失敗。
不可被序列化的字段
有時候可能要序列化的對象中存在某些字段不需要被序列化。例如用戶密碼,為了保證安全我們不需要將密碼字段進行序列化,那如何能做到這一點呢?實現Serializable
接口時靜態(tài)變量(被static
修飾的變量)不會被序列化、另外被transient
關鍵字修飾的變量也是不會被序列化的。
public class User implements Serializable {
private static final long serialVersionUID = 8279379322154244252L;
private String name;
private int age;
private transient String password;
// getter/setter
}
因為靜態(tài)變量不能被序列化,所以serialVersionUID
需要聲明為static
的,另外password
被聲明為transient
也不會被序列化。
靜態(tài)成員返回序列化時會取內存中的值,被transient
修飾的成員變量使用其類型的默認值,例如password
的默認值則為null
。
繼承或組合關系中的序列化
public class Person {
private boolean sex;
// getter/setter
}
public class User extends Person implements Serializable {
private static final long serialVersionUID = 8279379322154244252L;
private String name;
private int age;
private transient String password;
// getter/setter
}
父類Person
沒有實現Serializable
接口,單其子類實現了Serializable
接口,所以父類的信息不回被序列化,當我們保存User
信息時,父類的sex
字段是不會被保存的。反序列化時sex
會使用boolean
類型的默認值false
。
另外當父類沒有實現Serializable
接口時,必須有一個可用的無參數構造函數,例如上面的Person
代碼并沒有顯示聲明構造,JVM會生成一個無參數構造函數,但是如果我們將其代碼改成如下形式:
public class Person {
private boolean sex;
public Person(boolean sex) {
this.sex = sex;
}
// getter/setter
}
這里顯示聲明了Person
的構造函數,其參數為sex
,這也是Person
的唯一構造函數了。因為根據Java機制,當顯示聲明構造函數時JVM就不會生成無參數的構造函數。這樣就會導致反序列化時候無法構造Person
對象,拋出java.io.InvalidClassException: com.eonliu.sample.serialization.User; no valid constructor
異常。
我們對上面的代碼稍作修改。
當父類實現了Serializable
接口時,其子類也可以被序列化。
public class Person implements Serializable {
private static final long serialVersionUID = 2622760185052917383L;
private boolean sex;
// getter/setter
}
public class User extends Person {
private static final long serialVersionUID = 8279379322154244252L;
private String name;
private int age;
private transient String password;
// getter/setter
}
當父類Person
實現了Serializable
接口時,則子類User
也可以被序列化。這時sex
、name
、age
這三個字段都會被序列化。
還有一種情況就是當我們序列化的類中有一個成員變量是一個自定義類的情形。
public class Car {
private String product;
// getter/setter
}
public class Person implements Serializable {
private static final long serialVersionUID = 2622760185052917383L;
private boolean sex;
// getter/setter
}
public class User extends Person {
private static final long serialVersionUID = 8279379322154244252L;
private String name;
private int age;
private transient String password;
private Car car;
// getter/setter
}
在User
中有一個成員變量為Car
類型,因為Car
沒有實現Serializable
接口,所以會導致User
序列化失敗,拋出java.io.NotSerializableException: com.eonliu.sample.serialization.Car
異常,這時解決辦法有兩個,一個是使用transient
修飾Car
字段,使其在序列化時被忽略。另一個辦法就是Car
實現Serializable
接口,使其擁有可序列化功能。
總結:
繼承關系中,父類實現
Serializable
接口,則父類和子類都可被序列化。集成關系中,父類沒有實現
Serializable
接口,則父類信息不會被序列化,子類實現Serializable
接口則只會序列化子類信息。如果被序列化的類中有Class類型的字段則這個Class需要實現
Serializable
接口,否則序列化時候回拋出``java.io.NotSerializableException異常?;蛘呤褂?/code>transient`將其標記為不需要被序列化。
如果父類沒有實現
Serializable
接口,則必須要有一個可用的無參數構造函數。否則拋出java.io.InvalidClassException: com.eonliu.sample.serialization.User; no valid constructor
異常。
自定義序列化過程
Serializable
接口預留了幾個方法可以用來實現自定義序列化過程。
private void writeObject(java.io.ObjectOutputStream out)throws IOException
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException
上面五個方法就是Java序列化機制中可以用來干預序列化過程的五個方法,他們具體能感謝什么繼續(xù)往下看。
writeObject&readObject
writeObject
和readObject
這兩個方法從名字可以看出來,就是用來讀寫對象的,在序列化過程中我們需要把對象信息通過ObjectOutputStream
保存在存儲介質上,反序列化的時候就是通過ObjectInputStream
從存儲介質上將對象信息讀取出來,然后在內存中生成一個新的對象。這兩個方法就可以用來定義這一過程。
// 序列化
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
// 寫入性別信息(sex是Person的字段信息)
out.writeBoolean(isSex());
// 寫入年齡信息
out.writeInt(age);
}
// 反序列化
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
// 恢復性別信息
setSex(in.readBoolean());
// 恢復年齡信息
age = in.readInt();
}
首先這兩個方法要成對出現,否則一個都不要寫。在readObject
中read的次序要與在writeObject
中write的次序保持一致,否則可能會導致反序列化的數據出現混亂的現象。另外我們這兩個方法不關心父類是否實現了Serializable
接口,如上面代碼所示,out.writeBoolean(isSex());
中的sex
字段就是來自父類Person
的,即使Person
沒有實現Serializable
接口這個序列化也會正常運行。
如果不需要自定義過程可以使用out.defaultWriteObject();
來實現默認的序列化過程,使用in.defaultReadObject();
實現默認的反序列化過程。
重寫這兩個方法可以自定義序列化和反序列的過程、例如可以自己定義那些字段可以序列化,哪些不被序列化,也可以對字段進行加密、解密的操作等。如果使用默認的序列化、反序列化的過程我們也可以在其過程的前后插入其他的邏輯代碼來完成其他的任務。
readObjectNoData
readObjectNoData
主要是用來處理當類發(fā)生結構性的變化時處理數據初始化的,這么說可能有點抽象,我們還拿上面的案例來說明。
public class User implements Serializable {
private static final String TAG = "SerializationActivity";
private static final long serialVersionUID = -5795919384959747554L;
private String name;
private int age;
private transient String password;
private Car car;
// getter/setter
}
第一版本User
類如上所示,這時候序列化User
對象將其保存在SDCard
上了,然后發(fā)現User
取消性別字段,無法滿足需求,于是就有了下一版。
public class Person implements Serializable {
private static final long serialVersionUID = -3824243371733653209L;
private boolean sex;
...
}
public class User extends Person implements Serializable {
...
}
在第二版本中User
類繼承了Person
,同時也有用了性別的屬性。此時User
相對于第一版本中緩存的數據發(fā)生了結構性的變化,當使用第二版的User
反序列化第一版的User
信息時父類Person
中的sex
就沒辦法初始化了,只能使用boolean
類型的默認值,也就是false
了。那如何才能在反序列化過程中修改sex
的值呢?就可以通過readObjectNoData
方法來完成。
當反序列化過程中類發(fā)生了結構性的變化時readObjectNoData
方法就會被調用,解決上面的問題我們就可以在Person
中重寫readObjectNoData
方法來對sex
進行初始化操作。
private void readObjectNoData() throws ObjectStreamException {
sex = true;
}
writeReplace
writeReplace
方法會在writeObject
方法之前被調用,它返回一個Object
,用來替換當前需要序列化的對象,并且在其內部可以用this
來調用當前對象的信息。
// 返回值Object則是真正被序列化的對象
private Object writeReplace() throws ObjectStreamException {
// 新創(chuàng)建一個User對象
User user = new User();
// 新User的name為當前對象的name值
user.name = this.name;
// 新User的age為20
user.age = 20;
// 返回新User對象
return user;
}
上面重寫了writeReplace
方法,并新建一個User
對象,其name
賦值為當前對象的name
,this
即表示當前對象。其age
賦值為20,然后返回新的user
對象,之后writeObject
方法就會被調用,將在writeReplace
方法中返回的user
對象進行序列化。在反序列化中的得到user
信息與writeReplace
方法中新建的user
信息一致。
在writeReplace
方法中我們可以對其對象信息做一些過濾或者添加,甚至可以返回其他類型的對象都是可以的。只不過反序列化的過程也要做響應的轉換。
readResolve
readResolve
方法會在readObject
方法之后調用,返回值也是Object
,它表示反序列化最終的對象。在其方法內部可以使用this
表示最終反序列化對象。
private Object readResolve() throws ObjectStreamException {
User user = new User();
user.name = this.name;
user.age = 20;
return user;
}
這里的實現代碼與writeReplace
方式一致,也很好理解,就不過多解釋了。了解其運行機制之后至于怎么用大家就可以腦洞大開了。
在上面了解到writeReplace
和readResolve
的訪問修飾符為ANY-ACCESS-MODIFIER
,及代表著可以是任意類型的權限修飾符,例如private
、protected
、public
。但是因為這兩個方法主要的作用是用來處理當前類對象的序列化與反序列化,所以通常推薦使用private
修飾,以防止其子類重寫。
Externalizable
Externalizable
是Java提供的一個Serializable
接口擴展的接口。
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
使用也很簡單,與Serializable
類似。
public class User implements Externalizable {
private static final long serialVersionUID = -5795919384959747554L;
private String name;
private int age;
...
@Override
public void writeExternal(ObjectOutput out) throws IOException {
Log.d(TAG, "writeExternal: ");
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws ClassNotFoundException, IOException {
Log.d(TAG, "readExternal: ");
age = in.read();
}
}
與Serializable
的去別就是實現Externalizable
接口必須重啟writeExternal
和readExternal
兩個方法,其功能就是實現序列化和反序列化的過程。與Serializable
中的writeObject
和readObject
功能一樣。另外使用Externalizable
實現序列化需要提供一個public
的無參構造函數,否則在反序列化的過程中拋出java.io.InvalidClassException: com.eonliu.sample.serialization.User; no valid constructor
異常。
Serializable vs Externalizable
Serializable
和Externalizable
都可以實現序列化,那么他們有什么區(qū)別呢?該如何選擇呢?
-
Serializable
只是標記接口,其序列化過程都交給了JVM處理,使用相比Externalizable
更簡單。 -
Externalizable
并不是標記接口,實現它就必須重寫兩個方法來實現序列化和反序列化,相對復雜一點。 - 由于
Serializable
把序列化和反序列化的過程都交給了JVM,所以在個別情況可能其效率不如Externalizable
。
所以通常情況下使用Serializable
來實現序列化和反序列化過程即可。只有充分的了解到使用Externalizable
實現其序列化和反序列化會使其效率有所提升才或者需要完全自定義序列化和反序列化過程才考慮使用Externalizable
。
Github: https://github.com/Eon-Liu