深度解析JAVA序列化

fulton發表於2017-05-05

一、序列化

java序列化提供了一個框架,用來將物件編碼成位元組流,並從位元組流編碼中重新構建的物件。將物件編碼為位元組流稱之為序列化,反之將位元組流重建成物件稱之為反序列化。java序列為物件的可持久化及遠端共享提供了一種簡單機制。它實現起來非常方便,只需要實現serializble介面即可。但往往表面的上簡單,隱藏了背後巨大的風險,如果你不瞭解serializable請慎用,因為其中有太多坑,且當你遇到時可能會不知道所措。effective java在序列化一章第一條就提出“謹慎地實現serializable介面”,可見serializable介面背後實現可能隱藏著“坑人的祕密”。
本文參考了網上大量的技術文章和effective java,將從序列化的原理、注意事項及實際應用幾個方面,通過例項來揭開java序列化的面紗。
在這裡補充研究序列化的背景:有一個Object持久化於快取中,經常需要變更欄位(新增或刪除),每次做變更就要更改快取表(擔心不相容帶來問題,一直不確定哪些變更會來問題或引起什麼樣的問題),我希望實現一種序列化,當變更或刪除欄位時不需要變更快取表,這需要達到兩個目的:1、新的類訪問舊的快取時沒問題;2.舊的類訪問新的快取時也沒問題。這個問題雖然在我的需求背景之下得到了快速解決,但還是希望將序列化給出一個充分研究,以備後續信手拈來。

二、序列化實現方式

2.1 簡單示例

一個Person類,具有兩個屬性:name和age;

public class Person implements Serializable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }
 }複製程式碼

生成一個Person的例項p,將期通過ObjectOutputStream寫入檔案,並通過ObjectInputStream讀出來。這是一個完整的序列化/反序列化過程:ObjectOutputStream將p轉化成位元組流寫入檔案,ObjectInputStream將從檔案中讀出的位元組流重新建立newPerson例項。


@Test
public void testSerializable() throws Exception {
    File file = new File("p.dat");
    Person p = new Person("xiaoming", 10);
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
    oos.writeObject(p);
    oos.close();

    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
    Object newPerson  = ois.readObject();
    ois.close();
    System.out.println(newPerson);
}複製程式碼

通過上面的過程,我們可以看出預設的序列化機制對使用者而言是非常簡單的。序列化具體的實現是由ObjectOutputStream完成的;反序列化的具體實現是由ObjectInputStream完成的。那接下來我們就看一下它們具體做了什麼事

2.2 Serializable

在介紹具體實現之前,我們先來看一下Serializable介面,這畢竟是預設情況下的,使用者看到的唯一的東西。

