Java 对象在 JVM 运行时被创建、更新和销毁,当 JVM 退出时,对象也会随之销毁,即这些对象的生命周期不会比 JVM 的生命周期长。但在现实应用中,我们常常需要将对象及其状态在多个应用之间传递、共享,或者将对象及其状态持久化,在其它地方重新读取被保存的对象及其状态继续进行处理。这就需要通过将 Java 对象序列化来实现。
在使用 Java 序列化技术保存对象及其状态信息时,对象及其状态信息会被保存在一组字节数组中,在需要时再将这些字节数组反序列化为对象。
注意,对象序列化保存的是对象的状态,即对象的成员变量,因此类中的静态变量不会被序列化。
对象序列化除了用于持久化对象,在 RPC (远程过程调用)或者网络传输中也经常被使用。在实际使用过程中处理可以使用 Java 序列化技术来实现,还可以使用 Kryo、Arvo、ProtoBuf、FastJson 等序列化框架来实现。
Java 序列化 API 为处理对象序列化提供了一种标准机制,在进行 Java 序列化时需要注意如下事项:
具体的序列化实现代码如下:
//通过实现 Serializable 接口定义可序列化的 Worker 类
public class Worker implements Serializable {
//定义序列化的 ID
private static final long serialVersionUID = 123456789L;
//name 属性将被序列化
private String name;
//transient 修饰的变量不会被序列化
private transient int salary;
//静态变量属于类信息,不属于对象的状态,因此不会被序列化
static int age = 100;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
以上代码通过 implements Serializable 实现了一个序列化的类。注意,transient 修饰的属性和 static 修饰的静态属性不会被序列化。
对象通过序列化后在网络上传输,基于网络安全,我们可以在序列化前将一些敏感字段(用户名、密码、身份证号)使用密钥进行加密,在反序列化后再基于密钥对数据进行解密,这样即使数据在网络中被劫持,由于缺少密钥也无法对数据进行解密,这样可以在一定程度上保证序列化对象的数据安全。
我们可以基于 JDK 原生的 ObjectOutputStream 和 ObjectInputStream 类实现对象的序列化及反序列化,并调用其 writeObject 和 readObject 方法实现自定义序列化策略。
具体的实现代码如下:
public static void main(String[] args) throws IOException, ClassNotFoundException {
//序列化数据到磁盘
long startTime = System.currentTimeMillis();
FileOutputStream fos = new FileOutputStream("worker.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
for (int i = 0; i < 50000; i++) {
Worker worker = new Worker();
worker.setName("小明" + i);
oos.writeObject(worker);
}
oos.flush();
oos.close();
long endTime = System.currentTimeMillis();
System.out.println(String.format("使用 java 序列化时间:%d", (endTime - startTime)));
//反序列化磁盘数据并解析数据状态
FileInputStream fis = new FileInputStream("worker.out");
ObjectInputStream ois = new ObjectInputStream(fis);
Worker worker = null;
try {
while ((worker = (Worker) ois.readObject()) != null) {
//worker 为反序列化后的对象
}
} catch (EOFException e) {
//在文件读取完成时会抛出 EOFException
}
long deEndTime = System.currentTimeMillis();
System.out.println(String.format("使用 java 反序列化时间:%d", deEndTime - endTime));
}
以上代码通过文件流的方式将 5 万个 worker 对象的状态写入磁盘,在需要使用时再以文件流的方式将其读取并反序列化成我们需要的对象及其状态数据。在执行后打印出如下结果:
使用 java 序列化时间:1319
使用 java 反序列化时间:1359
Kryo 是一个快速序列化和反序列化工具,依赖于 ASM(一个 Java 字节码操控框架),基于字节码生成机制实现,序列化的结果以二进制形式存储。Kryo 的特性是速度快,在使用过程中需要注意如下事项:
在进行 Kryo 序列化时,首先需要将序列化对象注册到 Kryo ,然后在 Kryo 上调用 writeObject 方法进行序列化,调用 readObject 方法进行反序列化。具体的使用方法如下:
<dependency>
<groupId>com.esotericsoftwaregroupId>
<artifactId>kryoartifactId>
<version>4.0.0version>
dependency>
public static void main(String[] args) throws FileNotFoundException {
//序列化文件路径及名称
String file = "F:\\KryoSerializable.bin";
long startTime = System.currentTimeMillis();
//1.定义序列化对象
Kryo kryo = new Kryo();
kryo.setReferences(false);
kryo.setRegistrationRequired(false);
kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
kryo.register(Worker.class);
//2.定义序列化对象存储的文件
Output output = new Output(new FileOutputStream(file));
for (int i = 0; i < 50000; i++) {
Worker worker = new Worker();
worker.setName("小黑" + i);
//3.通过Kryo序列化5万个对象
kryo.writeObject(output, worker);
}
output.flush();
output.close();
long endTime = System.currentTimeMillis();
System.out.println(String.format("使用 Kryo 序列化用时:%d", (endTime - startTime)));
try {
//4.将序列化对象读取到 Input 中
Input input = new Input(new FileInputStream(file));
//5.使用 kryo.readObject 将 Input 中的数据按行进行反序列化
Worker worker = null;
//注意:原书中为以下代码(开始)
//以下代码出现异常 com.esotericsoftware.kryo.KryoException: Buffer underflow.
//原因是读到第 50001个 Worker 对象时(下标为50000),读取的值不为null,
//而是直接抛出了异常
//while ((worker = kryo.readObject(input,Worker.class)) != null){
// System.out.println(worker.getName());
//}
//注意:原书中为以上代码(结束)
for (int i = 0; i < 50000; i++) {
worker = kryo.readObject(input, Worker.class);
//System.out.println(worker.getName());
}
input.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
long deEndTime = System.currentTimeMillis();
System.out.println(String.format("使用 Kryo 反序列化用时:%d", (deEndTime - endTime)));
}
以上代码定义了 Kryo 对象 kryo ,并通过 kryo.register(Worker.class) 将 Worker 类注册到 Kryo ,接着定义了 5 万个 Worker 对象,并通过 kryo.writeObject(output,Worker) 将 Worker 对象序列化到 output 对应的文件中。在进行反序列化时,首先将序列化对象读取到 Input 中,然后调用 kryo.readObject 将 Input 中的数据按行进行反序列化。
以上代码的执行结果如下:
使用 Kryo 序列化用时:250
使用 Kryo 反序列化用时:180
Avro 是一种序列化框架,由 Hadoop 之父 Doug Ctting 创建,设计初衷是解决 Hadoop 的 Writable 类型无法在多种语言之间移植的问题。Avro 的 Schema 信息采用 JSON 格式来记录,数据采用二进制编码或 JSON 编码的记录方式。
Avro 具有以下特性:
Avro 的 Schema 定义了简单的数据类型(string、boolean、int、long、float、double、byte、null)和复杂的数据类型(Record、Enum、Array、Map、Union、Fixed)。
Avro 的使用流程:定义 Schema 文件,生成 Java 类,定义 DataFileWriter 并通过 append 方法将对象序列化到文件中,定义 DataFileReader 并通过 next 方法读取和反序列化文件中的对象。具体使用过程如下:
{
"namespace": "com.offer.test1",
"type": "record",
"name": "User",
"fields": [{
"name": "name",
"type": "string"
},
{
"name": "favorite_number",
"type": ["int", "null"]
},
{
"name": "favorite_color",
"type": ["string", "null"]
}
]
}
<dependency>
<groupId>org.apache.avrogroupId>
<artifactId>avroartifactId>
<version>1.8.2version>
dependency>
<plugins>
<plugin>
<groupId>org.apache.avrogroupId>
<artifactId>avro-maven-pluginartifactId>
<version>1.8.2version>
<executions>
<execution>
<phase>generate-sourcesphase>
<goals>
<goal>schemagoal>
goals>
<configuration>
<sourceDirectory>${project.basedir}/src/main/avro/sourceDirectory>
<outputDirectory>${project.basedir}/src/main/java/outputDirectory>
configuration>
execution>
executions>
plugin>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<configuration>
<source>1.8source>
<target>1.8target>
configuration>
plugin>
plugins>
java -jar avro-tools-1.8.2.jar compile schema user.avsc .
public static void main(String[] args) throws IOException {
//1.定义 Avro 文件存放目录
String path = "./user.avro";
long startTime = System.currentTimeMillis();
//2.定义序列化对象
DatumWriter<User> userDatumWriter = new SpecificDatumWriter<User>(User.class);
DataFileWriter<User> dataFileWriter = new DataFileWriter<User>(userDatumWriter);
dataFileWriter.create(new User().getSchema(),new File(path));
//3.生成5万个 user 对象并写入 Avro 文件
for (int i = 0; i < 50000; i++) {
//通过 Avor 序列化 5万个对象
User user = User.newBuilder()
.setName("小明"+i)
.setFavoriteColor("blue"+i)
.setFavoriteNumber(i)
.build();
dataFileWriter.append(user);
}
dataFileWriter.close();
long endTime = System.currentTimeMillis();
System.out.println(String.format("使用 avro 序列化耗时:%d",(endTime-startTime)));
//4.将序列化对象读取到 DataFileReader 中
DatumReader<User> reader = new SpecificDatumReader<>();
DataFileReader<User> dataFileReader = new DataFileReader<User>(new File(path),reader);
User user = null;
//5.使用 next 方法按行读取 DataFileReader 中的数据
while (dataFileReader.hasNext()){
user = dataFileReader.next();
}
long deEndTime = System.currentTimeMillis();
System.out.println(String.format("使用 avro 序列化耗时:%d",(deEndTime-endTime)));
}
以上代码的执行结果如下:
使用 avro 序列化耗时:1985
使用 avro 反序列化耗时:225
ProtoBuf(Google Protocol Buffers)是由 Google 开源的一款跨平台、与语言无关、可扩展的数据序列化框架,主要用于不同系统及语言之间的数据交换存储,在 RPC 框架中被广泛使用。
在使用 ProtoBuf 时,首先需要在 .proto 文件中定义数据结构,然后使用 ProtoBuf 编译器 protoc 生成指定语言的数据访问类。这些类为每个字段都提供了简单的访问器(例如 get 和 set 方法)及整个数据结构序列化为原始字节的方法。对于数据结构的变更问题,在数据格式中新加入字段后,老文件在解析时会忽略新的字段,因此担心数据结构向后兼容的问题。最后使用 ProtoBuf 提供的 API 进行序列化或反序列化。
具体使用流程如下:
注意:根据书中内容以下步骤并未走通。
原因:命令“protoc -I=.–java_out=.student.proto”提示未找到命令。
说明:应该需要下载 ProtoBuf 相关插件,而书中并没有提及;另外,个人感觉 ProtoBuf 序列化不重要,作为了解即可,便没有尝试去下载完善以下步骤。
syntax = "proto2";
package "serialization";
option java_generic_services = true;
option java_package = "com.offer.test1";
option java_outer_classname = "ProtoSample";
message Student {
required int32 id = 1;
optional string name = 2;
}
以上代码定义了名为 Student 的数据结构。其中,java_package 表示类所在的包;Student 为具体的类名,在该类中定义了 id 和 name 两个字段,数据类型分别为 int32 和 string。
ProtoSample.Student student = ProtoSample.Student.newBuilder()
.setId(1234)
.setName("小明")
.build();
相关面试题: