序列化 (Serialization)是將對象的狀態信息轉換爲能夠存儲或傳輸的形式的過程。在序列化期間,對象將其當前狀態寫入到臨時或持久性存儲區。之後,能夠經過從存儲區中讀取或反序列化對象的狀態,從新建立該對象。——百度百科。java
在Android中序列化最多見的使用場景就是緩存數據了。如今的App中基本須要緩存數據,例如緩存用戶登陸信息。git
// 用來保存用戶信息
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
接口就能夠了。github
Serializable是Java中提供的序列化接口。緩存
package java.io;
public interface Serializable {
}
複製代碼
Serializable
是一個空接口,它僅僅是用來標識一個對象是可序列化的。安全
若是想要使User
可被序列化只要實現Serializable
接口便可。ide
public class User implements Serializable {
private static final long serialVersionUID = 8279379322154244252L;
private String name;
private int age;
// getter/setter
}
複製代碼
能夠看到User類實現了Serializable
接口,這時User
就能夠被序列化了。而且還多了一個serialVersionUID
字段。那麼這個字段是幹什麼用的呢?函數
serialVersionUID
是用來標記User
類版本用的。其聲明的格式是任意訪問權限修飾符 static final long serialVersionUID = longValue;
由於其做用是標識每一個類的版本,因此最好使用private
控制serialVersionUID
的訪問權限僅在當前類有用,不會被其餘子類繼承使用。工具
若是不顯示聲明serialVersionUID
那麼JVM會根據類的信息生成一個版本號,因爲不一樣的JVM生成的版本號的能不一致,類的結構也可能發生變化等這些因素均可能致使序列化時候的版本號和反序列化時的版本號不止一次致使運行時拋出InvalidClassException異常
。因此最佳實踐仍是在序列化時顯示的指定serialVersionUID
字段。其值是一個long
類型的數值。這個值在Android Studio
中默認是不能自動生成的,能夠打開Perferences-Editor-Code Style-Inspections-Serialization issues-Serializable class without serialVersionUID
,這樣在實現Serializable
接口是若是沒有聲明serialVersionUID
字段編譯器就會給出警告⚠️,根據警告提示就能夠自動生成serialVersionUID
字段了。this
總結:加密
serialVersionUID
字段。private
修飾serialVersionUID
字段。Android Studio
或者其餘工具生成serialVersionUID
的值。serialVersionUID
值儘可能保持一致,不要隨意修改,不然反序列化時會拋出InvalidClassException
異常,反序列化失敗。有時候可能要序列化的對象中存在某些字段不須要被序列化。例如用戶密碼,爲了保證安全咱們不須要將密碼字段進行序列化,那如何能作到這一點呢?實現Serializable
接口時靜態變量(被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
}
複製代碼
由於靜態變量不能被序列化,因此serialVersionUID
須要聲明爲static
的,另外password
被聲明爲transient
也不會被序列化。
靜態成員返回序列化時會取內存中的值,被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異常。或者使用
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序列化機制中能夠用來干預序列化過程的五個方法,他們具體能感謝什麼繼續往下看。
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
主要是用來處理當類發生結構性的變化時處理數據初始化的,這麼說可能有點抽象,咱們還拿上面的案例來講明。
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
上了,而後發現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
相對於初版本中緩存的數據發生告終構性的變化,當使用第二版的User
反序列化初版的User
信息時父類Person
中的sex
就沒辦法初始化了,只能使用boolean
類型的默認值,也就是false
了。那如何才能在反序列化過程當中修改sex
的值呢?就能夠經過readObjectNoData
方法來完成。
當反序列化過程當中類發生告終構性的變化時readObjectNoData
方法就會被調用,解決上面的問題咱們就能夠在Person
中重寫readObjectNoData
方法來對sex
進行初始化操做。
private void readObjectNoData() throws ObjectStreamException {
sex = true;
}
複製代碼
writeReplace
方法會在writeObject
方法以前被調用,它返回一個Object
,用來替換當前須要序列化的對象,而且在其內部能夠用this
來調用當前對象的信息。
// 返回值Object則是真正被序列化的對象
private Object writeReplace() throws ObjectStreamException {
// 新建立一個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
方法會在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
是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
和Externalizable
均可以實現序列化,那麼他們有什麼區別呢?該如何選擇呢?
Serializable
只是標記接口,其序列化過程都交給了JVM處理,使用相比Externalizable
更簡單。Externalizable
並非標記接口,實現它就必須重寫兩個方法來實現序列化和反序列化,相對複雜一點。Serializable
把序列化和反序列化的過程都交給了JVM,因此在個別狀況可能其效率不如Externalizable
。因此一般狀況下使用Serializable
來實現序列化和反序列化過程便可。只有充分的瞭解到使用Externalizable
實現其序列化和反序列化會使其效率有所提高才或者須要徹底自定義序列化和反序列化過程才考慮使用Externalizable
。
郵箱:eonliu1024@gmail.com
Github: github.com/Eon-Liu
CSDN:blog.csdn.net/EonLiu