序列化
序列化是指将对象转换为字节流的过程,以便能够将其存储到文件、内存、网络传输等介质中,或者在不同的进程、网络或机器之间进行数据交换。
序列化的逆过程称为反序列化,即将字节流转换为对象。过反序列化,可以从存储介质或网络传输中读取数据,并重新构建对象。
Java中的序列化通过实现 Serializable
接口来实现。Serializable
接口是一个标记接口,没有方法需要实现。当一个类实现了 Serializable 接口时,表示该类的实例对象可以被序列化,如果一个字段不需要序列化,则需要使用 transient
进行修饰。
ObjectOutputStream
java.io.ObjectOutputStream
继承自 OutputStream
类,它可以将Java对象序列化成字节流,以便在文件、网络传输等场景中进行存储、传输或持久化操作。
构造方法:ObjectOutputStream(OutputStream out)
该构造方法接收一个 OutputStream
对象作为参数,用于将序列化后的字节序列输出到指定的输出流中。
ObjectOutputStream
序列化的时候会依次调用 writeObject() → writeObject0() → writeOrdinaryObject() → writeSerialData() → invokeWriteObject() → defaultWriteFields()
。
wirteObject()
writeObject (Object obj)
方法,该方法是 ObjectOutputStream
类中用于将对象序列化成字节序列并输出到输出流中的方法,可以处理对象之间的引用关系、继承关系、静态字段和 transient 字段。
ObjectOutputStream
在序列化的时候,会判断被序列化的对象是哪一种类型,字符串?数组?枚举?还是 Serializable
,如果全都不是的话,抛出 NotSerializableException
。
部分源码
ObjectInputStream
ObjectInputStream
可以读取 ObjectOutputStream
写入的字节流,并将其反序列化为相应的原始的对象(包含 对象的数据
、对象的类型
和 对象中存储的属性
等信息)。序列化之前是什么样子,反序列化后就是什么样子。
ObjectInputStream
在反序列化的时候会依次调用 readObject() → readObject0() → readOrdinaryObject() → readSerialData() → defaultReadFields()
。
构造方法
ObjectInputStream(InputStream in)
:创建一个指定 InputStream
的 ObjectInputStream
。
其中,ObjectInputStream
的 readObject
方法用来读取指定文件中的对象
readObject()
ObjectInputStream
类中的 readObject
方法用于从输入流中读取并反序列化一个对象。这是 Java 序列化机制中的一个重要组成部分,使得对象的状态可以在不同进程之间传输或持久化到文件中。
读取并反序列化对象
下面是一个使用 ObjectInputStream
的 readObject
方法来读取并反序列化对象的示例。
import java.io.*;
public class ObjectInputStreamExample {
public static void main(String[] args) {
// 定义对象存储文件路径
String filePath = "person.dat";
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath))) {
// 读取并反序列化对象
Person person = (Person) ois.readObject();
// 输出反序列化后的对象
System.out.println(person);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
// 用于序列化的类
class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
代码解释:
- 创建序列化对象
首先,需要有一个实现了 Serializable
接口的类。在这个例子中,Person
类实现了 Serializable
接口,并且有两个属性:name
和 age
。
- 反序列化对象
-
创建
ObjectInputStream
:使用FileInputStream
创建一个ObjectInputStream
对象,指向包含序列化对象的文件。 -
读取对象:通过调用
readObject
方法从输入流中读取并反序列化对象。注意,readObject
方法返回的是Object
类型的对象,因此需要强制转换为具体的类型(在这里是Person
类型)。 -
输出对象:将反序列化后的对象输出到控制台。
- 处理异常
IOException
:当读取过程中发生 I/O 错误时抛出。ClassNotFoundException
:当反序列化的对象类无法找到时抛出。这种情况通常发生在序列化对象所在的类没有在目标环境中存在的情况。
序列化优点
-
数据持久化:序列化可以将对象保存到文件系统、数据库或其他存储介质中,以便在需要时进行读取和恢复。
-
网络传输:序列化可以将对象转换为字节流,方便在网络上进行传输和交换。
-
分布式系统:序列化使得在分布式系统中可以通过网络传递对象,共享数据。
序列化注意事项
-
序列化版本控制:为了避免对象的不同版本之间的兼容性问题,推荐在进行对象序列化时定义
serialVersionUID
,并在后续的版本中进行维护。 -
敏感数据处理:在进行序列化时,敏感数据(如密码、私密信息)需要加密或者通过
transient
关键字进行忽略。 -
不可序列化的对象:某些对象不能被序列化,如线程、套接字等,需要进行特殊处理或者将其字段标记为
transient
。
总结来说,序列化是将对象转换为字节流的过程,使得对象可以持久化存储、网络传输和分布式系统中共享。Java提供了简单易用的序列化机制,能够方便地实现对象的序列化和反序列化操作。但要注意版本控制、敏感数据保护和不可序列化对象的处理。
反序列化
反序列化(Deserialization) 是将序列化后的字节流转换回对象的过程。在 Java 中,可以使用 ObjectInputStream
类来实现反序列化操作。
在进行反序列化之前,要确保序列化和反序列化的类是相同的版本,即类的结构没有发生变化。否则,在反序列化时可能会出现 InvalidClassException
或其他兼容性问题。
反序列化的过程是将存储在字节流中的对象信息读取出来,并还原为对象实例。以下是进行反序列化的基本步骤:
-
创建一个
ObjectInputStream
对象,并传入一个InputStream
(如FileInputStream
)参数,用于读取字节流数据。 -
使用
ObjectInputStream
对象的readObject()
方法从字节流中读取对象,并将其转换为实际的对象。需要注意的是,readObject()
方法返回的是一个 Object 类型的引用,需要进行强制类型转换才能得到原始对象的引用。 -
对读取的对象进行操作和使用,如调用对象的方法、访问对象的属性等。
-
关闭
ObjectInputStream
对象和相关的输入流。
反序列化过程中可能会抛出 ClassNotFoundException
、InvalidClassException
或 IOException
等异常,因此需要进行异常处理。
反序列化过程:通过 ObjectInputStream
类将字节流转换为对象进行反序列化。例如:
// 将字节流反序列化为对象
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.ser"));
Object obj = in.readObject();
in.close();
反序列化过程会调用对象的构造方法来创建新的对象实例,并且不会触发类的静态代码块。此外,如果对象中定义了 writeObject()
和 readObject()
方法,可以通过这两个方法实现对序列化和反序列化的自定义处理。
Kryo
实际开发中,很少使用 JDK 自带的序列化和反序列化,这是因为:
-
可移植性差:Java 特有的,无法跨语言进行序列化和反序列化。
-
性能差:序列化后的字节体积大,增加了传输/保存成本。
-
安全问题:攻击者可以通过构造恶意数据来实现远程代码执行,从而对系统造成严重的安全威胁。
相关阅读:Java 反序列化漏洞之殇
Kryo
是一个优秀的 Java 序列化和反序列化库,具有高性能、高效率和易于使用和扩展等特点,有效地解决了 JDK 自带的序列化机制的痛点
已经在 Twitter、Groupon、Yahoo 以及多个著名开源项目(如 Hive、Storm)中广泛使用
GitHub 地址:https://github.com/EsotericSoftware/kryo
使用示例
第一步,在 pom.xml
中引入依赖。
<!-- 引入 Kryo 序列化工具 -->
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.4.0</version>
</dependency>
第二步,创建一个 Kryo 对象,并使用 register()
方法将对象进行注册。然后,使用 writeObject()
方法将 Java 对象序列化为二进制流,再使用 readObject()
方法将二进制流反序列化为 Java 对象。最后,输出反序列化后的 Java 对象。
序列化接口 Serializable
Java 序列化是 JDK 1.1 时引入的一组开创性的特性,用于将 Java 对象转换为字节数组,便于存储或传输。此后,仍然可以将字节数组转换回 Java 对象原有的状态。
序列化的思想是“冻结”对象状态,然后写到磁盘或者在网络中传输;
反序列化的思想是“解冻”对象状态,重新获得可用的 Java 对象。
序列化有一条规则,就是要序列化的对象必须实现 Serializbale
接口,否则就会报 NotSerializableException
异常。
Serializable
Serializable
是 Java 标准库提供的接口,用于支持对象的序列化和反序列化操作。当一个类实现了 Serializable
接口,就意味着该类的对象可以被序列化为字节流,以便在网络传输或存储到本地文件系统等场景中使用。
要实现 Serializable
接口,只需在类的声明中添加关键字 “implements Serializable
”,并确保类的所有成员变量也是可序列化的。
这意味着类的所有成员变量要么是原始类型(如 int,double 等),要么是实现了 Serializable
接口的对象。如果类中的成员变量不是可序列化的,则需要标记为 transient
,表示在序列化过程中忽略该成员变量。
序列化过程通过 ObjectOutputStream
类的 writeObject()
方法实现,可以将对象转换为字节流。反序列化过程通过 ObjectInputStream
类的 readObject()
方法实现,可以将字节流还原为一个对象。
Serializable
接口的存在使得 Java 对象可以在不同的虚拟机和操作系统之间进行传输和共享,它在分布式系统、网络通信和持久化存储等场景中很常见。然而,需要注意的是,对于一些敏感的数据,可能需要额外的安全措施来保护序列化和反序列化的过程。
字段序列化
static
和 transient
修饰的字段是不会被序列化的。
-
序列化保存的是对象的状态,而
static
修饰的字段属于类的状态,因此可以证明序列化并不保存static
修饰的字段。 -
transient
(临时的),它可以阻止字段被序列化到文件中,在被反序列化后,transient
字段的值被设为初始值,比如int
型的初始值为 0,对象型的初始值为null
。
序列化接口 Externalizable
Externalizable
是 Serializable
的子接口
实现 Externalizable
接口的类 和 实现 Serializable
接口的类有一些不同:
-
新增了一个无参的构造方法。
使用
Externalizable
进行反序列化的时候,会调用被序列化类的无参构造方法去创建一个新的对象,然后再将被保存对象的字段值复制过去。否则的话,会抛出异常。 -
新增了两个方法
writeExternal()
和readExternal()
,实现Externalizable
接口所必须的,以此来手动完成序列化和反序列化的过程,而 Serializable 接口不需要实现任何方法。transient
在Externalizable
中不起作用。若未重写以上两个方法,反序列化后得到的对象字段都变成了默认值,也就是说,序列化之前的对象状态没有被“冻结”下来。
Externalizable
接口提供了更高的序列化控制能力,可以在序列化和反序列化过程中对对象进行自定义的处理,如对一些敏感信息进行加密和解密。
序列化 ID
Java 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,还有一个非常重要的因素就是序列化 ID 是否一致
serialVersionUID
被称为序列化 ID,它是决定 Java 对象能否反序列化成功的重要因子。
在反序列化时,Java 虚拟机会把字节流中的 serialVersionUID
与被序列化类中的 serialVersionUID
进行比较,如果相同则可以进行反序列化,否则就会抛出序列化版本不一致的异常。
异常堆栈信息里面告诉我们,从持久化文件里面读取到的序列化 ID 和本地的序列化 ID 不一致,无法反序列化。
当一个类实现了 Serializable
接口后,该类最好生成一个序列化 ID
生成序列化 ID 的方法
- 添加一个默认版本的序列化 ID:
private static final long serialVersionUID = 1L。
如果没有特殊需求,采用默认的序列化 ID(1L)就可以,也可以确保代码一致时反序列化成功。
- 添加一个随机生成的不重复的序列化 ID。
private static final long serialVersionUID = -2095916884810199532L;
若在序列化和反序列化的过程中更改了 序列化ID,会导致 JVM 抛出序列化版本不一致的异常
- 添加
@SuppressWarnings
注解。
@SuppressWarnings("serial")
使用
@SuppressWarnings("serial")
注解时,该注解会为被序列化类自动生成一个随机的序列化 ID,无需再手动设置。