/**
 * Serializability of a class is enabled by the class implementing the
 * java.io.Serializable interface. Classes that do not implement this
 * interface will not have any of their state serialized or
 * deserialized.  All subtypes of a serializable class are themselves
 * serializable.  The serialization interface has no methods or fields
 * and serves only to identify the semantics of being serializable. <p>
 *
 * To allow subtypes of non-serializable classes to be serialized, the
 * subtype may assume responsibility for saving and restoring the
 * state of the supertype's public, protected, and (if accessible)
 * package fields.  The subtype may assume this responsibility only if
 * the class it extends has an accessible no-arg constructor to
 * initialize the class's state.  It is an error to declare a class
 * Serializable if this is not the case.  The error will be detected at
 * runtime. <p>
 *
 * During deserialization, the fields of non-serializable classes will
 * be initialized using the public or protected no-arg constructor of
 * the class.  A no-arg constructor must be accessible to the subclass
 * that is serializable.  The fields of serializable subclasses will
 * be restored from the stream. <p>
 *
 * When traversing a graph, an object may be encountered that does not
 * support the Serializable interface. In this case the
 * NotSerializableException will be thrown and will identify the class
 * of the non-serializable object. <p>
 *
 * Classes that require special handling during the serialization and
 * deserialization process must implement special methods with these exact
 * signatures:
 *
 * <PRE>
 * private void writeObject(java.io.ObjectOutputStream out)
 *     throws IOException
 * private void readObject(java.io.ObjectInputStream in)
 *     throws IOException, ClassNotFoundException;
 * private void readObjectNoData()
 *     throws ObjectStreamException;
 * </PRE>
 *
 * <p>The writeObject method is responsible for writing the state of the
 * object for its particular class so that the corresponding
 * readObject method can restore it.  The default mechanism for saving
 * the Object's fields can be invoked by calling
 * out.defaultWriteObject. The method does not need to concern
 * itself with the state belonging to its superclasses or subclasses.
 * State is saved by writing the individual fields to the
 * ObjectOutputStream using the writeObject method or by using the
 * methods for primitive data types supported by DataOutput.
 *
 * <p>The readObject method is responsible for reading from the stream and
 * restoring the classes fields. It may call in.defaultReadObject to invoke
 * the default mechanism for restoring the object's non-static and
 * non-transient fields.  The defaultReadObject method uses information in
 * the stream to assign the fields of the object saved in the stream with the
 * correspondingly named fields in the current object.  This handles the case
 * when the class has evolved to add new fields. The method does not need to
 * concern itself with the state belonging to its superclasses or subclasses.
 * State is saved by writing the individual fields to the
 * ObjectOutputStream using the writeObject method or by using the
 * methods for primitive data types supported by DataOutput.
 *
 * <p>The readObjectNoData method is responsible for initializing the state of
 * the object for its particular class in the event that the serialization
 * stream does not list the given class as a superclass of the object being
 * deserialized.  This may occur in cases where the receiving party uses a
 * different version of the deserialized instance's class than the sending
 * party, and the receiver's version extends classes that are not extended by
 * the sender's version.  This may also occur if the serialization stream has
 * been tampered; hence, readObjectNoData is useful for initializing
 * deserialized objects properly despite a "hostile" or incomplete source
 * stream.
 *
 * <p>Serializable classes that need to designate an alternative object to be
 * used when writing an object to the stream should implement this
 * special method with the exact signature:
 *
 * <PRE>
 * ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
 * </PRE><p>
 *
 * This writeReplace method is invoked by serialization if the method
 * exists and it would be accessible from a method defined within the
 * class of the object being serialized. Thus, the method can have private,
 * protected and package-private access. Subclass access to this method
 * follows java accessibility rules. <p>
 *
 * Classes that need to designate a replacement when an instance of it
 * is read from the stream should implement this special method with the
 * exact signature.
 *
 * <PRE>
 * ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
 * </PRE><p>
 *
 * This readResolve method follows the same invocation rules and
 * accessibility rules as writeReplace.<p>
 *
 * The serialization runtime associates with each serializable class a version
 * number, called a serialVersionUID, which is used during deserialization to
 * verify that the sender and receiver of a serialized object have loaded
 * classes for that object that are compatible with respect to serialization.
 * If the receiver has loaded a class for the object that has a different
 * serialVersionUID than that of the corresponding sender's class, then
 * deserialization will result in an {@link InvalidClassException}.  A
 * serializable class can declare its own serialVersionUID explicitly by
 * declaring a field named <code>"serialVersionUID"</code> that must be static,
 * final, and of type <code>long</code>:
 *
 * <PRE>
 * ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;
 * </PRE>
 *
 * If a serializable class does not explicitly declare a serialVersionUID, then
 * the serialization runtime will calculate a default serialVersionUID value
 * for that class based on various aspects of the class, as described in the
 * Java(TM) Object Serialization Specification.  However, it is <em>strongly
 * recommended</em> that all serializable classes explicitly declare
 * serialVersionUID values, since the default serialVersionUID computation is
 * highly sensitive to class details that may vary depending on compiler
 * implementations, and can thus result in unexpected
 * <code>InvalidClassException</code>s during deserialization.  Therefore, to
 * guarantee a consistent serialVersionUID value across different java compiler
 * implementations, a serializable class must declare an explicit
 * serialVersionUID value.  It is also strongly advised that explicit
 * serialVersionUID declarations use the <code>private</code> modifier where
 * possible, since such declarations apply only to the immediately declaring
 * class--serialVersionUID fields are not useful as inherited members. Array
 * classes cannot declare an explicit serialVersionUID, so they always have
 * the default computed value, but the requirement for matching
 * serialVersionUID values is waived for array classes.
 *
 * @author  unascribed
 * @see java.io.ObjectOutputStream
 * @see java.io.ObjectInputStream
 * @see java.io.ObjectOutput
 * @see java.io.ObjectInput
 * @see java.io.Externalizable
 * @since   JDK1.1
 */
public interface Serializable {
}複製程式碼

