Serializable是怎么一回事

前言

在Java中,Serializable作為一種序列化手段最為方便不過,其使用成本之低,使在完全不了解它原理的情況下,均可正常使用。

需要序列化的場景很多,當涉及如果將數(shù)據(jù)從一個地方,有效地傳輸?shù)搅硪粋€地方,就可涉及到序列化的使用。側(cè)重于目標不同,實現(xiàn)的序列化方式也就不同,Serializable作為出鏡率超高的序列化手段,自然有不同于其他序列化方式的地方。本文也主要講出自己對于Serializable的理解,通過本文,可以得知:

  • 什么是對象序列化
  • Serializable如何實現(xiàn)
  • Serializable如何存儲
  • Serializable有什么使用技巧

如果以上問題你不知道答案,本文或許有些幫助。

什么是對象序列化

簡單來說,對象序列化,就是把運行時的對象信息,按照一定的規(guī)則,翻譯成一串有跡可循的二進制流,然后將此二進制流,傳輸?shù)綇囊环絺鬏數(shù)搅硪环健6诮邮辗剑诮邮艿酱硕M制流之后,可以按照約定好的解析規(guī)則,進行反序列化,將對象信息解析出來,得到有用的數(shù)據(jù)信息。

對象信息,包括Class信息、繼承關(guān)系、訪問權(quán)限、變量類型以及數(shù)值信息等。因此,序列化后得到的二進制流,不僅僅包含了描述一個對象的Class的關(guān)鍵信息,也存儲了具有實際意義的數(shù)值信息。因此,序列化可以描述為——轉(zhuǎn)述對象信息,存儲對象數(shù)據(jù)。

Serializable如何實現(xiàn)

既然如此,那么Serializable是如何實現(xiàn)序列化的呢?

對于使用來說,我們僅需要通過聲明實現(xiàn)Serializable接口,即可使用。如果再順嘴問一下,Serializable怎么實現(xiàn),絕大多數(shù)人都能脫口而出,使用反射使用。如果再深入地問下去,為什么要使用反射實現(xiàn),不少人會啞火。

在進入真正的源碼講解之前,不妨假設(shè),如果讓你來寫Serializable,你會如何實現(xiàn)?

開發(fā)者寫出的對象千差萬別,并且對于Serializable的使用說來,也不會提供更多的信息。但是可以知道,每一個對象的類信息可以用一個對應(yīng)的Class類型來進行描述,其中Class類型包含了一個類所的方法,成員屬性,作用域,訪問權(quán)限,繼承關(guān)系等。每一種類型都是Object的子類,并且在運行時,可以通過對象,訪問到存儲于heep中的對象數(shù)據(jù)。

既然如此,當?shù)媚玫揭粋€將要進行序列化的對象時,可以通過反射的手段提取信息。如果將一個對象視為根節(jié)點,那么一個對象的所有信息,可以粗略地用下面一課樹來表示:


對象信息樹.png

上圖中,Class節(jié)點可以繼續(xù)往下分解,直到為Object類型。

那么剩下的問題,就是如何通過反射,遍歷這棵樹,提取并存儲關(guān)鍵信息了。

ObjectStreamClass

Serializable中,使用ObjectStreamClass來描述一種對象的存在。在ObjectStreamClass中,除了Serializable提供的此對象類型可以實現(xiàn)以達到其他目的的幾個方法外,并不關(guān)心所包含的其他方法。因為對于將要序列化傳輸?shù)膶ο髞碚f,關(guān)心他的數(shù)據(jù)結(jié)構(gòu),以及各屬性的基礎(chǔ)屬性值。

