阅读笔记-为什么Java序列化要实现接口

什么是序列化和反序列化?

序列化:把对象转换为字节序列的过程称为对象的序列化,可以简单理解为:Object->byte[]
反序列化:把字节序列恢复为对象的过程称为对象的反序列化,可以简单理解为:byte[]->Object

字节序列:也被称为是二进制序列。

序列化文件格式:
在这里插入图片描述
AC ED被称为魔幻数字,是序列化文件都会有的开头数字
(可以通过Notepad++安装Hex View插件来看)

什么时候需要用到序列化和反序列化呢?

当我们只在本地JVM里运行下Java实例, 这个时候是不需要什么序列化和反序列化的, 但是对于以下场景:

  1. 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
  2. 在网络上传送对象,或者说跟浏览器交互
  3. 实现RPC

这个时候就需要序列化和反序列化了,序列化机制使得对象可以脱离程序的运行而独立存在

前两个需要用到序列化和反序列化的场景, 是不是让我们有一个很大的疑问? 我们在与浏览器交互时, 还有将内存中的对象持久化到数据库中时, 好像都没有去进行序列化和反序列化, 因为我们都没有实现Serializable接口, 但一直正常运行.

下面先给出结论:
只要我们对内存中的对象进行持久化或网络传输, 这个时候都需要序列化和反序列化.

理由:
服务器与浏览器交互时真的没有用到Serializable接口吗? JSON格式实际上就是将一个对象转化为字符串, 所以服务器与浏览器交互时的数据格式其实是字符串, 我们来看来String类型的源码:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;
    ......
}

实际上我们平时开发的接口,通过@ResponseBody返回给前端时会先把对象进行JSON化,得到一个字符串,字符串本身已经实现了序列化接口,对于我们来说就不需要主动实现序列化接口了。
而@RequestBody接收请求也是先是对JSON字符串操作,再转化为Java中的对象,几乎对序列化无感知。

然后我们再来看对象持久化到数据库中时的情况, Mybatis数据库映射文件里的insert代码:

<insert id="insertUser" parameterType="org.tyshawn.bean.User">  
    INSERT INTO t_user(name, age) VALUES (#{name}, #{age})  
</insert>  

实际上我们并不是将整个对象持久化到数据库中, 而是将对象中的属性持久化到数据库中, 而这些属性都是实现了Serializable接口的基本属性.

现序列化和反序列化为什么要实现Serializable接口?

在Java中实现了Serializable接口后, JVM才会在底层帮我们实现序列化和反序列化, 如果我们不实现Serializable接口, 那自己去写一套序列化和反序列化代码也行, 至于具体怎么写, Google一下你就知道了.
PS:稍微Google了一下,序列化的流程真的很长,不建议自己写一套

如果不实现Serializable接口,是没有办法使用Java提供的序列化程序的,会报错:java.io.NotSerializableException
在这里插入图片描述

为什么还要显示指定serialVersionUID的值?

如果不显示指定serialVersionUID, JVM在序列化时会根据属性自动生成一个serialVersionUID, 然后与属性一起序列化, 再进行持久化或网络传输. 在反序列化时, JVM会再根据属性自动生成一个新版serialVersionUID, 然后将这个新版serialVersionUID与序列化时生成的旧版serialVersionUID进行比较, 如果相同则反序列化成功, 否则报错.

如果显示指定了serialVersionUID, JVM在序列化和反序列化时仍然都会生成一个serialVersionUID, 但值为我们显示指定的值, 这样在反序列化时新旧版本的serialVersionUID就一致了.

在实际开发中, 不显示指定serialVersionUID的情况会导致什么问题? 如果我们的类写完后不再修改或者仅修改transient关键字修饰的属性(下面transient例子证明), 那当然不会有问题, 但这在实际开发中是不可能的, 我们的类会不断迭代, 一旦类被修改了, 那旧对象反序列化就会报错. 所以在实际开发中, 我们都会显示指定一个serialVersionUID, 值是多少无所谓, 只要不变就行.

简单来说,这个serialVersionUID就是证明序列化前后的类是同一个版本的类。

这里我演示一下没有指定serialVersionUID的值 的后果:
测试实体类:

@Data
public class TestSerialize implements Serializable {
    
    /**
     * 书本id
     */
    private Integer id;

    /**
     * 书本名字
     */
    private String name;

    /**
     * 书本权重
     */
    private Integer weight;

    /**
     * 书本分类
     */
    private String classifyName;

    private LocalDateTime ctime;

    private LocalDateTime mtime;

    private static Integer year = 2021;

    public static Integer getYear() {
        return year;
    }

    public static void setYear(Integer year) {
        TestSerialize.year = year;
    }
}

序列化方法:

 public void testSerialize() {
        TestSerialize testSerialize = new TestSerialize();
        testSerialize.setClassifyName("AA");
        testSerialize.setName("京东派");
        testSerialize.setWeight(3);
        testSerialize.setId(1);
        testSerialize.setCtime(LocalDateTime.now());
        testSerialize.setMtime(LocalDateTime.now());
        //这里我设置了静态成员year=2022,如果序列化是包含静态成员,那么反序列化时就应该也是2022
        TestSerialize.setYear(2022);
        //try...resource语法可以在try块结束后自动调用close方法
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:" + File.pathSeparator + "111.txt")))) {
            log.info("序列化对象:{}", testSerialize.toString());
            log.info("序列化对象静态成员year:{}", TestSerialize.getYear());
            oos.writeObject(testSerialize);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            log.error("序列化错误:", e);
        } catch (IOException e) {
            e.printStackTrace();
            log.error("序列化错误:", e);
        }
    }

