Serializable接口的作用

 
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 进行比较,如果相同则可以进行反序列化,否则就会抛出序列化版本不一致的异常。
 
3.4、反序列化测试序列化前后的对象名称不一致。
// 序列化前的对象名称
 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。

 

 

 

 

 

posted @ 2023-01-02 16:13  邓维-java  阅读(5151)  评论(0)    收藏  举报