深度解析JAVA序列化

1、序列化

java序列化提供了一個框架,用來將對象編碼成字節流,並從字節流編碼中從新構建的對象。將對象編碼爲字節流稱之爲序列化,反之將字節流重建成對象稱之爲反序列化。java序列爲對象的可持久化及遠程共享提供了一種簡單機制。它實現起來很是方便,只須要實現serializble接口便可。但每每表面的上簡單,隱藏了背後巨大的風險,若是你不瞭解serializable請慎用,由於其中有太多坑,且當你遇到時可能會不知道所措。effective java在序列化一章第一條就提出「謹慎地實現serializable接口」,可見serializable接口背後實現可能隱藏着「坑人的祕密」。
本文參考了網上大量的技術文章和effective java,將從序列化的原理、注意事項及實際應用幾個方面,經過實例來揭開java序列化的面紗。
在這裏補充研究序列化的背景:有一個Object持久化於緩存中,常常須要變動字段(添加或刪除),每次作變動就要更改緩存表(擔憂不兼容帶來問題,一直不肯定哪些變動會來問題或引發什麼樣的問題),我但願實現一種序列化,當變動或刪除字段時不須要變動緩存表,這須要達到兩個目的:一、新的類訪問舊的緩存時沒問題;2.舊的類訪問新的緩存時也沒問題。這個問題雖然在個人需求背景之下獲得了快速解決,但仍是但願將序列化給出一個充分研究,以備後續信手拈來。 javascript

2、序列化實現方式

2.1 簡單示例

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

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完成的。那接下來咱們就看一下它們具體作了什麼事app

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 {
}複製代碼

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

  • 一個類的序列化能力是由實現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具體實現一個類的寫入,源碼以下(只保留了關鍵部分): ui

摺疊原碼
/** * 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) this

/** * 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);
    }
}複製代碼

3、兼容性

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的值在實現。
強烈建議使用顯示指定的方式,以防範潛在的不兼容根源,且能夠帶來小小的性能提高。

4、坑

(下面的坑都是在指定顯示指定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對象如何更改都會保持兼容性。

5、重寫readObject,writeObject

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

相關文章
相關標籤/搜索