Java中对象的转移之序列化与反序列化方式
序列化将对象转换为字节流以便存储或传输,反序列化则将其还原。Java通过Serializable接口和ObjectOutputStream/ObjectInputStream实现该过程,transient关键字可保护敏感字段。原生序列化存在性能、跨语言及安全问题,因此常采用FastJson或Protobuf等替代方案,它们分别提供跨平台JSON处理与高效二进
1. 序列化与反序列化的定义与实现流程
在分布式系统、缓存以及跨应用数据交换的场景中,我们经常需要将对象“打包”发送或“封存”起来。这背后依赖的核心技术,就是序列化与反序列化。

序列化是什么?
简单来说,序列化就是把一个活生生的对象,转换成一串连续的字节流。这个过程就好比把一件复杂的家具拆解、打包成一个规整的包裹,目的是为了方便存储到文件或数据库,或者通过网络“寄送”到另一个系统。
反序列化是什么?
反序列化自然是序列化的逆过程。当系统收到这个“包裹”或者从存储介质中读取到这串字节流时,需要按照原来的图纸,把零件重新组装成完整的家具对象。这个过程就是反序列化,它让字节流重新获得生命,变回可操作的程序对象。
序列化的实现流程
整个流程可以清晰地分为三步走:
第一步:对象状态转换为字节流
- 系统会提取对象的所有属性、数据结构以及类标识等信息,将它们编码成一组有序的字节。
- 这组字节不仅包含了字段的值,还隐含了对象的“出生证明”(类信息),确保接收方能准确识别并还原。
第二步:传输或存储字节流
- 生成的字节流可以被写入文件系统、存入数据库,或者直接通过网络套接字发送到远程服务器。
第三步:将字节流转为对象(反序列化)
- 当需要用到这个对象时,系统会读取字节流,根据其中包含的类信息和字段数据,在内存中重新构造出一个状态完全相同的对象实例。
正是这套机制,实现了对象跨越时间(持久化)和空间(网络传输)的转移。虽然这里以Ja va为例,但序列化的思想在众多编程语言和框架中都有体现,只是实现方式各有千秋。
2. Ja va中的序列化与反序列化实现
Ja va语言内置了一套完整的序列化机制,让对象转换变得非常直接。其核心是Serializable接口和一对流类:ObjectOutputStream与ObjectInputStream。
1.Serializable接口:标识对象可序列化
想让一个类的对象能被序列化,第一步就是让它实现Serializable接口。这个接口没有任何方法,是一个纯粹的“标记接口”,它的作用就是告诉JVM:“我这个类的对象可以被序列化。”
示例:
import ja va.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接口。这意味着,这个类的所有实例字段(除非被特殊标记)在序列化时都会被自动转换成字节流。
2.ObjectOutputStream和ObjectInputStream:读写字节流的对象
ObjectOutputStream:负责将对象“写出去”,即执行序列化操作。ObjectInputStream:负责将字节流“读进来”,即执行反序列化操作。
示例:序列化对象
import ja va.io.FileOutputStream;
import ja va.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文件中。 - 至此,对象的数据已被持久化保存。
示例:反序列化对象
import ja va.io.FileInputStream;
import ja va.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接口:自定义序列化逻辑
对于更复杂的控制需求,Ja va提供了Externalizable接口。它继承自Serializable,但要求开发者自己动手,完全掌控序列化和反序列化的每一个细节。实现它需要重写两个方法:
writeExternal(ObjectOutput out):定义如何将对象写入字节流。readExternal(ObjectInput in):定义如何从字节流中读取并重建对象。
示例:
import ja va.io.Externalizable;
import ja va.io.ObjectOutput;
import ja va.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;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(title);
out.writeDouble(price);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
title = in.readUTF();
price = in.readDouble();
}
}
解释:
- 在
writeExternal方法中,我们明确指定了将title和price字段以何种顺序、何种格式写入流中。 - 在
readExternal方法中,我们按照写入的顺序和格式,再将数据读取出来,赋值给对象的字段。 - 这种方式牺牲了便利性,换来了极高的灵活性和控制力,适用于有特殊序列化需求的场景。
总的来说,Ja va内置的序列化机制为开发者提供了从便捷到灵活的不同选择。Serializable搭配ObjectOutputStream/ObjectInputStream足以应对大多数常规需求。而当需要对过程进行精细控制时,Externalizable接口则提供了强大的自定义能力。
3. Ja va序列化的缺点和替代方案
尽管Ja va原生序列化用起来方便,但在构建高性能、跨平台、高安全的现代分布式系统时,它的几个固有缺陷就会变得非常突出。
Ja va序列化的缺点
性能较差:
- Ja va序列化生成的字节流包含了大量元数据(如完整的类描述信息),导致数据体积臃肿。序列化和反序列化的过程也涉及大量反射操作,速度相对较慢。在处理海量数据或高并发请求时,这很容易成为性能瓶颈。
不跨语言:
- 它生成的是一种Ja va特有的二进制格式。这意味着,用Ja va序列化保存的数据,几乎无法被C++、Python、Go等其他主流语言直接解析。在微服务架构和多语言技术栈并存的今天,这无疑是一个巨大的限制。
存在安全漏洞:
- Ja va反序列化机制历史上存在严重的安全隐患。攻击者可以精心构造一个恶意的序列化字节流,当程序对其进行反序列化时,可能触发任意代码执行。这使得它不适合处理来自不可信源的数据。
缺乏版本控制:
- 当序列化后的类结构发生变化(例如增删字段),反序列化很容易失败。虽然可以通过手动维护
serialVersionUID来缓解,但在复杂的项目迭代中,维护版本兼容性依然是个头疼的问题。
正因为这些局限性,在实际生产环境中,尤其是对性能、安全性和跨语言能力有要求的场景下,开发者往往会转向更优秀的替代方案。下面就来聊聊两个主流选择:FastJson和Protobuf。
替代方案一:FastJson
FastJson是阿里巴巴开源的高性能JSON处理库。它的核心是将Ja va对象与JSON格式相互转换,而JSON是一种几乎无处不在的、人类可读的数据交换格式。
FastJson的优势:
跨平台:
- JSON是标准的数据格式,所有编程语言都支持解析和生成。因此,用FastJson序列化的数据可以毫无障碍地在Ja va、前端Ja vaScript、Python后端等不同组件间传递。
速度快,性能优异:
- 正如其名,FastJson在速度上做了大量优化,其序列化/反序列化性能通常远超Ja va原生序列化,甚至优于许多其他JSON库。
支持灵活的数据结构:
- 基于JSON的键值对特性,FastJson能很好地处理动态或结构易变的数据。即使类的字段有增减,反序列化时也能通过字段名进行匹配,兼容性更好。
安全性更高:
- JSON格式本身不携带可执行代码,从根本上避免了Ja va原生反序列化漏洞。同时,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或Ja va原生序列化结果。其编码和解码速度也极快,专为高性能网络通信设计。
跨语言:
- 通过预先定义
.proto文件,Protobuf编译器可以生成Ja va、C++、Python、Go等多种语言的代码。不同语言编写的服务可以使用同一份数据定义进行无缝通信。
支持严格的版本控制:
- Protobuf的协议设计天生支持向前和向后兼容。新增或废弃字段不会破坏旧版程序的解析,大大简化了系统升级和数据迁移的复杂度。
安全性强:
- 由于其强类型和预定义结构的特性,Protobuf不易受到不可控反序列化攻击,安全性比Ja va原生序列化高得多。
Protobuf使用示例: 使用Protobuf首先需要定义一个.proto文件来描述数据结构:
// person.proto
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
}
然后,使用Protobuf编译器生成对应的Ja va类,之后就可以像下面这样使用:
Person person = Person.newBuilder().setName("Alice").setAge(25).build();
byte[] data = person.toByteArray(); // 序列化
Person deserializedPerson = Person.parseFrom(data); // 反序列化
简单来说,FastJson以其通用性和可读性见长,非常适合前后端交互和需要日志调试的场景。而Protobuf则以绝对的性能和跨语言能力取胜,是微服务、分布式存储等对效率和带宽敏感场景的首选。两者都有效地弥补了Ja va原生序列化的短板。
4. 总结
序列化与反序列化是构建可交互、可扩展软件系统的基石技术。它们让对象得以跨越进程、网络甚至语言的边界,是实现数据持久化和远程通信的关键。
1. Ja va原生序列化的基础
Ja va通过Serializable接口和ObjectOutputStream/ObjectInputStream这一套组合拳,提供了开箱即用的序列化支持。其流程直观:对象变字节流,存储或传输;字节流再变回对象,恢复状态。这套方案在单一的Ja va环境中简单快捷。
2. Ja va原生序列化的缺点
然而,一旦进入生产环境,面对性能、安全、跨语言的需求,原生序列化的局限性便暴露无遗:
- 性能低:生成的负载大,处理速度慢。
- 不跨语言:二进制格式与其他语言不兼容。
- 安全性欠佳:存在已知的反序列化安全漏洞。
因此,它更适用于简单的、临时的、信任边界内的数据存储场景。
3. 替代方案:FastJson和Protobuf
为了应对更复杂的挑战,业界广泛采用了更先进的序列化方案:
- FastJson:在需要人类可读、跨平台(尤其是Web前后端)且对性能有一定要求的场景下表现出色。JSON格式的通用性是其最大优势。
- Protobuf:在追求极致性能、低带宽消耗和严格跨语言兼容的分布式系统、微服务架构中几乎是标准选择。其二进制格式高效且安全。
使用场景总结
- 选择Ja va原生序列化:适用于简单的Ja va进程内缓存、Session存储等封闭场景。
- 选择FastJson:适用于前后端API交互、配置文件存储、需要人工查看和调试数据的场景。
- 选择Protobuf:适用于服务间RPC通信、大数据量持久化、多语言微服务集群等对效率和兼容性要求极高的场景。
技术选型没有银弹,关键在于理解不同序列化工具的特性,并根据实际应用场景在性能、可读性、安全性和开发成本之间做出平衡。选择合适的序列化方案,能显著提升系统的整体健壮性和效率。


































