跳到主要内容

深入理解Java序列化

· 阅读需 13 分钟

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

序列化可以将对象的字节序列持久化——保存在内存、文件、数据库中,在网络上传送对象的字节序列,或者用于 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序列化机制 - 掘金