Java序列化总结

什么是序列化?

序列化是指把对象保存为二进制字节码的过程。

反序列化是指把二进制码重新转换成对象的过程。

Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。

将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化,也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。

整个过程都是 Java 虚拟机(JVM)独立的,也就是说,在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。

如何进行序列化?

如果要让每个对象支持序列化机制,必须让它的类是可序列化的,则该类必须实现如下两个接口之一:

  • Serializable
  • Extmalizable

这里先只讨论实现Serializable接口来进行序列化的方法。
需要注意以下几点:

  • Serializable是一个标示性接口,接口中没有定义任何的方法或字段,仅用于标示可序列化的语义。
  • 静态变量和成员方法不可序列化。
  • 一个类要能被序列化,该类中的所有引用对象也必须是可以被序列化的。否则整个序列化操作将会失败,并且会抛出一个NotSerializableException,除非我们将不可序列化的引用标记为transient。
  • 声明成transient的变量不被序列化工具存储,同样,static变量也不被存储。

我们先来看下面这个例子:
将一个对象序列化之后存储到指定文件中

1
2
3
4
5
6
7
8
9
10
11
12
public class Person implements Serializable
{
int age;
String address;
double height;
public Person(int age, String address, double height)
{
this.age = age;
this.address = address;
this.height = height;
}
}

1
2
3
4
5
6
7
8
9
10
11
public class SerializableTest
{
public static void main(String[] args) throws IOException, IOException
{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
"d:/data.txt"));
Person p = new Person(25,"China",180);
oos.writeObject(p);
oos.close();
}
}

运行之后,我们打开data.txt文件会发现里面是一堆乱码。这是因为对象序列化之后,写入的是一个二进制文件。
Person对象实现了Serializable接口,这个接口没有任何方法需要被实现,只是一个标记接口,表示这个类的对象可以被序列化。

下面我们来实现反序列化对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SerializableTest
{
public static void main(String[] args) throws IOException, IOException,
ClassNotFoundException
{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
"d:/data.txt"));
Person p = new Person(25, "China", 180);
oos.writeObject(p);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
"d:/data.txt"));
Person p1 = (Person) ois.readObject();
System.out.println("age=" + p1.age + ";address=" + p1.address
+ ";height=" + p1.height);
ois.close();
}
}

执行结果:

1
age=25;address=China;height=180.0

如果使用序列化机制向文件中写入了多个对象,在反序列化时,需要按实际写入的顺序读取。

使用transient关键字实现自定义序列化

现在我们考虑一下这个问题,对象的成员变量中含有引用类型,会有什么不同?

这个引用类型的成员变量必须也是可序列化的,否则拥有该类型成员变量的类的对象不可序列化。

在引用对象这个地方,会出现一种特殊的情况。例如,小明是语文老师的学生也是数学老师的学生。

小明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.io.Serializable;

public class Student implements Serializable{

private String name;
private int age;
private int no;

public Student(String name, int age, int no) {
super();
this.name = name;
this.age = age;
this.no = no;
}
public Student() {
super();
}
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 int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + ", no=" + no + "]";
}
}

语文老师
1
2
3
4
5
6
7
8
9
10
11
import java.io.Serializable;

public class Teacher implements Serializable{

private string type = "语文老师";
private Student student; // 小明

// get、set方法、构造方法

// toString方法
}
英语老师
1
2
3
4
5
6
7
8
9
10
11
import java.io.Serializable;

public class Teacher implements Serializable{

private string type = "英语老师";
private Student student; // 小明

// get、set方法、构造方法

// toString方法
}

对于这三个对象,序列化语文老师的时候,会隐式的序列化小明,同样的序列化英语老师的时候也会隐士的序列化小明,所以在反序列化的时候会得到三个小明对象。

为了避免这种情况,JAVA的序列化机制采用了一种特殊的算法:

  • 所有保存到磁盘中的对象都有一个序列化编号。
  • 当程序试图序列化一个对象时,会先检查该对象是否已经被序列化过,只有该对象从未(在本次虚拟机中)被序列化,系统才会将该对象转换成字节序列并输出。
  • 如果对象已经被序列化,程序将直接输出一个序列化编号,而不是重新序列化。

transient关键字

当某个字段被声明为transient后,默认序列化机制就会忽略该字段。
我们将上面例子中的Person对象的address变量用transient来修饰。

1
2
3
4
5
6
7
8
9
10
11
12
public class Person implements Serializable
{
int age;
transient String address;
double height;
public Person(int age, String address, double height)
{
this.age = age;
this.address = address;
this.height = height;
}
}

同时反序列化结果为:
age=25;address=null;height=180.0

我们也可以在对象所在类中重写writerObject和readObject方法来自定义序列化。