ObjectStreamClass接受Class類型作為參數(shù)進行實例話,并提取各層信息

    private ObjectStreamClass(final Class<?> cl) {
        // 代表的類類型
        this.cl = cl;
        // 類名
        name = cl.getName();
        // 是否是動態(tài)代理產(chǎn)生的類
        isProxy = Proxy.isProxyClass(cl);
        // 是否是Enum
        isEnum = Enum.class.isAssignableFrom(cl);
        // 是否實現(xiàn)了 serializable
        serializable = Serializable.class.isAssignableFrom(cl);
        // 是否實現(xiàn)了 externalizable
        externalizable = Externalizable.class.isAssignableFrom(cl);

        // 父類類型
        Class<?> superCl = cl.getSuperclass();
        // superDesc是一個ObjectStreamClass,代表父類的描述
        superDesc = (superCl != null) ? lookup(superCl, false) : null;
        // 指向自己的ObjectStreamClass
        localDesc = this;

        if (serializable) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    if (isEnum) {
                        // serialVersionUID
                        suid = Long.valueOf(0);
                        // Enum沒有屬性
                        fields = NO_FIELDS;
                        return null;
                    }
                    if (cl.isArray()) {
                        // 為集合的話,沒有屬性
                        fields = NO_FIELDS;
                        return null;
                    }
                    
                    // 獲取serialVersionUID
                    suid = getDeclaredSUID(cl);
                    try {
                        // 獲取此類所有需要被序列化的屬性,即Filed
                        fields = getSerialFields(cl);
                        computeFieldOffsets();
                    } catch (InvalidClassException e) {
                        serializeEx = deserializeEx =
                            new ExceptionInfo(e.classname, e.getMessage());
                        fields = NO_FIELDS;
                    }
                    
                    if (externalizable) {
                        // 實現(xiàn)了Enternalizable則反射獲取構(gòu)造方法
                        cons = getExternalizableConstructor(cl);
                    } else {
                        // 反射獲取構(gòu)造方法
                        cons = getSerializableConstructor(cl);
                        // 反射獲取writeObject方法,實現(xiàn)了則有
                        writeObjectMethod = getPrivateMethod(cl, "writeObject",
                            new Class<?>[] { ObjectOutputStream.class },
                            Void.TYPE);
                        // 反射獲取readObject方法,實現(xiàn)了則有
                        readObjectMethod = getPrivateMethod(cl, "readObject",
                            new Class<?>[] { ObjectInputStream.class },
                            Void.TYPE);
                        // 反射獲取readObjectNoData方法,實現(xiàn)了則有
                        readObjectNoDataMethod = getPrivateMethod(
                            cl, "readObjectNoData", null, Void.TYPE);
                        hasWriteObjectData = (writeObjectMethod != null);
                    }
                    
                    // 反射獲取是否實現(xiàn)了 writeReplace,實現(xiàn)了則有
                    writeReplaceMethod = getInheritableMethod(
                        cl, "writeReplace", null, Object.class);
                    // 反射獲取是否實現(xiàn)了 writeReplace,實現(xiàn)了則有
                    readResolveMethod = getInheritableMethod(
                        cl, "readResolve", null, Object.class);
                    return null;
                }
            });
        } else {
            suid = Long.valueOf(0);
            fields = NO_FIELDS;
        }
        ......
    }

ObjectStreamClass的實例化過程可看到的關(guān)鍵點為:

  • Enum和集合類型是不會收集FIELDS的
  • 會獲取自定義的serialVersionUID,沒有則生成
  • 成員屬性以O(shè)bjectStreamField來表示,通過getSerialFields()來獲取,在此方法中,先通過getDeclaredSerialFields()先嘗試獲取通過變量serialPersistentFields聲明的要進行序列化的成員屬性,如果獲取不到則通過getDefaultSerialFields()反射獲取類型中聲明的成員屬性

看一眼getDefaultSerialFields()

    private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
        // 獲取所有 Field
        Field[] clFields = cl.getDeclaredFields();
        ArrayList<ObjectStreamField> list = new ArrayList<>();
        // static 和 transient 的掩碼
        int mask = Modifier.STATIC | Modifier.TRANSIENT;

        for (int i = 0; i < clFields.length; i++) {
            if ((clFields[i].getModifiers() & mask) == 0) {
                // 添加所有不被聲明為 STATIC或 TRANSIENT 的 FIELD,以O(shè)bjectStreamField來描述
                list.add(new ObjectStreamField(clFields[i], false, true));
            }
        }
        int size = list.size();
        return (size == 0) ? NO_FIELDS :
            list.toArray(new ObjectStreamField[size]);
    }

獲取其他各種信息也主要通過反射來獲取。上文源碼中看到的一些方法:

  • writeObject()
  • readObject()
  • readObjectNoData()
  • writeReplace()
  • readResolve()

即是Serializable提供的一些可自定義操作的方法入口,在下文中會提到。

ObjectStreamField

ObjectStreamClass用來描述一個對象,對象有屬性。ObjectStreamField則描述了屬性信息。

ObjectStreamField的結(jié)構(gòu)相對簡單不少,它只需要描述一個對象的屬性信息

    ObjectStreamField(Field field, boolean unshared, boolean showType) {
        // 反射中代表屬性的類,F(xiàn)ield
        this.field = field;
        this.unshared = unshared;
        // 屬性名
        name = field.getName();
        // 屬性的類類型,因為類的類型信息由ObjectStreamClass保存,因此只要知道類型即可
        Class<?> ftype = field.getType();
        // 此屬性的類型,是基本類型,或者是對象類型
        type = (showType || ftype.isPrimitive()) ? ftype : Object.class;
        // 獲取類標識符
        signature = getClassSignature(ftype).intern();
    }

signature記錄類標識符,類標識符是指用一串字符串來表示一種類型,在當前上下文中為:

  • I: Integer
  • B: Byte
  • J: Long
  • F: Float
  • D: Double
  • S: Short
  • C: Character
  • Z: Boolean
  • V: Void
  • L...: 引用類型

L...用來表示引用類型,假如有類為com.qinx.Example,則類標識符為: L/com/qinx/Example .

HandleTable

HandleTable是Serializable中的緩存池,在序列化或者反序列化是,當遇到同一種信息時,如果緩存池中有緩存信息,則可以減少很多不必要的解析,引用到緩存池的那個信息即可。以序列化時為列,會緩存下來的信息有:

  • Class
  • ObjectStreamClass
  • String
  • Array
  • Enum