介面本身未實現任何方法,但其註釋值得好好看一下(只翻譯部分,最好自己看原文吧):

  • 一個類的序列化能力是由實現Serializable介面決定的。未實現該介面的類將無法實現序列化和反序列化,實現序列化的類的子類也可以實現序列化。Serializable介面沒有任何方法和屬性,只是一個類可以實現序列化的標誌。
  • 子類實現序列化,父類不實現序列化,此時父類要實現一個無引數構造器,否則會報錯(見坑二)
    遇到不支援序列化的類會丟擲NotSerializableException
    在序列化的過程中需要特殊處理時,可以通過實現writeObject,readObject,readObjectNoData來實現
  • writeObject實現序列化將屬性和值寫入,預設的寫入機制由defaultWriteObject來實現
    readObject實現從資料流中重建對像,預設的讀出機制由defaultReadObject來實現,(This handles the case when the class has evolved to add new fields)而且可以處理類演化(新增欄位)的情況,那刪除一個欄位呢?見坑三.)

  • 如果某個超類不支援序列化,但又不希望使用預設值怎麼辦?實現readObjectNoData
    writeReplace() 方法可以使物件被寫入流以前,用一個物件來替換自己。當序列化時,可序列化的類要將物件寫入流,如果我們想要另一個物件來替換當前物件來寫入流,則可以要實現下面這個方法,方法的簽名也要完全一致:

  • readResolve (常用於單例模式)方法在物件從流中讀取出來的時候呼叫, ObjectInputStream 會檢查反序列化的物件是否已經定義了這個方法,如果定義了,則讀出來的物件返回一個替代物件。同 writeReplace()方法,返回的物件也必須是與它替換的物件相容,否則丟擲 ClassCastException
    serialVersionUID 相關見下面的(相容性)

2.3 ObjectOutputStream

/**
 * Write the specified object to the ObjectOutputStream.  The class of the
 * object, the signature of the class, and the values of the non-transient
 * and non-static fields of the class and all of its supertypes are
 * written.  Default serialization for a class can be overridden using the
 * writeObject and the readObject methods.  Objects referenced by this
 * object are written transitively so that a complete equivalent graph of
 * objects can be reconstructed by an ObjectInputStream.
 *
 * <p>Exceptions are thrown for problems with the OutputStream and for
 * classes that should not be serialized.  All exceptions are fatal to the
 * OutputStream, which is left in an indeterminate state, and it is up to
 * the caller to ignore or recover the stream state.
 *
 * @throws  InvalidClassException Something is wrong with a class used by
 *          serialization.
 * @throws  NotSerializableException Some object to be serialized does not
 *          implement the java.io.Serializable interface.
 * @throws  IOException Any exception thrown by the underlying
 *          OutputStream.
 */
public final void writeObject(Object obj) throws IOException {
    //是否重寫了Object方法
    if (enableOverride) {
        writeObjectOverride(obj);
        return;
    }
    try {
        // 寫入操作的具體實現
        writeObject0(obj, false);
    } catch (IOException ex) {
        if (depth == 0) {
            writeFatalException(ex);
        }
        throw ex;
    }
}複製程式碼

先來對上面的註釋翻譯一下:將一個具體的object寫入ObjectOutputStream.類名、類的簽名(可以理解為類名和UID,雖然不止這些),除non-transient和靜態屬性外屬於和值以及其超類。可以在子類中重寫writeObject 和 readObject 方法,一個例項的多個引用,採用瞬態的寫入方式(坑1參考下面的介紹),因此可以構造出一個完整的類的結構圖。
writeObject0具體實現一個類的寫入,原始碼如下(只保留了關鍵部分):

 摺疊原碼
/**
 * Underlying writeObject/writeUnshared implementation.
 */
private void writeObject0(Object obj, boolean unshared)
    throws IOException
{

       ....
        // remaining cases
        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());
            }
        }
  .....
}複製程式碼

可以看出,支援寫入的有幾種型別,包括String,Array,Enum,和Serializable(這就是實現Serializable的目的),當然原生型別也會以資料塊的形式寫入(其實最終寫入的肯定是原生型別)。
對於Enum型別有必要單獨說一下(見坑四)。
此時我們可能會想知道,到底寫了哪些值(writeOrdinaryObject)

/**
 * Writes representation of a "ordinary" (i.e., not a String, Class,
 * ObjectStreamClass, array, or enum constant) serializable object to the
 * stream.
 */
