Day61 | Java IO之序列化
Day59中我们通过InputStream/OutputStream,学会了怎么读写原始字节。
Day60中我们通Reader/Writer,学会了怎么读写人类字符。
今天我们看一下另外一个问题,假如我们内存里面有一个Java对象,比如User user = new User("LazySnail", 28);。
我们不想手动把他转成JSON或者CSV,而是想把他原封不动的存到文件里,然后明天程序重启的时候,再从文件里把他复原成一个一模一样的User 对象,该怎么办?
这个过程其实就是Java的序列化和反序列化机制。
序列化就是把一个Java内存对象转换成一个字节序列,这样就可以存到磁盘或者在网络间传输。
反序列化就是把一个字节序列还原,重新构造成一个Java内存对象。
一、Serializable接口
Serializable是一个标记接口。打开他的源码,我们会发现里面一个方法都没有。

这个接口的唯一作用,就是向JVM的序列化算法发出一个信号,这个类我们已经检查过了,同意这个类的对象(包括其所有字段)可以被序列化。
换句话说,我们想要序列化的对象的类必须实现Serializable,不然就会出现NotSerializableException。
package com.lazy.snail.day61;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
/**
* @ClassName SerializableDemo1
* @Description TODO
* @Author lazysnail
* @Date 2025/11/17 16:12
* @Version 1.0
*/
public class SerializableDemo1 {
public static void main(String[] args) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\lazysnail\\Desktop\\user.ser"))) {
oos.writeObject(new User("懒惰蜗牛", 28));
} catch (IOException e) {
System.err.println("序列化失败: " + e.getMessage());
e.printStackTrace();
}
}
}
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
示例代码中,我们尝试将一个User对象序列化存储到本地文件里,运行时出现java.io.NotSerializableException。
我们把User实现Serializable再运行代码,就能成功序列化了。

二、序列化和反序列化
2.1 序列化
在上一节中我们已经演示了怎么将一个Java对象序列化成字节序列。
序列化后存储的文件是user.ser。我们直接用UltraEdit打开看一下。是一堆我们看不懂的乱码。

切换到16进制显示模式。

文件里开头的两个字节AC ED是一个魔数(固定值),就是用来识别文件类型的,表示这是个序列化文件。
我们在输出序列化文件时使用的ObjectOutputStream,其中使用到的一个常量就是0xaced。

文件中后两个字节是流版本(00 05),也就是STREAM_VERSION。
这些都是ObjectOutputStream的构造函数在创建时自动写入的,这是Java序列化格式的标准开头。
序列化还有个特性就是Serializable的传递性。
当我们序列化一个User对象时,JVM的序列化算法会使用反射来检查User类的所有字段,并递归的尝试序列化每一个字段。

那假设User类中有字段的对象没有实现Serializable,会发生什么呢?
package com.lazy.snail.day61;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* @ClassName SerializableDemo2
* @Description TODO
* @Author lazysnail
* @Date 2025/11/17 16:12
* @Version 1.0
*/
public class SerializableDemo2 {
public static void main(String[] args) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\lazysnail\\Desktop\\stu.ser"))) {
oos.writeObject(new Student("懒惰蜗牛", 28, new Address("江州", "长安街")));
} catch (IOException e) {
System.err.println("序列化失败: " + e.getMessage());
e.printStackTrace();
}
}
}
class Student implements Serializable {
private String name;
private int age;
private Address address;
public Student(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
}
class Address {
private String city;
private String street;
public Address(String city, String street) {
this.city = city;
this.street = street;
}
}
示例代码中Student类实现了Serializable。其中有一个引用类型的属性Address,没有实现Serializable。我们尝试序列化一个Student对象。
结果抛出了java.io.NotSerializableException。
所以Java序列化要求整个对象图中的所有对象都必须可序列化。
那如果我们想不序列化某些字段,比如有些字段包含了敏感信息,我们想在序列化的时候剔除掉,有解决方案吗?
那就需要使用一个关键字transient。
package com.lazy.snail.day61;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* @ClassName SerializableDemo3
* @Description TODO
* @Author lazysnail
* @Date 2025/11/18 9:55
* @Version 1.0
*/
public class SerializableDemo3 {
public static void main(String[] args) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\lazysnail\\Desktop\\ui1.ser"))) {
oos.writeObject(new UserInfo("懒惰蜗牛", "123456"));
} catch (IOException e) {
System.err.println("序列化失败: " + e.getMessage());
e.printStackTrace();
}
}
}
class UserInfo implements Serializable {
private String name;
private transient String password;
public UserInfo(String name, String password) {
this.name = name;
this.password = password;
}
}
示例代码中,password字段使用了transient进行修饰,那么在序列化过程中就不会将该字段进行序列化。
下图是ui.ser是没有使用transient关键字产生的序列化文件,ui1.ser是加上了transient关键字产生的序列化文件,能够直观的看出后者没有password字段相关的内容。

