Java中对象的转移(1)——序列化与反序列化
1. 序列化与反序列化的定义与实现流程
序列化(Serialization)和反序列化(Deserialization)是处理对象持久化和网络传输的关键技术。它们在分布式系统、缓存系统和跨应用传输数据中应用广泛。
序列化是什么?
序列化是将一个对象转换成字节流的过程,这样的数据形式便于存储到文件、数据库,或通过网络传输到其他系统。简单来说,序列化就是把数据打包成一个可转移、存储的“快递包裹”。
反序列化是什么?
反序列化是序列化的逆过程,即将字节流还原为对象的过程。通过反序列化,系统可以读取存储的对象数据,或通过网络传输接收到的数据重构出对象。
序列化的实现流程
序列化和反序列化的基本流程可以分为以下几步:
-
对象状态转换为字节流:
- 将对象中的属性、数据结构和标识符等信息转换成一组字节,形成一个流。
- 这个流包含了该对象的类信息以及它的字段值,使得接收方能够准确地还原对象。
-
传输或存储字节流:
- 将得到的字节流传输到目标位置(例如:远程服务器)或存储到介质中(如文件系统、数据库),以便后续的访问或复用。
-
将字节流转为对象(反序列化):
- 当需要读取对象时,字节流会被重新转换回对象,这时会从流中解析出类信息及字段数据,恢复对象的完整状态。
这种流程实现了对象在不同系统、不同时刻的持久化保存和跨网络、跨进程的数据传输。序列化的技术不仅限于Java,在很多语言和框架中都有类似实现,适用于不同的应用需求。
2. Java中的序列化与反序列化实现
Java 提供了内置的序列化工具,使对象能方便地转换为字节流。主要工具包括ObjectOutputStream
和ObjectInputStream
,以及Serializable
和Externalizable
接口。
1. Serializable
接口:标识对象可序列化
要实现序列化的对象类需要实现Serializable
接口。这是一个标记接口,也就是不包含方法,只是标记该类可以序列化。
示例:
import java.io.Serializable;public class Person implements Serializable {private String name;private int age;public Person(String name, int age) {this.name = name;this.age = age;}// 省略getters和setters
}
解释: 在这个例子中,Person
类实现了Serializable
接口,说明可以把Person
对象转成字节流。类中包含的所有字段都会在序列化过程中转换成字节流。
2. ObjectOutputStream
和ObjectInputStream
:读写字节流的对象
ObjectOutputStream
:用来写入对象的字节流,执行序列化。ObjectInputStream
:用来读取对象的字节流,执行反序列化。
示例:序列化对象
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;public class SerializeExample {public static void main(String[] args) {Person person = new Person("Alice", 30);try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) {oos.writeObject(person); // 将Person对象写入文件System.out.println("对象已序列化");} catch (Exception e) {e.printStackTrace();}}
}
解释:
- 创建
ObjectOutputStream
,用于将对象写入文件。 - 使用
writeObject
方法将person
对象转为字节流写入文件person.ser
中。 - 文件中保存了
person
对象的状态数据,以便后续读取。
示例:反序列化对象
import java.io.FileInputStream;
import java.io.ObjectInputStream;public class DeserializeExample {public static void main(String[] args) {try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ser"))) {Person person = (Person) ois.readObject(); // 从文件中读取对象System.out.println("对象已反序列化: " + person.getName() + ", " + person.getAge());} catch (Exception e) {e.printStackTrace();}}
}
解释:
- 创建
ObjectInputStream
,读取存储的字节流。 - 使用
readObject
方法从字节流中还原对象。 - 将反序列化的
person
对象信息打印出来。
3. transient
关键字:保护敏感数据
在某些场景中,可能不希望某些字段被序列化,例如密码、敏感信息等。可以用transient
关键字声明这些字段,以忽略序列化。
示例:
public class User implements Serializable {private String username;private transient String password; // 不会被序列化public User(String username, String password) {this.username = username;this.password = password;}
}
在这个例子中,password
字段不会被序列化,也不会被写入字节流中,从而保护了敏感数据。
4. Externalizable
接口:自定义序列化逻辑
除了Serializable
,Java还提供了Externalizable
接口,允许完全自定义序列化过程。实现Externalizable
接口的类需要覆盖两个方法:
writeExternal(ObjectOutput out)
:定义如何序列化对象。readExternal(ObjectInput in)
:定义如何反序列化对象。
示例:
import java.io.Externalizable;
import java.io.ObjectOutput;
import java.io.ObjectInput;public class Book implements Externalizable {private String title;private double price;public Book() {} // 必须有无参构造方法public Book(String title, double price) {this.title = title;this.price = price;}@Overridepublic void writeExternal(ObjectOutput out) throws IOException {out.writeUTF(title);out.writeDouble(price);}@Overridepublic void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {title = in.readUTF();price = in.readDouble();}
}
解释:
writeExternal
方法中定义了如何把title
和price
字段写入字节流。readExternal
方法中定义了从字节流读取数据的顺序和方式。- 这种方式比
Serializable
更灵活,可以完全控制序列化的细节。
在Java中,序列化和反序列化的基本实现方式非常直接,Serializable
接口和ObjectOutputStream
/ObjectInputStream
提供了便捷的工具。对于更复杂的需求,Externalizable
接口可以让开发者自定义序列化和反序列化的过程。这些方法不仅在Java内使用方便,同时为跨网络传输、存储等应用场景提供了基础支持。
3. Java序列化的缺点和替代方案
Java原生序列化是一种方便的方式,特别适合在同一系统中使用。不过,它在实际应用中存在以下明显的缺点:
Java序列化的缺点
-
性能较差:
- Java序列化默认包含了很多额外的元数据(比如类信息),导致生成的字节流文件偏大。序列化和反序列化速度较慢,尤其在大规模数据传输中更明显。
- 这种速度较慢的序列化方式在处理大量数据、需要频繁传输的分布式系统中效率不足。
-
不跨语言:
- Java原生序列化生成的二进制格式只适用于Java平台。也就是说,其他编程语言无法直接解析Java序列化生成的字节流。因此,如果系统需要与其他语言(例如C++、Python)交互,就不适用Java原生序列化。
-
存在安全漏洞:
- Java序列化容易受到“反序列化攻击”的威胁。恶意用户可以构造特定的序列化数据流,在反序列化过程中触发恶意代码执行。这种攻击常见于接受外部输入的系统,安全风险很大。
- 由于这种安全隐患,Java原生序列化通常不适合在公开的API或网络传输中使用。
-
缺乏版本控制:
- 当类的结构发生改变(例如添加或移除字段)时,Java的序列化和反序列化很容易出现兼容性问题。例如,老版本的对象在字段缺失时可能导致反序列化失败。
- 尽管可以通过
serialVersionUID
标识版本,但在实际应用中维护起来仍然较麻烦。
这些缺点在很多分布式、跨平台系统中都是阻碍,因此现在很多开发者会考虑使用更高效、安全、灵活的序列化框架。下面介绍两个主流替代方案:FastJson和Protobuf。
替代方案一:FastJson
FastJson是阿里巴巴开发的一个高性能JSON库,主要用于Java对象和JSON格式之间的相互转换。JSON格式是一种人类可读的数据格式,可以方便地在不同语言间共享。
FastJson的优势:
-
跨平台:
- JSON是一种轻量级的数据交换格式,几乎每种编程语言都支持。因此,使用FastJson序列化的对象能够轻松地在不同编程语言之间传递。
-
速度快,性能优异:
- FastJson专为速度和性能优化,在Java中使用非常高效。它支持流式解析、缓存等多种优化方式,在大数据量处理上比Java原生序列化性能更佳。
-
支持灵活的数据结构:
- FastJson序列化的数据格式(JSON)具有良好的兼容性,特别适合于动态数据结构和频繁变化的数据格式。即便类的结构发生变化,JSON也不会导致反序列化失败,因为可以通过字段名来匹配数据。
-
安全性更高:
- JSON格式天然安全,不会引发Java序列化中的反序列化漏洞。同时FastJson提供了对各种对象类型的安全处理机制,能过滤不安全的类型,进一步保障系统安全。
FastJson使用示例:
import com.alibaba.fastjson.JSON;public class FastJsonExample {public static void main(String[] args) {// 序列化Person person = new Person("Alice", 25);String jsonString = JSON.toJSONString(person);System.out.println("序列化为JSON字符串:" + jsonString);// 反序列化Person deserializedPerson = JSON.parseObject(jsonString, Person.class);System.out.println("反序列化对象:" + deserializedPerson.getName());}
}
替代方案二:Protobuf
Protobuf(Protocol Buffers)是由Google推出的一种序列化框架,专门用于跨语言、跨平台的高效数据序列化。
Protobuf的优势:
-
高效的二进制格式:
- Protobuf会将对象数据序列化成紧凑的二进制格式,占用的空间远小于JSON或Java原生序列化生成的数据。
- 序列化和反序列化速度极快,非常适合在网络传输和高频读写场景下使用。
-
跨语言:
- Protobuf支持多种编程语言(如Java、Python、C++等),可以在不同平台之间无缝共享数据。这是Protobuf在分布式系统中被广泛采用的重要原因。
-
支持严格的版本控制:
- Protobuf的“消息结构”定义文件可以兼容新旧版本,当数据结构发生变化时,Protobuf可以自动适应新旧字段,极大减少了版本兼容问题。
-
安全性强:
- Protobuf的格式是结构化的,因此不易受到反序列化攻击。数据结构和解析过程非常明确,使用起来更安全。
Protobuf使用示例: Protobuf使用时,需要先定义一个.proto
文件,指定数据结构:
// person.proto
syntax = "proto3";message Person {string name = 1;int32 age = 2;
}
然后,通过Protobuf编译工具生成对应的Java类,进行序列化和反序列化:
Person person = Person.newBuilder().setName("Alice").setAge(25).build();
byte[] data = person.toByteArray(); // 序列化
Person deserializedPerson = Person.parseFrom(data); // 反序列化
在实际项目中,由于Java原生序列化的性能、安全和跨语言问题,开发者更倾向于选择FastJson或Protobuf。FastJson适合数据格式简单、语言可读性要求高的应用场景;Protobuf则适合大数据传输、分布式和跨平台应用中对效率和兼容性要求高的场景。两者都比Java原生序列化更灵活、安全和高效。
4. 总结
序列化与反序列化是现代软件开发中用于数据传输、存储的基本技术,尤其在分布式系统、缓存系统和跨语言数据交换中应用广泛。通过这些技术,可以将复杂的对象在不同进程、系统甚至平台间传递和共享。
1. Java原生序列化的基础
Java为对象序列化提供了内置支持,使用Serializable
接口和ObjectOutputStream
/ObjectInputStream
流实现简单直接的序列化。Java原生序列化的流程包括:
- 将对象及其字段数据转化为字节流,保存或传输。
- 在需要时通过字节流反序列化为对象,恢复对象状态。
这种方式在同一Java系统内部传输数据非常便捷,但存在性能、安全和兼容性等问题。
2. Java原生序列化的缺点
Java原生序列化适用于小规模数据传输或非跨平台的Java系统。但在生产级别应用中,它的缺陷较为明显:
- 性能低:字节流数据体积大,序列化和反序列化速度相对较慢。
- 不跨语言:生成的字节流只能在Java环境中解析,无法直接用于与其他语言的交互。
- 安全性欠佳:存在反序列化漏洞风险,容易受到反序列化攻击。
因此,Java原生序列化适合临时数据保存,但在涉及敏感数据和跨系统场景时,存在较大风险。
3. 替代方案:FastJson和Protobuf
为了解决Java原生序列化的不足,许多开发者选择更高效的序列化框架,例如FastJson和Protobuf:
-
FastJson:适合JSON格式的数据交换,提供跨平台、通用的人类可读格式。其序列化过程速度快,兼容性好,非常适合在前后端通信和跨平台轻量级数据传输中使用。
-
Protobuf:适合分布式系统、大数据和高效跨平台传输。Protobuf的二进制格式占用空间小,解析速度快,版本控制友好,是数据高效传输的理想选择,尤其在微服务和多语言系统中应用广泛。
4. 使用场景总结
- 选择Java原生序列化:适合在同一Java应用内的数据存储(如缓存、简单对象的临时保存),对性能和跨平台要求不高的情况下。
- 选择FastJson:适合需要将数据转化为人类可读格式且在前后端或不同系统中共享时,JSON格式兼容性强且更便于调试。
- 选择Protobuf:适合大型、复杂的分布式系统和跨语言数据传输,对性能和传输效率要求高的情况。
综上所述,序列化和反序列化技术的发展,为现代软件提供了多样化的数据传输和持久化方案。通过合理选择合适的序列化工具,可以优化系统的性能、安全性和兼容性,从而满足不同应用的需求。