private void writeOrdinaryObject(Object obj,
                                 ObjectStreamClass desc,
                                 boolean unshared)
    throws IOException
{
    if (extendedDebugInfo) {
        debugInfoStack.push(
            (depth == 1 ? "root " : "") + "object (class \"" +
            obj.getClass().getName() + "\", " + obj.toString() + ")");
    }
    try {
        desc.checkSerialize();

        bout.writeByte(TC_OBJECT);
        writeClassDesc(desc, false);
        handles.assign(unshared ? null : obj);
        if (desc.isExternalizable() && !desc.isProxy()) {
            writeExternalData((Externalizable) obj);
        } else {
            writeSerialData(obj, desc);
        }
    } finally {
        if (extendedDebugInfo) {
            debugInfoStack.pop();
        }
    }
}複製程式碼
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()) {
            PutFieldImpl oldPut = curPut;
            curPut = null;
            SerialCallbackContext oldContext = curContext;

            if (extendedDebugInfo) {
                debugInfoStack.push(
                    "custom writeObject data (class \"" +
                    slotDesc.getName() + "\")");
            }
            try {
                curContext = new SerialCallbackContext(obj, slotDesc);
                bout.setBlockDataMode(true);
                slotDesc.invokeWriteObject(obj, this);
                bout.setBlockDataMode(false);
                bout.writeByte(TC_ENDBLOCKDATA);
            } finally {
                curContext.setUsed();
                curContext = oldContext;
                if (extendedDebugInfo) {
                    debugInfoStack.pop();
                }
            }

            curPut = oldPut;
        } else {
            defaultWriteFields(obj, slotDesc);
        }
    }
}複製程式碼

到此為止,我們知道寫入了一個二進位制資料塊,其中包含類名、簽名、屬性名、屬性型別、及屬性值,當然還有開頭結尾等資料。我們將二進位制轉換為UTF-8後如下

¬í^@^Esr^@)com.sankuai.meituan.meishi.poi.tag.PersonÝ<9f>;<9d><8e>^B°³^B^@^BI^@^CageL^@^Dnamet^@^RLjava/lang/String;xp^@^@^@t^@^Hxiaoming複製程式碼

2.4 ObjectInputStream

理解ObjectOutputStream再來理解ObjectInputStream就簡單很多了,大概過一下

/**
 * Read an object from the ObjectInputStream.  The class of the object, the
 * signature of the class, and the values of the non-transient and
 * non-static fields of the class and all of its supertypes are read.
 * Default deserializing for a class can be overriden using the writeObject
 * and readObject methods.  Objects referenced by this object are read
 * transitively so that a complete equivalent graph of objects is
 * reconstructed by readObject.
 *
 * <p>The root object is completely restored when all of its fields and the
 * objects it references are completely restored.  At this point the object
 * validation callbacks are executed in order based on their registered
 * priorities. The callbacks are registered by objects (in the readObject
 * special methods) as they are individually restored.
 *
 * <p>Exceptions are thrown for problems with the InputStream and for
 * classes that should not be deserialized.  All exceptions are fatal to
 * the InputStream and leave it in an indeterminate state; it is up to the
 * caller to ignore or recover the stream state.
 *
 * @throws  ClassNotFoundException Class of a serialized object cannot be
 *          found.
 * @throws  InvalidClassException Something is wrong with a class used by
 *          serialization.
 * @throws  StreamCorruptedException Control information in the
 *          stream is inconsistent.
 * @throws  OptionalDataException Primitive data was found in the
 *          stream instead of objects.
 * @throws  IOException Any of the usual Input/Output related exceptions.
 */
public final Object readObject()
    throws IOException, ClassNotFoundException
{
    if (enableOverride) {
        return readObjectOverride();
    }

    // if nested read, passHandle contains handle of enclosing object
    int outerHandle = passHandle;
    try {
        Object obj = readObject0(false);
        handles.markDependency(outerHandle, passHandle);
        ClassNotFoundException ex = handles.lookupException(passHandle);
        if (ex != null) {
            throw ex;
        }
        if (depth == 0) {
            vlist.doCallbacks();
        }
        return obj;
    } finally {
        passHandle = outerHandle;
        if (closed && depth == 0) {
            clear();
        }
    }
}複製程式碼

還是看一下注釋:讀關類(類名), 簽名、非瞬態非靜態屬性值和屬性名。
剩下的註解也和ObjectOutputStream基本一致
實際解析的資料是readObject0
readObject0就是按照協議進行解析資料了


