6 序列化
序列化是Java提供的一种机制。
传统的Java程序中的对象对内存都有依赖关系,即如果Java程序的进程终止了,该进程所使用的内存将被收回,该进程创建的对象将被销毁。然而,有时在进程终止后,保存Java对象的信息仍然非常有用,例如资料保存、网络传输对象。Java的序列化功能能够将对象转化为二进制字节流,然后通过流写入文件在硬盘进行存储或通过网络进行传输。之后的程序可以对流化后的对象进行读写操作。
6.1 对象序列化
6.1.1 序列化实例
Java提供了两个标识用于声明序列化类:Serializable、Externalizable。只要实现这两个接口中的其中一个,类的对象都将可以进行序列化。
Java的序列化实例流程如下:
- 根据某种序列化算法,将对象转换成字节流。Java支持程序员自定义序列化算法,也可以直接默认使用JDK提供的序列化算法。
- 将字节流写入到数据流中。
- 使用输出流存储或传输对象自己序列。
对象序列化成字节流存储或传输后,当下一次使用它时,需要将其翻译回来原来的样子,这个过程称为反序列化,即序列化的逆过程。
Java的反序列化流程如下:
- 从存储介质中,读取对象的字节流。
- 将字节流读取到流载体中。
- 通过反序列化将字节流翻译成对象。
下面定义了一个可序列化的类Person:
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name = "simple";
private Integer age = 15;
public String getName() {
return this.name;
}
}
类内定义了一个长整型常量serialVersionUID,是用来识别类的,不同的类有不同的serialVersionUID,若序列化和反序列化的serialVersionUID不一致,则无法进行反序列化操作。
下面演示了序列化的使用:
public class Test1 {
public static void main(String[] args) throws IOException {
Person person = new Person();
File file = new File("test.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
objectOutputStream.writeObject(person);
objectOutputStream.flush();
objectOutputStream.close();
}
}
此时文件“test.txt”就存有该对象的二进制数据。
下面演示了反序列化的使用:
public class Test2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
File file = new File("test.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
Person person = (Person) objectInputStream.readObject();
System.out.println(person.getName());
objectInputStream.close();
}
}
输出结果:
simple
6.1.2 序列化注意
在Java中,并不是所有的对象都需要进行序列化,下面给出两个原因:
- 安全问题。Java中有的类属于敏感类,此类的对象数据不便对外公开,而序列化的对象数据很容易进行破解,无法保证器数据的安全性,因此一般这种类型的对象不会进行序列化。
- 资源问题。可以使用序列化字节流创建对象,而且这种创建是不受限制的,有时过多地创建对象会造成很大的资源问题。
6.1.3 序列化机制
1. 类成员序列化
在对象的类成员中,有以下三种类成员不会被序列化进都流中:
-
静态变量
静态变量属于类的属性,并不会属于某个具体实例,因此在序列化的时候无须进行序列化,反序列化时,可以直接获取类的静态成员引用。
-
方法
方法知识一系列操作的集合,方法不会依赖于对象,不会因对象的不同而操作不同,反序列化时,可以从类中获取方法信息。
-
添加transient属性的变量
Java支持通过对变量添加transient属性来表示该变量在进行序列化时被忽略,不会被保存在序列化的结果中。
private transient String age;
常用于标记一些敏感信息。即使被标记为了transient,但仍然可以通过重写序列化方法来实现序列化。
2. 继承关系的序列化
下面两种情况:
- 若父类未实现serializable接口,不会被保存在序列化的结果中。
- 若父类已实现serializable接口,将会被保存在序列化的结果中。
3. 引用关系的序列化
例如以下情况:
public class Person implements Serializable {
private String name = "simple";
private Hair hair = new Hair();
private Tool tool;
}
public class Hair implements Serializable {
private String color = "Black";
}
public class Tool implements Serializable {
private String name = "knife";
}
下面两种情况:
- 若已被初始化,例如Person中创建了一个Hair实例,则该实例以及其属性也会被序列化,并保存在序列化的结果中。
- 若未被初始化,例如Person中未创建Tool实例,则不会保存在序列化的结果中,因为它指向null;
需要说明的是,当引用类没有实现Serializable接口时,JVM会抛出java.io.NotSerializableException。
6.1.4 序列化标识ID
上文提到过Person中有一个变量serialVersion:
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
}
}
为了保证序列和反序列的类相同,Java要求实现序列化接口的类都必须声明一个serialVersionUID静态属性,如果没有该属性JVM也会自动声明该属性,并未该属性赋值。该属性的值是唯一的,用于表示不同的序列化类。只有类的序列化表示完全相同,Java才会进行反序列化工作。
6.2 自定义序列化
本节将介绍Java序列化的两个重要接口Serializable和Externalizable。Serializable是一种mark interface,即标记接口,它没有任何的属性和方法,仅用于表示序列化语义;Externalizable继承自Serializable,但它的内部一定量两个方法,用于制定自定义序列化策略。
6.2.1 Serializable接口
Serializable接口中并未定义任何方法或字段,然而有时需要对序列化的对象做一些特殊的处理,以满足特定的功能需求,例如安全检测、传输信息描述等。Java为此指定了一套特殊的机制,用于解决这些特定问题。
1. 定制序列化策略
进行序列化传输时,有时不仅需要对象本身的数据,还需要传输一些额外的辅助信息,以保证信息的安全、完整和正确。Java利用反射机制,提供了一套有效的机制,允许在序列化和反序列化时,使用定制的方法进行相应的处理。当传输的双方协定好序列化策略后,只要在需要传输的序列化类中添加一组方法来实现这组策略(不是重写,而是自己创建该方法),在序列化时便会自动调用这些规定好的方法进行序列化和反序列化。方法如下:
private void writeObejct(java.io.ObjectOutputStream out) throws IOException
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
这两个方法的作用分别是将特定的对象写入到输出流中以及从输出流中恢复特定的对象,通过这两个方法,用户即可实现自定义的序列化。
下面演示了这两个方法的使用:
public class Person implements Serializable {
private static final long serialVersion = 1L;
private String name = "simple";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private void writeObject(ObjectOutputStream out) throws IOException {
Date date = new Date();
out.writeObject(date);
out.defaultWriteObject();
System.out.println("Serialized.");
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
Date date = (Date) in.readObject();
Date now = new Date();
long offset = now.getTime() - date.getTime();
if (offset >= 100) {
System.out.println("Overtime, deserialize failed.");
return;
}
System.out.println("Allowed deserialize.");
in.defaultReadObject();
}
}
测试1:
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Person person = new Person();
File file = new File("test.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
objectOutputStream.writeObject(person);
objectOutputStream.flush();
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
person = (Person) objectInputStream.readObject();
System.out.println(person.getName());
objectInputStream.close();
}
}
运行结果:
Serialized.
Allowed deserialize.
simple
测试2:
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Person person = new Person();
File file = new File("test.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
objectOutputStream.writeObject(person);
objectOutputStream.flush();
objectOutputStream.close();
Thread.currentThread().sleep(100);
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
person = (Person) objectInputStream.readObject();
System.out.println(person.getName());
objectInputStream.close();
}
}
运行结果:
Serialized.
Overtime, deserialize failed.
null
2. 限制序列化对象的数量
反序列化机制不是简单地将二进制流转化为对象流然后创造对象(因为这无法在Java中实现),而是先利用对应类的空参构造方法创造一个实例(无论是否public都会被调用,因为其底层是通过反射来实现的),然后将对应的属性值传递给实例。这会导致单例模式失效,单例对象被读取两次后不同。
下面演示了单例模式被破坏:
public class Person implements Serializable {
private static final long serialVersion = 1L;
private static Person instance;
private Person() {
}
public static Person getInstance() {
if (instance == null) instance = new Person();
return instance;
}
}
测试:
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Person person = Person.getInstance();
File file = new File("test.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
objectOutputStream.writeObject(person);
objectOutputStream.flush();
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
Person person1 = (Person) objectInputStream.readObject();
objectInputStream = new ObjectInputStream(new FileInputStream(file));
Person person2 = (Person) objectInputStream.readObject();
objectInputStream.close();
if (person1 == person2) {
System.out.println("==");
} else {
System.out.println("!=");
}
}
}
运行结果:
!=
结果并不是我们想要的,因为它证明了获取的并不是同一个对象,这就意味着单例模式在这种情况下会失效。
Java设计者提供了另一种方法,让我们在序列化和反序列化时,可以根据自己的需要,写入或读取指定的实例。使用这种机制,需要在序列化中添加以下两个方法:
方法体 | 说明 | |
---|---|---|
private Object readResolve() | 如果用户在序列化类种添加了该方法,则在进行反序列时,使用该方法返回的对象,作为反序列化对象 | |
private Object writeReplace() | 如果用户在序列化类中添加了该方法,则在进行序列时,序列化该类返回的对象,因此可以指定任意的对象进行序列化 |
下面演示了这两个方法的使用:
public class Person implements Serializable {
private static final long serialVersion = 1L;
private static Person instance;
private Person() {
}
public static Person getInstance() {
if (instance == null) instance = new Person();
return instance;
}
private Object readResolve() {
return getInstance();
}
private Object writeReplace() {
return getInstance();
}
}
测试类与上相同,运行结果:
==
6.2.2 Externalizable接口
Externalizable接口继承自Serializable接口,与Serializable几口不同的是,它内部定义了两个方法用于指定序列化策略和反序列化策略。这两个方法是readExternal()和writeExternal()。它的运行机制是,序列化时使用writeExternal()方法将对象写入输出流中,反序列化时,JVM首先使用一个无参的构造方法实例化一个对象,然后调用该对象的readExternal()方法反序列化一个新对象,因此要求序列化类必须拥有无参的构造函数。此外,还可以使用writeReplace()和readResolve()这两个方法来替换序列化和反序列化的对象。
下面演示了Externalizable的使用方法:
public class Person implements Externalizable {
private static final long serialVersion = 1L;
private String name = "simple";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.write(name.getBytes());
}
@Override
public void readExternal(ObjectInput in) throws IOException {
this.name = in.readLine();
}
}
测试:
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Person person = new Person();
File file = new File("test.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
objectOutputStream.writeObject(person);
objectOutputStream.flush();
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
person = (Person) objectInputStream.readObject();
System.out.println(person.getName());
}
}
运行效果:
simple
实现Externalizable接口的序列化类需要由用户决定传输哪些数据,并制定相应的规则,这样可以提高序列化的效率,也可以保证序列化的安全性。但这种方式要比Serializable方式复杂得多。
6.2.3 Serializable与Externalizable对比
区别 | Serializable | Externalizable |
---|---|---|
实现复杂度 | 实现简单,Java对其由内建支持 | 实现复杂,由开发人员自己完成 |
执行效率 | 所有对象由Java统一保存,性能较低 | 开发人员决定哪个对象保存,可能造成速度提升 |
保存信息 | 保存时占用空间大 | 部分存储,可能造成空间减少 |