深入理解Java序列化

所谓序列化,就是将对象转为字节流,而反序列化则是将字节流还原为对象。

序列化可以将对象的字节序列持久化——保存在内存、文件、数据库中,在网络上传送对象的字节序列,或者用于 RMI(远程方法调用)。

例子

首先来看一个简单的例子。定义一个 User 类,并实现 Serializable 接口。

package top.jlice.demo;

import java.io.*;

class User implements Serializable {
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

public class SerializableDemo {
    public static void main(String[] args) {
        User user = new User("jlice", 25);

        try {
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data.dat"));
            out.writeObject(user);
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.dat"));
            User obj = (User) in.readObject();
            System.out.println(obj.getName() + "\t" + obj.getAge());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

输出:

jlice    25

在主函数中,通过ObjectOutputStream进行对象的序列化,通过ObjectInputStream进行对象的反序列化。

字节码

查看例子里生成的文件的字节码:

$ xxd data.dat    
00000000: aced 0005 7372 0013 746f 702e 6a6c 6963  ....sr..top.jlic
00000010: 652e 6465 6d6f 2e55 7365 7200 0000 0000  e.demo.User.....
00000020: 0000 0102 0002 4900 0361 6765 4c00 046e  ......I..ageL..n
00000030: 616d 6574 0012 4c6a 6176 612f 6c61 6e67  amet..Ljava/lang
00000040: 2f53 7472 696e 673b 7870 0000 0019 7400  /String;xp....t.
00000050: 056a 6c69 6365                           .jlice

关于以上字节码的含义可以参考 java.io.ObjectStreamConstants 中的定义。

aced 是魔数;0005 是版本号。

73是 TC_OBJECT,表示一个对象。72是 TC_CLASSDESC,表示类的描述。之后是类名的长度,0013 十进制是19,之后就是类名 top.jlice.demo.User。之后就是 serialVersionUID 的值,也就是 1L。之后的 02 表示SC_SERIALIZABLE,之后就是属性数量。

49,字符 I,表示属性是 int 类型,03是属性名长度,之后是属性名 age。4C,字符 L,表示是对象类型(而不是基本类型),04是属性名长度,之后是属性名 name。74是 TC_STRING,12是字符串长度,也就是18,类型是Ljava/lang/String;

78是 TC_ENDBLOCKDATA,对象块结束的标志;70是 TC_NULL,说明没有其他超类的标志。

19是 age 的值,也就是25,74 00 05 表示长度为5的字符串,之后是字符串的值 jlice。

Serializable 接口

Serializable 接口没有任何方法,仅作为一个可序列化的标记。被序列化的类必须属于 EnumArraySerializable 类型其中的任何一种。

如果不是 EnumArray 的类,如果需要序列化,必须实现 java.io.Serializable 接口,否则将抛出NotSerializableException 异常。

serialVersionUID

serialVersionUID 是 Java 为每个序列化类产生的版本标识。它可以用来保证在反序列时,发送方发送的和接受方接收的是可兼容的对象。如果接收方接收的类的 serialVersionUID 与发送方发送的 serialVersionUID 不一致,会抛出InvalidClassException。虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的 serialVersionUID 是否一致。

如果可序列化类没有显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认serialVersionUID 值。尽管这样,还是建议在每一个序列化的类中显式指定 serialVersionUID 的值。因为不同的 jdk 编译很可能会生成不同的 serialVersionUID 默认值,从而导致在反序列化时抛出 InvalidClassExceptions 异常。

serialVersionUID 字段必须是 static final long 类型。serialVersionUID 用于控制序列化版本是否兼容。若我们认为修改的可序列化类是向后兼容的,则不修改 serialVersionUID

transient

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

writeObject 与 readObject

在序列化过程中,虚拟机会试图调用对象类里的 writeObjectreadObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStreamdefaultWriteObject 方法以及ObjectInputStreamdefaultReadObject 方法。用户自定义的 writeObjectreadObject 方法可以允许用户控制序列化的过程。

writeObject()readObject() 都是 private 方法,那么它们是如何被调用的呢?毫无疑问,是使用反射。

下面这个例子演示了通过这两个方法突破了 transient 关键字的作用:

import java.io.*;

public class Test implements Serializable {
    private static final long serialVersionUID = 1L;

    private transient String password;

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        password = (String) in.readObject();
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeObject(password);
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.setPassword("hello");

        try {
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("test.dat"));
            out.writeObject(test);
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("test.dat"));
            Test obj = (Test) in.readObject();
            System.out.println(obj.getPassword());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

输出:

hello

readResolve

当我们使用 Singleton 模式时,应该是期望某个类的实例应该是唯一的,但如果该类是可序列化的,那么情况可能会略有不同。如果既想要做到可序列化,又想要反序列化为同一对象,则需要实现 readResolve 方法。

import java.io.*;

public class Singleton implements Serializable {
    private Singleton() { }
    private static final Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return instance;
    }

    private Object readResolve() {
        return instance;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Singleton singleton = new Singleton();

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.dat"));
        out.writeObject(singleton);
        out.close();

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.dat"));
        Singleton obj = (Singleton) in.readObject();

        System.out.println(Singleton.getInstance() == obj);
    }
}

输出:

true

writeReplace

实现了 writeReplace 方法后,那么在序列化时会先调用 writeReplace 方法将当前对象替换成另一个对象(该方法会返回替换后的对象)并将其写入流中。

import java.io.*;

public class WriteReplaceDemo implements Serializable {
    private Object writeReplace() {
        return 10;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        WriteReplaceDemo demo = new WriteReplaceDemo();

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("demo.dat"));
        out.writeObject(demo);
        out.close();

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("demo.dat"));
        int obj = (int) in.readObject();

        System.out.println(obj);
    }
}

输出:

10

Externalizable

可序列化类实现 Externalizable 接口之后,基于 Serializable 接口的默认序列化机制就会失效,也就是,Externalizable 序列化的优先级比 Serializable 的优先级高。。实现Externalizable 接口后,属性字段使用 transient 和不使用没有任何区别。

Externalizable 继承于 Serializable,它增添了两个方法:writeExternal()readExternal()。这两个方法在序列化和反序列化过程中会被自动调用,序列化的细节需要由开发人员自己实现。

若使用 Externalizable 进行序列化,当读取对象时,会调用被序列化类的无参构造方法去创建一个新的对象;然后再将被保存对象的字段的值分别填充到新对象中。由于这个原因,实现 Externalizable 接口的类必须要提供一个无参的构造方法,且它的访问权限为 public。而 Serializable 可以没有默认的构造方法。

import java.io.*;

public class ExternalizableDemo implements Externalizable {
    private String username = "jlice";

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(username);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        username = (String) in.readObject();
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ExternalizableDemo demo = new ExternalizableDemo();

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("demo.dat"));
        out.writeObject(demo);
        out.close();

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("demo.dat"));
        ExternalizableDemo obj = (ExternalizableDemo) in.readObject();
        System.out.println(obj.getUsername());
    }
}