反序列化方法:
注:这里为了完美测试,我是重启了应用后然后再执行反序列化方法

public void testDeserialize() {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:" + File.pathSeparator + "111.txt")))) {
            TestSerialize testSerialize = (TestSerialize) ois.readObject();
            log.info("反序列化对象:{}", testSerialize.toString());
            log.info("反序列化对象静态成员year:{}", TestSerialize.getYear());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            log.error("反序列化错误:", e);
        } catch (IOException e) {
            e.printStackTrace();
            log.error("反序列化错误:", e);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            log.error("反序列化错误:", e);
        }
    }

OK,现在准备好了,然后先进行序列化过程:

序列化对象:TestSerialize(id=1, name=京东派, weight=3, classifyName=AA, ctime=2022-10-08T14:22:24.649, mtime=2022-10-08T14:22:24.649)
序列化对象静态成员year:2022

序列化正常,然后在实体类新增一个age字段:

@Data
public class TestSerialize implements Serializable {
	......
    private Integer age;
    ......

}

重启后进行反序列化:
出现报错:java.io.InvalidClassException: com.zby.model.dto.TestSerialize; local class incompatible: stream classdesc serialVersionUID = 1784859443472419136, local class serialVersionUID = 1986709366121845600
在这里插入图片描述

Java序列化的其他特性

使用transient关键字修饰隔离不序列化的字段

先说结论, 被transient关键字修饰的属性不会被序列化, static属性也不会被序列化(static没法序列化的demo在下面有)

测试程序:
实体类:增加一个transient关键字修饰的字段age

@Data
public class TestSerialize implements Serializable {

    /**
     * 书本id
     */
    private Integer id;

    /**
     * 书本名字
     */
    private String name;

    /**
     * 书本权重
     */
    private Integer weight;

    /**
     * 书本分类
     */
    private String classifyName;

    private LocalDateTime ctime;

    private LocalDateTime mtime;

    private transient Integer age;

    private static Integer year = 2021;

    public static Integer getYear() {
        return year;
    }

    public static void setYear(Integer year) {
        TestSerialize.year = year;
    }
}

测试程序:

	//序列化方法
    public void testSerialize() {
        TestSerialize testSerialize = new TestSerialize();
        testSerialize.setClassifyName("AA");
        testSerialize.setName("京东派");
        testSerialize.setWeight(3);
        testSerialize.setId(1);
        testSerialize.setCtime(LocalDateTime.now());
        testSerialize.setMtime(LocalDateTime.now());
        testSerialize.setAge(18);
        TestSerialize.setYear(2022);
        //try...resource语法可以在try块结束后自动调用close方法
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:" + File.pathSeparator + "111.txt")))) {
            log.info("序列化对象:{}", testSerialize.toString());
            log.info("序列化对象静态成员year:{}", TestSerialize.getYear());
            oos.writeObject(testSerialize);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            log.error("序列化错误:", e);
        } catch (IOException e) {
            e.printStackTrace();
            log.error("序列化错误:", e);
        }
    }
    
	//反序列化方法
    public void testDeserialize() {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:" + File.pathSeparator + "111.txt")))) {
            TestSerialize testSerialize = (TestSerialize) ois.readObject();
            log.info("反序列化对象:{}", testSerialize.toString());
            log.info("反序列化对象静态成员year:{}", TestSerialize.getYear());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            log.error("反序列化错误:", e);
        } catch (IOException e) {
            e.printStackTrace();
            log.error("反序列化错误:", e);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            log.error("反序列化错误:", e);
        }
    }
}

