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

· 浏览次数 : 0

小编点评

中午一个网友来信说自己和面试官干起来了,看完他的描述真是苦笑不得。这年头互联网CS消息满天飞,怎么连面试官都SB起来了呢? 大概是这样:这位网友面试时被问及了Serializable接口的底层实现原理,因为这是一个标识性的空接口,大部分同学在学习时都秉持着会用就行(说实话,Build哥在这之前也没怎么细研究过,都是拿来就用),几乎不太去关注底层的东西,这位网友亦是如此,在这种情况下,自然回答的心虚,这下可被面试官抓住了把柄,一顿带有人身攻击的狂输出,让面试现场变成了撕B现场。 基于这位网友的面试经历,Build哥又赶紧去重新学了一下Serializable关键字,以及它背后的实现。别到时候咱也被暴怼。 一、序列化与反序列化 首先,我们先来了解一下两个概念:序列化与反序列化。 序列化:将Java对象转换为一个字节序列(包含对象的数据、对象的类型和对象中存储的属性等信息)的过程,以便于在网络上传输或者存储在文件中。 反序列化:是序列化的逆过程,将字节序列转为Java对象的过程。 1.1 序列化与反序列化的应用场景 对象在进行网络传输(比如远程方法调用RPC的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;将对象存储到文件(如系统中excel的上传与下载)之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;将对象存储到数据库(如Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。 序列化与发序列化的流转过程可参考下图: 有个问题,如果在我的对象中,有些变量并不想被序列化应该怎么办呢?答:不想被序列化的变量我们可以使用transient或static关键字修饰;transient关键字的作用是阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复;而static关键字修饰的变量并不属于对象本身,所以也同样不会被序列化! 具体原因,我们在后面会解释,继续往下看。 二、Java中的序列流 为了探讨Java对象序列化与反序列化的过程,以及Serializable关键字在整个过程中的作用,我们先来提一个序列流的概念,刚好我们最近也在写关于Java IO的相关博客。 Java的序列流(ObjectInputStream和ObjectOutputStream)是一种可以将Java对象序列化和反序列化的流。这个属于基本的字节输入流与输出流的演变,之前的博文中已经介绍了它们的用法,在这里就不再展开了。 ObjectOutputStream:将序列化后的字节序列写入到文件、网络等输出流中。 ObjectInputStream:可以读取ObjectOutputStream写入的字节流,并将其反序列化为相应的对象(包含对象的数据、对象的类型和对象中存储的属性等信息)。 三、序列化实战 OK,有了上面两个理论知识作为铺垫,我们接下来就可以进行序列化的实战了,首先,我们要先创建一个包含简单属性的类,这里我们创建了一个Person类,里面有name和age两个属性字段。然后,我们通过ObjectOutputStream流将对象写出到文件(序列化),然后再通过ObjectInputStream读取文件中的数据,输出为一个person对象(反序列化)。 话不多说,直接上代码: ```java 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接口。 很多初学的同学会很奇怪,跟进这个Serializable接口中发现里面空空如也,为啥我们不实现它就无法进行序列化呢?跟着上面报错中的堆栈信息,我们进入ObjectOutputStream的writeObject0方法中一探究竟! 其中部分源码如下: ```java // 判断对象是否为字符串类型,如果是,则调用 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() + "\\" + debugInfoStack.toString()); } else { throw new NotSerializableException(cl.getName()); } } ``` 从这段源码中我们可以发现,在序列化的时候,writeObject0方法内部会对对象进行类型判断,包括字符串、数组、枚举或Serializable,这些条件都不满足的话,就会抛出NotSerializableException异常,因此,即便Serializable接口什么都没有,但需要是初始化的类实现了它的话,就满足了obj instanceof Serializable,可以进行序列化操作! 我们将上面的测试代码中Person类实现Serializable接口后,再看结果:序列化与反序列化都成功了,并获得了预期的打印结果。 那么它们的具体实现流程是怎么样的呢? 序列化:以ObjectOutputStream为例吧,跟如它的源码时发现,它在序列化的时候会依次调用writeObject()→writeObject0()→writeOrdinaryObject()→writeSerialData()→invokeWriteObject()→defaultWriteFields()。 反序列化:以ObjectInputStream为例,它在反序列化的时候会依次调用readObject()→readObject0()→readOrdinaryObject()→readSerialData()→defaultReadFields()。 四、总结 由此可见,Serializable接口之所以定义为空,是因为它只起到了一个标识的作用,告诉程序实现了它的对象是可以被序列化的,但真正序列化和反序列化的操作并不需要它来完成,就像这里的序列流才是主要实现序列化的驱动器! 归纳总结以上内容,生成内容时需要带简单的排版

正文

    中午一个网友来信说自己和面试官干起来了,看完他的描述真是苦笑不得,这年头是怎么了,最近互联网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 接口之所以定义为空,是因为它只起到了一个标识的作用,告诉程序实现了它的对象是可以被序列化的,但真正序列化和反序列化的操作并不需要它来完成,就像这里的序列流才是主要实现序列化的驱动器!

与关于面试被面试官暴怼:“几年研究生白读” 的前因后果相似的内容:

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

中午一个网友来信说自己和面试官干起来了,看完他的描述真是苦笑不得,这年头是怎么了,最近互联网CS消息满天飞,怎么连面试官都SB起来了呢? 大概是这样的:这位网友面试时被问及了Serializable接口的底层实现原理,因为这是一个标识性的空接口,大部分同学在学习时都秉持着会用就行(说实话,Build

面试官问我:CSS有哪些属性可以继承?

摘要:本文带大家学习一下关于CSS属性的继承。 本文分享自华为云社区《关于CSS有哪些属性可以继承?》,作者:黛琳ghz。 前言 今天遇到一个很有意思的题目,通过题目可以顺便学习一下关于CSS属性的继承。(答案是ACD) 关于CSS属性继承 字体系列属性 font:组合字体font-family:规

面试官:transient关键字修饰的变量当真不可序列化?我:烦请先生教我!

一、写在开头 在这篇文章中记录一下之前自己面试时学到的东西,是关于transient关键字的,当时面试官问我IO的相关问题,基本上全答出来了,关于如何不序列化对象中某个字段时,我果断的选择了static和transient,但面试官紧接着问了我:“transient关键字修饰的变量当真不可序列化吗?

算法金 | 不愧是腾讯,问基础巨细节 。。。

大侠幸会,在下全网同名「算法金」 0 基础转 AI 上岸,多个算法赛 Top 「日更万日,让更多人享受智能乐趣」 最近,有读者参加了腾讯算法岗位的面试,面试着重考察了基础知识,并且提问非常详细。 特别是关于AdaBoost算法的问题,面试官问了很多。 今天,我们就来和大家探讨一下 AdaBoost

《HelloTester》第4期

1.前言 终于到了谈面试的部分了! 我在这也说明一下,有同学说之前简历篇的时候一直在说项目的介绍,而面试官真正关心的是技术啊?我在这做个解释,因为我写的这些文章主要针对的是软件测试的同学,所以其他职位的请根据自己的情况来改,比如你是面的前端或者java等,那当然要突出你在编程中的表现了! 首先来说,

面试官:说一说如何优雅的关闭线程池,我:shutdownNow,面试官:粗鲁!

写在开头 面试官:“小伙子,线程池使用过吗,来聊一聊它吧!” 我:“好的,然后巴拉巴拉一顿输出之前看过的build哥线程池十八问...” 面试官满意的点了点头,紧接着问道:“那你知道如何优雅的关闭线程池吗?” 我:“知道知道,直接调用shutdownNow()方法就好了呀!” 面试官脸色一变,微怒道

面试官:Java线程可以无限创建吗?

哈喽,大家好,我是世杰。 ⏩本次给大家介绍一下操作系统线程和Java的线程以及二者的关联 1. 面试连环call Java线程可以无限创建吗? Java线程和操作系统线程有什么关联? 操作系统为什么要区分内核态和用户态? ⏩要想解答这些问题,我们要先从操作系统线程开始说起,让我们开始吧�

面试官:什么是伪共享,如何避免?

theme: jzman 本文已收录到 GitHub · AndroidFamily,有 Android 进阶知识体系,欢迎 Star。技术和职场问题,请关注公众号 [彭旭锐] 私信我提问。 前言 大家好,我是小彭。 在前面的文章里,我们聊到了 CPU 的高速缓存机制。由于 CPU 和内存的速度差距

高德面试:为什么Map不能插入null?

在 Java 中,Map 是属于 java.util 包下的一个接口(interface),所以说“为什么 Map 不能插入 null?”这个问题本身问的不严谨。Map 部分类关系图如下: 所以,这里面试官其实想问的是:为什么 ConcurrentHashMap 不能插入 null? 1.HashM

微服务面试必读:拆分、事务、设计的综合解析与实践指南

微服务的应用级别确实相对简单,但在实际开发中仍有一些技术难点需要解决。对于微服务组件的使用,确实不存在太大差距,但在设计和开发过程中需要积累经验。学习微服务的上手时间相对较短,可能只需一周到一个月的时间。然而,设计经验和技术难点是需要个人长期积累的,不能急于求成。因此,在使用和开发微服务时,更应该关注方案思考,展示自己对该领域的理解和见解。这样能够体现出你对问题的思考深度和解决方案的创新性。希望这次面试种子题目的解答能够帮助你应对面试官的问题!