private Object readObject0(boolean unshared) throws IOException {
    boolean oldMode = bin.getBlockDataMode();
    if (oldMode) {
        int remain = bin.currentBlockRemaining();
        if (remain > 0) {
            throw new OptionalDataException(remain);
        } else if (defaultDataEnd) {
            /*
             * Fix for 4360508: stream is currently at the end of a field
             * value block written via default serialization; since there
             * is no terminating TC_ENDBLOCKDATA tag, simulate
             * end-of-custom-data behavior explicitly.
             */
            throw new OptionalDataException(true);
        }
        bin.setBlockDataMode(false);
    }

    byte tc;
    while ((tc = bin.peekByte()) == TC_RESET) {
        bin.readByte();
        handleReset();
    }

    depth++;
    try {
        switch (tc) {
            case TC_NULL:
                return readNull();

            case TC_REFERENCE:
                return readHandle(unshared);

            case TC_CLASS:
                return readClass(unshared);

            case TC_CLASSDESC:
            case TC_PROXYCLASSDESC:
                return readClassDesc(unshared);

            case TC_STRING:
            case TC_LONGSTRING:
                return checkResolve(readString(unshared));

            case TC_ARRAY:
                return checkResolve(readArray(unshared));

            case TC_ENUM:
                return checkResolve(readEnum(unshared));

            case TC_OBJECT:
                return checkResolve(readOrdinaryObject(unshared));

            case TC_EXCEPTION:
                IOException ex = readFatalException();
                throw new WriteAbortedException("writing aborted", ex);

            case TC_BLOCKDATA:
            case TC_BLOCKDATALONG:
                if (oldMode) {
                    bin.setBlockDataMode(true);
                    bin.peek();             // force header read
                    throw new OptionalDataException(
                        bin.currentBlockRemaining());
                } else {
                    throw new StreamCorruptedException(
                        "unexpected block data");
                }

            case TC_ENDBLOCKDATA:
                if (oldMode) {
                    throw new OptionalDataException(true);
                } else {
                    throw new StreamCorruptedException(
                        "unexpected end of block data");
                }

            default:
                throw new StreamCorruptedException(
                    String.format("invalid type code: %02X", tc));
        }
    } finally {
        depth--;
        bin.setBlockDataMode(oldMode);
    }
}複製程式碼

三、相容性

java序列化是通過在執行時判斷serialVersionUID來驗證版本的一致性。在進行反序列化時,JVM會把傳過來的位元組流中serialVersionUID與本地相應的實體(類)的serialVersionUID進行對比, 如果相同則是認為一致的,否則就會丟擲異常InvalidClassException。
serialVersionUID有兩種生成方式:預設生成和顯示指定。具體實現方式如下:

/**
 * Adds serialVersionUID if one does not already exist. Call this before
 * modifying a class to maintain serialization compatability.
 */
public static void setSerialVersionUID(CtClass clazz)
    throws CannotCompileException, NotFoundException
{
    // check for pre-existing field.
    try {
        clazz.getDeclaredField("serialVersionUID");
        return;
    }
    catch (NotFoundException e) {}

    // check if the class is serializable.
    if (!isSerializable(clazz))
        return;

    // add field with default value.
    CtField field = new CtField(CtClass.longType, "serialVersionUID",
                                clazz);
    field.setModifiers(Modifier.PRIVATE | Modifier.STATIC |
                       Modifier.FINAL);
    clazz.addField(field, calculateDefault(clazz) + "L");
}複製程式碼

預設生成的UID的值計算方式參考如下原始碼:
可以看出UID的值來源於類的幾個方面:類名(class name)、類及其屬性的修飾符(class modifiers)、 介面及介面順序(interfaces)、屬性(fields)、靜態初始化(static initializer), 構造器(constructors)。也就是說這其中任何一個的改變都會影響UID的值,導致不相容性。

/**
 * Calculate default value. See Java Serialization Specification, Stream
 * Unique Identifiers.
 */
