Java序列化和Serializable

本人最近主要忙于iOS的开发,好久没有整理Java的文章了,这是很久之前的一片草稿,今天重新整理下。

在《Thinking in Java》这本书中,IO一章的结尾也提到了序列化相关的一些内容。而且在Java自带的对象的序列化和反序列化都是使用ObjectInputStreame和ObjectOutputStream来完成的,可见序列化和IO还是有着密切联系的,本篇就简要整理一下Java本身序列化相关的内容。

我们都知道,一段程序代码运行起来就会有进程的概念,而程序代码本身是静态的,不随程序运行而改变的。在Java程序中的对象其实也是一样,可以随着程序运行而存在,也可以独立于运行的程序,当程序结束时,Java程序将运行时的对象固定下来写在程序之外,当程序再次跑起来的时候可以再次将这个Java对象使用起来,这可以叫做Java对象的持久化。而Java对象本身可以是互相引用的网状结构,想要将其持久化就要考虑怎么把特定对象按照一定的格式记下来,变为有顺序的数据,这就是序列化。

Java体系结构有三大特点:安全性、平台无关性、网络移动性。其中后两点也需要序列化来支持,如两个Java应用系统之间进行通信,可以使用RMI,调用传参可能是对象,而通过网络传递对象参数,也是需要将其数据序列化之后进行顺序传递的。还有一个比较常见的序列化需求的例子,就是对象的“深拷贝”。

Java的序列化是一个很大的话题, 而使用ObjectOutputStream和ObjectInputStream进行序列化和反序列化的例子也很多,下面仅对于一些常见的问题整理下,如果有未提及的点,后续补充更新。

0. Serializable

先来看下Serializable这个接口:

public interface Serializable {
}

从源码上来看,这个接口什么也没有说明。在使用Serializable进行序列化和反序列化的时候,需要注意一下一些点:

  • 实现java.io.Serializable这个interface是使用Java自身序列化的最基本条件。
  • 如果父类实现了Serializable 接口,子类自动可以序列化,而如果子类实现了Serializable接口而父类没有实现,需要子类帮助父类完成序列化和反序列化工作,并且要求父类提供无参构造方法。
  • 反序列化中构造方法不会被调用。
  • transiant和static的字段不会被序列化。
  • 实现Serializable的类对象的序列化和反序列化通常需要和ObjectOutputStream和ObjectInputStream结合使用
  • Serializable接口虽然没有声明方法,但在需要定制序列化和反序列化过程时,可以用“隐藏方法”实现:                                                                                                                
     private void writeObject(java.io.ObjectOutputStream out)
         throws IOException
     private void readObject(java.io.ObjectInputStream in)
         throws IOException, ClassNotFoundException;
     private void readObjectNoData()
         throws ObjectStreamException;
    

    只要根据特定的需求逻辑,使用对应的ObjectOutputStream和ObjectInputStream参数对象进行操作即可。

  • 除了使用Serializable接口,还可以考虑使用java.io.Externalizable接口,通过这个接口提供的两个方法来实现序列化过程的定制。
        void writeExternal(ObjectOutput out) throws IOException;
        void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
    
  • 同一对象多次被引用的序列化情况。在使用ObjectOutputStream对一个对象的一次序列化当中,如果引用图中出现对某个对象的多次重复引用,Java序列化中使用了引用共享机制,能够保证在反序列化的时候,这个应用图中这个对象仍然是同一个。
  • 序列化版本号serialVersionUID。Serializable这个接口里什么都没显式声明,没有方法,也没有属性。但是对于一个类,可能会有修改,那么修改之前和修改之后的对象序列化和反序列化就会有版本兼容的问题需要考虑。如果我们在实现Serializable的类中没有定义,在序列化和反序列化的时候Java平台会做计算和比对,如果版本号不匹配,进行反序列化的时候会出现异常。由于Java平台自身对serialVersionUID的计算受编译环境因素影响,官方文档强烈建议自定义,通常的格式为:
    private static final long serialVersionUID=1L;
    

    其中,私有是为了避免影响子类,static是因为没必要被序列化,final则是不用被变化。

  • 此外,Java还提供了序列化过程中对象的替换机制,详细可以参看java.io.Serializable的API文档

1. ObjectOutputStream和ObjectInputStream

Java自身的序列化机制通常是和ObjectOutputStream/ObjectInputStream结合在一起的。

public class ObjectOutputStream
    extends OutputStream implements ObjectOutput, ObjectStreamConstants
public class ObjectInputStream
    extends InputStream implements ObjectInput, ObjectStreamConstants

两个类分别集成了OutputStream和InputStream,即字节流的积累。同时分别实现了ObjectOutput和ObjectInput接口,间接实现了DataOutput和DataInput中的各个基本类型的write和read方法,同时增加了writeObject()和readObject()。此外,使用到了ObjectStreamConstants定义的常量。

这两个类的具体实现比较复杂,下面简要分析下ObjectOutputStream的writeObject()方法大致实现思路。要了解到有这样几个东西:

  • private final BlockDataOutputStream bout,ObjectOutputStream内包装的输出字节流类,具体的输出操作由其来完成。
  • private final HandleTable handles, handles是一个简单的hash表,存放已经处理过的对象
  • private final ReplaceTable subs,subs也是一个hash表,负责存储要replace的对象

writeObject() / writeUnshared() / writeObjectOverride()等最后都会调用到writeObject0(),而writeObject0()会先根据是否有替换处理、是否共享同一对象引用(unshared)做预处理,对subs和handles操作,紧接着:

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

其中的desc是ObjectStreamClass类对象。而writeOrdinaryObject()会根据对象是否为Externalizable做不同处理:

            if (desc.isExternalizable() && !desc.isProxy()) {
                writeExternalData((Externalizable) obj);
            } else {
                writeSerialData(obj, desc);
            }

而writeSerialData()最后根据是否有private的writeObject()实现进行调用。

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

由上可见,Java自身的序列化步骤可通过ObjectOutputStream和ObjectInputStream得到更好的理解。更详细的序列化步骤,可参看Java的ObjectOutputStream类文档,或直接查看源码。

关于Java内建的序列化内容就整理这些。在当前的实际应用当中,JSON方式和XML方式的序列化应用较为广泛。

常用的开源工具有Jackson、FastJSON和Xstream等。

相关文章:

此条目发表在 IO, Java, Java语言 分类目录,贴了 , , , , , 标签。将固定链接加入收藏夹。

发表评论

电子邮件地址不会被公开。 必填项已用 * 标注

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>