Serializable 接口解读

一、Serializable 接口概述
  • Serializablejava.io 包中定义的、用于实现 Java 类的序列化和反序列化操作而提供的一个语义级别的接口,告诉 JVM 此类可被序列化,可被默认的序列化机制序列化

  • 序列化是指将对象转换为字节序列的过程,我们称之为对象的序列化,就是将内存中的这些对象变成一连串的字节描述过程

  • 反序列化就是将持久化的字节文件数据恢复为指定对象的过程

  • Serializable 序列化接口没有任何方法或者字段,是一个空类,只是用于标志可序列化的语义,只要实现了 Serializable 接口的类都可以被序列化和反序列化

    // 源码如下
    package java.io;
    public interface Serializable {
    }
    
  • 实现了 Serializable 接口的类可以被 ObjectOutputStream 转换为字节流,同时也可以通过 ObjectInputStream 再将字节流解析为指定得对象

二、Serializable 使用场景

  无论什么编程语言,其底层涉及到 IO 操作的部分都是交由操作系统帮其完成的,而底层 IO 操作都是以字节流的方式进行的,所以写操作都涉及将编程语言数据类型转换为字节流,而读操作则又涉及将字节流转换为相应编程语言的特定数据类型。Java 作为一门面向对象的编程语言,对象作为其主要数据的类型载体,为了完成对象数据类型的读写操作,也就需要一种方式来让 JVM 知道在进行 IO 操作的时候如何将对象数据转换为字节流,以及如何将字节流转换为特定对象时,匹配到正确的数据类型,而 Serializable 接口就承担了这样一个角色,告诉负责完成这些操作的 JVM 保证 Java 对象序列化和反序列化的时候,对象数据结构的准确性。

  • 将内存中的对象写入到磁盘的场景

    比如内存不够用的时候,计算机就要将内存中的部分对象暂时保存到硬盘中,等到需要再次使用的时候,再将写入到硬盘文件中的字节流反序列化出来

  • 采用 Socket 将对象进行网络传输的场景

    在进行 JavaSocket 编程时,有时候需要将某一类对象传输到访问服务器,那么被传输的类就需要实现 Serializable 接口,可以将数据转换为二进制来传输

  • 通过 RMI 传输对象的场景

    如果要通过远程方法调用(RMI)去调用一个远程对象的方法,如在计算机 A 中调用另一台计算机 B 对象的方法,那么你需要通过 JNDI 服务获取计算机 B 目标对象的引用,将对象从 B 传到 A ,就需要实现序列化接口。

  • Serializable 接口就是 Java 提供用于进行高效率的异地共享实例对象的机制,实现这个接口并定义 serialVersionUID 即可

