-
概述
在Java开发过程中,我们常常需要将JVM中的对象持久化下来,并且能够在需要的时候把对象重新读取出来。数据库可以帮助我们解决这个问题,另外Java原生的对象序列化也可以帮助我们实现该功能,这里我们深入了解一下序列化的知识。在实际应用场景中,对象的序列化与反序列化被广泛应用到RMI(远程方法调用)及网络传输中。
-
Serializable接口
- 类通过实现 java.io.Serializable 接口以启用其序列化功能;
- 如果要序列化的类有父类,要想同时将在父类中定义过的变量持久化下来,那么父类也应该继承java.io.Serializable接口。
public class Person implements Serializable{
private String name;
private Integer age;
private Education education;
public Person() {
System.out.println("default constructor");
}
public Person(String name, Integer age) {
System.out.println("arg constructor");
this.name = name;
this.age = age;
}
@Override
public String toString(){
return "Person{name=" + name + ", age=" + age + ", school=" + education.getSchool() + "}";
}
}
public static void main(String[] args) throws Exception {
Person person = new Person("kobe",24);
Education education = new Education();
education.setSchool("浙江大学");
person.setEducation(education);
System.out.println(person);
File file = new File("C:\\Users\\Administrator\\Desktop\\person.txt");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(person);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
Person newPerson = (Person)in.readObject();
in.close();
System.out.println(newPerson);
}
输出结果:
arg constructor
Person{name=kobe, age=24, school=浙江大学}
Person{name=kobe, age=24, school=浙江大学}
这里有两点是值得注意的,一是如果Education未实现Serializable接口,会抛出java.io.NotSerializableException;二是当反序列化Person对象时,并没有调用Person的任何构造器,看起来就像是直接使用字节将Person对象还原出来的。
-
序列化的控制
通过Serializable 接口都是按照默认机制实现序列化,有些场景下,我们希望能控制序列化的机制。比如age字段被认为是敏感数据,我们希望不被持久化下来,又或者持久化过程中对该字段进行加密。那么有哪些方法可以实现呢?
-
transient关键字
transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
public class Person implements Serializable {
private String name;
transient private Integer age;
public Person() {
System.out.println("default constructor");
}
public Person(String name, Integer age) {
System.out.println("arg constructor");
this.name = name;
this.age = age;
}
@Override
public String toString(){
return "Person{name=" + name + ", age=" + age + "}";
}
}
public static void main(String[] args) throws Exception {
Person person = new Person("kobe",24);
System.out.println(person);
File file = new File("C:\\Users\\Administrator\\Desktop\\person.txt");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(person);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
Person newPerson = (Person)in.readObject();
in.close();
System.out.println(newPerson);
}
输出结果:
arg constructor
Person{name=kobe, age=24}
Person{name=kobe, age=null}
我们熟悉使用 Transient 关键字可以使得字段不被序列化,那么还有别的方法吗?根据父类对象序列化的规则,我们可以将不需要被序列化的字段抽取出来放到父类中,子类实现 Serializable 接口,父类不实现,根据父类序列化规则,父类的字段数据将不被序列化。
-
Externalizable
除了Serializable 之外,java中还提供了另一个序列化接口Externalizable。Externalizable继承了Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()与readExternal()方法。我们把上面的例子改成实现Externalizable接口:
public class Person implements Externalizable {
private String name;
private Integer age;
public Person() {
System.out.println("default constructor");
}
public Person(String name, Integer age) {
System.out.println("arg constructor");
this.name = name;
this.age = age;
}
@Override
public String toString(){
return "Person{name=" + name + ", age=" + age + "}";
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeObject(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
//这里的读顺序要和writeExternal中的写顺序保持一致
name = (String) in.readObject();
age = (Integer) in.readObject();
}
}
public static void main(String[] args) throws Exception {
Person person = new Person("kobe",24);
System.out.println(person);
File file = new File("C:\\Users\\Administrator\\Desktop\\person.txt");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(person);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
Person newPerson = (Person)in.readObject();
in.close();
System.out.println(newPerson);
}
输出结果:
arg constructor
Person{name=kobe, age=24}
default constructor
Person{name=kobe, age=24}
由此可见,Externalizable接口亦可以实现序列化和反序列化。在writeExternal()与readExternal()方法中可以手动指定要序列化的字段,并且两个方法中的字段读写顺序应该完全保持一致。如果不指定字段,则不会被序列化:
public class Person implements Externalizable {
private String name;
private Integer age;
public Person() {
System.out.println("default constructor");
}
public Person(String name, Integer age) {
System.out.println("arg constructor");
this.name = name;
this.age = age;
}
@Override
public String toString(){
return "Person{name=" + name + ", age=" + age + "}";
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
}
public static void main(String[] args) throws Exception {
Person person = new Person("kobe",24);
System.out.println(person);
File file = new File("C:\\Users\\Administrator\\Desktop\\person.txt");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(person);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
Person newPerson = (Person)in.readObject();
in.close();
System.out.println(newPerson);
}
输出结果:
arg constructor
Person{name=kobe, age=24}
default constructor
Person{name=null, age=null}
我们看到在writeExternal()与readExternal()方法中不指定字段,那么就不会被序列化,反序列化之后就被赋为默认值。因此我们可以在这两个方法中自由控制序列化内容,并可以在序列化之前做一些特殊处理,比如对敏感字段加密。
一个有意思的地方是输出结果中打印出了default constructor,我们发现通过Externalizable接口反序列化的过程中调用了默认的无参构造方法。原来在使用Externalizable进行反序列化时,会调用类的无参构造方法去创建一个新对象,然后通过writeExternal()与readExternal()方法将字段的值填充到新对象中。由于这个原因,实现Externalizable接口的类必须要提供一个无参构造方法,且它的访问权限要为public。
-
writeObject()与readObject()
实现Externalizable 接口可以控制序列化,其实使用Serializable 接口也可以达到相同的目的,只要添加writeObject()与readObject()方法:
public class Person implements Serializable {
private String name;
private Integer age;
public Person() {
System.out.println("default constructor");
}
public Person(String name, Integer age) {
System.out.println("arg constructor");
this.name = name;
this.age = age;
}
@Override
public String toString(){
return "Person{name=" + name + ", age=" + age + "}";
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeObject(name);
out.writeObject(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = (Integer) in.readObject();
}
}
public static void main(String[] args) throws Exception {
Person person = new Person("kobe",24);
System.out.println(person);
File file = new File("C:\\Users\\Administrator\\Desktop\\person.txt");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(person);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
Person newPerson = (Person)in.readObject();
in.close();
System.out.println(newPerson);
}
输出结果:
arg constructor
Person{name=kobe, age=24}
Person{name=kobe, age=24}
所以实现Serializable 接口并且在类中添加writeObject()与readObject()方法,也可以达到与实现Externalizable 接口相同的目的,两者都能控制序列化过程。请注意这里说的是添加writeObject()与readObject()方法,而不是覆盖或者实现,并且这两个方法还被定义成了private,那么我们能想到的就是它们是通过反射被调用的。事实上,在ObjectOutputStream 和ObjectInputStream 的writeObject()与readObject()方法中通过反射调用了Person类中自定义的writeObject()与readObject()方法。
-
序列化ID
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。序列化ID在IDEA下提供了两种生成策略,一个是固定的1L,一个是随机生成一个不重复的long类型数据(实际上是使用JDK工具生成),在这里有一个建议,如果没有特殊需求,就是用默认的1L就可以,这样可以确保代码一致时反序列化成功。
当类中没有显示定义serialVersionUID字段,虚拟机会根据编译时的class自动生成一个serialVersionUID。如果后续该类发生变更,加了字段甚至是加了空格,都会使虚拟机重新生成一个serialVersionUID,这时反序列化便会出现serialVersionUID不一致,导致反序列化失败。所以建议要有序列化需求的实体类都要显示定义serialVersionUID字段。
-
静态变量序列化
public class Person implements Serializable {
private String name;
private Integer age;
private static int staticVar = 1;
public Person() {
System.out.println("default constructor");
}
public Person(String name, Integer age) {
System.out.println("arg constructor");
this.name = name;
this.age = age;
}
@Override
public String toString(){
return "Person{name=" + name + ", age=" + age + ", staticVar=" + staticVar + "}";
}
}
public static void main(String[] args) throws Exception {
Person person = new Person("kobe",24);
System.out.println(person);
File file = new File("C:\\Users\\Administrator\\Desktop\\person.txt");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(person);
out.close();
person.setName("T-mac");
person.setAge(23);
staticVar = 2;
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
Person newPerson = (Person)in.readObject();
in.close();
System.out.println(newPerson);
}
输出结果:
arg constructor
Person{name=kobe, age=24, staticVar=1}
Person{name=kobe, age=24, staticVar=2}
这个例子中,person对象被序列化后我们修改了它的状态以及类的静态变量,反序列化后发现状态值的修改不生效,而静态变量的修改却生效了,就是说静态变量没有从序列化文件中恢复出来。原来,序列化时并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量。