Java 序列化与反序列化


        

序列化与反序列化

什么时序列化与反序列化:

(1)Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程;

(2)序列化:对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。序列化后的字节流保存了Java对象的状态以及相关的描述信息。序列化机制的核心作用就是对象状态的保存与重建。

(3)反序列化:客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。

(4)本质上讲,序列化就是把实体对象状态按照一定的格式写入到有序字节流,反序列化就是从有序字节流重建对象,恢复对象状态。

为什么需要序列化与反序列化:

我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。

那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的!如何做到呢?这就需要Java序列化与反序列化了!

换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。

当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。其好处一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。

总的来说可以归结为以下几点:

(1)永久性保存对象,保存对象的字节序列到本地文件或者数据库中;
(2)通过序列化以字节流的形式使对象在网络中进行传递和接收;
(3)通过序列化在进程间传递对象;

序列化算法一般会按步骤做如下事情:

(1)将对象实例相关的类元数据输出。
(2)递归地输出类的超类描述直到不再有超类。
(3)类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
(4)从上至下递归输出实例的数据

Serializable

序列化步骤:

  • 步骤一:创建一个 ObjectOutputStream 输出流
  • 步骤二:调用 ObjectOutputStream 对象的 writeObject 输出可序列化对象

代码实现:

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
class Person implements Serializable {
private String name;
private int age;
private String weight;
// transient 修饰的变量可以不被序列化
// 也可以进行控制序列化的方式,或者对序列化数据进行编码加密等

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

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

public static void writeObject() {
// 创建一个 ObjectOutputStream 输出流
try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://test//per.txt"))) {
// 将对象序列化到文件 per.txt
Person per = new Person("wx", 21, "60");
oos.writeObject(per);
}catch (IOException e) {
e.printStackTrace();
}
}

反序列化步骤:

  • 步骤一:创建一个 ObjectInputStream 输出流
  • 步骤二:调用 ObjectInputStream 对象的 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
class Person implements Serializable {
private String name;
private int age;
private String weight;
// transient 修饰的变量可以不被序列化
// 也可以进行控制序列化的方式,或者对序列化数据进行编码加密等

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

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

public static void readObject() {
// 创建一个 ObjectInputStream 输入流
try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D://test//per.txt"))) {
Person tmp = (Person) ois.readObject();
System.out.println(tmp);
}catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}

输出:

1
Person{name='wx', age=21, weight=60kg}

可选的自定义序列化

使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。

通过重写writeObject与readObject方法,可以自己选择哪些属性需要序列化, 哪些属性不需要。如果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
class Person implements Serializable {
private String name;
private int age;
private String weight;
// private int height;

// 序列化数据之后,修改类的结构, 反序列会报InvalidClassException异常
// 自己提供一个 serialVersionUID
private static final long serialVersionUID = 10086L;

/* public Person(String name, int age, String weight, int height) {
this.name = name;
this.age = age;
this.weight = weight;
this.height = height;
}*/

// transient 修饰的变量可以不被序列化
// 也可以进行控制序列化的方式,或者对序列化数据进行编码加密等

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

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

private void writeObject(ObjectOutputStream out) throws IOException {
// 将名字反转写入二进制流
out.writeObject(new StringBuffer(this.name).reverse());
out.writeInt(age);
// 在体重后加上单位
out.writeObject(new StringBuffer(this.weight).append("kg"));
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 将读出的字符串反转回复回来
this.name = ((StringBuffer) in.readObject()).reverse().toString();
this.age = in.readInt();
this.weight = ((StringBuffer) in.readObject()).toString();
}
}

Externalizable

强制自定义序列化
通过实现Externalizable接口,必须实现writeExternal、readExternal方法。

注意:Externalizable接口不同于Serializable接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;特别之处是必须提供pulic的无参构造器,因为在反序列化的时候需要反射创建对象。

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
public interface Externalizable extends java.io.Serializable {     
void writeExternal(ObjectOutput out) throws IOException;     
void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException;
}

public class ExternalizableDemo {
public static void main(String[] args) {
try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://test//exp.txt"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D://test//exp.txt"))) {
oos.writeObject(new Experson("wangxin",21));
Experson ep = (Experson) ois.readObject();
System.out.println(ep);
}catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}

class Experson implements Externalizable {
public String name;
public int age;
// 必须加上无参的构造函数
public Experson(){
}

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

@Override
public void writeExternal(ObjectOutput out) throws IOException {
// 将名字反转写入二进制流
out.writeObject(new StringBuffer(this.name).reverse());
out.writeInt(age);
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// 将读出的字符串反转回复回来
this.name = ((StringBuffer) in.readObject()).reverse().toString();
this.age = in.readInt();
}

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

两种序列化对比

Kseb5V.png

虽然Externalizable接口带来了一定的性能提升,但变成复杂度也提高了,所以一般通过实现Serializable接口进行序列化。


序列化版本号serialVersionUID

序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。

什么情况下需要修改serialVersionUID呢?分三种情况。

* 如果只是修改了方法,反序列化不容影响,则无需修改版本号;

* 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;

* 如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。

总结

1. 所有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。

2. 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。

3. 如果想让某个变量不被序列化,使用transient修饰。

4. 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。

5. 反序列化时必须有序列化对象的class文件。

6. 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。

7. 单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。

8. 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。

9. 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级
---------------- The End ----------------

本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
本文地址:https://philxin.top/2019/10/26/Java-序列化与反序列化/
转载请注明出处,谢谢!

0%