深度解析JAVA序列化

2,663 阅读19分钟

一、序列化

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的角度来讲,是强烈建议我们重写的,这样有助于我们更好地把控序列化过程,防范未知风险