写在前面

本文主要是对Java序列化学习的一些总结,一来是方便自己以后查阅,二来是希望通过本文能给他人带来一些帮助。

对象序列化

Java提供了一种对象序列化的机制,在该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。将序列化对象写入文件之后,可以再从文件中读取出来,并且对它进行反序列化,也就是说,可以根据对象的类型信息、对象的数据、还有对象中的数据类型可以在内存中新建该对象。

在实际的应用中,我们为什么需要对象序列化机制呢?因为在一般情况下,只有当JVM进程处于运行时,JVM建立的对象才可能存在,也就是说,这些对象的生命周期不会比JVM进程的生命周期更长。但是在现实的应用中,有时需要在JVM进程停止运行之后能够保存(持久化)指定的对象,并且在将来某个时刻重新读取被保存的对象,或者我们希望将一个进程创建的对象传送到另一个JVM进程中。Java的对象序列化机制就能够帮助我们实现这些功能(这就意味着序列化机制是可以自动弥补不同操作系统之间的差异)。

一般来说,对象的序列化主要有两种用途:

  • 把对象的字节序列持久化到硬盘,通常保存在一个文件中;
  • 在网络上传输对象的字节序列;

我们在使用Java对象序列化时,会把对象的状态保存为一组字节序列,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的状态,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量,因为静态变量是类的状态。

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

但是一个类的对象如果要想序列化成功,必须满足两个条件:

  • 该类必须实现 java.io.Serializable 接口。
  • 该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。

如果你想知道一个Java标准类是否是可序列化的,请查看该类的文档。检验一个类的实例是否能序列化十分简单, 只需要查看该类有没有实现java.io.Serializable接口即可。

序列化实现的示例

  • 对象序列化时,首先要创建某个OutputStream对象,然后将其封装在一个ObjectOutputStream对象内,这时,只需要调用writeObject()即可将对象序列化,并将其发送给OutputStream(对象化序列是基于字节的,因要使用InputStreamOutputStream继承层次结构);
  • 对象反序列化时,需要将一个InputStream对象封装在ObjectInputStream内,然后调用readObject()方法;

对象序列化时,不仅保存了对象的“全景图”,而且能追踪对象内所包含的所有引用,并保存那些对象,接着又能对对象内包含的每个这样的引用进行追踪,并保存那些对象,这种情况有时被称为“对象网”。

这里我们先建立一个对象Person,如下(示例参考):

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
41
42
43
44
45
46
// Person.java
import java.io.Serializable;
import java.util.Random;

public class Person implements Serializable{
private String name;
private int age;
private Gender gender;
private int id;

public Person(String name, int age, Gender gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.id = (new Random()).nextInt(10);
}

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 Gender getGender() {
return gender;
}

public void setGender(Gender gender) {
this.gender = gender;
}

@Override
public String toString() {
return "[" + name + ", " + age + ", " + gender + ", " + id + "]";
}
}

