面试题:Java序列化与反序列化

作者:小牛呼噜噜 | https://xiaoniuhululu.com
计算机内功、JAVA底层、面试相关资料等更多精彩文章在公众号「小牛呼噜噜 」

序列化和反序列化的概念

当我们在Java中创建对象的时候,对象会一直存在,直到程序终止时。但有时候可能存在一种”持久化”场景:我们需要让对象能够在程序不运行的情况下,仍能存在并保存其信息。当程序再次运行时 还可以通过该对象的保存下来的信息 来重建该对象。序列化和反序列化 就应运而生了, 序列化机制可以使对象可以脱离程序的运行而独立存在。

  • 序列化: 将对象转换成二进制字节流的过程
  • 反序列化:从二进制字节流中恢复对象的过程

应用场景?

  1. 对象在进行 网络传输的时候,需要先被序列化,接收到序列化的对象之后需要再进行反序列化;比如远程方法调用 RPC
  2. 将对象存储到 文件中的时候需要进行序列化,将对象从文件中读取出来需要进行反序列化。
  3. 将对象存储到 内存中,需要进行序列化,将对象从内存中读取出来需要进行反序列化。
  4. 将对象存储到 数据库(如 Redis)时,需要用到序列化,将对象从缓存数据库中读取出来需要反序列化。

面试题:Java序列化与反序列化

序列化实现的方式

如果使用Jdk自带的序列化方式实现对象序列化的话,那么这个类应该实现Serializable接口或者Externalizable接口

继承Serializable接口,普通序列化

首先我们定义一个对象类User

public class User implements Serializable {
    //序列化ID
    private static final long serialVersionUID = 1L;
    private int age;
    private String name;