static long calculateDefault(CtClass clazz)
    throws CannotCompileException
{
    try {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        DataOutputStream out = new DataOutputStream(bout);
        ClassFile classFile = clazz.getClassFile();

        // class name.
        String javaName = javaName(clazz);
        out.writeUTF(javaName);

        CtMethod[] methods = clazz.getDeclaredMethods();

        // class modifiers.
        int classMods = clazz.getModifiers();
        if ((classMods & Modifier.INTERFACE) != 0)
            if (methods.length > 0)
                classMods = classMods | Modifier.ABSTRACT;
            else
                classMods = classMods & ~Modifier.ABSTRACT;

        out.writeInt(classMods);

        // interfaces.
        String[] interfaces = classFile.getInterfaces();
        for (int i = 0; i < interfaces.length; i++)
            interfaces[i] = javaName(interfaces[i]);

        Arrays.sort(interfaces);
        for (int i = 0; i < interfaces.length; i++)
            out.writeUTF(interfaces[i]);

        // fields.
        CtField[] fields = clazz.getDeclaredFields();
        Arrays.sort(fields, new Comparator() {
            public int compare(Object o1, Object o2) {
                CtField field1 = (CtField)o1;
                CtField field2 = (CtField)o2;
                return field1.getName().compareTo(field2.getName());
            }
        });

        for (int i = 0; i < fields.length; i++) {
            CtField field = (CtField) fields[i];
            int mods = field.getModifiers();
            if (((mods & Modifier.PRIVATE) == 0) ||
                ((mods & (Modifier.STATIC | Modifier.TRANSIENT)) == 0)) {
                out.writeUTF(field.getName());
                out.writeInt(mods);
                out.writeUTF(field.getFieldInfo2().getDescriptor());
            }
        }

        // static initializer.
        if (classFile.getStaticInitializer() != null) {
            out.writeUTF("<clinit>");
            out.writeInt(Modifier.STATIC);
            out.writeUTF("()V");
        }

        // constructors.
        CtConstructor[] constructors = clazz.getDeclaredConstructors();
        Arrays.sort(constructors, new Comparator() {
            public int compare(Object o1, Object o2) {
                CtConstructor c1 = (CtConstructor)o1;
                CtConstructor c2 = (CtConstructor)o2;
                return c1.getMethodInfo2().getDescriptor().compareTo(
                                    c2.getMethodInfo2().getDescriptor());
            }
        });

        for (int i = 0; i < constructors.length; i++) {
            CtConstructor constructor = constructors[i];
            int mods = constructor.getModifiers();
            if ((mods & Modifier.PRIVATE) == 0) {
                out.writeUTF("<init>");
                out.writeInt(mods);
                out.writeUTF(constructor.getMethodInfo2()
                             .getDescriptor().replace('/', '.'));
            }
        }

        // methods.
        Arrays.sort(methods, new Comparator() {
            public int compare(Object o1, Object o2) {
                CtMethod m1 = (CtMethod)o1;
                CtMethod m2 = (CtMethod)o2;
                int value = m1.getName().compareTo(m2.getName());
                if (value == 0)
                    value = m1.getMethodInfo2().getDescriptor()
                        .compareTo(m2.getMethodInfo2().getDescriptor());

                return value;
            }
        });

        for (int i = 0; i < methods.length; i++) {
            CtMethod method = methods[i];
            int mods = method.getModifiers()
                       & (Modifier.PUBLIC | Modifier.PRIVATE
                          | Modifier.PROTECTED | Modifier.STATIC
                          | Modifier.FINAL | Modifier.SYNCHRONIZED
                          | Modifier.NATIVE | Modifier.ABSTRACT | Modifier.STRICT);
            if ((mods & Modifier.PRIVATE) == 0) {
                out.writeUTF(method.getName());
                out.writeInt(mods);
                out.writeUTF(method.getMethodInfo2()
                             .getDescriptor().replace('/', '.'));
            }
        }

        // calculate hash.
        out.flush();
        MessageDigest digest = MessageDigest.getInstance("SHA");
        byte[] digested = digest.digest(bout.toByteArray());
        long hash = 0;
        for (int i = Math.min(digested.length, 8) - 1; i >= 0; i--)
            hash = (hash << 8) | (digested[i] & 0xFF);

        return hash;
    }
    catch (IOException e) {
        throw new CannotCompileException(e);
    }
    catch (NoSuchAlgorithmException e) {
        throw new CannotCompileException(e);
    }
}複製程式碼

顯示指定:

private static final long serialVersionUID = 1L;複製程式碼