HandleTable源碼可不深究,僅了解作用也可。

實際上,有了上面提到ObjectStreamClass、ObjectStreamField、HandleTable以及前面提到的從一個對象中提取信息的思路,Serializable的實現(xiàn)能猜出個雛型。因為,知道了獲取信息的思路,對象和屬性的表示方式,以及過程中減少解析的手段。下一小節(jié)則是源碼過程。

ObjectOutputStream

序列化的入口類為ObjectOutputStream

    public ObjectOutputStream(OutputStream out) throws IOException {
        // 校驗繼承權(quán)限
        verifySubclass();
        // 構(gòu)造類型為BlockDataOutputStream的OutputStream,解析過程中的數(shù)據(jù)會先寫入bout,經(jīng)過一些處理后,寫入out
        bout = new BlockDataOutputStream(out);
        // 緩存
        handles = new HandleTable(10, (float) 3.00);
        // 替換表,在解析過程中,可以通過ObjectOutputStream提供的一些入口,來替換一些對象
        subs = new ReplaceTable(10, (float) 3.00);
        // 如果這個變量為true時,意味著需要重寫writeObjectOverride()
        enableOverride = false;
        // 向bout先寫入魔數(shù)以及版本號
        writeStreamHeader();
        // 設(shè)置BlockDataOutputStream的模式,為true則數(shù)據(jù)顯寫入bout
        bout.setBlockDataMode(true);
        ......
    }

序列化的入口方法為writeObject()

    public final void writeObject(Object obj) throws IOException {
        if (enableOverride) {
        //  enableOverride為前面所說,也就是當自定義ObjectOutputStream并使用無參構(gòu)造時,需要重寫writeObjectOverride()來做序列化工作。
            writeObjectOverride(obj);
            return;
        }
        try {
            writeObject0(obj, false);
        } catch (IOException ex) {
            if (depth == 0) {
            // 說明遍歷信息的方式有錯誤
                writeFatalException(ex);
            }
            throw ex;
        }
    }

一般來說,使用ObjectOutputStream()有參構(gòu)造,走writeObject0()來實現(xiàn)序列化。還記得前面說過的提取對象信息的思路嗎, 這里看到的變量depth,也就是當前遍歷這顆樹的節(jié)點所處的深度。

下面的源碼有點長,耐心看

       boolean oldMode = bout.setBlockDataMode(false);
        depth++;
        try {
            // handle previously written and non-replaceable objects
            int h;
            if ((obj = subs.lookup(obj)) == null) {
                // 處理需要被替換的對象,如果obj不需要被替換,subs.lookup()會返回obj本身,而如果當obj需要被替換,會出現(xiàn)被替換為null的情況
                // 這里是寫入null
                writeNull();
                return;
            } else if (!unshared && (h = handles.lookup(obj)) != -1) {
                // 如果obj已經(jīng)被解析過, 寫入索引信息就好,不需要重復解析
                writeHandle(h);
                return;
            } else if (obj instanceof Class) {
                // Class類型的處理方式,不深入
                writeClass((Class) obj, unshared);
                return;
            } else if (obj instanceof ObjectStreamClass) {
                // ObjectStreamClass類型的處理方式,不深入
                writeClassDesc((ObjectStreamClass) obj, unshared);
                return;
            }

            // check for replacement object
            Object orig = obj;
            Class<?> cl = obj.getClass();
            ObjectStreamClass desc;
            for (;;) {
                // REMIND: skip this check for strings/arrays?
                Class<?> repCl;
                // 拿到當前obj的類型的描述ObjectStreamClass
                desc = ObjectStreamClass.lookup(cl, true);
                if (!desc.hasWriteReplaceMethod() ||
                    (obj = desc.invokeWriteReplace(obj)) == null ||
                    (repCl = obj.getClass()) == cl)
                {
                    // 三個條件滿足一個即可
                    // 1. 此類型沒有實現(xiàn)writeReplace()
                    // 2. 此類型實現(xiàn)了writeReplace()替換對象,但返回了空對象
                    // 3. 此類型實現(xiàn)了writeReplace()并返回了空對象,獲取被替換對象的類型
                    break;
                }
                cl = repCl;
            }
            if (enableReplace) {
                // 如果允許替換對象的話,進到這里
                // 默認情況下replaceObject()返回本是,也就是說,ObjectOutputStream的子類可以重寫replaceObject()來對一些對象進行操作
                Object rep = replaceObject(obj);
                if (rep != obj && rep != null) {
                    // 如果obj被替換成不同類型的對象,通過ObjectOutputStream解析出此類型的信息
                    cl = rep.getClass();
                    desc = ObjectStreamClass.lookup(cl, true);
                }
                // obj賦值為更換后的對象
                obj = rep;
            }

            // 如果發(fā)生替換時,要再此做前面的檢查
            if (obj != orig) {
                // 記錄替換關(guān)系
                subs.assign(orig, obj);
                if (obj == null) {
                // null的情況
                    writeNull();
                    return;
                } else if (!unshared && (h = handles.lookup(obj)) != -1) {          // 類型已被解析過的情況
                    writeHandle(h);
                    return;
                } else if (obj instanceof Class) {
                // Class類型的情況
                    writeClass((Class) obj, unshared);
                    return;
                } else if (obj instanceof ObjectStreamClass) {
                // ObjectStreamClass類型的情況
                    writeClassDesc((ObjectStreamClass) obj, unshared);
                    return;
                }
            }

            // 下面分別是obj為String、集合、枚舉、對象類型時的處理情況
            if (obj instanceof String) {
                writeString((String) obj, unshared);
            } else if (cl.isArray()) {
                writeArray(obj, desc, unshared);
            } else if (obj instanceof Enum) {
                writeEnum((Enum<?>) obj, desc, unshared);
            } else if (obj instanceof Serializable) {
                writeOrdinaryObject(obj, desc, unshared);
            } else {
                if (extendedDebugInfo) {
                    throw new NotSerializableException(
                        cl.getName() + "\n" + debugInfoStack.toString());
                } else {
                    throw new NotSerializableException(cl.getName());
                }
            }
        } finally {
            // 遍歷完了,恢復遍歷深度信息
            depth--;
            bout.setBlockDataMode(oldMode);
        }

