Java序列化機(jī)制

Java序列化機(jī)制


序列化和反序列化

Java序列化是Java內(nèi)建的數(shù)據(jù)(對象)持久化機(jī)制,通過序列化可以將運行時的對象數(shù)據(jù)按照特殊的格式存儲到文件中,以便在將來重建對象。

Java序列化是Java IO的一部分,需要ObjectStream提供支持。支撐序列化機(jī)制的基礎(chǔ)是運行期對對象的信息(如類信息)和數(shù)據(jù)(如成員域)的讀取和存儲,從而將數(shù)據(jù)的字節(jié)流寫入文件或字節(jié)數(shù)組中。反序列化過程則是讀取字節(jié)流中對象的描述信息和數(shù)據(jù),通過指定類的構(gòu)造器和持久化的數(shù)據(jù)動態(tài)地重建出對象。

當(dāng)然,對于一般對象而已,重建后的對象和被序列化到文件中的對象,不會是同一個了(地址),因為可能都不在同一臺計算機(jī)中,序列化中的“內(nèi)存地址”這個對象相等的標(biāo)志,變成了對象的序列號,這也是序列化的名稱由來。不過對于枚舉對象,因為在反序列化過程做了特殊的處理,保證了對象的唯一性。

需要注意的是,序列化是保存對象的狀態(tài),和類狀態(tài)無關(guān),所以它不會讀取靜態(tài)數(shù)據(jù)。


序列化和反序列化的序列號

序列號是關(guān)聯(lián)到對象的,是內(nèi)部流處理的機(jī)制,外部是不可見的。

  1. 序列化
  • 序列化的每個對象對應(yīng)一個序列號,這個序列號是唯一對應(yīng)一個對象的,相當(dāng)于對象內(nèi)存地址的作用;
  • 對于每個首次遇到的對象,會將其數(shù)據(jù)存儲到輸出流中;
  • 對于相同的對象(這個相同的判斷是通過地址),會引用之前的對象的序列號,相當(dāng)于引用一個地址;
  1. 反序列化(這個過程序列號就是對象的唯一標(biāo)識符了)
  • 對于首次遇到的序列號,會根據(jù)流中的信息構(gòu)建一個Java對象;
  • 對于與之前的序列號相同的序列號,直接引用之前構(gòu)建的對象(這里是對象的內(nèi)存地址)

序列化的版本號

序列化的版本號是在類中定義的private static final long serialVersionUID,這個字段用來標(biāo)示類的版本,它是數(shù)據(jù)域類型和方法簽名信息通過SHA算法取到的指紋,采用了SHA碼的前8個字節(jié),主要用在反序列化過程中的類的校驗。如果在反序列化過程中,當(dāng)前類的serialVersionUID和序列化文件中的serialVersionUID不一致,那么就無法從流中構(gòu)建對象,并拋出異常。
那么什么時候這個serialVersionUID會變呢?
主要有下面兩種情況

  • 在實現(xiàn)Serializable接口時,沒有定義serialVersionUID屬性,但在序列化之后,修改了類結(jié)構(gòu);
  • 手動修改了serialVersionUID的值;

serialVersionUID是用來應(yīng)對類在序列化之后類發(fā)生了變更的情況。對于第一個情況,在定義可序列化類時沒有定義serialVersionUID,不會影響序列化過程,因為在序列化過程中會自動生成一個寫入到流中,如果在之后沒有修改類的任何域或方法,反序列化是沒有問題的,(因為生成指紋的基礎(chǔ)沒變,自動生成的指紋即serialVersionUID是一致的)。但是如果在序列化之后修改了類,反序列化就會失敗,除非手動加上serialVersionUID值和前面自動生成的值一致。查看舊值的方法是:使用jdk的命令serialver <類名>,能拿到他早期版本的版本號。

對于第二種情況,一般而言,除非是有此類需求,不然不會手動去破壞反序列化。


相關(guān)的類、接口

序列化標(biāo)識接口

  • java.io.Externalizable
  • java.io.Serializable

序列化機(jī)制實現(xiàn)類

  • java.io.ObjectInputStream
  • java.io.ObjectOutputStream

實現(xiàn)Serializable接口或者Externalizable接口的才可以被序列化。Serializable接口沒有任何方法,是一個標(biāo)記接口,Externalizable接口是Serializable接口的子接口,擁有兩個公共方法,用來自定義序列化過程。

Serializable和Externalizable的區(qū)別

實現(xiàn)了Externalizable接口的類,需要實現(xiàn)接口的兩個抽象方法,readExternal和writeExternal,需要讀寫哪些數(shù)據(jù)都需要顯式的調(diào)用流的讀寫方法,也就是序列化的任務(wù)從Java內(nèi)建轉(zhuǎn)移到了開發(fā)者,提供了更大自由度的同時,也提高了復(fù)雜度。而且在Externalizable反序列化時,會調(diào)用類的public無參構(gòu)造器,如果類中沒有定義無參構(gòu)造器(沒有重載構(gòu)造器的話,會有默認(rèn)的無參構(gòu)造器的),會拋出異常。后面再詳細(xì)說明此接口的使用。