    public User(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

然后我们编写一下测试类:

public class serTest {
    public static void main(String[] args) throws Exception, IOException {
        SerializeUser();
        DeSerializeUser();
    }

    /**
     * 序列化方法
     * @throws IOException
     */
    private static void SerializeUser() throws  IOException {
        User user = new User(11, "小张");

        //序列化对象到指定的文件中
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\jun\\Desktop\\example"));
        oos.writeObject(user);
        oos.close();
        System.out.println("序列化对象成功");
    }

    /**
     * 反序列化方法
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private static void DeSerializeUser() throws  IOException, ClassNotFoundException {
        //读取指定的文件
        File file = new File("C:\\Users\\jun\\Desktop\\example");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        User newUser = (User)ois.readObject();
        System.out.println("反序列化对象成功:"+ newUser.getName()+ ","+newUser.getAge());
    }
}

结果:

序列化对象成功
反序列化对象成功:小张,11

一个对象想要被序列化,那么它的类就要 继承Serializable接口或者 它的子接口

继承Serializable接口类的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。如果不想序列化的字段可以使用 transient关键字修饰

private int age;
private String name;
private transient password;//属性:密码,不想被序列化

我们需要注意的是:使用 transient关键字阻止序列化虽然简单方便,但被它修饰的属性被 完全隔离在序列化机制之外,这必然会导致了在反序列化时无法获取该属性的值。
其实我们完全可以在通过在需要序列化的对象的Java类里加入 writeObject()方法readObject()方法来控制如何序列化各属性,某些属性是否被序列化

如果User有一个属性是引用类型的呢?比如User其中有一个属性是类Person:

private Person person;

那如果要想User可以序列化,那Person类也必须得继承 Serializable接口,不然程序会报错

另外大家应该注意到 serialVersionUID了吧,在日常开发的过程中,经常遇到,暂且放放,我们后文再详细讲解

继承Externalizable接口,强制自定义序列化

对于Externalizable接口,我们需要知道以下几点:

  1. Externalizable继承自Serializable接口
  2. 需要我们重写writeExternal()与readExternal()方法, 这是强制性的
  3. 实现Externalizable接口的类必须要提供一个public的无参的构造器,因为 反序列化的时候需要反射创建对象
  4. Externalizable接口实现序列化,性能稍微比继承自Serializable接口好一点

首先我们定义一个对象类ExUser

public class ExUser implements Externalizable {
    private int age;
    private String name;

    //注意,必须加上pulic 无参构造器
    public ExUser() {
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeInt(age);
    }
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.name = (String)in.readObject();
        this.age = in.readInt();
    }
}

我们接着编写测试类:

public class serTest2 {
    public static void main(String[] args) throws Exception, IOException {
        SerializeUser();
        DeSerializeUser();
    }

    /**
     * 序列化方法
     * @throws IOException
     */
    private static void SerializeUser() throws  IOException {
        ExUser user = new ExUser();
        user.setAge(10);
        user.setName("小王");

        //序列化对象到指定的文件中
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\jun\\Desktop\\example"));
        oos.writeObject(user);
        oos.close();
        System.out.println("序列化对象成功");
    }

    /**
     * 反序列化方法
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private static void DeSerializeUser() throws  IOException, ClassNotFoundException {
        File file = new File("C:\\Users\\jun\\Desktop\\example");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        ExUser newUser = (ExUser)ois.readObject();
        System.out.println("反序列化对象成功:"+ newUser.getName()+ ","+newUser.getAge());
    }
}

结果:

序列化对象成功
反序列化对象成功:小王,10

因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性, transient关键字在这里是无效的。

Externalizable对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为 private、默认或 protected级别,会抛出 java.io.InvalidException: no valid constructor异常,因此 Externalizable对象必须有默认构造函数,而且必需是public的。

serialVersionUID的作用

如果反序列化使用的 serialVersionUID与序列化时使用的 serialVersionUID 不一致,会报 InvalidCalssException异常。这样就保证了项目迭代升级前后的兼容性
serialVersionUID是序列化前后的唯一标识符,只要版本号serialVersionUID相同,即使更改了序列化属性,对象也可以正确被反序列化回来。
默认如果没有人为显式定义过serialVersionUID,那编译器会为它自动声明一个!

serialVersionUID有两种显式的生成方式:

  1. 默认的1L,比如: private static final long serialVersionUID = 1L;
  2. 根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:

private static final long serialVersionUID = xxxxL;

静态变量不会被序列化

凡是被static修饰的字段是不会被序列化的,我们来看一个例子:

//实体类
public class Student implements Serializable {
    private String name;
    public static Integer age;//静态变量

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public static Integer getAge() {
        return age;
    }

    public static void setAge(Integer age) {
        Student.age = age;
    }
}

//测试类
public class shallowCopyTest {

    public static void main(String[] args) throws Exception {
        Student student1 = new Student();
        student1.age = 11;

        //序列化,将数据写入指定的文件中
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\student1"));
        oos.writeObject(student1);
        oos.close();

        Student student2 = new Student();
        student2.age = 21;

        //序列化,将数据写入指定的文件中
        ObjectOutputStream oos2 = new ObjectOutputStream(new FileOutputStream("D:\\student2"));
        oos2.writeObject(student1);
        oos2.close();

        //读取指定的文件
        File file = new File("D:\\student1");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        Student student1_new = (Student)ois.readObject();
        System.out.println("反序列化对象,student1.age="+ student1_new.getAge());

        //读取指定的文件
        File file2 = new File("D:\\student1");
        ObjectInputStream ois2 = new ObjectInputStream(new FileInputStream(file2));
        Student student2_new = (Student)ois2.readObject();
        System.out.println("反序列化对象,student2.age="+ student2_new.getAge());

    }

}

结果:

反序列化对象,student1.age=21
反序列化对象,student2.age=21

为啥结果都是 21
我们知道对象的序列化是操作的 堆内存中的数据,而静态的变量又称作类变量,其数据存放在 方法区里,类一加载,就初始化了。
又因为静态变量 age没有被序列化,根本就没写入文件流中,所以我们打印的值其实一直都是当前Student类的静态变量 age的值,而静态变量又是所有的对象共享的一个变量,所以就都是 21

使用序列化实现深拷贝

我们再来看一个例子:

//实体类 继承Cloneable
public class Person implements Serializable{
    public String name;//姓名
    public int height;//身高
    public StringBuilder something;

...//省略 getter setter

    public Object deepClone() throws Exception{
        // 序列化
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);

        oos.writeObject(this);

        // 反序列化
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);

        return ois.readObject();
    }

}

//测试类,这边类名笔者就不换了,在之前的基础上改改
public class shallowCopyTest {

    public static void main(String[] args) throws Exception {
        Person p1 = new Person("小张", 180, new StringBuilder("今天天气很好"));
        Person p2 = (Person)p1.deepClone();

        System.out.println("对象是否相等:"+ (p1 == p2));
        System.out.println("p1 属性值=" + p1.getName()+ ","+ p1.getHeight() + ","+ p1.getSomething());
        System.out.println("p2 属性值=" + p2.getName()+ ","+ p2.getHeight() + ","+ p2.getSomething());

        // change
        p1.setName("小王");
        p1.setHeight(200);
        p1.getSomething().append(",适合出去玩");
        System.out.println("...after p1 change....");

        System.out.println("p1 属性值=" + p1.getName()+ ","+ p1.getHeight() + ","+ p1.getSomething());
        System.out.println("p2 属性值=" + p2.getName()+ ","+ p2.getHeight() + ","+ p2.getSomething());

    }
}

结果:

对象是否相等:false
p1 属性值=小张,180,今天天气很好
p2 属性值=小张,180,今天天气很好
…after p1 change….

p1 属性值=小王,200,今天天气很好,适合出去玩
p2 属性值=小张,180,今天天气很好

详情见:https://mp.weixin.qq.com/s/M4–Btn24NIggq8UBdWvAw

常见序列化协议对比

除了JDK 自带的序列化方式,还有一些其他常见的序列化协议:

  1. 基于二进制: hessian、kyro、protostuff
  2. 文本类序列化方式: JSON 和 XML

采用哪种序列化方式,我们一般需要考虑序列化之后的数据大小,序列化的耗时,是否支持跨平台、语言,或者公司团队的技术积累。这边就不展开讲了,大家感兴趣自行去了解

小结

  1. JDK自带序列化方法一般有2种: 继承Serializable接口继承Externalizable接口
  2. static修饰的类变量、transient修饰的实例变量都不会被序列化。
  3. 序列化对象的引用类型成员变量,也必须是可序列化的
  4. serialVersionUID 版本号是序列化和反序列化前后唯一标识,建议显式定义
  5. 序列化和反序列化的过程其实是有漏洞的,因为从序列化到反序列化是有中间过程的,如果被别人拿到了中间字节流,然后加以伪造或者篡改,反序列化出来的对象会有一定风险。可以重写readObject()方法,加以限制
  6. 除了JDK自带序列化方法,还有hessian、kyro、protostuff、 JSON 和 XML等

参考资料:
《On Java 8》
https://tech.meituan.com/2015/02/26/serialization-vs-deserialization.html
https://www.zhihu.com/question/26475281/answer/1898221893

本篇文章到这里就结束啦,很感谢你能看到最后,如果觉得文章对你有帮助,别忘记关注我!更多精彩的文章

面试题:Java序列化与反序列化

Original: https://www.cnblogs.com/xiaoniuhululu/p/16626652.html
Author: 小牛呼噜噜
Title: 面试题:Java序列化与反序列化

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/577854/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Docker 搭建 Nexus3 私服 | 基本操作

    1 Docker 安装 Nexus3 1.1 创建目录 在硬盘上创建 Nexus3 的主目录: mkdir -p /Users/yygnb/dockerMe/nexus3 为该目录…

    Linux 2023年6月7日
    073
  • 【Java8新特性】- 接口中默认方法修饰为普通方法

    Java8新特性 – 接口中默认方法修饰为普通方法 😄生命不息,写作不止🔥 继续踏上学习之路,学之分享笔记👊 总有一天我也能像各位大佬一样🏆 一个有梦有戏的人 @怒放吧…

    Linux 2023年6月6日
    097
  • python小技巧

    关于 ipython 1 Tab补全 从外观上,IPython shell和标准的Python解释器只是看起来不同。IPython shell的 进步之一是具备其它IDE和交互计算…

    Linux 2023年6月8日
    088
  • 磁盘操作指令 dd

    dd if=/home/thotf/PaperOS/boot/mbr.bin of=/home/thotf/bochs/hd60M.img bs=512 count=1 conv=…

    Linux 2023年6月7日
    0102
  • linux常用命令

    linux常用目录 /bin :bin是Binary的缩写,这个目录存放着最经常使用的命令。 /ect :这个目录用来存放所有的系统所需要的配置文件和子目录。 /home:用户的主…

    Linux 2023年6月13日
    094
  • 特殊进制

    //0xaaaaaaaa = 10101010101010101010101010101010 (偶数位为1,奇数位为0) //0x55555555 = 1010101010101…

    Linux 2023年6月13日
    090
  • 一文让你明白Redis持久化

    网上虽然已经有很多类似的介绍了,但我还是自己总结归纳了一下,自认为内容和细节都是比较齐全的。 文章篇幅有 4k 多字,货有点干,断断续续写了好几天,希望对大家有帮助。不出意外地话,…

    Linux 2023年5月28日
    079
  • shell echo单行和多行文字定向写入到文件中

    单行文本: #!/bin/bash echo "192.168.85.24 tsedb">> /etc/hosts 多行文本: < #!/bi…

    Linux 2023年5月28日
    073
  • 什么是视频编码?编解码器和压缩技术

    想知道什么是视频编码,为什么它很重要? 在本文中,我们将研究编码、编解码器和压缩技术的过程。这包括什么使得一个推荐的编解码器,虽然是取决于情况。它还涵盖了为什么某些伪影,与压缩有关…

    Linux 2023年6月7日
    0107
  • C++ inline

    inline的坏处:若在一台内存有限的机器上,过度热衷inlining会造成程序体积太大,即使拥有虚拟内存,inline造成的代码膨胀也会导致额外的换页行为,降低指令高速缓存装置的…

    Linux 2023年6月7日
    093
  • 【计算题】考研数据结构计算题型整理

    题型1:递归程序,一般使用公式进行递推 int fact(int n){ if(n 本题是求阶乘的递归代码,即n * (n-1) * …. * 1。每次递归调用 fac…

    Linux 2023年6月13日
    093
  • Spring cloud gateway 如何在路由时进行负载均衡

    本文为博主原创,转载请注明出处: 1.spring cloud gateway 配置路由 在网关模块的配置文件中配置路由: 其中lb表示采用了负载均衡,user-server表示服…

    Linux 2023年6月14日
    084
  • 【Example】C++ STL 常用容器概述

    前排提醒: 由于 Microsoft Docs 全是机翻。所以本文表格是我人脑补翻+审校。 如果有纰漏、模糊及时反馈。 了解每一种容器的特性、知道什么情况下用什么容器就可以。 序列…

    Linux 2023年6月13日
    077
  • Python 定义类时候加括号和不加括号的区别

    新式类与经典类 只有python2.x 中有新式类和经典类的说法,而python3.x 没有,因为其默认都是新式类 python2.x 中默认都是经典类,只有显式的继承了objec…

    Linux 2023年6月7日
    066
  • [云计算]OpenStack这一篇就够了!

    OpenStack简介 OpenStack背景介绍 OpenStack应用场景 OpenStack发展历程 OpenStack架构 架构设计原则 架构全景图 核心服务组件 系统通信…

    Linux 2023年6月13日
    0192
  • Git 代码提交和下载

    1、新建一个目录,存放下载下来的项目; 2、进入刚刚新建的文件夹,点击鼠标右键,选择”Git Bash Here” 3、进行基础配置,作为 Git 的基础配…

    Linux 2023年6月13日
    0108
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球