序列化
序列化
序列化与反序列化是什么?为什么要序列化?
序列化:将Java对象转换为字节流的过程,使对象可以在网络上传输或持久化存储到文件中。
反序列化:将字节流恢复为Java对象的过程,从文件读取或网络接收字节流后重构对象。
为什么需要序列化?
- 跨JVM通信:将对象从一个JVM传递到另一个JVM
- 网络传输:通过Socket、HTTP等方式在分布式系统中传输数据
- 持久化存储:将对象状态保存到文件或数据库
- 缓存存储:将对象存储在Redis等缓存中,提高访问效率
怎么把一个对象从一个JVM转移到另一个JVM?
序列化与反序列化
将对象序列化为字节流,通过网络传输或文件存储发送到另一个JVM,再在目标JVM中反序列化字节流恢复对象。Java原生通过ObjectOutputStream(序列化)和ObjectInputStream(反序列化)实现,需确保对象类实现Serializable接口。消息传递机制
利用消息队列(如RabbitMQ、Kafka)或网络套接字(Socket),自定义协议将对象序列化后发送。例如,通过Socket建立两个JVM间的连接,将序列化后的字节流写入输出流,目标JVM从输入流读取并反序列化。远程方法调用(RPC)
使用RPC框架(如gRPC、Dubbo),框架内部封装对象序列化、网络传输和反序列化逻辑,开发者可直接调用远程JVM上的对象方法,无需手动处理传输细节。例如,gRPC基于Protocol Buffers序列化对象,实现跨语言、跨JVM的方法调用。共享数据库或缓存
将对象存储在共享数据库(如MySQL)或分布式缓存(如Redis)中,两个JVM通过读写共享存储实现对象"转移"。适用于无需实时传输、仅需共享数据的场景,需将对象转换为数据库字段或缓存支持的格式(如JSON)。
序列化和反序列化让你自己实现你会怎么做?
Java原生序列化存在无法跨语言、安全漏洞、序列化后流体积大三大缺陷,实际开发中更推荐使用主流序列化框架替代,具体选择如下:
1. 优先选择成熟框架(而非手动实现)
追求跨语言与性能:选择Protocol Buffers(Protobuf)
Protobuf是Google开源的二进制序列化框架,定义.proto文件描述对象结构,通过编译器生成不同语言的序列化/反序列化代码。其优势在于:- 跨语言:支持Java、C++、Python等多种语言,适合多语言协作的分布式系统。
- 高性能:二进制格式紧凑,序列化后流体积小,编码/解码仅需简单数据运算和位移操作,效率远超原生序列化。
- 可扩展性:支持字段新增和废弃,兼容旧版本数据。
追求易用性与JSON格式:选择FastJson、Jackson
若需序列化后的格式可读(如JSON),可使用FastJson或Jackson。这类框架基于反射实现,无需定义额外结构文件,直接将对象与JSON字符串互转,适用于HTTP接口传输、配置文件存储等场景。
2. 若必须手动实现(了解核心逻辑)
手动实现需定义一套序列化协议(即字节流格式),核心步骤如下:
- 定义协议格式:规定字节流中各字段的顺序、类型和长度。例如,对象包含
int id和String name,协议可定义为:[4字节id][4字节name长度][name字节数组]。 - 序列化逻辑:将对象字段按协议格式转换为字节数组。例如,用
DataOutputStream将id(int转4字节)、name长度(int转4字节)、name(字符串转字节数组)依次写入输出流。 - 反序列化逻辑:从字节流中按协议格式读取数据,恢复对象。例如,用
DataInputStream依次读取4字节(解析为id)、4字节(解析为name长度)、指定长度的字节数组(解析为name),最终创建对象。
手动实现缺点:需处理类型转换、版本兼容、异常情况(如字段缺失),代码复杂度高,且无法跨语言,实际中极少使用。
将对象转为二进制字节流具体怎么实现?
对象转二进制字节流的核心是遵循序列化协议(定义字节流格式),Java原生通过序列化流实现,步骤如下:
1. 前提:对象类实现Serializable接口
只有实现java.io.Serializable接口的类,其对象才能被序列化(接口无方法,仅为"标记接口",告知JVM该类可序列化)。若类有不可序列化的成员变量,需用transient关键字修饰(序列化时会忽略该字段)。
import java.io.Serializable;
// 实现Serializable接口,支持序列化
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 序列化版本号,确保版本兼容
private int id;
private String name;
private transient String password; // transient修饰,不参与序列化
// 构造方法、getter、setter省略
}2. 序列化:ObjectOutputStream写入对象
通过ObjectOutputStream的writeObject()方法,将对象按Java原生协议转换为字节流,可写入文件、网络流等。
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class SerializeDemo {
public static void main(String[] args) {
User user = new User(1, "Alice", "123456");
try (// 1. 创建文件输出流(目标存储位置)
FileOutputStream fileOut = new FileOutputStream("user.ser");
// 2. 包装为对象输出流
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
// 3. 序列化对象,写入字节流
out.writeObject(user);
System.out.println("对象已序列化到user.ser");
} catch (Exception e) {
e.printStackTrace();
}
}
}3. 反序列化:ObjectInputStream读取字节流
通过ObjectInputStream的readObject()方法,从字节流中读取数据,恢复为对象,需强制转换为目标类类型。
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class DeserializeDemo {
public static void main(String[] args) {
User user = null;
try (// 1. 创建文件输入流(读取字节流)
FileInputStream fileIn = new FileInputStream("user.ser");
// 2. 包装为对象输入流
ObjectInputStream in = new ObjectInputStream(fileIn)) {
// 3. 反序列化,恢复对象
user = (User) in.readObject();
System.out.println("对象反序列化完成,name:" + user.getName());
System.out.println("transient字段password:" + user.getPassword()); // 输出null(未序列化)
} catch (Exception e) {
e.printStackTrace();
}
}
}关键说明
- serialVersionUID:序列化版本号,若类结构修改(如新增字段),需保持版本号与旧版本一致,否则反序列化时会抛出
InvalidClassException。 - transient关键字:修饰的字段不参与序列化,反序列化后该字段为默认值(如String为null,int为0)。
- 适用场景:原生序列化仅适用于Java单语言环境,若需跨语言或高性能,需替换为Protobuf、FastJson等框架。