结果:

序列化对象:TestSerialize(id=1, name=京东派, weight=3, classifyName=AA, ctime=2022-10-08T14:39:18.440, mtime=2022-10-08T14:39:18.440, age=18)
序列化对象静态成员year:2022
反序列化对象:TestSerialize(id=1, name=京东派, weight=3, classifyName=AA, ctime=2022-10-08T14:39:18.440, mtime=2022-10-08T14:39:18.440, age=null)
反序列化对象静态成员year:2022

最终age的反序列化结果是null,由此证明序列化文件并不包含age属性,因为age属性并没有赋值为18。

反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。

而且,这个例子也让我发现了有关序列化号的一个特性,改动transient关键字修饰的属性也无需提供serialVersionUID,反序列化时不报错。

自定义序列化流程

static属性不会被序列化

因为序列化是针对对象而言的, 而static属性优先于对象存在, 随着类的加载而加载, 所以不会被序列化.

看到这个结论, 是不是有人会问, serialVersionUID也被static修饰, 为什么serialVersionUID会被序列化? 其实serialVersionUID属性并没有被序列化, JVM在序列化对象时会自动生成一个serialVersionUID, 然后将我们显示指定的serialVersionUID属性值赋给自动生成的serialVersionUID.(这里我只是了解到serialVersionUID不会写入,感兴趣的可以深入)

测试程序:
实体类:

@Data
public class TestSerialize implements Serializable {
    /**
     * 书本id
     */
    private Integer id;

    /**
     * 书本名字
     */
    private String name;

    /**
     * 书本权重
     */
    private Integer weight;

    /**
     * 书本分类
     */
    private String classifyName;

    private LocalDateTime ctime;

    private LocalDateTime mtime;

    private static Integer year = 2021;

    public static Integer getYear() {
        return year;
    }

    public static void setYear(Integer year) {
        TestSerialize.year = year;
    }
}

序列化方法:

 public void testSerialize() {
        TestSerialize testSerialize = new TestSerialize();
        testSerialize.setClassifyName("AA");
        testSerialize.setName("京东派");
        testSerialize.setWeight(3);
        testSerialize.setId(1);
        testSerialize.setCtime(LocalDateTime.now());
        testSerialize.setMtime(LocalDateTime.now());
        //这里我设置了静态成员year=2022,如果序列化是包含静态成员,那么反序列化时就应该也是2022
        TestSerialize.setYear(2022);
        //try...resource语法可以在try块结束后自动调用close方法
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:" + File.pathSeparator + "111.txt")))) {
            log.info("序列化对象:{}", testSerialize.toString());
            log.info("序列化对象静态成员year:{}", TestSerialize.getYear());
            oos.writeObject(testSerialize);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            log.error("序列化错误:", e);
        } catch (IOException e) {
            e.printStackTrace();
            log.error("序列化错误:", e);
        }
    }

反序列化方法:
注:这里为了完美测试,我是重启了应用后然后再执行反序列化方法

public void testDeserialize() {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:" + File.pathSeparator + "111.txt")))) {
            TestSerialize testSerialize = (TestSerialize) ois.readObject();
            log.info("反序列化对象:{}", testSerialize.toString());
            log.info("反序列化对象静态成员year:{}", TestSerialize.getYear());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            log.error("反序列化错误:", e);
        } catch (IOException e) {
            e.printStackTrace();
            log.error("反序列化错误:", e);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            log.error("反序列化错误:", e);
        }
    }

结果输出:

序列化对象:TestSerialize(id=1, name=京东派, weight=3, classifyName=AA, ctime=2022-10-08T14:02:42.889, mtime=2022-10-08T14:02:42.889)
序列化对象静态成员year:2022
反序列化对象:TestSerialize(id=1, name=京东派, weight=3, classifyName=AA, ctime=2022-10-08T13:59:03.428, mtime=2022-10-08T13:59:03.428)
反序列化对象静态成员year:2021

由此可以证明序列化并不包含static成员

IDEA如何提示产生serialVersionUID?

Settings->搜索Inspections,勾选下面的选项。
在这里插入图片描述
然后移到类名处就会弹出选项为你自动生成serialVersionUID
在这里插入图片描述

参考

https://mp.weixin.qq.com/s/YKKwpQaqVsvK4xXow5kbvA

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值