2.2 反序列化
反序列化就是序列化反向的一个过程,从字节序列构建Java对象。
package com.lazy.snail.day61;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
/**
* @ClassName SerializableDemo4
* @Description TODO
* @Author lazysnail
* @Date 2025/11/18 10:09
* @Version 1.0
*/
public class SerializableDemo4 {
public static void main(String[] args) {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\lazysnail\\Desktop\\user.ser"))) {
User user = (User) ois.readObject();
System.out.println(user);
} catch (Exception e) {
e.printStackTrace();
}
}
}
上面的代码我们尝试使用ObjectInputStream,将user.ser文件中的数据恢复成Java对象。
但是却抛出了一个异常:
java.io.InvalidClassException: com.lazy.snail.day61.User; local class incompatible: stream classdesc serialVersionUID = -4181543053864524157, local class serialVersionUID = 2346668906149080388
这是一个典型的serialVersionUID不匹配导致的异常。当序列化和反序列化时的serialVersionUID不一致时,Java会抛出InvalidClassException。
我们之前也没有在任何地方看到serialVersionUID这个东西,是哪里冒出来的?
当我们没有显示的指定serialVersionUID时,Java序列化运行的时候会根据类的各个方面自动计算一个默认值。
当反序列化的时候,如果我们的类没有被修改(或者进行了兼容性的修改,比如新增字段等),那就能成功的反序列化。
但是我们的类如果发生了结构性的改变,比如删了字段、改了字段类型等等,自动生成的serialVersionUID就可能改变。就会导致InvalidClassException。
class User implements Serializable {
private static final long serialVersionUID = 1L;
}
我们可以在类中简单的显示指定序列化版本。
只要我们提供了这个1L,后续再给User类增加新字段(比如email),反序列化旧文件的时候,JVM都会信任我们。会正常填充name和age,新加的email字段会被赋予默认值(null)。
就算显示指定了序列化版本,也不能真正解决版本兼容性问题。
三、反序列化漏洞
前面我们学会了怎么使用序列化,现在我们看看什么时候我们不应该使用他。
当ois.readObject()执行的时候,会读取字节流里的类名,然后JVM会自动findClass()来构造这个对象。
现在有这样一个场景,有个坏小子精心构造了一个恶意的字节流,这个流声称自己是一个合法的Serializable类(比如某个我们依赖的第三方库commons-collections里的类),但在反序列化(构造)这个类的过程中,他的 readObject方法(被黑客重写过的)会执行恶意代码。
// 伪代码
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 正常反序列化逻辑...
in.defaultReadObject();
// 恶意代码!
Runtime.getRuntime().exec("rm -rf /");
// 或者建立反向shell连接等
}
我们的服务器在调用ois.readObject()的时候,就可能被远程代码执行。
这些字节流会来自客户端HTTP请求体、网络socket接收的数据、用户上传的文件等等。
所以一般情况下我们不会对任何不可信来源的字节流执行ObjectInputStream.readObject()。
现代的Java应用也基本上没有使用Java原生序列化在网络间传输数据了。
现在一般使用的是JSON、Protocol Buffers、Kryo、Avro等等。
结语
通过本文的学习,我们掌握了Java原生序列化将对象状态转换为持久字节流的核心机制。
我们从Serializable标记接口出发,理解了序列化与反序列化的基本过程,探讨了transient关键字的作用和serialVersionUID的意义和局限。
下一篇预告
Day62 | Java IO之NIO三件套--通道和缓冲区(上)
如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!
更多推荐


所有评论(0)