• 关于面试被面试官暴怼:“几年研究生白读” 的前因后果


        中午一个网友来信说自己和面试官干起来了,看完他的描述真是苦笑不得,这年头是怎么了,最近互联网CS消息满天飞,怎么连面试官都SB起来了呢?

        大概是这样的:这位网友面试时被问及了Serializable接口的底层实现原理,因为这是一个标识性的空接口,大部分同学在学习时都秉持着会用就行(说实话,Build哥在这之前也没怎么细研究过,都是拿来就用),几乎不太去关注底层的东西,这位网友亦是如此,在这种情况下,自然回答的心虚,这下可被面试官抓住了把柄,一顿带有人身攻击的狂输出,让面试现场变成了撕B现场,具体可看聊天截图😂😂😂

    image

        基于这位网友的面试经历,Build哥又赶紧去重新学了一下Serializable关键字,以及它背后的实现,别到时候咱也被暴怼,下面咱们一起来重温一下。

    一、序列化与反序列化

    首先,我们先来了解一下两个概念 序列化反序列化

    • 序列化: 将Java对象转换为一个字节序列(包含对象的数据、对象的类型和对象中存储的属性等信息)的过程,以便于在网络上传输或者存储在文件中。
    • 反序列化: 是序列化的逆过程,将字节序列转为Java对象的过程。

    1.1 序列化与反序列化的应用场景

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

    序列化与发序列化的流转过程可参考下图:
    image

    有个问题,如果在我的对象中,有些变量并不想被序列化应该怎么办呢?

    答:不想被序列化的变量我们可以使用transientstatic关键字修饰;transient 关键字的作用是阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复;而static关键字修饰的变量并不属于对象本身,所以也同样不会被序列化!具体原因,我们在后面会解释,继续往下看。

    二、Java中的序列流

        为了探讨Java对象序列化与反序列化的过程,以及Serializable关键字在整个过程中的作用,我们先来提一个 序列流 的概念,刚好我们最近也在写关于Java IO的相关博客。

        Java 的序列流(ObjectInputStream 和 ObjectOutputStream)是一种可以将 Java 对象序列化和反序列化的流。这个属于基本的字节输入流与输出流的演变,之前的博文中已经介绍了它们的用法,在这里就不再展开了。

    • ObjectOutputStream:将序列化后的字节序列写入到文件、网络等输出流中。
    • ObjectInputStream:可以读取 ObjectOutputStream 写入的字节流,并将其反序列化为相应的对象(包含对象的数据、对象的类型和对象中存储的属性等信息)。

    三、序列化实战

        OK,有了上面两个理论知识作为铺垫,我们接下来就可以进行序列化的实战了,首先,我们要先创建一个包含简单属性的类,这里我们创建了一个Person类,里面有name和age两个属性字段。然后,我们通过ObjectOutputStream流将对象写出到文件(序列化),然后再通过ObjectInputStream读取文件中的数据,输出为一个person对象(反序列化)。

    话不多说,直接上代码:

    public class Test {
    public static void main(String[] args) throws IOException {
    //初始化对象信息
    Person person = new Person();
    person.setName("JavaBuild");
    person.setAge(30);
    System.out.println(person.getName()+" "+person.getAge());
    //序列化过程
    try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("E:\\person.txt"));) {
    objectOutputStream.writeObject(person);
    } catch (IOException e) {
    e.printStackTrace();
    }
    //反序列化过程
    try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("E:\\person.txt"));) {
    Person p = (Person) objectInputStream.readObject();
    System.out.println(p.getName() + " " + p.getAge());
    } catch (IOException | ClassNotFoundException e) {
    e.printStackTrace();
    }
    }
    }
    class Person {
    private String name;
    private int age;
    public String getName() {
    return name;
    }
    public void setName(String name) {
    this.name = name;
    }
    public int getAge() {
    return age;
    }
    public void setAge(int age) {
    this.age = age;
    }
    }

    然后我们执行一下,结果,哦吼!报错了,提示了NotSerializableException,原因是我们在创建Person类时,并没有实现Serializable接口。

    image

    很多初学的同学会很奇怪,跟进这个Serializable接口中发现里面空空如也,为啥我们不实现它就无法进行序列化呢?

    image

    跟着上面报错中的堆栈信息,我们进入ObjectOutputStream的writeObject0方法中一探究竟!其中有部分源码如下:

    // 判断对象是否为字符串类型,如果是,则调用 writeString 方法进行序列化
    if (obj instanceof String) {
    writeString((String) obj, unshared);
    }
    // 判断对象是否为数组类型,如果是,则调用 writeArray 方法进行序列化
    else if (cl.isArray()) {
    writeArray(obj, desc, unshared);
    }
    // 判断对象是否为枚举类型,如果是,则调用 writeEnum 方法进行序列化
    else if (obj instanceof Enum) {
    writeEnum((Enum) obj, desc, unshared);
    }
    // 判断对象是否为可序列化类型,如果是,则调用 writeOrdinaryObject 方法进行序列化
    else if (obj instanceof Serializable) {
    writeOrdinaryObject(obj, desc, unshared);
    }
    // 如果对象不能被序列化,则抛出 NotSerializableException 异常
    else {
    if (extendedDebugInfo) {
    throw new NotSerializableException(
    cl.getName() + "\n" + debugInfoStack.toString());
    } else {
    throw new NotSerializableException(cl.getName());
    }
    }

    从这段源码中我们可以发现,在序列化的时候,writeObject0方法内部会对对象进行类型判断,包括字符串、数组、枚举或Serializable,这些条件都不满足的话,就会抛出NotSerializableException异常,因此,即便Serializable接口什么都没有,但需要是初始化的类实现了它的话,就满足了obj instanceof Serializable,可以进行序列话操作!

    我们将上面的测试代码中Person类实现Serializable接口后,再看结果:
    image

    序列化与反序列化都成功了,并获得了预期的打印结果。

    那么它们的具体实现流程是怎么样的呢?

    • 序列化: 以 ObjectOutputStream 为例吧,跟如它的源码时发现,它在序列化的时候会依次调用 writeObject()→writeObject0()→writeOrdinaryObject()→writeSerialData()→invokeWriteObject()→defaultWriteFields()。
    • 反序列化: 以 ObjectInputStream 为例,它在反序列化的时候会依次调用 readObject()→readObject0()→readOrdinaryObject()→readSerialData()→defaultReadFields()。

    四、总结

    由此可见,Serializable 接口之所以定义为空,是因为它只起到了一个标识的作用,告诉程序实现了它的对象是可以被序列化的,但真正序列化和反序列化的操作并不需要它来完成,就像这里的序列流才是主要实现序列化的驱动器!

  • 相关阅读:
    【ML】 第四章 训练模型
    数控机床传动装置机械及PLC电气控制系统设计
    对梯度回传的理解
    总结springboot项目中一些后端接收前端传参的方法
    为什么会出现需求变更,如何应对?
    设备树覆盖:概念与术语
    【Redis】数据结构之dict
    LeetCode50天刷题计划第二季(Day 24 — LRU 缓存(8.00- 8.50)对链表进行插入排序(11.30-11.50)
    HashTable HashMap 区别
    《数据结构》顺序表ArrayList
  • 原文地址:https://www.cnblogs.com/JavaBuild/p/18258555