對象流是序列化的核心,readObject和writeObject方法從文件中反序列化對象和將對象序列化到文件中。


使用默認(rèn)的序列化

以下是實現(xiàn)了Serializable接口的類,main方法演示了對象的序列化和反序列化的使用。

public class SerializableUser implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    private SerializableUser(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public SerializableUser() {
    }

    @Override
    public String toString() {
        return "User  -> name:" + name + " ,age=" + age;

    }

    public static void main(String[] args) throws IOException {
        SerializableUser user = new SerializableUser("harry", 19);
        SerialUtil.writeToFile("ser", user);
        SerializableUser u = (SerializableUser) SerialUtil.readFromFile("ser");
        System.out.println(u);
        // Files.deleteIfExists(Paths.get("ser"));
    }
}

// 工具類

public class SerialUtil {
    public static void writeToFile(String path, Object obj) {
        ObjectOutputStream oos;
        try {
            oos = new ObjectOutputStream(new FileOutputStream(path));
            oos.writeObject(obj);
            oos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static Object readFromFile(String path) {
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream(path));
            return ois.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (ois != null)
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }
        return null;

    }

結(jié)果:

User  -> name:harry ,age=19
序列化文件內(nèi)容:
aced 0005 7372 0026 6a64 6b2e 7465 7374
2e73 6572 6961 6c69 7a61 626c 652e 5365
7269 616c 697a 6162 6c65 5573 6572 0000
0000 0000 0001 0200 0249 0003 6167 654c
0004 6e61 6d65 7400 124c 6a61 7661 2f6c
616e 672f 5374 7269 6e67 3b78 7000 0000
1374 0005 6861 7272 79

自定義序列化

想要修改Serializable接口實現(xiàn)類的序列化邏輯,比如有些字段不需要寫入,或者自己想控制寫入的過程,如想給寫入文件的字節(jié)流加密等等,就不能直接使用對象流提供的read和write方法了。

transient關(guān)鍵字

使用transient關(guān)鍵字修飾的字段,在序列化時會被忽略,不會寫入流中。transient意思是瞬時的,既然不是長久的,就代表不要持久化。

重寫writeObject()和readObject()

在實現(xiàn)類中重寫writeObject和readObject方法可以改變序列化的方法調(diào)用,會調(diào)用類中重寫的方法,而不會再走對象流的方法,實現(xiàn)邏輯是在對象流的writeObject和readObject中通過反射判斷類中是否重寫了方法,然后反射調(diào)用。
一般重寫的邏輯是先調(diào)用流的defaultWriteObject和defaultReadObject方法,然后追加自己的寫入邏輯。這兩個default方法是原本對象流的writeObject和readObject方法會調(diào)用的,所以具備默認(rèn)的讀寫作用??梢詤⒖糀rrayList類的設(shè)計,ArrayList內(nèi)部使用數(shù)組來存儲元素,且數(shù)組是會動態(tài)擴(kuò)容的,如果直接序列化數(shù)組,會序列化很多的空元素即null到流中,既浪費空間也降低了效率,所以在類中將數(shù)組變量標(biāo)注成transient,然后在重寫writeObject和readObect方法,將實際的元素寫入對象流和從對象流中讀出。

readResolve()

這個方法是應(yīng)對enum出現(xiàn)之前設(shè)計的枚舉的代替代碼和單例代碼的措施。
看一段示例:

public class SimulateEnum {

    /**
     * 模擬一個枚舉類,在Java內(nèi)建枚舉出現(xiàn)之前的情況
     */
    static class MyEnum implements Serializable {
        private static final long serialVersionUID = 1L;
        private String name;
        private static int order = 0;
        private int ordernum;

        private MyEnum(String name) {
            this.name = name;
            this.ordernum = order++;
        }

        public String getName() {
            return name;
        }

        public int getOrdernum() {
            return ordernum;
        }

        public static final MyEnum A = new MyEnum("A");
        public static final MyEnum B = new MyEnum("B");
        public static final MyEnum C = new MyEnum("C");
    }

    public static void main(String[] args) throws IOException {
        MyEnum a = MyEnum.C;
        System.out.println(a.getName());
        System.out.println(a.getOrdernum());
        SerialUtil.writeToFile("enum", a);
        MyEnum rebuilda = (MyEnum) SerialUtil.readFromFile("enum");
        System.out.println(rebuilda.getName());
        System.out.println(rebuilda.getOrdernum());
        System.out.println(a == rebuilda);
        Files.deleteIfExists(Paths.get("enum"));
    }
}

在示例中我們模擬了一個枚舉的實現(xiàn),我們定義了私有的構(gòu)造器和幾個作為枚舉對象的靜態(tài)對象,我們期待的是枚舉是安全的,不會再生成除了定義的三個變量之外的其他變量,但是反序列化過程會破壞這種設(shè)計。看結(jié)果:

C
2
C
2
false

因為對象的反序列化會創(chuàng)建新的對象的,即使類的構(gòu)造器是私有的,這會破壞單例模式的設(shè)計。為了去保護(hù)在Java枚舉出現(xiàn)之前的模擬枚舉,對象留提供了一個解決措施——在類中實現(xiàn)readResolve方法。在readResolve方法中攔截新對象的生成,使之返回已有的對象。
我們在MyEnum類中加入以下方法:

private Object readResolve() throws Exception {
            switch (name) {
            case "A":
                return A;
            case "B":
                return B;
            case "C":
                return C;
            default:
                throw new Exception();
            }

        }

再執(zhí)行返回的結(jié)果就是true了。
那么為什么在反序列真正的枚舉的時候就不用再考慮在類中實現(xiàn)readReslove方法呢,原因是ObjectStream對枚舉做了支持。看一下調(diào)用棧。

readObejct()-> readObject0()-> checkResolve(readEnum(unshared))->readEnum():

        String name = readString(false);
        Enum<?> result = null;
        Class<?> cl = desc.forClass();
        if (cl != null) {
            try {
                @SuppressWarnings("unchecked")
                Enum<?> en = Enum.valueOf((Class)cl, name);
                result = en;
            } catch (IllegalArgumentException ex) {
                throw (IOException) new InvalidObjectException(
                    "enum constant " + name + " does not exist in " +
                    cl).initCause(ex);
            }
            if (!unshared) {
                handles.setObject(enumHandle, result);
            }
        }

看一下源碼,實際上和我們自己寫的readResolve里面的意思是差不多的。

Externalizable接口

一個實現(xiàn)Externalizable接口的類表明它是可被外部化的,也是可序列化,只是這個序列化的責(zé)任轉(zhuǎn)移給了外部控制。這個接口的定義:

public interface Externalizable extends java.io.Serializable {
    
    void writeExternal(ObjectOutput out) throws IOException;

    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

兩個抽象方法給開發(fā)者自行實現(xiàn)序列化的處理。
示例:

public class ExternalizableUser implements Externalizable {

    private String name;
    private int age;

    private ExternalizableUser(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public ExternalizableUser() {
    }

    @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();
    }

    @Override
    public String toString() {
        return "User  -> name:" + name + " ,age=" + age;

    }

    public static void main(String[] args) throws IOException {
        ExternalizableUser user = new ExternalizableUser("harry",19);
        SerialUtil.writeToFile("ext", user);
        ExternalizableUser u = (ExternalizableUser) SerialUtil.readFromFile("ext");
        System.out.println(u);
        Files.deleteIfExists(Paths.get("ext"));
    }
}
//User  -> name:harry ,age=19

如果上面示例中的writeExternal和readExternal方法沒有實現(xiàn),那么返回的對象是一個空的對象,數(shù)據(jù)是默認(rèn)值。
序列化的文件內(nèi)容是:

aced 0005 7372 0028 6a64 6b2e 7465 7374
2e73 6572 6961 6c69 7a61 626c 652e 4578
7465 726e 616c 697a 6162 6c65 5573 6572
c8f9 50ef 76db 2f15 0c00 0078 7074 0005
6861 7272 7977 0400 0000 1378 

感興趣的同學(xué)可以去研究一下序列化文件的格式。

The end

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

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

  • JAVA序列化機(jī)制的深入研究 對象序列化的最主要的用處就是在傳遞,和保存對象(object)的時候,保證對象的完整...
    時待吾閱讀 11,203評論 0 24
  • java的序列化機(jī)制支持將對象序列化為本地文件或者通過網(wǎng)絡(luò)傳輸至別處, 而反序列化則可以讀取流中的數(shù)據(jù), 并將其轉(zhuǎn)...
    Ten_Minutes閱讀 767評論 0 1
  • 張小凡蹲在地上扔石頭,他記得自己穿越之前是在泡澡,用綠茶泡澡,清香解乏。泡著泡著他就睡著了!一睜眼,他就發(fā)現(xiàn)自己穿...
    哇呵呵a閱讀 559評論 0 2
  • 2018.4.11 【閱讀打卡】 Day48---【閱讀一小時】 時間管理四象限: 第一優(yōu)先緊急重要工作 第二優(yōu)先...
    Karen娟兒閱讀 333評論 0 1
  • 七律·豐收(新韻) 賀信鶯聲喜氣揚,潮升雪化漲春江; 起飛雛燕戲田地,黃透枇杷綴麥場; 墩就高岡顆粒小,集成經(jīng)典字...
    補(bǔ)缺樓丨胡德棒閱讀 1,776評論 0 2

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