那兩種方式使用的情景是什麼呢?我認為應該把握一個判斷原則:是否允許向下相容。
預設方式使用情景:一旦建立則不允許改變
顯示方式使用情景:對類有一定的向下相容性(稍後將具體分析哪些情況相容),當不允許相容時,可以通過改變UID的值在實現。
強烈建議使用顯示指定的方式,以防範潛在的不相容根源,且可以帶來小小的效能提升。

四、坑

(下面的坑都是在指定顯示指定UID並且一致的情況下產生的,非顯示指定UID的坑更多,不再介紹了)

4.1 坑1(多引用寫入)

@Test
public void testSerializable() throws Exception {
    File file = new File("p.dat");
    Person p = new Person("xiaoming", 10);
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
    oos.writeObject(p);
    p.setAge(20);
    oos.writeObject(p);
    oos.close();


    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
    Person p1  = (Person) ois.readObject();
    Person p2  = (Person) ois.readObject();
    ois.close();
    System.out.println(p1.toString() + "name:"+p1.getName() + "age:"+p1.getAge());
    System.out.println(p2.toString() + "name:"+p2.getName() + "age:"+p2.getAge());

}複製程式碼

讀出來的結果

com.sankuai.meituan.meishi.poi.tag.Person@b7f23d9name:xiaomingage:10
com.sankuai.meituan.meishi.poi.tag.Person@b7f23d9name:xiaomingage:10複製程式碼

是不是和希望的不一樣?其實在預設情況下,對於一個例項的多個引用,為了節省空間,只會寫入一次,後面會追加幾個位元組代表某個例項的引用。
我們可能通過rest或writeUnshared方法對一個例項多次寫入,如下:

@Test
public void testSerializable() throws Exception {
    File file = new File("p.dat");
    Person p = new Person("xiaoming", 10);
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
    oos.writeObject(p);
    p.setAge(20);
    oos.reset();
    //oos.writeUnshared(p);
    oos.writeObject(p);
    oos.close();


    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
    Person p1  = (Person) ois.readObject();
    Person p2  = (Person) ois.readObject();
    ois.close();
    System.out.println(p1.toString() + "name:"+p1.getName() + "age:"+p1.getAge());
    System.out.println(p2.toString() + "name:"+p2.getName() + "age:"+p2.getAge());
    System.out.println(p2.toString() + "name:"+p2.getName() + "age:"+p2.getAge());
}複製程式碼

結果如下:

com.sankuai.meituan.meishi.poi.tag.Person@b7f23d9name:xiaomingage:10
com.sankuai.meituan.meishi.poi.tag.Person@61d47554name:xiaomingage:20複製程式碼

4.2 坑2(子父引用序列化)

子類實現序列化,父類不實現序列化

父類是Person,定義一個字類Student

public class Student extends Person implements Serializable  {
    private static final long serialVersionUID = 1L;
    private int studentId;

    public Student(String name, int age, int studentId) {
        super(name,age);
        this.studentId = studentId;
    }

    public int getStudentId() {
        return studentId;
    }

    public void setStudentId(int studentId) {
        this.studentId = studentId;
    }
}複製程式碼

測試程式碼如下:

@Test
public void testSerializable() throws Exception {
    File file = new File("p.dat");
    Student s = new Student( "xiaoming", 10, 1 );
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
    oos.writeObject(s);

    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
    Student s1  = (Student) ois.readObject();
    ois.close();
    System.out.println(s1.toString() + "name:"+s1.getName() + "age:"+s1.getAge() + "height:"+s1.getStudentId());
    System.out.println(s1.toString() + "name:"+s1.getName() + "age:"+s1.getAge() + "height:"+s1.getStudentId());
}複製程式碼

在readObject時丟擲java.io.NotSerializableException異常。
我們更改一下Person,新增一個無引數構造器

public class Student extends Person implements Serializable  {
    private static final long serialVersionUID = 1L;
    private int studentId;

    public Student(String name, int age, int studentId) {
        super(name,age);
        this.studentId = studentId;
    }

    public int getStudentId() {
        return studentId;
    }

    public void setStudentId(int studentId) {
        this.studentId = studentId;
    }
}複製程式碼

結果如下

com.sankuai.meituan.meishi.poi.tag.Student@12405818name:nullage:0height:1複製程式碼

這是因為當父類不可序列化時,需要呼叫預設無參構造器初始化屬性的值。

物件引用

public class Student implements Serializable  {
    private static final long serialVersionUID = 1L;
    private int studentId;
    private Person person;