三、Serializable 示例演示
  1. 创建演示对象 User 并实现 Serializable 接口

    public class User implements Serializable {
        private static final long serialVersionUID = 1L;
    
        private int id;
    
        private int userId;
    
        private String userName;
    }    
    
  2. 编写测试类将 User 对象序列化到磁盘文件

    private static final String FILEPATH = "/Users/rambo/Desktop/Temp/user.txt";
    
    @Test
    public void serialObjectToFile() throws IOException {
        User user = new User(1, 123, "Rambo");
    
        // 1. 创建文件输出流
        FileOutputStream fileOutputStream = new FileOutputStream(FILEPATH);
        // 2. 创建对象输出流
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        // 3. 将 User 对象写入到输出流中并保存到文件
        objectOutputStream.writeObject(user);
        // 4. 关闭输对象输出流
        objectOutputStream.close();
        // 5. 控制台提示信息
        System.out.println("成功序列化对象到磁盘文件");
    }
    

    P.S

    通过以上方式得到的序列化后的二进制流保存在 txt 文本中的内容如下

    ’sr%com.rambo.springbootsample.model.UserIidIuserIdLuserNametLjava/lang/String;xp{tRambo
    
  3. User 对象中的 Serializable 实现代码片段去掉,再次执行序列化的测试类

    java.io.NotSerializableException: com.rambo.springbootsample.model.User
    
    	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
    	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
    	at com.montnets.model.UserTest.serialObjectToFile(UserTest.java:29)
    	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    	at java.lang.reflect.Method.invoke(Method.java:498)
    	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
    	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
    	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
    	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
    	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
    	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
    	at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
    	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
    	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
    	at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
    	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
    	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    	at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
    	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
    	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
    

    P.S

    也就是说没有实现 Serializable 接口的对象是无法通过 IO 操作序列化的

  4. User 类再次实现序列化,并执行序列化到磁盘后,编写反序列化测试类并执行

    @Test
    public void unSerialFileToObject() throws IOException, ClassNotFoundException {
        // 1. 创建文件输入流
        FileInputStream fileInputStream = new FileInputStream(FILEPATH);
        // 2. 创建对象输入流
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        // 3. 将输入流中的数据转换为 User 对象
        User user = (User) objectInputStream.readObject();
        // 4. 打印 User 对象校验反序列化结果
        System.out.println("反序列化磁盘文件转为对象成功:\n" + user.toString());
    }
    

    P.S

    通过反序列化操作,可以再次将序列化到对象字节流通过 IO 转换为 Java 对象,结果如下

    反序列化磁盘文件转为对象成功:
    User(id=1, userId=123, userName=Rambo)
    
  5. 再次将 User 对象中的 Serializable 实现代码片段去掉,再次执行反序列化的测试类

    java.io.InvalidClassException: com.rambo.springbootsample.model.User; class invalid for deserialization
    
    	at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:169)
    	at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:874)
    	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2097)
    	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1624)
    	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:464)
    	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
    	at com.montnets.model.UserTest.unSerialFileToObject(UserTest.java:43)
    	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    	at java.lang.reflect.Method.invoke(Method.java:498)
    	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
    	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
    	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
    	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
    	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
    	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
    	at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
    	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
    	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
    	at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
    	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
    	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    	at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
    	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
    	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
    

    P.S
    提示无效类型异常,说明在 Java 中如要实现对象到 IO 读写操作,都必须要实现 Serializable 接口

四、关于 SerialVersionUID
  1. 实验演示

    • 紧接着上述到示例,我们将 User 对象中的 serialVersionUID = 1L 修改为 serialVersionUID = 2L,再次执行反序列化的测试类,效果如下

      java.io.InvalidClassException: com.rambo.springbootsample.model.User; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2
      
      	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
      	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1939)
      	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1805)
      	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2096)
      	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1624)
      	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:464)
      	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
      	at com.montnets.model.UserTest.unSerialFileToObject(UserTest.java:43)
      	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
      	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
      	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
      	at java.lang.reflect.Method.invoke(Method.java:498)
      	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
      	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
      	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
      	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
      	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
      	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
      	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
      	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
      	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
      	at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
      	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
      	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
      	at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
      	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
      	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
      	at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
      	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
      	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
      	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
      	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
      	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
      
    • 通过实验结果可以看到,serialVersionUID 的值其实是一个标记,在进行序列化和反序列化的时候,让 JVM 能识别采用哪个类来接收此实例

  2. 结果分析

      对于 JVM 来说,要进行持久化的类必须要有一个标记,只有持有这个标记, JVM 才允许类创建的对象可以通过其 IO 系统转换为字节数据,从而实现序列化后的持久化,而这个标记就是 serialVersionUID 属性。而在反序列化的过程中则需要使用 serialVersionUID 来确定由哪个类来加载这个对象,所以我们在实现 Serializable 接口的时候,一般还要尽量显示地定义 serialVersionUID

    private static final long serialVersionUID = 1L;
    
    • 在反序列化的过程中,如果接收方为对象加载了一个类,如果该对象的 serialVersionUID 与对应持久化时类的 serialVersionUID 不同,那么反序列化到过程中将会导致 InvalidClassException 异常

    • 如果我们在序列化对象之前,没有对该对象显示的声明 serialVersionUID ,则序列化进行时 JVM 将会根据该类的各个方面计算该类默认的 serialVersionUID 值。但是 Java 官方强烈建议所有要序列化的类都显示到声明 serialVersionUID 字段,因为如果高度依赖于 JVM 默认生成 serialVersionUID,可能会导致其与编译器的实现细节耦合,这样可能导致在反序列化的过程中发生意外的 InvalidClassException 异常

    • 声明 serialVersionUID 字段尽可能采用 private 关键字来修饰,因为该字段的声明只适用于声明的类,该字段作为成员变量被子类继承并没有什么用处。有个特别到地方需要注意:数组类是不需要显示地声明 serivalVersionUID 字段的。因为它们始终具有默认计算值,不过数组类反序列化也是放弃了匹配 serialVersionUID 值的要求

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值