下面,我们在一个进程里对其进行序列化,然后再反序列化出该对象(示例

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
41
42
43
44
45
46
47
48
49
50
51
// Serialize.java
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class Serialize {
public static void main(String[] args) {
Person p1 = new Person("wm0", 10, Gender.MALE);
Person p2 = new Person("wm1", 18, Gender.MALE);

System.out.println("p1 = " + p1);
ObjectOutputStream out = null;
try {
out = new ObjectOutputStream(new FileOutputStream("/home/matt/person.out")); // 写入本地文件
out.writeObject("Person1 storage\n");
out.writeObject(p1);
out.close(); // Also flushes output
ObjectInputStream in = new ObjectInputStream(new FileInputStream("/home/matt/person.out")); // 从本地文件读取
String s = null;
Person p11 = null;
try {
s = (String) in.readObject();
p11 = (Person) in.readObject();
System.out.println("after Serialize: " + p11);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

System.out.println("\n"+ "p2 = " + p2);
ByteArrayOutputStream bout = new ByteArrayOutputStream(); // 将数据写入缓冲区
ObjectOutputStream out2 = new ObjectOutputStream(bout);
out2.writeObject("Person2 storage\n");
out2.writeObject(p2);
out2.flush();
ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray())); // 通过toByteArray()获取数据
try {
s = (String) in2.readObject();
Person p22 = (Person) in2.readObject();
System.out.println("after Serialize: " + p22);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

这里有一点要注意的是:反序列化Person对象时,需要要能找到Person.class,否者就会抛出ClassNotFoundException的异常。

序列化的控制

通过上面的例子,我们可以看出序列化的使用,其实还是很简单的,但是,如果我们有特殊的需要那又该怎么办呢?下面我们介绍几种序列化的控制机制。

Externalizable接口

如果我们希望对象的一部分被序列化,而另一部分不被序列化;或者一个对象被还原之后,某子对象需要重新创建,从而不必将该子对象序列化。在这种情况下,我们可以通过实现Externalization接口——该接口实现Serializable接口,同时增加两个方法:writeExternal()readExternal(),这两个方法会在序列化和反序列化还原的过程中被自动调用以便执行一些特殊操作。

这与恢复Serializable对象不同,对于Serializable对象,对象完全以它存储的二进制位为基础来构造,而不调用构造器。但是对于一个Externalization对象,所有普通的默认构造器都会被调用(包括字段定义时的初始化),然后调用readExternal()

必须要注意这一点:所有默认的构造器都会被调用,才能使Externalization对象产生正确的行为。

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// Blip3.java
import java.io.Externalizable;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class Blip3 implements Externalizable {
private int i;
private String s; // No initialization

public Blip3() {
System.out.println("Blip3 Constructor");
// s, i not initialized
}

public Blip3(String x, int a) {
System.out.println("Blip3(String x, int a)");
s = x;
i = a;
// s & i initialized only in non-default constructor.
}

public String toString() {
return s + i;
}

public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("Blip3.writeExternal");
// You must do this:
out.writeObject(s);
out.writeInt(i);
}

public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
System.out.println("Blip3.readExternal");
// You must do this:
s = (String) in.readObject();
i = in.readInt();
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
System.out.println("Constructing objects:");
Blip3 b3 = new Blip3("A String ", 47);
System.out.println(b3);
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("/home/matt/Blip3.out"));
System.out.println("Saving object:");
o.writeObject(b3);
o.close();
// Now get it back:
ObjectInputStream in = new ObjectInputStream(new FileInputStream("/home/matt/Blip3.out"));
System.out.println("Recovering b3:");
b3 = (Blip3) in.readObject();
System.out.println(b3);
}
}
/* Output:
* Constructing objects:
* Blip3(String x, int a)
* A String 47
* Saving object:
* Blip3.writeExternal
* Recovering b3:
* Blip3 Constructor
* Blip3.readExternal
* A String 47
*/

在上面的例子中,字段si只会在第二个构造器中初始化,而不是在默认的构造器中初始化。这意味着假如不在readExternal()中初始化sis就会为null,而i就会为零(因为在创建对象的第一步中将对象的存储空间清理为0)。如果我们把writeExternal()方法中两行注释掉,对象还原后,snull,而i是零。

我们如果从一个Externalization对象继承,通常需要调用基类版本的writeExternal()readExternal()来为基类组件提供恰当的存储和恢复功能。

因此,为了正常运行,我们不仅需要在writeExternal()方法(没有任何默认行为来为Externalization对象写入任何成员对象)中将来自对象的重要信息写入,还必须在readExternal()方法中恢复数据。

Transient关键字

在进行序列化控制时,可能某个特定子对象不想让Java的序列化机制自动保存和恢复。如果子对象表示的是我们不希望将其序列化的敏感信息(如密码),那么我们就会面临这种情况。即使对象中的这些信息是private属性,一经序列化处理,人们就可以通过读取文件或者拦截网络传输的方式来访问到它。

有两种方法可以实现上述要求:

  1. 将类实现Externalizable接口,这样的话,没有任何东西是可以自动序列化,并且可以在writeExternal()内部只对所需部门进行显式的序列化;
  2. 如果在操作的是一个Serializable对象,那么所有序列化操作都会自动进行,为了能够进行控制,可以用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
41
42
43
44
45
46
47
48
49
// Login.java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Date;
//: io/Logon.java
//Demonstrates the "transient" keyword.
import java.util.concurrent.TimeUnit;

public class Login implements Serializable {
private Date date = new Date();
private String username;
private transient String password;

public Login(String name, String pwd) {
username = name;
password = pwd;
}

public String toString() {
return "logon info: \n username: " + username + "\n date: " + date + "\n password: " + password;
}

public static void main(String[] args) throws Exception {
Login a = new Login("Hulk", "myLittlePony");
System.out.println("logon a = " + a);
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("/home/matt/Logon.out"));
o.writeObject(a);
o.close();
TimeUnit.SECONDS.sleep(1); // Delay
// Now get them back:
ObjectInputStream in = new ObjectInputStream(new FileInputStream("/home/matt/Logon.out"));
System.out.println("Recovering object at " + new Date());
a = (Login) in.readObject();
System.out.println("logon a = " + a);
}
} /*
* logon a = logon info:
* username: Hulk
* date: Tue May 17 11:11:28 CST 2016
* password: myLittlePony
* Recovering object at Tue May 17 11:11:29 CST 2016
* logon a = logon info:
* username: Hulk
* date: Tue May 17 11:11:28 CST 2016
* password: null
*/

可以看到,其中的dateusername域是一般的(不是transient的),所以它们会被自动序列化。而passwordtransient的,所以不会被自动保存到磁盘;另外,自动序列化机制也不会尝试去恢复它。当对象被恢复时,password域就会变成null。我们还可以发现,date字段也是从存储到了磁盘并从磁盘上被恢复出来,而且没有再重新生成。

由于实现Externalizable接口的对象在默认情况下不保存它们的任何字段,所以transient关键字只能和Serializable对象一起使用。

重写writeObject()和readObject()方法

