深入理解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
接口没有任何方法,仅作为一个可序列化的标记。被序列化的类必须属于 Enum
、Array
和Serializable
类型其中的任何一种。
如果不是 Enum
、Array
的类,如果需要序列化,必须实现 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
在序列化过程中,虚拟机会试图调用对象类里的 writeObject
和 readObject
方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStream
的 defaultWriteObject
方法以及ObjectInputStream
的 defaultReadObject
方法。用户自定义的 writeObject
和 readObject
方法可以允许用户控制序列化的过程。
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