序列化其实就是将数据转化成一种可逆的数据结构,自然,逆向的过程就叫做反序列化,序列化与反序列化的根本目的是数据的传输

# 序列化以反序列化

Java 序列化是指把 Java 对象转换为字节序列的过程,将数据分解成字节流以便存储在文件中或在网络上传输;而 Java 反序列化是指把字节序列恢复为 Java 对象的过程,打开字节流并重构对象

1
2
序列化:对象 -> 字符串(字节流)
反序列化:字符串(字节流) -> 对象

比如:现在我们都会在淘宝上买桌子,桌子这种很不规则的东西,该怎么从一个城市运输到另一个城市,这时候一般都会把它拆掉成板子,再装到箱子里面,就可以快递寄出去了,这个过程就类似我们的序列化的过程(把数据转化为可以存储或者传输的形式)。当买家收到货后,就需要自己把这些板子组装成桌子的样子,这个过程就像反序列的过程(转化成当初的数据对象)

# ObjectOutputStream 序列化

1
2
3
4
5
// 创建序列化对象oss
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
// 创建好反序列化对象oss后使用ObjectOutputStream类自带的writeObject方法,
//将指定对象序列化写入输出流 writeObject方法传入的对象会转化 为字节流将对象的状态保存到文件中
oos.writeObject(obj);

# ObjectInputStream 反序列化

1
2
3
4
5
6
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
// 调用反序列化对象中的readObject从输入流中读取对象的字节流,并将其反序列化为对象。
// 读取对象的字节表示形式,并将其转换为Java对象
Object obj = ois.readObject();
// 返回 反序列化后的对象
return obj;

# 为什么需要序列化

网站中两个进程进行远程通信,互相发送各种类型的数据,图片 视频 文档,这些数据会以二进制序列形式进行传递,使用 Java 序列化和反序列化实现进程间对象传送

  • 发送方将发送的数据对象转换为 Java 字节序列,拆分为字节流 序列化过程
  • 接收方将收到的数据从字节流重构转化为当初的数据读性对象

# Serializable 接口序列化

# 接口 Serializable

序列化接口 Serializable 接口是一个标识接口,它的主要作用就是标识这个对象是可序列化,
接口应该使用 Java 标准库中的 java.io.Serializable 而不是自定义的 a.Serializable 接口

