Java基礎(chǔ)學(xué)習(xí)之序列化

序列化

將數(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í)候要注意:

  1. 讀寫(xiě)的順序要求一致
  2. 讀寫(xiě)的成員變量個(gè)數(shù)要求一致,否則會(huì)報(bào)EOFException異常
  3. 必須要有一個(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)題
  1. 什么是serialVersionUID ?如果你不定義這個(gè), 會(huì)發(fā)生什么?
  2. 假設(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ā)。

  1. 序列化時(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ì)序列化該成員變量。

  1. 如果類(lèi)中的一個(gè)成員未實(shí)現(xiàn)可序列化接口, 會(huì)發(fā)生什么情況?

如果嘗試序列化實(shí)現(xiàn)可序列化的類(lèi)的對(duì)象,但該對(duì)象包含對(duì)不可序列化類(lèi)的引用,則在運(yùn)行時(shí)將引發(fā)不可序列化異常 NotSerializableException。

  1. 如果類(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)建,而不再是反序列化得到。

  1. 是否可以自定義序列化過(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 序列化將正常工作。

  1. 假設(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ì)象序列化和反序列化的行為。

  1. 在 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步驟如下

  1. implements Parcelable

  2. 重寫(xiě)writeToParcel方法,將你的對(duì)象序列化為一個(gè)Parcel對(duì)象,即:將類(lèi)的數(shù)據(jù)寫(xiě)入外部提供的Parcel中,打包需要傳遞的數(shù)據(jù)到Parcel容器保存,以便從 Parcel容器獲取數(shù)據(jù)

  3. 重寫(xiě)describeContents方法,內(nèi)容接口描述,默認(rèn)返回0就可以

  4. 實(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)

  1. 反序列化后的對(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ì)象

  1. 序列前的對(duì)象與序列化后的對(duì)象是什么關(guān)系?是("=="還是equal?是淺復(fù)制還是深復(fù)制?)

答:是一個(gè)深拷貝, 前后對(duì)象的引用地址不同,但是有一個(gè)例外是枚舉,枚舉類(lèi)在被序列化的時(shí)候,存儲(chǔ)起來(lái)的對(duì)象是枚舉的引用和名稱,反序列化的時(shí)候,從運(yùn)行環(huán)境中查找枚舉對(duì)象。

  1. 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 更為合適。

  1. SerialVersionID的作用是什么?

答:用于數(shù)據(jù)的版本控制, 如果反序列化后發(fā)現(xiàn) ID 不一樣, 認(rèn)為不是之前序列化的對(duì)象。

  1. 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.

  1. 為何Intent不能直接在組件間傳遞對(duì)象而要通過(guò)序列化機(jī)制?

答: 因?yàn)?Activity 啟動(dòng)過(guò)程是需要與 AMS 交互, AMS 與 UI 進(jìn)程是不同一個(gè)的, 因此進(jìn)程間需要交互數(shù)據(jù), 就必須序列化。

  1. 序列化與持久化的關(guān)系和區(qū)別是什么?

答: 序列化是為了進(jìn)程間數(shù)據(jù)交互而設(shè)計(jì)的, 持久化是為了把數(shù)據(jù)存儲(chǔ)下來(lái)而設(shè)計(jì)的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容