Serializable接口的作用
1、Serializable 简介
Serializable 接口是 Java 中的一个标记接口(marker interface),它没有定义任何方法,仅用于标识一个类的对象可以被序列化。
序列化的概念:
- 序列化是指将对象的状态转换为字节流的过程,以便将其保存到文件、数据库或通过网络传输。
- 反序列化则是将字节流重新转换为对象的过程。
Serializable 的作用:
- 当一个类实现了 Serializable 接口时,表示该类的对象可以被序列化。
- JVM 会根据这个接口的存在与否,决定是否允许对该类的对象进行序列化操作。
public interface Serializable { }
2、默认序列化行为:
如果类实现了 Serializable 接口,JVM 会按照默认规则序列化该类的所有非静态、非 transient 字段。
- 静态字段属于类本身,不会被序列化。
- 使用 transient 关键字修饰的字段会被忽略,不会参与序列化。
3、serialVersionUID 的作用
serialVersionUID 是与 Serializable 接口密切相关的一个字段。它是一个 long 类型的值,用于标识类的版本号。在反序列化过程中,JVM 会检查类的 serialVersionUID 是否与字节流中的版本号一致:如果一致,则允许反序列化。如果不一致,则抛出 InvalidClassException 异常。默认情况下,如果开发者没有显式声明 serialVersionUID,JVM 会根据类的结构生成一个默认的 serialVersionUID 值(通常是基于类名、接口、方法和字段等信息计算出的 64 位哈希值)。
默认值 vs 显式值
3.1、默认值 (64 位哈希):
如果未显式定义 serialVersionUID,Java 会根据类的元数据(如类名、方法签名、字段等)自动生成一个唯一的哈希值。
这个值是动态生成的,当类的结构发生变化时(例如添加或删除字段、修改方法签名等),生成的 serialVersionUID 可能会发生变化。从而引发 InvalidClassException 异常。
3.2、显式值 (通常为 1L):
开发者可以手动指定 serialVersionUID 的值,最常见的做法是将其设置为 1L。手动指定后,无论类的结构如何变化,serialVersionUID 都保持不变,除非开发者主动修改该值。序列化和反序列化仍然可以正常工作。
4、使用场景
- 文件存储:将对象状态保存到文件中,以便后续恢复。
- 网络传输:将对象通过网络发送到远程系统。
- 缓存:将对象序列化后存储在内存或磁盘中,以提高性能。
5、注意事项
未实现 Serializable 的后果:如果尝试对未实现 Serializable 接口的类的对象进行序列化,会抛出 NotSerializableException 异常。
父类字段的序列化:如果父类没有实现 Serializable 接口,那么父类的字段不会被序列化。如果父类实现了 Serializable,则其所有非静态、非 transient 字段也会被序列化。
构造器调用:反序列化时不会调用类的构造器,而是直接创建对象实例并恢复字段值。
6、Serializable 序列化测试
3.1、新建序列化测试类
/**
* @Author dw
* @ClassName SerializableTest
* @Description
* @Date 2023/1/2 15:08
* @Version 1.0
*/
public class SerializableTest {
public static void main(String[] args) {
// 初始化
User user = new User();
user.setName("王二");
user.setAge(18);
System.out.println(user);
// 把对象写到文件中
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.txt"))){
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
}
// 从文件中读出对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("user.txt")))){
User userRead = (User) ois.readObject();
System.out.println(userRead);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
public static class User {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
}
由于 User没有实现 Serializable 接口,所以系统会报错。运行后报错如下:
顺着堆栈信息,我们来看一下 ObjectOutputStream 的 writeObject0 () 方法。其部分源码如下:
这段代码的意思是,ObjectOutPutStream 在序列化的时候,会判断对象的类型,如果不是字符串、数组、枚举、Serializable 的实例,会抛出 NotSerializableException。但是,如果 SClass 实现了 Serializable 接口的话,就可以被序列化和反序列化了。
3.2、修改User类实现Serialiable接口。
public static class User implements Serializable{
private static final long serialVersionUID = -9085952353374185653L;
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
再次测试, 序列化成功。
3.3、反序列化测试:修改User类中的序列化id, 测试能否序列化成功
public static class User implements Serializable{
private static final long serialVersionUID = -9085952353374185654L;
...
}
提示如下错误:
com.dw.study.test.SerializableTest$User@5f184fc6
java.io.InvalidClassException: com.dw.study.test.SerializableTest$User;
local class incompatible: stream classdesc serialVersionUID = -9085952353374185654, local class serialVersionUID = -9085952353374185655
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at com.dw.study.test.SerializableTest.main(SerializableTest.java:31)
serialVersionUID
被称为序列化 ID,它是决定 Java 对象能否反序列化成功的重要因子。在反序列化时,Java 虚拟机会把字节流中的 serialVersionUID
与被序列化类中的 serialVersionUID
进行比较,如果相同则可以进行反序列化,否则就会抛出序列化版本不一致的异常。// 序列化前的对象名称
public static class User implements Serializable{}
// 序列化后的对象名称
public static class MyUser implements Serializable{}
反序列化结果:
提示User类未找到。
7、思考?在我们的springBoot项目中, 类没有实现Serializable接口为啥也可以在网络上传输?
在 Spring Boot 项目中,即使类没有实现 Serializable 接口,对象也可以在网络上传输,这是因为 Spring 框架使用了不同的机制来处理对象的传输,而不完全依赖于 Java 原生的序列化机制。以下是具体原因和相关机制:
Spring Boot 是基于 HTTP 协议构建的框架,对象在网络中的传输通常是通过 HTTP 请求和响应完成的。在这个过程中,Spring 使用的是JSON 或 XML 等格式来序列化和反序列化对象,而不是 Java 原生的二进制序列化。
JSON 序列化:
- Spring 默认使用 Jackson(或其他类似的库)将 Java 对象转换为 JSON 格式。在 Spring MVC 中,对象的序列化和反序列化是通过 HttpMessageConverter 实现的。
- 这种方式不需要类实现 Serializable 接口,因为 Jackson 只需要访问对象的字段或 getter 方法即可完成序列化。
XML 序列化:
- 如果使用 XML 格式,Spring 可能会借助其他库(如 JAXB)将对象转换为 XML。
- 同样,这种方式也不需要类实现 Serializable 接口。
远程调用(如 RESTful API)
在 Spring Boot 中,远程调用通常通过 RESTful API 实现,而不是传统的 RMI(Remote Method Invocation)。RESTful API 使用 HTTP 协议传输数据,因此对象的序列化和反序列化依赖于 JSON 或 XML,而不是 Java 原生的序列化机制。
7.2、Java 原生序列化的场景
尽管 Spring Boot 不依赖于 Serializable 接口进行网络传输,但在以下场景中,仍然需要类实现 Serializable:
Session 共享:
如果使用分布式 Session(如 Redis 存储 Session),对象可能需要被序列化为二进制形式存储。
消息队列:
在某些消息队列(如 ActiveMQ)中,如果使用 Java 原生序列化机制传递对象,则需要类实现 Serializable。
RMI (Remote Method Invocation) 等远程调用:
如果使用 RMI 或 Hessian 等技术进行远程调用,对象需要实现 Serializable。