有实现了 Serializable 或者 Externalizable 接口的类的对象才能被序列化为字节序列。(不是则会抛出异常;实现 Serializable 接口,类表明它是可序列化的,可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化

1
2
3
4
5
6
// 实现Serializable 接口的类才可以被序列化/反序列化,自定义的无法使用需要使用库内自带的

public interface Serializable {
// 实现即可无须写入什么东西
}

# 实现代码

person.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
import java.io.Serializable; // 需要实现的是自带的接口

// 类实现Serializable接口才能序列化
public class person implements Serializable {

private String name;
private int age;

public person(){

}
// 有参构造器
public person(String name, int age){
this.name = name;
this.age = age;
}

@Override
// 自带方法
public String toString(){
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

}

SerializationTest.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
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class SerializationTest {

// 静态方法将传入的对象序列化并写入到ser.bin 文件中
// 并且里面有一个形参用与在下文中使用此方法传递对象进去
public static void serialize(Object obj) throws IOException{
// 创建序列化对象oss
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
// 创建好反序列化对象oss后使用ObjectOutputStream类自带的writeObject方法,将指定对象序列化写入输出流
// writeObject方法传入的对象会转化 为字节流将对象的状态保存到文件中
oos.writeObject(obj);
}

public static void main(String[] args) throws Exception{
person person = new person("aa",22); //实例化类 使用User类 有参构造器
System.out.println(person); // 打印对象会调用类里面的toString()方法输出值
serialize(person); // 调用上文序列化方法将实例化的对象传入
}
}


-----------------------------------------------

输出:

Person{name='aa', age=22}


UnserializeTest.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
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class UnserializeTest {
// 形参为 文件名
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
// 创建一个反序列化对象 ois将其关联到名为Filename的文件输出流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
// 调用反序列化对象中的readObject从输入流中读取对象的字节流,并将其反序列化为对象。
// 读取对象的字节表示形式,并将其转换为Java对象
Object obj = ois.readObject();
// 返回 反序列化后的对象
return obj;
}

public static void main(String[] args) throws Exception{
// 调用上文的unserialize方法传入实参需要被反序列化的文件,然后将其转换为person对象,
// 先转换 将返回的反序列化后的person对象赋值给了person变量,实现了从文件中恢复对象的操作。
person person = (person)unserialize("ser.bin");

// 打印该person转换后的对象,这里会调用person类的toString()方法来输出对象的字符串表示形式。
System.out.println(person);
}
}


-----------------------------------------------

输出:

Person{name='aa', age=22}

# 序列化注意点

反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象

序列化的对象是已经继承了其他类变成了子类的化,如果父类没有实现接口 Serializable , 那么父类要么选择实现这个接口,要么是提供无参构造器,在反序列化 Child 对象时, Java 会调用 Parent 类的无参构造函数来重新创建 Parent 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.io.Serializable;

public class Parent {
private int id;

public Parent() {
// 提供无参构造函数
}

public Parent(int id) {
this.id = id;
}
}

// 继承了Parent类并实现了Serializable接口

public class Child extends Parent implements Serializable {
private String name;

public Child(String name, int id) {
super(id);
this.name = name;
}
}

实现 Serializable 接口的子类也是可以被序列化的

当一个父类类实现了 Serializable 接口,它的子类也会自动被序列化。这意味着,如果一个父类实现了 Serializable 接口,那么它的所有子类都可以被序列化,无需再单独实现 Serializable 接口;即子类也被视为可序列化的。这样可以确保整个继承层次结构在序列化和反序列化过程中的一致性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.Serializable;

public class Parent implements Serializable {
private int id;

public Parent(int id) {
this.id = id;
}

// 其他类内容
}

public class Child extends Parent {
private String name;

public Child(String name, int id) {
super(id);
this.name = name;
}

// 其他类内容
}

静态成员变量是不能被序列化 序列化是针对对象属性的,而静态成员变量是属于类的

静态成员变量属于类级别而不是实例级别,它们不会被包含在序列化的过程中。当对象被序列化时,只有实例变量会被保存到序列化的数据流中,静态成员变量不会被包含在序列化的数据中,。如果需要在序列化过程中保存静态成员变量的状态,可以通过自定义序列化和反序列化方法来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.Serializable; // 导入Serializable接口

public class MyClass implements Serializable { // MyClass类实现Serializable接口
private static int staticVar = 10; // 静态成员变量staticVar 不会被序列化
private int instanceVar; // 实例成员变量instanceVar

public MyClass(int instanceVar) { // MyClass类的构造函数
this.instanceVar = instanceVar; // 初始化实例成员变量instanceVar
}

public static void main(String[] args) { // 主方法
MyClass obj = new MyClass(20); // 创建MyClass对象obj
// 将对象序列化的过程
// 省略序列化代码
}
}


transient 标识的对象成员变量不参与序列化

一个对象的成员变量被标记为 transient 时,在对象进行序列化时,这些被标记的成员变量将不会被序列化,即它们的值不会被保存到序列化的数据流中。只会参与这个过程并输出对应数据类型的默认值, int 类型输出 ** 0 ** , 但原始对象中的值并没有改变,仍然是 ** 30 **,只要添加了 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
57
58
59
package a;

import java.io.*;

// 实现 Serializable接口
class Person implements Serializable {
private String name;
private transient int age; // 使用transient关键字标记age成员变量不被序列化

// 构造函数
public Person(String name, int age) {
this.name = name;
this.age = age;
}

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

public class SerializationExample {
public static void main(String[] args) {
// 实例化类person
Person person = new Person("Alice", 30);

try {
// 将对象序列化到文件
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(person);
out.close();

// 从文件中反序列化对象
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
Person deserializedPerson = (Person) in.readObject();
in.close();

// 输出反序列化后的对象
System.out.println(deserializedPerson);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}


--------------------------------------

Person{name='Alice', age=0}

// 0 就是age默认的的值,但实际的数据并没有受到影响
// 这在某些情况下很有用,比如某些敏感信息或临时数据不需要被序列化保存




# Externalizable 接口序列化

参考文章:

Java 序列化之 Externalizable - 腾讯云开发者社区 - 腾讯云 (tencent.com)

# 接口 Externalizable

JDK 中除了提供 Serializable 序列化接口外,还提供了另一个序列化接口 Externalizable ,使用该接口之后,之前基于 Serializable 接口的序列化机制就将失效。 Externalizable 的序列化机制优先级要高于 Serializable

源码中 Externalizable 接口继承了 Serializable 接口,并定义了两个方法,实现此接口后,类中属性字段使用 transient 和不使用没有任何区别

1
2
writeExterna(写入 userName 和 password 两个数据)
readExternal(读取反序列化的信息并赋值给 userName 和 password 两个字段)

# 实现代码

User.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
43
44
45
46
47
48
package a;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class User implements Externalizable {

private static final long serialVersionUID = 1318824539146791009L;
private String userName;
private transient String password; // 被标识不参与序列化,但是继承了Externalizable就无区别

public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {

// 输出自带的方法打印字符串查看效果

return "User [userName=" + userName + ", password=" + password + "]";
}

// 下面的两个方法是需要填写数据的
// 在 writeExternal 方法中写入 userName 和 password 两个数据,
// readExternal 方法中读取反序列化的信息并赋值给 userName 和 password 两个字段。

@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(this.userName); // 保存属性
out.writeObject(this.password);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.userName = in.readObject().toString(); // 读取属性
this.password = in.readObject().toString();
}
}

Test.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

import java.io.*;

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

// 创建文件对象给出路径和名称 1.txt 把变量给file

File file = new File("d:\\a.user");
// 序列化过程
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
User user1 = new User(); // 实例化上文的 User类
user1.setUserName("zhangsan"); // 赋值
user1.setPassword("123456"); // 赋值
oos.writeObject(user1);


// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
User user2 = (User) ois.readObject();
System.out.println(user2);
}
}



-----------------------------------------

输出:


User [userName=zhangsan, password=123456]

# 序列化注意点

Externalizable 进行反序列化时,需要有默认的构造方法,有参的构造器是不够的,还需要自己手写一个默认无参的构造器,因为使用 Externalizable 进行反序列化时,需要有默认的构造方法,通过反射先创建出该类的实例,然后再把解析后的属性值,通过反射赋值。

Serializable 接口则不需要单独写默认无参的构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class User implements Externalizable {

// 序列ID
private static final long serialVersionUID = 1318824539146791009L;
private String userName;
private transient String password;

// 无参/默认构造器

public User(){

}

// 有参构造器

public User(String userName, String password) {
super();
this.userName = userName;
this.password = password;
}

}

User.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
43
44
45
46
47
48
49
50
51
52
53
package a;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class User implements Externalizable {

// 序列ID
private static final long serialVersionUID = 1318824539146791009L;
private String userName;
private transient String password;

// 无参/默认构造器
public User(){

}

// 有参构造器

public User(String userName, String password) {
super();
this.userName = userName;
this.password = password;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User [userName=" + userName + ", password=" + password + "]";
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(this.userName);
out.writeObject("externalizable:"+this.password);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.userName = (String)in.readObject();
this.password = (String)in.readObject();
}
}

Test.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
43
44
45
package a;

import java.io.*;

public class Test {
public static void main(String[] args) throws Exception {
File file = new File("d:\\a.user");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
// 通过User类的有参构造器
User user1 = new User("zhangsan", "123456");
oos.writeObject(user1); // 序列对象
System.out.println(oos);

// 将序列化后的数据写入到一个ByteArrayOutputStream中
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream tempOos = new ObjectOutputStream(bos);
tempOos.writeObject(user1);
tempOos.close();

System.out.println("-------序列化成功");

ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
User user2 = (User) ois.readObject();
System.out.println(user2);
System.out.println("-------反序列化成功");

// 查看序列化后的数据
byte[] bytes = bos.toByteArray();
String serializedData = new String(bytes, "UTF-8");
System.out.println(serializedData);
}
}



--------------------------------

输出:

java.io.ObjectOutputStream@20ad9418
-------序列化成功
User [userName=zhangsan, password=externalizable:123456]
-------反序列化成功
�� sr a.UserMg�P�a xptzhangsant externalizable:123456x

# 序列化安全

序列化安全主要是由于 writeObject() readObject() 这两种方法是可以通过开发者重写的,重写的场景一般是这样的

假设有一个名为 MyList 的类,其中定义了一个名为 arr 的数组属性,初始数组长度为 100 。在实际序列化过程中,如果让 arr 属性参与序列化,那么整个长度为 100 的数组都会被序列化,,但是数组中存放的数据只有 50 个数据,,如果整个数组全部序列化的数据量过大且浪费空间,所以需要重写方法自定义序列化过程


在上文代码的基础上加入重写的 readObject 反序列化方法,然后按照顺序运行序列化程序 SerializationTest.java 再运行反序列化程序 UnserializeTest.java , 会执行重写的方法并且执行外部命令弹出计算机 calc , 这是最理想的情况,但是此情况几乎不会出现

  • 入口参数中包含可控类,该类有危险方法, readObject 时调用
  • 入口类参数中包含可控类,该类又调用其他有危险方法的类, readObject 时调用
  • 构造函数 / 静态代码块等类加载时隐式执行

person.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
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable; // 需要实现的是自带的接口

// 类实现Serializable接口才能序列化
public class person implements Serializable {

private String name;
private int age;

public person(){

}
// 构造函数
public person(String name, int age){
this.name = name;
this.age = age;
}

@Override
// 自带方法
public String toString(){
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

// 使用自定义反序列化的readObject方法,形参是指读取序列化的参数,传入的对象就是需要被反序列的东西
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
// defaultReadObject 于从对象流中读取对象的字段。这个方法会读取对象的所有字段,并且将它们设置到对象中
// 恢复对象的状态。
ois.defaultReadObject();
// 执行外部命令弹出计算机
Runtime.getRuntime().exec("calc");
}
}