概括地說,做了以下事情:

  1. 首先會檢查并處理obj被替換、被解析過、是Class類型、和ObjectStreamClass類型時的情況,此時會通過各自的處理方法,向bout想入信息。Class和ObjectStreamClass的處理情況不是本文的重點,因此不拓展。obj被替換以及被解析過的處理情況寫入代碼較簡單,也不多貼例行代碼
  2. obj沒有被處理過,通過ObjectStreamClass.lookup()拿到它的類型描述。如果ObjectOutputStream重寫了替換方法,則要將obj進行替換,并拿到替換的對象以及它的描述,因為后面的操作是基于真正要被寫入的obj來操作的
  3. 當發(fā)生了替換,重寫執(zhí)行1
  4. 分別處理obj為String、集合、枚舉、以及對象類型時的情況,其中前三者不難,不拓展。重點是對象類型時的處理方式。按照之前說的思路,最終當一個種類型的成員變量不再包含對象類型時,也就不再繼續(xù)往下解析(處理父類的情況當作處理對象類型的情況來處理)
  5. 恢復遍歷深度

因此,關(guān)注的點為ObjectStreamClass.lookup()如果拿到對象描述,以及writeOrdinaryObject()如何處理對象類型的解析。

獲取對象描述ObjectStreamClass

    static ObjectStreamClass lookup(Class<?> cl, boolean all) {
        if (!(all || Serializable.class.isAssignableFrom(cl))) {
            // 如果all為true,所有類型都能拿到對象描述
            // 否則,只有實現(xiàn)了Serializable的類型能拿到對象描述
            return null;
        }
        // 因為對象描述ObjectStreamClass是與Class關(guān)聯(lián)的,并且在JVM類可以被卸載,
        // 因此當類被卸載時,與Class相關(guān)的對象描述也也就無效了
        // 所要移除這層關(guān)系并重新建立關(guān)系
        processQueue(Caches.localDescsQueue, Caches.localDescs);
        // WeakClassKey是WeakReference,弱引用Class
        WeakClassKey key = new WeakClassKey(cl, Caches.localDescsQueue);
        // 獲取EntryFutrue或ObjectStreamClass
        // key實際為Class的hashCode
        Reference<?> ref = Caches.localDescs.get(key);
        Object entry = null;
        if (ref != null) {
            // 獲取實際的引用類型
            entry = ref.get();
        }
        EntryFuture future = null;
        if (entry == null) {
            // entry被回收了
            // 創(chuàng)建EntryFuture,使用軟引用關(guān)聯(lián)
            EntryFuture newEntry = new EntryFuture();
            Reference<?> newRef = new SoftReference<>(newEntry);
            do {
                if (ref != null) {
                    Caches.localDescs.remove(key, ref);
                }
                // 當?shù)谝淮谓馕鲞@個類型時,或者entry被回收時,必定拿不到entry,
                // 因此先將key與EntryFuture相關(guān)聯(lián)占位
                ref = Caches.localDescs.putIfAbsent(key, newRef);
                if (ref != null) {
                    entry = ref.get();
                }
            // 下面的判斷條件,是要保證ref弱引用到的對象沒有被卸載
            } while (ref != null && entry == null);
            if (entry == null) {
                future = newEntry;
            }
        }

        if (entry instanceof ObjectStreamClass) {
        // 如果拿到ObjectStreamClass,直接返回
            return (ObjectStreamClass) entry;
        }
        if (entry instanceof EntryFuture) {
        // 如果走這里,說明是第一次獲取次類型的描述,或者描述已經(jīng)被卸載
            future = (EntryFuture) entry;
            if (future.getOwner() == Thread.currentThread()) {
              // 如果是當前線程創(chuàng)建的,置空
              entry = null;
            } else {
             // 如果是其他線程創(chuàng)建的,獲取到其他線程創(chuàng)建的結(jié)果
                entry = future.get();
            }
        }
        if (entry == null) {
            try {
            // 說明類其他線程并未創(chuàng)建,創(chuàng)建對象描述
                entry = new ObjectStreamClass(cl);
            } catch (Throwable th) {
                entry = th;
            }
            if (future.set(entry)) {
                // 說明由當前線程創(chuàng)建的對象描述可用
                // 與Class建立關(guān)聯(lián)
                Caches.localDescs.put(key, new SoftReference<Object>(entry));
            } else {
                // 說明當前線程穿件的對象描述不可用,其它線程已經(jīng)建立好了
                // 使用其他線程創(chuàng)建的對象描述
                entry = future.get();
            }
        }

        ......
    }

