dcLunatic's blog

Java序列化

字数统计: 2.6k阅读时长: 11 min
2018/09/21 Share

序列化和反序列化

[TOC]

概念

把对象转换为字节序列的过程称为对象的序列化。
把字节序列恢复为对象的过程称为对象的反序列化

用途

对象的序列化主要有两种用途:
1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
2) 在网络上传送对象的字节序列。

Java中使用方法

java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可以对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。

java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

只有实现了Serializable接口可以被序列化,但序列化只能采用默认的序列化。而实现了Externalizable接口的类也可以实现序列化,因为它继承自Serializable接口,且可以完全由自身来控制序列化的行为。

一个简单的例子

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
import java.io.*;
public class Main implements Serializable{
public static void main(String[] args) {

System.out.println("Hello world");
SerializeDemo();
DeserializeDemo();

}
private static void SerializeDemo(){
try {
Test test = new Test("John", 15);
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("/root/Desktop/test.txt")));
oo.writeObject(test);
System.out.println("序列化结束");
oo.close();
}catch(FileNotFoundException e){
System.out.println("无法读写文件");
}catch(IOException e){
System.out.println("IO异常");
}
}
private static void DeserializeDemo(){
try{
ObjectInputStream oi = new ObjectInputStream(new FileInputStream(new File("/root/Desktop/test.txt")));
Test test = (Test)oi.readObject();
System.out.println("反序列化成功");
System.out.println(test.toString());
}catch(Exception e){
e.printStackTrace();
}

}
}

class Test implements Serializable{
private static final long serialVersionUID = 1L;
private int age;
private String name;
private String sex;
public Test(String name, int age){
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Test{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}

serialVersionUID的作用

serialVersionUID字面意思是序列化的版本号,凡是实现了Serializable接口的类都有一个表示序列化版本标识符的静态变量。

如果没有serialVersionUID,也是可以编译通过的,对正常的序列化反序列化是没有影响的,至于有影响的情况,看下面解释。

serialVersionUID有两种生成方式

  • 采用默认的serivalVersionUID也就是默认的1L,private static final long serialVersionUID = 1L;
  • 通过类名、接口名、方法和属性等来生成,如 private static final long serialVersionUID = 4603642343377807741L;

serialVersionUID的作用

问题:假如有这么一个类,序列化程二进制数据保存在磁盘中,但是该类的结构发生了变化,如添加了一个属性,此时可以再将磁盘中的二进制数据反序列成这个类吗?

先去除servialVersionUID属性,然后序列化保存
Test类中添加属性sex,然后直接反序列化

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
package com.dcLunatic;

import java.io.*;


public class Main implements Serializable{
public static void main(String[] args) {

System.out.println("Hello world");
//SerializeDemo();
DeserializeDemo();

}
private static void SerializeDemo(){
try {
Test test = new Test("John", 15);
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("/root/Desktop/test.txt")));
oo.writeObject(test);
System.out.println("序列化结束");
oo.close();
}catch(FileNotFoundException e){
System.out.println("无法读写文件");
}catch(IOException e){
System.out.println("IO异常");
}
}
private static void DeserializeDemo(){
try{
ObjectInputStream oi = new ObjectInputStream(new FileInputStream(new File("/root/Desktop/test.txt")));
Test test = (Test)oi.readObject();
System.out.println("反序列化成功");
System.out.println(test.toString());
}catch(Exception e){
e.printStackTrace();
}

}
}


class Test implements Serializable{
//private static final long serialVersionUID = 1L;
private int age;
private String name;
private String sex;
public Test(String name, int age){
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Test{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}

此时,运行后程序就会抛出异常

1
2
3
4
5
6
7
8
9
10
Hello world
java.io.InvalidClassException: com.dcLunatic.Test; local class incompatible: stream classdesc serialVersionUID = -3765794941312665207, local class serialVersionUID = -2796904131256425943
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:687)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1883)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1749)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2040)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1571)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at com.dcLunatic.Main.DeserializeDemo(Main.java:30)
at com.dcLunatic.Main.main(Main.java:11)

因为在Test类中缺少了serialVersionUID,而在编译时,该值会由编译器自动生成,因为前后两次的serialVersionUID不一致,所以导致反序列化失败(序列化版本不一致),此时,如果显式的指定serialVersionUID的值,就不会出现这个问题,反序列化时对后来新增的属性sex就不会有任何赋值修改。

就算类不会发生改动,但在不同的编译器中,自动生成的serialVersionUID的值可能也会存在差异的。

serialVersionUID的作用还是很大的,比如在那个face

transient关键字

在序列化的过程中,如果有一个特定的属性字段比较敏感,不想被序列化,可以考虑使用transient关键字关闭序列化处理。