    public Student(int studentId, Person person) {
        this.studentId = studentId;
        this.person = person;
    }

    public int getStudentId() {
        return studentId;
    }

    public void setStudentId(int studentId) {
        this.studentId = studentId;
    }

    public Person getPerson() {
        return person;
    }

    public void setPerson(Person person) {
        this.person = person;
    }
}複製程式碼

@Test
public void testSerializable() throws Exception {
    File file = new File("p.dat");
    Student s = new Student( 1 , new Person("xiaoming", 10));
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
    oos.writeObject(s);

    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
    Student s1  = (Student) ois.readObject();
    ois.close();
    System.out.println(s1.toString() + "name:"+s1.getPerson().getName() + "age:"+s1.getPerson().getAge() + "height:"+s1.getStudentId());

}複製程式碼

仍然模擬兩種情況(實現無參構造器和不實現無引數構造器),
發現兩種情況都會丟擲java.io.NotSerializableException異常,這就需要可序列化類的每個屬性都要可序列化(當然去瞬態屬性和靜態屬性).

4.3 坑三(類的演化)

演化類如下:

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    private int height;

    public Person(String name, int age, int height) {
        this.name = name;
        this.age = age;
        this.height = height;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }
}複製程式碼

反序列化目標類多一個欄位(height),序列化寫入的Person 包含兩個屬性:name,age

@Test
public void testSerializable() throws Exception {
    File file = new File("p.dat");
    /*Person p = new Person("xiaoming", 10);
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
    oos.writeObject(p);*/

    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
    Person p1  = (Person) ois.readObject();
    ois.close();
    System.out.println(p1.toString() + "name:"+p1.getName() + "age:"+p1.getAge() + "height:"+p1.getHeight());
}複製程式碼

結果如下


com.sankuai.meituan.meishi.poi.tag.Person@37574691name:xiaomingage:10height:0複製程式碼

可以看出反序列化之後,並沒有報錯,只是height實賦成了預設值。類似的其它物件也會賦值為預設值。

相反,如果寫入的多一個欄位,讀出的少一個欄位

com.sankuai.meituan.meishi.poi.tag.Person@37574691name:xiaomingage:10複製程式碼

其它演化,比如更改型別等,這種演化本身就有問題,沒必要再探討。

4.4 坑四(列舉型別)

對於列舉型別,我們經常會調整物件的值,我們這裡使用預設值(0,1,2)進行序列化,然後調整元素順序進行反序列化,看看會發生什麼現象(是0,1,2還是2,1,0);
列舉類

public enum Num {
    ONE,TWO,THREE;

    public void printValues() {
        System.out.println(ONE + "ONE.ordinal" + ONE.ordinal());
        System.out.println(TWO + "TWO.ordinal" + TWO.ordinal());
        System.out.println(THREE + "THREE.ordinal" + THREE.ordinal());
    }
}複製程式碼

序列化

@Test
public void testSerializable() throws Exception {
    File file = new File("p.dat");
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
    oos.writeObject(Num.ONE);
    oos.close();

 /*   ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
    Num s1  = (Num) ois.readObject();
    s1.printValues();
    ois.close();
    */

}複製程式碼

我們只寫入一個ONE值,

¬í^@^E~r^@&com.sankuai.meituan.meishi.poi.tag.Num^@^@^@^@^@^@^@^@^R^@^@xr^@^Njava.lang.Enum^@^@^@^@^@^@^@^@^R^@^@xpt^@^CONE複製程式碼

對其調整順序(THREE,TWO,ONE;)再讀出檔案中讀出結果,看看會是什麼現象

NEONE.ordinal2
TWOTWO.ordinal1
THREETHREE.ordinal0複製程式碼

可以看到ONE的值變成了2.
事實上序列化Enum物件時,並不會儲存元素的值,只會儲存元素的name。這樣,在不依賴元素值的前提下,ENUM物件如何更改都會保持相容性。

五、重寫readObject,writeObject

怎麼樣重寫這裡就不說了,在這裡引用effective java的一句話告訴你什麼時候重寫:
“只有當你自行設計的自定義序列化形式與預設的序列化形式基本相同時,才能接受預設的序列化形式”.
“當一個物件的物理表示方法與它的邏輯資料內容有實質性差別時,使用預設序列化形式有N種缺陷”.
其實從effective java的角度來講,是強烈建議我們重寫的,這樣有助於我們更好地把控序列化過程,防範未知風險

相關文章