lookup()方法最終目的,是拿到一個可用的對象描述:

  • 在開始階段,在all為false的情況下,會檢查此類型是否實現(xiàn)了Serializable,實現(xiàn)了才可進行下一步
  • 如果能拿到對象描述,需要檢查是否可能,因為類可能被卸載過,描述信息失效,需要重新建立
  • 拿不到可用的對象描述,只需要通過 ObjectStreamClass(final Class<?> cl)就可以拿到可用的對象描述。但是實際情況要復雜一些,因為在此時此刻,很可能有多個線程在序列化解析同一類型,因此需要考慮并發(fā)問題
  • 在處理并發(fā)問題時,會使用EntryFuture來對Class的關(guān)聯(lián)進行占位,EntryFuture中保存了當前線程。然后,再次從緩存里獲取EntryFuture,EntryFuture可用。此時,雖然之前使用當前線程使用了自己創(chuàng)建的EntryFuture來進行占位,但是不代表能占位成功,因此再次從緩存里獲取得到的EntryFuture就是真正占位成功的對象。再從EntryFuture獲取對象描述entry。需要注意的是,當前階段保證了所有線程拿到同一個EntryFuture,但是里面的entry還不一定存在。因此這種情況下,會去嘗試對象描述新建使用EntryFuture.(set)關(guān)聯(lián)。關(guān)聯(lián)上,說明可用;關(guān)聯(lián)不上,說明其他線程已經(jīng)新建關(guān)聯(lián)了,EntryFuture.get()拿到真正的對象描述即可

寫入對象信息

    private void writeOrdinaryObject(Object obj,
                                     ObjectStreamClass desc,
                                     boolean unshared)
        throws IOException
    {
        ......
        try {
            // 檢查obj是不是可以進行序列化
            desc.checkSerialize();
            // 寫入信息,TC_OBJECT值為0x73,代表要寫入一個新對象
            bout.writeByte(TC_OBJECT);
            // 寫入對象描述信息
            writeClassDesc(desc, false);
            // 如果unshared為true的話,每次都會重新解析出對象描述,
            // 正常情況為false,因此對象描述可以被緩存復用
            handles.assign(unshared ? null : obj);
            if (desc.isExternalizable() && !desc.isProxy()) {
                // 實現(xiàn)了Externalizable()的情況
                writeExternalData((Externalizable) obj);
            } else {
                // 實現(xiàn)了Serialazable的情況,前面已經(jīng)寫入了類型信息
                // 接下來要寫入數(shù)據(jù)信息
                writeSerialData(obj, desc);
            }
        } finally {
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }
    }

對象信息包含兩方面,類型信息以及數(shù)據(jù)信息,這兩種信息都會存在于序列化后的二進制流里。類型信息通過writeClassDesc()來寫入,其中包含四種情況的寫入規(guī)則:

  • 描述為null時
  • 描述被緩存時
  • 描述的類型時動態(tài)代理產(chǎn)生時
  • 描述還未曾被寫入時

這里不深入writeClassDesc(),深入下去是具體的二進制流寫入規(guī)則。

數(shù)據(jù)信息則是通過writeSerialData()寫入

    private void writeSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
        for (int i = 0; i < slots.length; i++) {
            ObjectStreamClass slotDesc = slots[i].desc;
            if (slotDesc.hasWriteObjectMethod()) {
              // 前面有提到過,對象有一個可以操作的方法入口,writeObject()
              // 如果對象實現(xiàn)了這個方法,這里就會反射用 writeObject()
              ....
            } else {
                // 一般情況下,執(zhí)行這個方法
                defaultWriteFields(obj, slotDesc);
            }
        }
    }

