Java 序列化

Java 序列化是把Java对象转换为字节序列的过程,相反,反序列化是指把字节序列恢复为Java对象的过程。Java序列化有助于将Java对象文件化,在JVM退出之后也能持久保存,而且序列化的二进制文件也有助于两个Java进程进行通讯。

一、Java 序列化与反序列化

需要实现序列化的类必须实现 Serializable 或者 Externalizable 接口,否则会抛出异常。

将类序列化时,首先要将需要写入的文件用 FileOutputStream 流来封装,然后再用 ObjectOutputStream 类输出流将文件输出流封装。(使用一个输出流来构造一个ObjectOutputStream(对象流)对象)得到对象流对象调用writeObject方法就可以将对应的对象写入相应的文件中。

反序列化的操作几乎一样,使用一个输入流来构造一个ObjectInputStream,调用对象的 readObject方法来得到刚才文件化的对象(得到的是Object对象,所以这里需要强制类型转换)。

下面是一段简单的示例代码来演示序列化的基本操作与功能。

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
/**
* Created by simon on 15/11/9.
*/


public class Text4Serializable {

public static void main(String[] args) throws Exception{
User user1 = new User();
user1.username = "simon";
user1.password = "123456";

User user2 = new User();
user2.username = "Grace";
user2.password = "2345678"

System.out.println("before Serializable: ");
System.out.println(user1);
System.out.println(user2);
try{ //乱七八糟的异常处理代码一大堆。。。
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("./SerializableText"));
os.writeObject(user1); // 将user对象写入文件
os.writeObject(user2);
os.flush();
os.close();
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream("./SerializableText"));
User newuser1 =(User) is.readObject();
User newuser2 =(User) is.readObject(); // 两个对象就按顺序读就好,读取的顺序和写入的顺序要一致。
System.out.println("After Serializable");
System.out.println(newuser1);
System.out.println(newuser2);
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}

}
}


class User implements Serializable{
String username;
String password;

@Override
public String toString() {
return "["+username+" "+password+"]";
}
}

二、transient 关键字

transient 关键字修饰的成员变量仅存在于调用者的内存中而不会写在磁盘里序列化。在上一段代码中,如果将 User 的 password 成员变量修饰为 transient ,反序列化后 password 的值为 null。

使用 transient 关键字时需要注意的是:

对于 static 修饰的成员变量,无论是否有 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
50
51
52
53
54
55
56
/**
* Created by simon on 15/11/9.
*/


public class Text4Transient {

public static void main(String[] args) throws Exception{
User user1 = new User();
user1.username = "simon";
user1.password = "123456";

User user2 = new User();
user2.username = "Grace";
user2.password = "234567";

System.out.println("before Serializable: ");
System.out.println(user1);
System.out.println(user2);
try{
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("./SerializableText"));
os.writeObject(user1); // 将user对象写入文件
os.writeObject(user2);
os.flush();
os.close();
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
user2.password = "22222"; // lable
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream("./SerializableText"));
User newuser1 =(User) is.readObject();
User newuser2 =(User) is.readObject();
System.out.println("After Serializable");
System.out.println(newuser1);
System.out.println(newuser2);
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}

}
}


class User implements Serializable{
String username;
static String password;

@Override
public String toString() {
return "["+username+" "+password+"]";
}
}

这段代码的输出为:

1
2
3
4
5
6
before Serializable: 
[simon 234567]
[Grace 234567]
After Serializable
[simon 22222]
[Grace 22222]

看出来了么?实际上不是无法序列化 static 修饰的成员变量,而是使用 static 修饰的成员变量,实例化时都会在静态常量区中取值,实际上这个特性与 transient 关键字无关,在序列化的过程中,只要是 static 修饰的成员变量,都会从静态常量池中取值,如果将上段代码中 注释 lable 的那行注释掉,那么程序段的输出将是:

1
2
3
4
5
6
before Serializable: 
[simon 234567]
[Grace 234567]
After Serializable
[simon 234567]
[Grace 234567]

三、Externalizable接口实现对象序列化

Serializable 接口序列化实现起来比较简单,但实际上对象的序列化可以通过实现两种接口来实现,若实现的是Serializable接口,则所有的序列化将会自动进行,但如果实现Externalizable接口,则没有任何东西可以自动序列化,需要在writeExternal方法中进行手工指定所要序列化的变量,这与是否被transient修饰无关。下面用一段简单的代码对Externalizable 接口进行说明。

在实现了Externalizable接口后,writeObject函数将通过writeExternal去赋值对象。readObject函数将通过readExterna函数去为对象赋值。

这段程序会输出 content 字符串

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
/**
* @descripiton Externalizable接口的使用
* @simon
*
*/

public class ExternalizableTest implements Externalizable {

private transient String content = "是的,我将会被序列化,不管我是否被transient关键字修饰";

@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(content);
}

@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {

content = (String) in.readObject();
}

public static void main(String[] args) throws Exception {

ExternalizableTest et = new ExternalizableTest();
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
new File("test")));
out.writeObject(et);

ObjectInput in = new ObjectInputStream(new FileInputStream(new File(
"test")));
et = (ExternalizableTest) in.readObject();
System.out.println(et.content);

out.close();
in.close();
}
}

四、serialVersionUID 的使用

Java 序列化时会根据类的serialVersimonUID来验证版本的一致性,同样的,反序列化时JVM会把传来的字节流中的serialVersionUID与本地相应类的serialVersionUID进行比较,如果相同就认为是一致的。

serialVersionUID 有两种设置方式,简单的就是默认的1L:
private static final long serialVersionUID = 1L;

还可以根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,可以在IDE中自动计算。如果没有显式的声明serialVersionUID,那么序列化机制会根据类名以及类中相应元素自动生成一个serialVersionUID作为序列化的版本号使用,理论上类名、方法等不发生任何变化时无论重新编译几次,serialVersionUID也不会发生任何变化。

通常情况下,如果我们不希望编译后强制划分类的版本,即实现序列化接口的实体能够兼容之前的版本,那么我们一定要指定serialVersionUID为一个long类型的变量,只要这个值不变就可以实现相同的序列化与反序列化。