一、引言
Java 序列化和反序列化是软件开发中的重要概念,在数据存储、网络通信和远程调用等场景中发挥着关键作用。本文将深入探讨 Java 基础序列化和反序列化的概念、实现方式、应用场景以及注意事项。
一、引言
Java 序列化和反序列化在软件开发中具有重要地位。它允许将 Java 对象转换为字节流以便存储或传输,同时也能够将字节流恢复为原始的 Java 对象。这种机制在数据存储、网络通信和远程调用等场景中发挥着关键作用。
序列化是将数据结构或对象转换为可以存储或传输的格式的过程。在 Java 中,通过序列化可以将对象转换为字节流,以便将其保存到文件、数据库或通过网络发送到其他系统。反序列化则是序列化过程的逆过程,即将存储或传输的数据格式恢复为原始数据结构或对象的过程。
Java 中的序列化和反序列化主要用于以下场景:
- 数据存储:在将对象信息存储到文件或数据库中时,需要先将其序列化。当需要读取这些数据时,则需要反序列化。
- 网络通信:在通过网络发送数据时,数据通常需要先序列化为字符串或字节流格式,以便在接收方能够正确解析和恢复原始数据。
- 远程调用:在分布式系统中,客户端与服务器之间的通信经常需要序列化和反序列化操作,以便在不同的系统之间传递对象和数据。
然而,在使用 Java 序列化和反序列化时,也需要注意一些事项:
- 数据一致性:在序列化和反序列化数据时,要确保数据结构的一致性,如果更改了数据结构,反序列化时可能会出现错误。
- 安全性:反序列化过程中可能会受到攻击,尤其是来自不可信源的数据。因此,处理反序列化时需加强验证和安全性检查。
- 性能:不同的序列化格式有不同的性能特征,选择合适的格式可以有效提高数据处理的效率。
总之,Java 序列化和反序列化在现代软件开发中发挥着至关重要的作用,它们使得数据的存储和传输变得简单和高效。理解这两个概念及其应用场景,对于开发人员在进行数据处理时至关重要。
二、什么是序列化和反序列化
1. 序列化
- 序列化是指将数据结构或对象转换为可以存储或传输的格式的过程。在 Java 中,通过序列化可以将对象转换为字节流,以便将其保存到文件、数据库或通过网络发送到其他系统。例如,当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个 Java 对象转换为字节序列,才能在网络上传送。
- 常见的序列化格式包括 JSON、XML 和二进制格式等。
- JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人读写,同时也易于机器解析和生成。
- XML(eXtensible Markup Language)是一种用于表示结构化数据的标记语言,具有良好的可扩展性和自描述性。
- 二进制格式如 Protocol Buffers、Thrift 等,是更高效的序列化方式,适合传输大数据量的场景。
- 序列化的好处包括数据的持久化存储和在不同环境之间的传输。
- 数据持久化:Java 序列化提供了一种简单方便的数据持久化方式。通过实现 Serializable 接口,可以将对象以字节流的形式进行传输或存储,而不需要进行额外的操作。这对于实现数据的持久性存储非常有用,例如保存应用程序的配置信息或用户数据。
- 跨平台性:由于 Java 序列化是将对象转换为字节流,而字节流是平台无关的数据形式,因此可以实现跨平台的数据传输。这使得 Java 序列化在分布式系统和客户端 - 服务器应用程序中非常有用。
2. 反序列化
- 反序列化是序列化过程的逆过程,即将存储或传输的数据格式恢复为原始数据结构或对象的过程。接收方则需要把字节序列再恢复为 Java 对象。客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。序列化就是把实体对象状态按照一定的格式写入到有序字节流,反序列化就是从有序字节流重建对象,恢复对象状态。
三、Java 中的序列化和反序列化实现
1. 实现 Serializable 接口
- 一个类必须实现 Serializable 接口才能使其对象序列化。在 Java 中,实现 Serializable 接口只是为了标注该对象是可被序列化的。当一个类实现了 Serializable 接口后,就代表这个类以及其子类是自动支持序列化和反序列化的。序列化时,只对对象的状态进行保存,而不管对象的方法。如果一个类没有实现 Serializable 接口,那么默认是不能被序列化的,除非使用其他办法。如果一个类实现了 Serializable 接口,其父类没有实现 Serializable 接口,那么父类必须有无参的构造器,并且父类中的状态默认不能被序列化。序列化的实例变量引用其他对象,则引用对象也会被序列化。
- 使用 ObjectOutputStream 的 writeObject () 方法将对象写入输出流,完成序列化。ObjectOutputStream 采用默认的序列化方式,对该类对象的非 transient 的实例变量进行序列化。例如:
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("output.txt")); oos.writeObject(objectToSerialize); oos.close(); |
- 使用 ObjectInputStream 的 readObject () 方法从输入流中读取字节序列,反序列化为对象。ObjectInputStream 会采用默认的反序列化方式,对该类对象的非 transient 的实例变量进行反序列化。例如:
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("input.txt")); Object deserializedObject = ois.readObject(); ois.close(); |
2. 自定义序列化
- 通过重写 writeObject 和 readObject 方法,可以实现自定义的序列化和反序列化过程。在某些情况下,我们可能不想对于一个对象的所有 field 进行序列化,例如银行信息中的设计账户信息的 field,我们不需要进行序列化,或者有些 field 本身就没有实现 Serializable 接口。此时可以使用自定义序列化机制,让程序控制如何序列化各 field,甚至完全不序列化某些 field(这样就与 transient 相同)。
在序列化和反序列化过程中需要特殊处理的类应该提供如下特殊签名的方法:
- private void writeObject(ObjectOutputStream out):负责写入特定类的实例状态,通过重写这个方法,程序员可以完全获得对序列化机制的控制,可以自主决定那些 field 需要序列化,需要怎么序列化,默认情况(函数体为空)该方法会调用 out.defaultWriteObject来保存 java 对象的各 field,从而达到实现序列化 java 对象状态的目的。
- private void readObject(ObjectInputStream in):负责从流中读取并且回复对象的 field,通过重写该方法,程序员,可以获得对反序列化机制的控制,对于反序列化各个 field 的顺序应该和序列化各个 field 的顺序相同。至于当序列化流不完整时,readObjectNoData可以正确的初始化反序列化的对象,例如接收方接收到的序列化流残缺,或者序列化版本不同,则使用 readObjectNoData来默认的初始化。
例如:
class Person implements Serializable { private String name; private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); Date date = new Date(); out.writeObject(date); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); Date date = (Date)in.readObject(); Date now = new Date(); long offset = now.getTime() - date.getTime(); if(offset < 100){ System.out.println("在正常的时间内接收到序列化对象"); } else { System.out.println("接收传输时间过长,请注意"); } } } |
3. transient 关键字
- transient 关键字用于标识某个类的字段不需要进行序列化。在 Java 中,transient 是一个关键字,用于声明类的某些属性不应该被序列化。当一个对象被序列化时,希望忽略类中某些非必要或敏感字段,可以使用 transient 关键字标记这些字段。被 transient 修饰的属性在序列化过程中不会被保存。
- 使用场景包括敏感数据、冗余数据和非持久化状态等。
- 安全性:敏感信息,如密码,不应序列化。例如:
public class User implements Serializable { private String name; private transient String password; // 不会被序列化 // 构造函数、Getter 和 Setter } |
- 临时数据:临时或日志数据,这些在对象恢复时不必要。
- 资源句柄:如文件句柄、数据库连接等,它们不能被序列化。
四、Java 序列化和反序列化的应用场景
1. 数据持久化
- 将对象的状态持久化到磁盘或数据库中,以便程序重新启动时恢复状态。Java 序列化提供了一种简单方便的数据持久化方式。通过实现 Serializable 接口,可以将对象以字节流的形式进行传输或存储,而不需要进行额外的操作。这对于实现数据的持久性存储非常有用,例如保存应用程序的配置信息或用户数据。当程序需要保存一个对象的状态时,可以使用 ObjectOutputStream 的 writeObject () 方法将对象写入到文件或数据库中。在程序重新启动时,再使用 ObjectInputStream 的 readObject () 方法从文件或数据库中读取字节序列,反序列化为对象,从而恢复对象的状态。
2. 网络通信
- 在分布式系统中,通过网络在不同的 Java 虚拟机之间传输对象。在分布式系统中,经常需要通过网络在不同的 Java 虚拟机之间传输对象。通过序列化和反序列化,可以将对象转换为字节流,通过网络发送到另一个 Java 虚拟机,然后再反序列化为对象。这样,两个 Java 虚拟机之间就可以共享和操作对象了。这在远程方法调用(RPC)、Web 服务、消息队列等场景中非常常见。例如,在一个分布式系统中,客户端需要向服务器发送一个请求对象,服务器处理请求后返回一个响应对象。这些对象可以通过序列化转换为字节流,在网络上传输,接收方再通过反序列化将字节流恢复为对象。
3. 远程调用
- 在远程方法调用中,作为方法参数或返回值在不同的地址空间间传递对象。在远程方法调用中,对象作为方法参数或返回值需要在不同的地址空间间传递,也需要经过序列化和反序列化。例如,在 Java 的 RMI(Remote Method Invocation)中,需要将对象序列化后通过网络发送给远程服务器,以便服务器能够正确地解析和调用相应的方法。当远程方法执行完毕后,返回的对象也需要通过序列化转换为字节流,发送回客户端,客户端再通过反序列化将字节流恢复为对象。
4. 深拷贝
- 通过序列化和反序列化实现对象的深拷贝,得到一个新的、完全独立的实例。在 Java 中,对象的拷贝操作分为浅拷贝和深拷贝。浅拷贝只是将对象的引用复制一份给新的对象,而深拷贝则是将对象的所有属性都复制一份给新的对象。在某些场景下,我们可能需要使用深拷贝来创建一个独立的对象,以避免原始对象和拷贝对象之间的关联。Java 提供了一种通过序列化实现深拷贝的机制。具体原理是先将对象序列化为字节流,然后再将字节流反序列化为新的对象。由于反序列化的过程会重新创建对象,因此可以实现深拷贝。例如,假设我们有一个 Person 类,它包含了一个 name 属性和一个 address 属性。我们希望通过序列化实现深拷贝,创建一个新的 Person 对象。可以使用以下步骤实现:首先,创建一个原始的 Person 对象并设置属性值;然后,使用 ByteArrayOutputStream 和 ObjectOutputStream 将原始对象序列化为字节流;接着,使用 ByteArrayInputStream 和 ObjectInputStream 从字节流中反序列化为新的对象;最后,验证拷贝的对象与原始对象的属性值相同,但是对象的引用不同,即实现了深拷贝。
五、注意事项
1. 数据一致性
- 在序列化和反序列化数据时,要确保数据结构的一致性,否则可能出现错误。如果在序列化后对数据结构进行了更改,比如添加、删除字段或者修改字段类型等,反序列化时可能会导致无法正确恢复对象,甚至抛出异常。例如,若将一个包含特定字段的对象序列化后,再在类中删除该字段,然后尝试反序列化之前保存的数据,就很可能会出现问题。
2. 安全性
- 反序列化过程中可能会受到攻击,需加强验证和安全性检查。反序列化漏洞的本质就是反序列化机制打破了数据和对象的边界,导致攻击者注入的恶意序列化数据在反序列化过程中被还原成对象,控制了对象就可能在目标系统上面执行攻击代码。Java 序列化应用于 RMI、JMX、JMS 技术中,存在一定的安全风险。为了防御反序列化漏洞,可以采取反序列化对象白名单控制,在resolveClass方法中校验对象名字;序列化数据采用对称加密进行传输,接口调用增加身份认证;对包含敏感数据的类,在特定对象的一个域上关闭serialization,在这个域前加上关键字transient等措施。
3. 性能
- 不同的序列化格式有不同的性能特征,选择合适的格式可以提高数据处理效率。例如,JDK 内置的序列化框架、Fastjson、Hessian、Kryo 等序列化框架在序列化后的字节大小、序列化 / 反序列化耗时等方面表现不同。一般来说,Kryo 在序列化后的字节长度和序列化耗时方面都有较好的表现。同时,Externalizable 接口可以优化 Java 序列化,对于小数目对象更有效。此外,像 Oracle Coherence POF 等商业产品提供的优化二进制格式也具有较高的性能,但需要许可证。在选择序列化格式时,需要根据具体的应用场景和性能需求进行综合考虑。
六、总结
Java 序列化和反序列化在现代软件开发中具有重要作用,理解这两个概念及其应用场景,对于开发人员进行数据处理至关重要。在使用过程中,需要注意数据一致性、安全性和性能等问题,以确保程序的稳定和高效运行。
序列化和反序列化为数据的存储和传输提供了便利,但在实际应用中也需要谨慎处理各种问题。
在数据一致性方面,若在序列化后更改数据结构,反序列化时可能出现错误。例如,若对已序列化的对象所属类进行字段添加、删除或类型修改,反序列化时可能导致无法正确恢复对象,甚至抛出异常。
安全性方面,反序列化过程中可能遭受攻击,需加强验证和安全性检查。反序列化漏洞本质是反序列化机制打破了数据和对象的边界,可能使恶意序列化数据被还原为对象并执行攻击代码。可采取反序列化对象白名单控制、序列化数据对称加密传输、接口调用增加身份认证、对敏感数据类的特定域添加transient关键字等措施防御漏洞。
性能方面,不同序列化格式性能特征各异,选择合适的格式能提高数据处理效率。如 JDK 内置的序列化框架、Fastjson、Hessian、Kryo 等,在序列化后的字节大小、序列化 / 反序列化耗时等方面表现不同。一般来说,Kryo 在字节长度和耗时方面有较好表现,Externalizable 接口可优化 Java 序列化,对小数目对象更有效,商业产品如 Oracle Coherence POF 提供的优化二进制格式性能高但需许可证。在选择序列化格式时,需根据具体应用场景和性能需求综合考虑。
总之,Java 序列化和反序列化在现代软件开发中不可或缺,但在使用过程中需注意各种问题,以确保程序的安全、稳定和高效运行。
标签:Java,字节,对象,基础,格式,序列化,数据 From: https://blog.csdn.net/qq_25699299/article/details/144182617