注釋已解釋清楚,直接看defaultWriteFields()

  private void defaultWriteFields(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        Class<?> cl = desc.forClass();
        // 做類型校驗,保證從Obj獲得的對象描述正確
        if (cl != null && obj != null && !cl.isInstance(obj)) {
            throw new ClassCastException();
        }

        ......
        
        // 獲取所有成員變量,在對象描述實例化時,已經(jīng)完成了解析
        ObjectStreamField[] fields = desc.getFields(false);
        // 之后對象的屬性值存在這里
        Object[] objVals = new Object[desc.getNumObjFields()];
        // 屬性個數(shù)
        int numPrimFields = fields.length - objVals.length;
        // 反射獲取屬性值
        desc.getObjFieldValues(obj, objVals);
        for (int i = 0; i < objVals.length; i++) {
            if (extendedDebugInfo) {
                debugInfoStack.push(
                    "field (class \"" + desc.getName() + "\", name: \"" +
                    fields[numPrimFields + i].getName() + "\", type: \"" +
                    fields[numPrimFields + i].getType() + "\")");
            }
            try {
                // 解析為對象類型的屬性
                writeObject0(objVals[i],
                             fields[numPrimFields + i].isUnshared());
            } finally {
                if (extendedDebugInfo) {
                    debugInfoStack.pop();
                }
            }
        }
    }

對象描述里,存有一種類型下,所有的成員變量信息。拿到這些信息后,又依次遍歷調(diào)用writeObject0()解析成員變量。這里需要注意,只有對象類型才會被遍歷解析,而基本類型能直接確定。在上面的代碼 desc.getNumObjFields() 處,就是獲取類的為對象類型的成員變量數(shù),此數(shù)值在對象描述實例化階段,解析出屬性后,通過computeFieldOffsets()計算。

回到writeObject0()后,就是熟悉的內(nèi)容了。

Serializable實現(xiàn)小結(jié)

  1. 將對象當成一棵樹,將類型、父類、成員變量視為節(jié)點,遍歷這顆樹獲取信息
  2. 默認情況下,通過ObjectOutputStream.writeObject0(),使用系統(tǒng)提供的方式解析信息;也可以通過重寫writeObjectOverride()來按照自己的規(guī)則解析
  3. 通過反射解析出對象描述、屬性的描述,其中成員變量的描述存于對象描述中
  4. 會以HandleTable來緩存解析信息,包括Class、ObjectStreamClass、String、Array、Enum
  5. 如果ObjectOutputStream重寫了replaceObject(),可以在序列化過程中,替換某些對象
  6. 寫入對象描述信息、為基本類型的成員信息,通過2-6過程,解析為對象類型的成員屬性,直到某一類為Object了型或者所有成員變量為基本類型,樹收斂。

存儲

序列化最終的解決,是得到一組有規(guī)則的二進制流。而這部分的寫入信息,分布在序列化過程代碼中的邊邊角角,如果要把這部分信息的相關(guān)代碼都貼出來,不免閱讀起來失去重心,也沒有營養(yǎng)。因此這部分的內(nèi)容不會看到實際的寫入代碼。

如果曾了解過Class文件結(jié)構(gòu),這部分內(nèi)容則非常容易消化,不了解也不妨礙理解。
Class 文件解析

簡單來說,序列化出的二進制流,需要包含所序列化對象的類型描述、父類、屬性描述、屬性值信息,并且最終目的是要進行傳輸以反序列化后形成有效的數(shù)據(jù)。因此這個二進制流是緊湊的,不攜帶多余信息,又應(yīng)該的安全的,不遺漏任何信息。除了描述、父類、屬性描述、屬性值這些必備的不可避免的信息外,序列化與反序列化雙方必須遵守閱讀此二進制流的規(guī)則,加上一些助記、索引信息,最終形成一張攜帶信息的索引表。

寫個例子

public class Phone implements Serializable {
    private Card card1;
    private Card card2;
    private String brand;
    private String color;
    private int price;
    .......
}

public class Card implements Serializable {
    private String number;
    ......
}