输出:

jlice

注意要点

序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。

父类的序列化

要想将父类对象也序列化,就需要让父类也实现 Serializable 接口。如果父类不实现的话的,就 需要有默认的无参的构造函数。在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。

import java.io.*;

class A {
    private int val;

    public A() {
        val = 10;
    }

    public int getVal() {
        return val;
    }

    public void setVal(int val) {
        this.val = val;
    }
}

class B extends A implements Serializable {

}

public class Demo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        B b = new B();
        b.setVal(5);

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("b.dat"));
        out.writeObject(b);
        out.close();

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("b.dat"));
        B obj = (B) in.readObject();

        System.out.println(obj.getVal());
    }
}

输出:

10

序列化存储规则

Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,该存储规则极大的节省了存储空间。

第一次写入对象以后,第二次再试图写的时候,虚拟机根据引用关系知道已经有一个相同对象已经写入文件,因此只保存第一次写的引用,所以读取时,都是第一次保存的对象。在使用一个文件多次 writeObject 需要特别注意这个问题。

import java.io.*;

public class ReWriteDemo implements Serializable {
    private int val;

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ReWriteDemo demo = new ReWriteDemo();

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("demo.dat"));
        demo.val = 5;
        out.writeObject(demo);
        out.flush();
        demo.val = 10;
        out.writeObject(demo);

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("demo.dat"));
        ReWriteDemo obj1 = (ReWriteDemo) in.readObject();
        ReWriteDemo obj2 = (ReWriteDemo) in.readObject();

        System.out.println(obj1.val + "\t" + obj2.val);
    }
}

输出:

5    5

参考文献

深入理解 Java 序列化 - 掘金

Java 序列化的高级认识

java基础---->Serializable的使用 - huhx - 博客园

深入理解Java序列化机制 - 掘金

Java

上一篇 HashMap源码分析
下一篇 ClassNotFoundException与NoClassDefFoundError【译】

添加新评论

*
*