序列化与反序列化
序列化(Serialization)是将对象转换为可传输的格式的过程,一般是以字节码或 XML 格式传输。而反序列化是将字节码或 XML 码编码格式还原为完全相等的对象的过程。
对象序列化机制也是一种对象持久化方式,通过对象序列化,可以把对象的状态保存为字节数组,并在需要时将这个字节数组通过反序列化的方式再转换成对象,如此可以很容易地在对象和字节数组之间进行转换。
除了持久化时用到序列化之外,当在网络中传输对象、使用 RMI 和 RPC 时,都会用到对象序列化。
如何序列化
Java 中提供了一套方便的 API 来支持,将对象进行序列化和反序列化。其中包括以下接口和类:
java.io.Serializablejava.io.ExternalizableObjectOutputStreamObjectInputStreamObjectOutputObjectInput
ObjectInput 和 ObjectOutput 接口
ObjectInput
ObjectInput 接口扩展自 DataInput 接口以包含对象的读操作。
DataInput 接口用于从二进制流中读取字节,并根据所有 Java 基本类型数据进行重构。同时还提供根据 UTF-8 修改版格式的数据重构 String 的工具。
对于此接口中的所有数据读取例程来说,如果在读取所需字节数之前已经到达文件末尾 (end of file),则将抛出 EOFException(IOException 的一种)。如果因为到达文件末尾以外的其他原因无法读取字节,则将抛出 IOException 而不是 EOFException。尤其是,在输入流已关闭的情况下,将抛出 IOException。
ObjectOutput
ObjectOutput 扩展 DataOutput 接口以包含对象的写入操作。
DataOutput 接口用于将数据从任意 Java 基本类型转换为一系列字节,并将这些字节写入二进制流。同时还提供了一个将 String 转换成 UTF-8 修改版格式并写入所得到的系列字节的工具。
对于此接口中写入字节的所有方法,如果由于某种原因无法写入某个字节,则抛出 IOException。
ObjectInputStream 和 ObjectOutputStream 类
通过 ObjectOutputStream 和 ObjectInputStream 可以对对象进行序列化及反序列化。一般使用 ObjectOutputStream 的 writeObject 方法把一个对象进行持久化,再使用 ObjectInputStream 的 readObject 从持久化存储中把对象读取出来。
Serializable 接口
Java 中,只要类实现 java.io.Serializable 接口就表示可以被序列化。没有实现此接口的类将无法使其任何状态序列化或反序列化。序列化接口没有方法或字段,仅用于标识可序列化的语义。
当试图对一个对象进行序列化的时候,如果遇到不支持 Serializable 接口的对象。在此情况下,将抛出 NotSerializableException。
如果要序列化的类有父类,要想同时将在父类中定义过的变量持久化下来,那么父类也应该实现 java.io.Serializable 接口。
举个栗子,下面是一个实现了 java.io.Serializable 接口的类:
public class User1 implements Serializable {
private String name;
private int 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;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
对这个 User1 对象进行序列化和反序列化:
public class SerializableTest {
public static void main(String[] args) {
User1 user = new User1();
user.setName("timber");
user.setAge(20);
System.out.println("before: " + user);
// Write Obj to File
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// Read Obj from File
File file = new File("tempFile");
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(file));
User1 newUser = (User1) ois.readObject();
System.out.println("after: " + newUser);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
file.delete();
}
}
}
打印结果为:
before: User1{name='timber', age=20}
after: User1{name='timber', age=20}
Externalizable 接口
除了 Serializable 之外, Java 中还提供了另一个序列化接口 Externalizable 来自定义序列化和反序列化策略。该接口中定义了抽象方法:writeExternal 与 readExternal 方法。当使用 Externalizable 进行序列化和反序列化时需要实现这两个方法。
如果在这两个方法中没有定义序列化实现细节,进行序列化和反序列化后得到的对象的所有属性的值都变成了默认值。也就是说之前的那个对象的状态并没有被持久化下来。
另外,若使用 Externalizable 接口进行序列化,在读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。所以,实现 Externalizable 接口的类必须要提供一个 public 的无参的构造器。
下面是一个实现 Externalizable 接口的类:
public class User2 implements Externalizable {
private String name;
private int 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;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}
@Override
public String toString() {
return "User1{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
对这个 User2 对象进行序列化和反序列化:
public class ExternalizableTest {
public static void main(String[] args) {
User2 user = new User2();
user.setName("timber");
user.setAge(20);
System.out.println(user);
//Write Obj to file
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"))) {
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
}
//Read Obj from file
File file = new File("tempFile");
User2 user2 = null;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
user2 = (User2) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(user2);
}
}
相关知识
transient 关键字
transient 关键字可以控制变量的序列化。如果在变量声明前加上该关键字,可以阻止该变量被序列化。在被反序列化时,transient 变量的值被设为初始值,如 int 类型是 0,引用类型是 null。
序列化ID
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)。
序列化 ID 一般有两种生成策略:
- 一种是固定的
1L。如果没有特殊需求,就是用默认的1L就可以; - 一种是随机生成一个不重复的
long类型数据。有时可通过改变序列化ID可以用来限制某些用户的使用。
静态变量
在 Java 序列化保存对象时,会将其状态保存为一组字节,在需要时将这些字节封装成对象。这里的状态指的是对象的成员变量,也就是说不会保存类中的静态变量。
Serializable接口为空?
Serializable 虽只是一个空接口。但底层实现中,在序列化操作时,会判断要被序列化的类是否是 Enum、Array 和 Serializable 类型,如果不是则直接抛出 NotSerializableException。以此来保证只有实现了该接口的方法才能进行序列化与反序列化。