coding……
但行好事 莫问前程

Java编程拾遗『序列化』

Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,也就是讲这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,有可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java对象序列化就能够帮助我们实现该功能。上节讲字节流的时候,讲了通过DataOutputStream/DataInputStream可以将对象从内存中写出,也可以从将写出的内容重新读入到内存,但是使用起来比较麻烦,需要逐个处理对象中的每个字段。为了风方便的实现序列化/反序列化,Java提供了ObjectOutputStream/ObjectOutInputStream实现了对象的序列化/反序列化功能,也是Java实现序列化/反序列化的默认方式。

1. Java序列化/反序列化示例

Java中如果需要让一个类支持序列化/反序列化,只需要让这个类实现接口java.io.Serializable接口,Serializable是一个标记接口,没有定义任何方法 。对于实现了Serializable接口的类实例对象,可以通过ObjectOutputStream/ObjectInputStream实现序列化/反序列化。

@Getter
@Setter
@AllArgsConstructor
@ToString
public class Student implements Serializable {
    private String name;
    private int age;
    private float grade;
}
@SneakyThrows
public static void main(String[] args) {
    try (OutputStream fileOutputStream = new FileOutputStream("d:/student.dat");
         ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {

        Student student = new Student("Michael", 23, 1);
        objectOutputStream.writeObject(student);
    }

    try (InputStream fileInputStream = new FileInputStream("d:/student.dat");
         ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
        Student students = (Student) objectInputStream.readObject();
        System.out.println(students);
    }
}

运行结果:

Student(name=Michael, age=23, grade=1.0)

可以看到,Student实现了Serializable接口,通过ObjectOutputStream可以将Student对象序列化,存储到文件中,然后通过ObjectInputSream反序列化文件中的内容,得到Student对象。如果Student未实现Serializable接口,上述方法执行时会抛java.io.NotSerializableException异常。

2. Java序列化/反序列的一些细节

2.1 多个对象引用同一个对象问题

如果a, b两个对象都引用同一个对象c,序列化后c是保存两份还是一份?反序列化后还能让a, b指向同一个对象?

@AllArgsConstructor
public class Common implements Serializable {
    private String c;
}

@Getter
@AllArgsConstructor
public class A implements Serializable {
    private String a;
    private Common c;
}

@Getter
@AllArgsConstructor
public class B implements Serializable {
    private String b;
    private Common c;
}
@SneakyThrows
public static void main(String[] args) {

    try (OutputStream fileOutputStream = new FileOutputStream("d:/student.dat");
         ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {
        Common c = new Common("common");
        A a = new A("a", c);
        B b = new B("b", c);

        objectOutputStream.writeObject(a);
        objectOutputStream.writeObject(b);
    }

    try (InputStream fileInputStream = new FileInputStream("d:/student.dat");
         ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
        A a = (A) objectInputStream.readObject();
        B b = (B) objectInputStream.readObject();

        if (a.getC() == b.getC()) {
            System.out.println("reference the same object");
        } else {
            System.out.println("reference different objects");
        }
    }
}

运行结果:

reference the same object

说明如果a, b两个对象都引用同一个对象c,序列化后c是保存一份对象,反序列化后a, b指向同一个对象

2.2 对象循环引用序列化问题

如果a, b两个对象有循环引用,序列化和反序列化还可以正常进行吗?

@Getter
@Setter
public class Parent implements Serializable {
    String name;
    Child child;

    public Parent(String name) {
        this.name = name;
    }
}

@Getter
@Setter
public class Child implements Serializable {
    String name;
    Parent parent;

    public Child(String name) {
        this.name = name;
    }
}
@SneakyThrows
public static void main(String[] args) {

    try (OutputStream fileOutputStream = new FileOutputStream("d:/student.dat");
         ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {
        Parent parent = new Parent("Parent");
        Child child = new Child("Child");
        parent.setChild(child);
        child.setParent(parent);

        objectOutputStream.writeObject(parent);
        objectOutputStream.writeObject(child);
    }

    try (InputStream fileInputStream = new FileInputStream("d:/student.dat");
         ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
        Parent parent = (Parent) objectInputStream.readObject();
        Child child = (Child) objectInputStream.readObject();

        if (parent.getChild() == child && child.getParent() == parent
                && parent.getChild().getParent() == parent
                && child.getParent().getChild() == child) {
            System.out.println("reference OK");
        } else {
            System.out.println("wrong reference");
        }
    }
}

运行结果:

reference OK

说明Java提供的默认序列化/反序列化机制可以正确处理循环引用的情形。

2.3 transient关键字

transient关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到结果中,在被反序列化后,transient变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

@Getter
@Setter
@AllArgsConstructor
@ToString
public class Student1 implements Serializable {
    private String name;
    private int age;
    private float grade;

    private transient String internalItem;
}
@SneakyThrows
public static void main(String[] args) {

    try (OutputStream fileOutputStream = new FileOutputStream("d:/student.dat");
         ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {

        Student1 student = new Student1("Michael", 23, 1, "internal");
        objectOutputStream.writeObject(student);
    }

    try (InputStream fileInputStream = new FileInputStream("d:/student.dat");
         ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
        Student1 student = (Student1) objectInputStream.readObject();
        System.out.println(student);
    }

}

运行结果:

Student1(name=Michael, age=23, grade=1.0, internalItem=null)

说明transient关键字修饰的成员变量internalItem未被写入到序列化文件中,在反序列化时,internalItem被设置为初始值null。

2.4 静态成员变量序列化

序列化会将对象的状态写入到文件或其他介质中,保存的是被序列化对象的状态,所以序列化不会保存静态变量。

@Getter
@Setter
@AllArgsConstructor
@ToString
public class Student1 implements Serializable {
    private String name;
    private int age;
    private float grade;

    private transient String internalItem;
    public static String staticItem = "static";

    public void setStaticItem(String staticItem) {
        Student1.staticItem = staticItem;
    }
}
@SneakyThrows
public static void main(String[] args) {

    try (OutputStream fileOutputStream = new FileOutputStream("d:/student.dat");
         ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {

        Student1 student = new Student1("Michael", 23, 1, "internal");
        student.setStaticItem("aaa");
        objectOutputStream.writeObject(student);
    }

    try (InputStream fileInputStream = new FileInputStream("d:/student.dat");
         ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
        Student1 student = (Student1) objectInputStream.readObject();
        System.out.println(student);
        System.out.println(student.staticItem);
    }

}

运行结果:

Student1(name=Michael, age=23, grade=1.0, internalItem=null)
aaa

通过writeObject将Student1对象序列化写入到文件,在序列化前,将Student对象的静态成员变量staticItem设置为”aaa”,之后通过反序列化,将序列化文件中的内容反序列化为对象。但是看运行结果发现,反序列化后通过对象访问静态成员staticItem,发现竟然是代码中修改后的值”aaa”,好像静态成员变量一起写入了序列化文件。但事实是因为static成员变量表示的是类的状态,是保存在方法去的,所有实例共享一份数据。上面序列化过程,没有将静态成员变量序列化,在反序列化时必然也不会影响方法区中静态成员变量的值,当程序打印student.staticItem时,取的是方法区中静态成员变量的值,该值在之前被set操作修改为”aaa”了,所以才会造成静态成员变量被序列化的假象。下面直接反序列化上述序列化结果文件,然后再访问静态成员变量:

@SneakyThrows
public static void main(String[] args) {

    /*try (OutputStream fileOutputStream = new FileOutputStream("d:/student.dat");
         ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {

        Student1 student = new Student1("Michael", 23, 1, "internal");
        student.setStaticItem("aaa");
        objectOutputStream.writeObject(student);
    }*/

    try (InputStream fileInputStream = new FileInputStream("d:/student.dat");
         ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
        Student1 student = (Student1) objectInputStream.readObject();
        System.out.println(student);
        System.out.println(student.staticItem);
    }
}

运行结果:

Student1(name=Michael, age=23, grade=1.0, internalItem=null)
static

可以发现,静态成员变量staticItem的值仍然是”static”,说明上述代码序列化时,修改的静态成员变量并没有写入到序列化文件中。

2.5 序列化ID

Java的序列化机制通过serialVersionUID来验证版本一致性。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地实体类中的serialVersionUID进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常。

但是我们在代码中可以省略serialVersionUID成员变量,这时候Java序列化机制会根据编译时的class自动生成一个serialVersionUID。在序列化时,该自动生成的serialVersionUID会写入到序列化结果中,反序列化时会从字节流中取出该serialVersionUID和根据本地class自动生成的serialVersionUID作比较。如果类没有做过修改,自动生成的serialVersionUID总是一致的。

2.6 定制序列化

Java默认的序列化功能已经很强大了,它可以自动将对象中的所有字段自动保存和恢复,但这种默认行为有时候不是我们想要的。

比如,对于有些字段,它的值可能与内存位置有关,比如默认的hashCode()方法的返回值,当恢复对象后,内存位置肯定变了,基于原内存位置的值也就没有了意义。

还有一些情况,如果类中的字段表示的是类的实现细节,而非逻辑信息,那默认序列化也是不适合的。为什么不适合呢?因为序列化格式表示一种契约,应该描述类的逻辑结构,而非与实现细节相绑定,绑定实现细节将使得难以修改,破坏封装。比如,我们在容器类中介绍的LinkedList,它的默认序列化就是不适合的,为什么呢?因为LinkedList表示一个List,它的逻辑信息是列表的长度,以及列表中的每个对象,但LinkedList类中的字段表示的是链表的实现细节,如头尾节点指针,对每个节点,还有前驱和后继节点指针等

那怎么办呢?Java提供了多种定制序列化的机制,主要的有两种,一种是transient关键字,另外一种是自定义writeObject和readObject方法

将字段声明为transient,默认序列化机制将忽略该字段,不会进行保存和恢复。比如,类LinkedList中,它的字段都声明为了transient,如下所示:

transient int size = 0;
transient Node<E> first;
transient Node<E> last;

声明为了transient,序列化时就不会保存该字段了。但是LinkedList的逻辑信息列表的长度size及列表中的每个对象肯定时要保存的,这时候要怎么才能将这些信息写到序列化结果中以及反序列化讲这些逻辑信息读取出来?就是通过自定义writeObject和readObject方法。

2.6.1 writeObject

类可以实现自定义writeObject方法,以自定义该类对象的序列化过程,其声明必须为:

private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException

比如LinkedList类中自定义的序列化方法writeObject如下:

private void writeObject(ObjectOutputStream var1) throws IOException {
    //调用默认的序列化机制,将可以序列化的成员变量写入到序列化结果中
    var1.defaultWriteObject();

    //将LinkedList逻辑信息,列表size写入到序列化结果中
    var1.writeInt(this.size);

    //遍历LinkedList,将将LinkedList逻辑信息,每个节点元素写入到序列化结果中
    for(LinkedList.Node var2 = this.first; var2 != null; var2 = var2.next) {
        var1.writeObject(var2.item);
    }

}

需要注意的是第一行代码:

var1.defaultWriteObject();

这一行是必须的,它会调用默认的序列化机制,默认机制会保存所有没声明为transient的字段,即使类中的所有字段都是transient,也应该写这一行,因为Java的序列化机制不仅会保存纯粹的数据信息,还会保存一些元数据描述等隐藏信息,这些隐藏的信息是序列化所必须的。

2.6.2 readObject

与writeObject对应的是readObject方法,通过它自定义反序列化过程,其声明必须为:

private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException 

LinkedList的反序列化方法readObject如下:

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
    //调用默认的反序列化机制
    var1.defaultReadObject();

    //将将LinkedList逻辑信息,列表size读取出来
    int var2 = var1.readInt();

    //将每个元素读取出来,按照尾插法重新组织为LinkedList,可以保证顺序和序列化前一致
    for(int var3 = 0; var3 < var2; ++var3) {
        this.linkLast(var1.readObject());
    }

}

注意第一行代码:

s.defaultReadObject(); 

这一行代码也是必须的。

2.6.3 定制序列化原理

上面讲了,LinkedList中将实现细节相关的成员变量都使用transient进行了声明,避免默认序列化机制将这些成员变量写入序列化结果,并通过自定义writeObject/readObject定制了序列化/反序列化实现。很多人肯定会有疑问,自定义writeObject/readObject是如何实现定制序列化的,或者讲,自定义writeObject/readObject方法是何时被调用的。这里直接给出调用链,如下所示:

writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->invokeWriteObject
public final void writeObject(Object obj) throws IOException {
    if (enableOverride) {
        writeObjectOverride(obj);
        return;
    }
    try {
        writeObject0(obj, false);
    } catch (IOException ex) {
        if (depth == 0) {
            writeFatalException(ex);
        }
        throw ex;
    }
}
private void writeObject0(Object obj, boolean unshared)
    throws IOException
{
    boolean oldMode = bout.setBlockDataMode(false);
    depth++;
    try {
        //……

        // 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) {
            //如果实现了Serializable接口,调用writeOrdinaryObject进行序列化操作
            writeOrdinaryObject(obj, desc, unshared);
        } else {
            //如果不是String、数组、Enum、或者实现了Serializable接口,
            //就会抛NotSerializableException异常
            if (extendedDebugInfo) {
                throw new NotSerializableException(
                    cl.getName() + "\n" + debugInfoStack.toString());
            } else {
                throw new NotSerializableException(cl.getName());
            }
        }
    } finally {
        depth--;
        bout.setBlockDataMode(oldMode);
    }
}
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);
                //通过反射调用自类中自定义writeObject方法
                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);
        }
    }
}
void invokeWriteObject(Object obj, ObjectOutputStream out)
    throws IOException, UnsupportedOperationException
{
    requireInitialized();
    if (writeObjectMethod != null) {
        try {
            writeObjectMethod.invoke(obj, new Object[]{ out });
        } catch (InvocationTargetException ex) {
            Throwable th = ex.getTargetException();
            if (th instanceof IOException) {
                throw (IOException) th;
            } else {
                throwMiscException(th);
            }
        } catch (IllegalAccessException ex) {
            // should not occur, as access checks have been suppressed
            throw new InternalError(ex);
        }
    } else {
        throw new UnsupportedOperationException();
    }
}

通过上述代码可以得到结论,在使用ObjectOutputStream的writeObject方法和ObjectInputStream的readObject方法时,会通过反射的方式调用自定义writeObject和readObject方法,从而实现定制序列化。

3. 总结

  • 在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化
  • Java默认通过ObjectOutputStream和ObjectInputStream对对象进行序列化及反序列化
  • 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)
  • 序列化并不保存静态变量
  • 要想将父类对象也序列化,就需要让父类也实现Serializable接口
  • transient关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient变量的值被设为初始值,如int型的是0,对象型的是null

参考链接:

1. 《Java编程的逻辑》

2. 《Java核心技术》

3. 深入分析Java的序列化与反序列化

赞(4) 打赏
Zhuoli's Blog » Java编程拾遗『序列化』
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址