这里有一个更加常用的方法,父类不实现Serializable接口,但是子类实现,将不想要序列化的放在父类中,子类中放想要序列化的内容即可。

  1. 如果子类实现Serializable接口而父类未实现时,父类不会被序列化。
  2. 如果父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口。

原因:跟子类父类的内存分配有关,点这里或者是这里阅读相关博文。

自定义writeObject方法和readObject方法

实现Serializable接口的类序列化的话,是使用默认的序列化方式。但我们可以使用writeObjectreadObject方法来实现对序列化的更多控制。

在序列化过程中,虚拟机会试图调用对象类里的writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及调用ObjectInputStream 的 defaultReadObject 方法。用户自定义的 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
public class Test implements Serializable{
private static final long serialVersionUID = 1L;
private String password = "pass";
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}

private void writeObject(ObjectOutputStream out) {
try {
PutField putFields = out.putFields();
System.out.println("原密码:" + password);
password = "encryption";//模拟加密
putFields.put("password", password);
System.out.println("加密后的密码" + password);
out.writeFields();
} catch (IOException e) {
e.printStackTrace();
}
}

private void readObject(ObjectInputStream in) {
try {
GetField readFields = in.readFields();
Object object = readFields.get("password", "");
System.out.println("要解密的字符串:" + object.toString());
password = "pass";//模拟解密,需要获得本地的密钥
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

}

public static void main(String[] args) {
try {
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("result.obj"));
out.writeObject(new Test());
out.close();

ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
"result.obj"));
Test t = (Test) oin.readObject();
System.out.println("解密后的字符串:" + t.getPassword());
oin.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}

defaultWriteObject方法和defaultReadObject方法不会检查serialVersionUID的值是否一致

Externalizable

Java默认的序列化机制非常简单,而且序列化后的对象不需要再次调用构造器重新生成,但是在实际中,我们可以会希望对象的某一部分不需要被序列化,或者说一个对象被还原之后,其内部的某些子对象需要重新创建,从而不必将该子对象序列化。在这些情况下,我们可以考虑实现Externalizable接口从而代替Serializable接口来对序列化过程进行控制。

Externalizable接口extends Serializable接口,而且在其基础上增加了两个方法:writeExternal()和readExternal()。这两个方法会在序列化和反序列化还原的过程中被自动调用,以便执行一些特殊的操作。

一个例子

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
package com.dcLunatic;
import java.io.*;
public class ExternDemo {
public static void main(String[] args) {
System.out.println("Hello world");
SerializeDemo();
DeserializeDemo();
}
private static void SerializeDemo(){
try {
Test1 test = new Test1("John", 15);
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("/root/Desktop/test.txt")));
test.writeExternal(oo);
System.out.println("序列化结束");
oo.close();
}catch(FileNotFoundException e){
System.out.println("无法读写文件");
}catch(IOException e){
System.out.println("IO异常");
}
}
private static void DeserializeDemo(){
try{
ObjectInputStream oi = new ObjectInputStream(new FileInputStream(new File("/root/Desktop/test.txt")));
Test1 test = new Test1();
test.readExternal(oi);
System.out.println("反序列化成功");
System.out.println(test.toString());
}catch(Exception e){
e.printStackTrace();
}

}
}

class Test1 implements Externalizable{
private String name;
private int age;
private static final long serialVersionUID = 1L;

public Test1(String name, int age) {
this.name = name;
this.age = age;
}
public Test1(){

}
@Override
public String toString() {
return "Test1{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

@Override
public void writeExternal(ObjectOutput objectOutput) throws IOException {
System.out.println("调用writeExternal方法");
objectOutput.writeObject(name);
objectOutput.writeInt(age);
}

@Override
public void readExternal(ObjectInput objectInput) throws IOException, ClassNotFoundException {
System.out.println("调用readExternal方法");
name = (String)objectInput.readObject();
age = objectInput.readInt();
}
}

注意事项

  • 序列化时,只对对象的状态进行保存,而不管对象的方法;
  • 记住,状态状态状态!
  • 当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
  • 当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;
  • 并非所有的对象都可以序列化,,至于为什么不可以,有很多原因了,比如:
    • 安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行rmi传输 等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的。
    • 资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分 配,而且,也是没有必要这样实现

原文作者:dcLunatic

原文链接:http://dclunatic.github.io/Java%E5%BA%8F%E5%88%97%E5%8C%96.html

发表日期:September 21st 2018, 1:49:31 pm

更新日期:July 11th 2021, 9:13:50 pm

版权声明:转载的时候,记得注明来处

CATALOG
  1. 1. 序列化和反序列化
    1. 1.1. 概念
    2. 1.2. 用途
    3. 1.3. Java中使用方法
    4. 1.4. serialVersionUID的作用
    5. 1.5. transient关键字
    6. 1.6. 自定义writeObject方法和readObject方法
    7. 1.7. Externalizable
    8. 1.8. 注意事项