序列化
將數(shù)據(jù)結(jié)構(gòu)或?qū)ο筠D(zhuǎn)換成二進(jìn)制串的過(guò)程。
序列化方案
Serializeble Java序列化方案
在Java中使用Serializeble有兩種方法,一種是實(shí)現(xiàn)Serializeble接口,另一種是實(shí)現(xiàn)Externalizable接口,它繼承自Java.io.Serializeble類(lèi)。
我們觀察源碼可以發(fā)現(xiàn),Serializeble接口內(nèi)部是沒(méi)有實(shí)現(xiàn)的。
public interface Serializeble {
}
實(shí)際上,Serializeble就相當(dāng)于是一個(gè)flag標(biāo)識(shí),使用的時(shí)候需要通過(guò)ObjectOutputStream來(lái)進(jìn)行序列化和ObjectInputStream進(jìn)行反序列化。
來(lái)看這么一個(gè)使用的例子:
//
public class demo01 {
static class User implements Serializable {
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String name;
public int age;
public String nickName;
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", nickName=" + nickName +
'}';
}
}
/**
* 序列化對(duì)象
* @param obj
* @param path
* @return
*/
synchronized public static boolean saveObject(Object obj, String path) {//持久化
if (obj == null) {
return false;
}
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream(path));// 創(chuàng)建序列化流對(duì)象
oos.writeObject(obj);
oos.close();
return true;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close(); // 釋放資源
} catch (IOException e) {
e.printStackTrace();
}
}
}
return false;
}
/**
* 反序列化對(duì)象
*
* @param path
* @param <T>
* @return
*/
@SuppressWarnings("unchecked ")
synchronized public static <T> T readObject(String path) {
ObjectInputStream ojs = null;
try {
ojs = new ObjectInputStream(new FileInputStream(path));// 創(chuàng)建反序列化對(duì)象
return (T) ojs.readObject();// 還原對(duì)象
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
if(ojs!=null){
try {
ojs.close();// 釋放資源
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
public static void main(String[] args) {
SerialTest();
}
public static String tmp = "D:\\xuliehuaTest\\";
private static void SerialTest(){
User user = new User("zero",18);
saveObject(user,tmp+"a.out");
System.out.println("1: " + user);
user = readObject(tmp+"a.out");
System.out.println("反序列化: 2: " + user);
}
}
而Externalizable類(lèi)內(nèi)部包含了writeExternal(ObjectOutput)和readExternal(ObjectInput)兩個(gè)方法。
public interface Externalizable extends Java.io.Serializeble{
void writeExternal(ObjectOutput out) throws IOExcption;
void readExternal(ObjectInput in) throws IOExcption,ClassNotFoundException;
}
但是使用的時(shí)候要注意:
- 讀寫(xiě)的順序要求一致
- 讀寫(xiě)的成員變量個(gè)數(shù)要求一致,否則會(huì)報(bào)EOFException異常
- 必須要有一個(gè)無(wú)參構(gòu)造函數(shù)
那么要怎么使用呢?
/**
* Externalizable 接口的作用
*/
public class demo02 {
static class User implements Externalizable {
//必須要一個(gè)public的無(wú)參構(gòu)造函數(shù)
public User(){}
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String name;
public int age;
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
// 1. 讀寫(xiě)的順序要求一致
// 2. 讀寫(xiě)的成員變量個(gè)數(shù)要求一致,否則會(huì)報(bào)EOFException異常
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String)in.readObject();
age = in.readInt();
}
}
public static String basePath = System.getProperty("user.dir") + "\\";
public static String tmp = "D:\\xuliehuaTest\\";
public static void main(String[] args) {
ExternalableTest();
}
private static void ExternalableTest() {
User user = new User("zero", 18);
System.out.println("1: " + user);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oos = null;
byte[] userData = null;
try {
oos = new ObjectOutputStream(out);
oos.writeObject(user);
userData = out.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new ByteArrayInputStream(userData));
user = (User)ois.readObject();
System.out.println("反序列化后 2: " + user);
} catch (Exception e) {
e.printStackTrace();
}
}
}
關(guān)于序列化的幾個(gè)問(wèn)題
- 什么是serialVersionUID ?如果你不定義這個(gè), 會(huì)發(fā)生什么?
- 假設(shè)你有一個(gè)類(lèi),它序列化并存儲(chǔ)在持久性中, 然后修改了該類(lèi)以添加新字段。如果對(duì)已序列化的對(duì)象進(jìn)行反序列化, 會(huì)發(fā)生什么情況?
serialVersionUID 是一個(gè) private static final long 型 ID, 當(dāng)它被印在對(duì)象上時(shí),它通常是對(duì)象的哈希碼,你可以使用 serialver 這個(gè)JDK工具來(lái)查看序列化對(duì)象的 serialVersionUID。
static class User1 implements Serializable{
private static final long serialVersionUID = 1;
...
}
SerialVerionUID 用于對(duì)象的版本控制。也可以在類(lèi)文件中指定 serialVersionUID。不指定serialVersionUID的后果是,當(dāng)你添加或修改類(lèi)中的任何字段時(shí),則已序列化類(lèi)將無(wú)法恢復(fù),因?yàn)闉樾骂?lèi)和舊序列化對(duì)象生成的 serialVersionUID 將有所不同,這時(shí)候?qū)?huì)報(bào)InvalidClassException的錯(cuò)誤。Java 序列化過(guò)程依賴于正確的序列化對(duì)象恢復(fù)狀態(tài)的,并在序列化對(duì)象序列版本不匹配的情況下引發(fā)。
- 序列化時(shí),你希望某些成員不要序列化?你如何實(shí)現(xiàn)它?
有時(shí)候也會(huì)變著形式問(wèn),比如問(wèn)什么是瞬態(tài) trasient 變量, 瞬態(tài)和靜態(tài)變量會(huì)不會(huì)得到序列化等,所以,如果你不希望任何字段是對(duì)象的狀態(tài)的一部分, 然后聲明它靜態(tài)或瞬態(tài)根據(jù)你的需要, 這樣就不會(huì)是在 Java 序列化過(guò)程中被包含在內(nèi)。
public transient String nickName;
比如User類(lèi)中聲明nickName為transient,則不會(huì)序列化該成員變量。
- 如果類(lèi)中的一個(gè)成員未實(shí)現(xiàn)可序列化接口, 會(huì)發(fā)生什么情況?
如果嘗試序列化實(shí)現(xiàn)可序列化的類(lèi)的對(duì)象,但該對(duì)象包含對(duì)不可序列化類(lèi)的引用,則在運(yùn)行時(shí)將引發(fā)不可序列化異常 NotSerializableException。
- 如果類(lèi)是可序列化的, 但其超類(lèi)不是, 則反序列化后從超類(lèi)繼承的實(shí)例變量的狀態(tài)如何?
Java 序列化過(guò)程僅在對(duì)象層級(jí)都是可序列化的類(lèi)中繼續(xù), 即:實(shí)現(xiàn)了可序列化接口。 如果超類(lèi)沒(méi)有實(shí)現(xiàn)可序列化接口,則從超類(lèi)繼承的實(shí)例變量的值將通過(guò)調(diào)用構(gòu)造函數(shù)初始化。
一旦構(gòu)造函數(shù)鏈啟動(dòng), 就不可能停止, 因此, 即使層次結(jié)構(gòu)中更高的類(lèi)成員變量實(shí)現(xiàn)了可序列化接口,也將通過(guò)執(zhí)行構(gòu)造函數(shù)創(chuàng)建,而不再是反序列化得到。
- 是否可以自定義序列化過(guò)程, 或者是否可以覆蓋 Java 中的默認(rèn)序列化過(guò)程?
答案是肯定的, 你可以。我們都知道,對(duì)于序列化一個(gè)對(duì)象需調(diào)用ObjectOutputStream.writeObject(saveThisObject), 并用 ObjectInputStream.readObject() 讀取對(duì)象, 但 Java 虛擬機(jī)為你提供的還有一件事, 是定義這兩個(gè)方法。
如果在類(lèi)中定義這兩種方法, 則 JVM 將調(diào)用這兩種方法, 而不是應(yīng)用默認(rèn)序列化機(jī)制。你可以在此處通過(guò)執(zhí)行任何類(lèi)型的預(yù)處理或后處理任務(wù)來(lái)自定義對(duì)象序列化和反序列化的行為。
需要注意的重要一點(diǎn)是要聲明這些方法為私有方法, 以避免被繼承、重寫(xiě)或重載。由于只有 Java 虛擬機(jī)可以調(diào)用類(lèi)的私有方法, 你的類(lèi)的完整性會(huì)得到保留, 并且 Java 序列化將正常工作。
- 假設(shè)新類(lèi)的超級(jí)類(lèi)實(shí)現(xiàn)可序列化接口, 如何避免新類(lèi)被序列化?
對(duì)于序列化一個(gè)對(duì)象需調(diào)用 ObjectOutputStream.writeObject(saveThisObject), 并用ObjectInputStream.readObject() 讀取對(duì)象, 但 Java 虛擬機(jī)為你提供的還有一件事, 是定義這兩個(gè)方法。如果在類(lèi)中定義這兩種方法, 則 JVM 將調(diào)用這兩種方法, 而不是應(yīng)用默認(rèn)序列化機(jī)制。你可以在此處通過(guò)執(zhí)行任何類(lèi)型的預(yù)處理或后處理任務(wù)來(lái)自定義對(duì)象序列化和反序列化的行為。
- 在 Java 中的序列化和反序列化過(guò)程中使用哪些方法?
考察你是否熟悉readObject() 、writeObject()、readExternal()和 writeExternal()的用法。Java 序列化由java.io.ObjectOutputStream類(lèi)完成。該類(lèi)是一個(gè)篩選器流, 它封裝在較低級(jí)別的字節(jié)流中, 以處理序列化機(jī)制。要通過(guò)序列化機(jī)制存儲(chǔ)任何對(duì)象, 我們調(diào)用ObjectOutputStream.writeObject(savethisobject), 若反序列化該對(duì)象, 我們使用ObjectInputStream.readObject()方法。
調(diào)用writeObject()方法在 java 中觸發(fā)序列化過(guò)程。關(guān)于 readObject()方法, 需要注意的一點(diǎn)很重要一點(diǎn)是, 它用于從持久性讀取字節(jié), 并從這些字節(jié)創(chuàng)建對(duì)象, 并返回一個(gè)對(duì)象, 該對(duì)象需要類(lèi)型強(qiáng)制轉(zhuǎn)換為正確的類(lèi)型。
Java的序列化機(jī)制針對(duì)枚舉類(lèi)型是特殊處理的。簡(jiǎn)單來(lái)講,在序列化枚舉類(lèi)型時(shí),只會(huì)存儲(chǔ)枚舉類(lèi)的引用和枚舉常量的名稱。隨后的反序列化的過(guò)程中,這些信息被用來(lái)在運(yùn)行時(shí)環(huán)境中查找存在的枚舉類(lèi)型對(duì)象。
Parcelable Android獨(dú)有的序列化方案
Parcelable是Android為我們提供的序列化的接口,跟Serializeble相比,Parcelable相使用相對(duì)復(fù)雜一些,但效率相對(duì)Serializable也高很多。
| Serializable | Parcelable |
|---|---|
| 通過(guò)IO對(duì)硬盤(pán)操作,速度較慢 | 直接在內(nèi)存操作,效率高,性能好 |
| 大小不受限制 | 一般不能超過(guò)1M,修改內(nèi)核也只能4M |
| 大量使用反射,產(chǎn)生內(nèi)存碎片 |
先來(lái)看Parcelable的定義:
public interface Parcelable
{
//內(nèi)容描述接口,基本不用管
public int describeContents();
//寫(xiě)入接口函數(shù),打包
public void writeToParcel(Parcel dest, int flags);
//讀取接口,目的是要從Parcel中構(gòu)造一個(gè)實(shí)現(xiàn)了Parcelable的類(lèi)的實(shí)例處理。因?yàn)閷?shí)現(xiàn)類(lèi)在這里還是不可知的,所以需要用到模板的方式,繼承類(lèi)名通過(guò)模板參數(shù)傳入
//為了能夠?qū)崿F(xiàn)模板參數(shù)的傳入,這里定義Creator嵌入接口,內(nèi)含兩個(gè)接口函數(shù)分別返回單個(gè)和多個(gè)繼承類(lèi)實(shí)例
public interface Creator<T>
{
public T createFromParcel(Parcel source);
public T[] newArray(int size);
}
}
我們需要實(shí)現(xiàn)Parcelable接口,并實(shí)現(xiàn)describeContents()和writeToParcel(Parcel dest, int flags)兩個(gè)方法。writeToParcel()是讀取接口,目的是要從Parcel中構(gòu)造一個(gè)實(shí)現(xiàn)了Parcelable的類(lèi)的實(shí)例處理。因?yàn)閷?shí)現(xiàn)類(lèi)在這里還是不可知的,所以需要用到模板的方式,繼承類(lèi)名通過(guò)模板參數(shù)傳入。
所以,實(shí)現(xiàn)Parcelable步驟如下
implements Parcelable
重寫(xiě)writeToParcel方法,將你的對(duì)象序列化為一個(gè)Parcel對(duì)象,即:將類(lèi)的數(shù)據(jù)寫(xiě)入外部提供的Parcel中,打包需要傳遞的數(shù)據(jù)到Parcel容器保存,以便從 Parcel容器獲取數(shù)據(jù)
重寫(xiě)describeContents方法,內(nèi)容接口描述,默認(rèn)返回0就可以
實(shí)例化靜態(tài)內(nèi)部對(duì)象CREATOR實(shí)現(xiàn)接口Parcelable.Creator
public static final Parcelable.Creator<T> CREATOR
注:其中public static final一個(gè)都不能少,內(nèi)部對(duì)象CREATOR的名稱也不能改變,必須全部大寫(xiě)。需重寫(xiě)本接口中的兩個(gè)方法:createFromParcel(Parcel in) 實(shí)現(xiàn)從Parcel容器中讀取傳遞數(shù)據(jù)值,封裝成Parcelable對(duì)象返回邏輯層,newArray(int size) 創(chuàng)建一個(gè)類(lèi)型為T(mén),長(zhǎng)度為size的數(shù)組,僅一句話即可(return new T[size]),供外部類(lèi)反序列化本類(lèi)數(shù)組使用。
public class Course implements Parcelable {
private String name;
private float score;
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.name);
dest.writeFloat(this.score);
}
protected Course(Parcel in) {
this.name = in.readString();
this.score = in.readFloat();
}
public static final Parcelable.Creator<Course> CREATOR = new Parcelable.Creator<Course>() {
@Override
public Course createFromParcel(Parcel source) {
return new Course(source);
}
@Override
public Course[] newArray(int size) {
return new Course[size];
}
};
}
簡(jiǎn)而言之:通過(guò)writeToParcel將你的對(duì)象映射成Parcel對(duì)象,再通過(guò)createFromParcel將Parcel對(duì)象映射成你的對(duì)象。也可以將Parcel看成是一個(gè)流,通過(guò)writeToParcel把對(duì)象寫(xiě)到流里面,在通過(guò)createFromParcel從流里讀取對(duì)象,只不過(guò)這個(gè)過(guò)程需要你來(lái)實(shí)現(xiàn),因此寫(xiě)的順序和讀的順序必須一致。
面試相關(guān)
- 反序列化后的對(duì)象,需要調(diào)用構(gòu)造函數(shù)重新構(gòu)造嗎
答:不會(huì), 因?yàn)槭菑亩M(jìn)制直接解析出來(lái)的. 適用的是 Object 進(jìn)行接收再?gòu)?qiáng)轉(zhuǎn), 因此不是原來(lái)的那個(gè)對(duì)象
- 序列前的對(duì)象與序列化后的對(duì)象是什么關(guān)系?是("=="還是equal?是淺復(fù)制還是深復(fù)制?)
答:是一個(gè)深拷貝, 前后對(duì)象的引用地址不同,但是有一個(gè)例外是枚舉,枚舉類(lèi)在被序列化的時(shí)候,存儲(chǔ)起來(lái)的對(duì)象是枚舉的引用和名稱,反序列化的時(shí)候,從運(yùn)行環(huán)境中查找枚舉對(duì)象。
- Android里面為什么要設(shè)計(jì)出Bundle而不是直接用Map結(jié)構(gòu)
答:bundle 內(nèi)部適用的是 ArrayMap, ArrayMap 相比 Hashmap 的優(yōu)點(diǎn)是, 擴(kuò)容方便, 每次擴(kuò)容是原容量的一半, 在[百量] 級(jí)別, 通過(guò)二分法查找 key 和 value (ArrayMap 有兩個(gè)數(shù)組, 一個(gè)存放 key 的 hashcode, 一個(gè)存放 key+value 的 Entry) 的效率要比 hashmap 快很多, 由于在內(nèi)存中或者 Android 內(nèi)部傳輸中一般數(shù)據(jù)量較小, 因此用 bundle 更為合適。
- SerialVersionID的作用是什么?
答:用于數(shù)據(jù)的版本控制, 如果反序列化后發(fā)現(xiàn) ID 不一樣, 認(rèn)為不是之前序列化的對(duì)象。
- Android 中 intent/bundle 的通信原理以及大小限制?
答: Android 中的 bundle 實(shí)現(xiàn)了 parcelable 的序列化接口, 目的是為了在進(jìn)程間進(jìn)行通訊, 不同的進(jìn)程共享一片固定大 小的內(nèi)存, parcelable 利用 parcel 對(duì)象的 read/write 方法, 對(duì)需要傳遞的數(shù)據(jù)進(jìn)行內(nèi)存讀寫(xiě), 因此這一塊共享內(nèi)存不能 過(guò)大, 在利用 bundle 進(jìn)行傳輸時(shí), 會(huì)初始化一個(gè) BINDER_VM_SIZE 的大小 = 1 * 1024 * 1024 - 4096 * 2, 即便通過(guò) 修改 Framework 的代碼, bundle 內(nèi)核的映射只有 4M, 最大只能擴(kuò)展到 4M.
- 為何Intent不能直接在組件間傳遞對(duì)象而要通過(guò)序列化機(jī)制?
答: 因?yàn)?Activity 啟動(dòng)過(guò)程是需要與 AMS 交互, AMS 與 UI 進(jìn)程是不同一個(gè)的, 因此進(jìn)程間需要交互數(shù)據(jù), 就必須序列化。
- 序列化與持久化的關(guān)系和區(qū)別是什么?
答: 序列化是為了進(jìn)程間數(shù)據(jù)交互而設(shè)計(jì)的, 持久化是為了把數(shù)據(jù)存儲(chǔ)下來(lái)而設(shè)計(jì)的。