public static void main(String[] args) {
    String FILE_PATH = "info.txt";

    try {
        File f = new File(FILE_PATH);
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(FILE_PATH));

        Phone phone = new Phone();
        phone.setColor("0xFFFFFF");
        phone.setBrand("OnePlus");
        phone.setPrice(3000);

        Card card1 = new Card("12345678901");
        Card card2 = new Card("98765432109");
        phone.setCard1(card1);
        phone.setCard2(card2);

        out.writeObject(phone);
        out.flush();
        out.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

上面代碼phone序列化后,寫入文件中,再借用二進制工具打開這個文件后,有如下信息


phone二進制信息.jpg

上面將二進制數(shù)據(jù)轉(zhuǎn)為了十六進制,每一個位置即是一個字節(jié)碼。如果沒有工具,可以用ByteArrayOutputSteam來接收流,然后配合DadatypeConverter也可以在控制臺輸出十六進制內(nèi)容。

加上閱讀規(guī)則,就可以閱讀以上信息。下圖是我看源碼過程中讀出的閱讀規(guī)則。


數(shù)據(jù)格式.png

圖中的含義為:

  • 帶顏色方形代表一種區(qū)塊
  • 顏色后面的一整塊結(jié)構(gòu),代表這種顏色區(qū)塊可能具有的結(jié)構(gòu)
  • 數(shù)字代表占多少個字節(jié)

上面所給出的閱讀規(guī)則不代表所有的規(guī)則,如果對更多的規(guī)則感興趣,可以參考:
Serializable Specification

如果你的例子不同,可以跟上圖去閱讀。下面是對于當前例子序列化出的二進制的具體閱讀解釋。其中很多常量可以在ObjectStreamConstants中找到,如果需要自己跟寫入規(guī)則,以O(shè)bjectStreamConstants來索引源碼會有幫助。再附上當前情況的HandleTable表便于查看


HandleTable.jpg

下面每一行十六個字節(jié)碼

 AC ED(魔數(shù)) 00 05(版本) 73(新對象) 72(新的對象描述) 00 05(類名長度) 50 68 6F 6E 65(代表Phone的ascil碼) 79 52 E9
 
 A2 1A 63 D9 5C(Phone的serialVersionUID) 02(代表實現(xiàn)了Serializable) 00 05(屬性數(shù)量為5) 49(屬性類型為I) 00 05(屬性名長度為5) 70 72 69 63 65(代表price的ascil碼)
 
 4C(屬性類型為L) 00 05(屬性名長度為5) 62 72 61 6E 64(代表brand的ascil碼) 74(新的String) 00 12(類名字面量長度為18) 4C 6A 61 76 61
 
 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B(代表Ljava/lang/String;的ascil碼) 4C(屬性類型為L) 00 05(屬性名長度為5)
 
 63 61 72 64 31(代表card1的ascil碼) 74(新的String,將指向的是后面屬性類型的字面量) 00 06(類名字面量長度為6) 4C 43 61 72 64 3B(LCard;的ascil碼) 4C(屬性類型為L) 00
 
 05(屬性名長度5) 63 61 72 64 32(代表card2的ascil碼) 71(已寫入標記) 00 7E 00 02(這里是0x7E0000 + 0x02,前者代表緩存索引標記,后者代表位置,因為按照Int寫入,占四字節(jié),所以前面有00) 4C(屬性類型為L) 00 05(屬性名長度為5) 63 6F
 
 6C 6F 72(color的ascil碼) 71(已寫入標記) 00 7E 00 01(緩存表第一個位置) 78(對象結(jié)束標記) 70(空對象) 00 00 0B B8(int,為3000,price的值) 74(新String) 00
 
 07(字面量長度7) 4F 6E 65 50 6C 75 73(OnePlus的ascil碼) 73(新對象) 72(新的對象描述) 00 04(類名) 43 61 72 64(Card的ascil碼)
 
 A0 66 51 7E C3 D5 7D 4A(Card的serialVersionUID) 02(代表實現(xiàn)了Serializable) 00 01(屬性數(shù)量為1) 4C(屬性類型為L) 00 06(屬性名長度為6) 6E 75
 
 6D 62 65 72(number的ascil碼) 71(已寫入標記) 00 7E 00 01(HandleTable第一個位置) 78(對象結(jié)束) 70(對象為空) 74(新的String) 00 0B(值字面量長度為11) 31 32
 
 33 34 35 36 37 38 39 30 31 73(1245678901的ascil碼) 71(已寫入標記) 00 7E 00 05(HandleTable第5個位置) 74(新String)
 
 00 0B(值字面量長度11) 39 38 37 36 35 34 33 32 31 30 39(98765432109的acill碼) 74(新String) 00 08(值字面量長度8)
 
 30 78 46 46 46 46 46 46(0xFFFFFF的ascil碼)

如有錯誤望指出。

使用技巧

前面有提到,Serializable的序列化機制里,提供了一些操作入口。在對象側(cè),有

  • writeObject()
  • readObject()
  • readObjectNoData()
  • writeReplace()
  • readResolve()

以及serialPersistentFields

在解析測,有:

  • writeObjectOverride()
  • replaceObject()

下面是以上一些操作的例子

serialPersistentFields

對象可以實現(xiàn)serialPersistentFields,用來聲明序列化規(guī)則,比如,在上面的Phone代碼中添加

    private static final ObjectStreamField[] serialPersistentFields = {
            new ObjectStreamField("brand", String.class),
            new ObjectStreamField("card1", Card.class),
    };

再次進行序列化,只有brand和card1的信息被包含了,其他屬性的信息沒有錄入。


serialPersistentFields例子.jpg

serialPersistentFields聲明了序列化哪些屬性,而transient聲明了不序列化哪些屬性,這里就不做驗證。關(guān)于serialPersistentFields的具體代碼,可見ObjectSteamClass.getSerialFields() -> ObjectSteamClass.getDeclaredSerialFields()

writeObject

對象可以聲明writeObject(),可以在屬性被序列化之前進行操作。可以在Phone中加入代碼

    private void writeObject(java.io.ObjectOutputStream steam) throws java.io.IOException{
        // 咱們的手機打折了
        price = price * 7 / 10;
        // 繼續(xù)使用默認序列化流程
        steam.defaultWriteObject();
    }
writeObject例子.jpg

原先的代碼里,price設(shè)置為3000,在序列化前被打了7折,變成了2100,對應(yīng)的16進制就是 0x00000834。在實際的場景里,這里可以用于對具體數(shù)據(jù)加密操作。既然有writeObject()做預處理,那么就會有readObject()做解預處理, 這里過程體現(xiàn)在反序列化中,這里就不做例子了。

writeObject代碼見 ObjectOutputStraem.writeOrdinaryObject() -> writeSerialData()

writeReplace

對象可以實現(xiàn)writeReplace(),在獲取對象描述前,可對對象進行替換。代碼位于ObjectOutputStraem.writeObject0()。而在那段代碼中,可以看出兩層信息:

  1. 可以在這里對對象里的屬性值進行修改
  2. 可以替換整個對象

先看第一種情況,在Phone中加入代碼

    private Object writeReplace(){
        Card replaceCard = new Card("super card!!!");
        card1 = replaceCard;
        return this;
    }

將card1替換為別的卡,下面是序列化后的二進制流


writeReplace例子1.jpg

card1已經(jīng)被替換

將剛才的代碼更換,看第二種情況

    private Object writeReplace(){
        return "all phone sale out";
    }

將Phone替換為String,下面是二進制流


writeReplace例子2.jpg

Phone已經(jīng)被整個替換。 這里也可以看出,默認的String是不帶serialVersionUID的。

readResolve

readResolve()與writeReplace()類似,也可以替換對象。readResolve()是在反序列化過程中,解析出對象后,對這個對象進行再操作并返回一個新的對象。這里就不做例子。

readObjectNoData

當序列化的過程中,如果這個對象所有的屬性都沒有值,可以實現(xiàn)readObjectNoData(),以在這種場景下做一些操作,比如設(shè)置特殊值,這里不做例子,具體的方法為:

private void readObjectNoData(){
}

replaceObject()

可以自定義ObjectOutputStream()來實現(xiàn)replaceObject(),反序列化過程,對一些對象進行替換,如自定義MyObjectOutputStream,并使用此類替換例子中的ObjectOutputStream

public class MyObjectOutputStream extends ObjectOutputStream {

    public MyObjectOutputStream(OutputStream out) throws IOException {
        super(out);
        enableReplaceObject(true);
    }

    @Override
    protected Object replaceObject(Object obj) throws IOException {
        if (obj instanceof Card){
            Card curCard = (Card)obj;
            curCard.setNumber("857857857");
        }
        return super.replaceObject(obj);
    }
}

序列化后的信息為


replaceObject例子.jpg

完成替換。 代碼位于ObjectOutputStream.writeObject0()

writeObjectOverride

如果對于系統(tǒng)提供的序列化過程不滿足,還可以實現(xiàn)writeObjectOverride()來自定義序列化過程。將MyObjectOutputStream的代碼替換為:

public class MyObjectOutputStream extends ObjectOutputStream {

    public MyObjectOutputStream(OutputStream out) throws IOException {
        super(out);
        // enableOverride 是私有的,為了方便這里反射更改
        Class<?> parentCl = getClass().getSuperclass();
        try {
            Field enableOverride = parentCl.getDeclaredField("enableOverride");
            enableOverride.setAccessible(true);
            enableOverride.set(this, true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void writeObjectOverride(Object obj) throws IOException {
        // 模擬自己先序列化過程
        writeBytes("I know the Serializable!!");
    }
}
writeObjectOverride.jpg

完成序列化,代碼位于ObjectOutputStream.writeObject().

總結(jié)

文章到這里就結(jié)束了,以writeObjectOverride()結(jié)尾,也祝大家在看完后,know the Serializable。文章中并未涉及到反序列化的講解,而在知道了序列化原理之后,反序列化可見一斑,想要了解反序列化的原理會很輕松,因此不做陳述。對于serialVersionUID的使用也沒有做例子驗證,大家感興趣的話,可以自行驗證。

回答

1、什么是對象序列化
將對象的類型、屬性、父類、數(shù)據(jù)信息,按照一定規(guī)則解析成二進制,存儲、傳輸,并能通過反序列化得到有意義的對象。

2、Serializable如何實現(xiàn)
以對象為根結(jié)點,將類型、父類、成員變量視為子節(jié)點,遍歷這顆樹,反射獲取信息。并借助ObjectSteamClass、ObjectStreamField、HandleTable記錄過程信息。最后將這些信息按約定規(guī)則輸出二進制流。

3、Serializable如何存儲
存儲于二進制流中。可以將這串流視為攜帶數(shù)據(jù)的索引表

4、Serializable有什么使用技巧
對象側(cè):

  • writeObject(): 序列化前操作對象數(shù)據(jù),比如加密
  • readObject(): 反序列化前操作對象數(shù)據(jù),比如揭秘
  • readObjectNoData():對象沒有屬性數(shù)據(jù)時,可以提供特殊處理
  • writeReplace():替換序列化的對象
  • readResolve(): 替換反序列化后的對象

解析側(cè):

  • writeObjectOverride(): 自定義序列化過程
  • replaceObject():切面替換要序列化的對象

參考

Android 對象序列化之你不知道的 Serializable
Java Serializable原理分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內(nèi)容