如果不是特别坚持使用Externalizable接口,那么还有一种方法。我们可以实现Serializable接口,并添加writeObject()readObject()方法。这样一旦对象被序列化或者被反序列化还原,就会自动地分别调用这两个方法,而不是使用默认的序列化机制。(示例

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// SerialCtl.java

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class SerialCtl implements Serializable {
private String a;
private transient String b;

public SerialCtl(String aa, String bb) {
a = "Not Transient: " + aa;
b = "Transient: " + bb;
}

public String toString() {
return a + " " + b;
}

/**
* 自定义该方法,这里要注意方法时private
* 在调用ObjectOutputStream.writeObject()时,会检查所传递的Serializable对象,看看是否实现了它自己的writeObject()。
* 如果是这样,就跳过正常的序列化过程并调用它的writeObject()
*
* @param stream
* @throws IOException
*/
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject(); // 执行默认的writeObject()
stream.writeObject(b); // transient字段需要明确保存和
}

private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject(); // 执行默认的readObject()
b = (String) stream.readObject(); // transient字段需要明确恢复
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
SerialCtl sc = new SerialCtl("Test1", "Test2");
System.out.println("Before:\n" + sc);
ByteArrayOutputStream buf = new ByteArrayOutputStream();
ObjectOutputStream o = new ObjectOutputStream(buf);
o.writeObject(sc);
// Now get it back:
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buf.toByteArray()));
SerialCtl sc2 = (SerialCtl) in.readObject();
System.out.println("After:\n" + sc2);
}
}
/*
* Before:
* Not Transient: Test1 Transient: Test2
* After:
* Not Transient: Test1 Transient: Test2
*/

上述的例子中,非transient字段由defaultReadObject保存,而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
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class StaticTest implements Serializable{
private static int id=10;

public static void main(String[] args) {
System.out.println("Constructing objects:");
ObjectOutputStream o = null;
try {
o = new ObjectOutputStream(new FileOutputStream("/home/matt/static.out"));
o.writeObject(new StaticTest());
o.close();
StaticTest.id=0;
ObjectInputStream in = new ObjectInputStream(new FileInputStream("/home/matt/static.out"));
StaticTest staticTest = null;
try {
staticTest = (StaticTest) in.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(staticTest.id);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* output:
* 0
*/

程序输出的结果为修改之后的结果,正如我们前面所述一样,对象序列化时并不会序列化静态变量,这一点可以这样理解:对象序列化是序列化对象的状态,而静态变量是类变量,也就是类的状态。因此,对象序列化并不保存静态变量

存储规则

这里我们通过一个例子来看一下Java序列化机制的存储规则,主要是多次写入同一个对象的情况(示例

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
41
42
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class StoreTest {
public static void main(String[] args) {
ObjectOutputStream o = null;
try {
o = new ObjectOutputStream(new FileOutputStream("/home/matt/store.out"));
Person person=new Person("matt1",20,Gender.MALE);
o.writeObject(person);
person.setAge(22);
System.out.println(new File("/home/matt/store.out").length());
o.writeObject(person);
System.out.println(new File("/home/matt/store.out").length());
o.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("/home/matt/store.out"));
try {
Person person1= (Person) in.readObject();
System.out.println(person1.getAge());
Person person2= (Person) in.readObject();
System.out.println(person2.getAge());
System.out.println(person1==person2);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* Output:
* 232
* 237
* 20
* 20
* true
*/

在上述示例中,对于同一个对象,在修改完年龄值后又重新将该实例对象序列化到文件。通过运行的结果我们可以发现:

  • 第二次将对象序列化到文件之后,文件的大小只增加了5个字节的大小;
  • 第二次序列化的对象年龄值已经修改为22,但是从反序列化的结果来看,该实例对象的年龄值并未改变。

大家是不是感觉到非常的奇怪,通过下面的两点解释之后,大家可能就会明白这其中的原因了:

  1. 因为写入的是同一个对象,Java序列化机制为了节省磁盘空间,当写入文件的为同一个对象时,并不会将对象的内容再次进行存储,而只是再次存储一份引用,上面增加的5个字节的存储空间就是新增的引用和一些控制信息的空间,从反序列化的结果也可以看出,两个引用指向的是同一个对象;
  2. 虽然第二次存储时将年龄修改为22,但是因为Java序列化机制在第二次序列化同一个对象时,并保存具体的数据,只是保存了第一次的引用,所以反序列化时,得到的对象都是第一次序列化的对象。

序列化ID

这里可以可以参考Java中序列化的serialVersionUID作用一文。

这里我们就简单说一下序列化ID的作用:

serialVersionUID用来表明类的不同版本间的兼容性。它有两种生成方式: 一个是默认的1L;另一种是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段 。

  1. 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;而在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID
  2. 当你序列化了一个类实例后,希望更改一个字段或添加一个字段,不设置serialVersionUID,所做的任何更改都将导致无法反序化旧有实例,并在反序列化时抛出一个异常。如果你添加了serialVersionUID,在反序列旧有实例时,新添加或更改的字段值将设为初始化值(对象为null,基本类型为相应的初始默认值),字段被删除将不设置。

参考: