why哥被阿里一道基础面试题给干懵了,一气之下写出万字长文。

这是why的第 65 篇原创文章html

荒腔走板

你们好,我是 why,欢迎来到我连续周更优质原创文章的第 65 篇。老规矩,先荒腔走板聊聊技术以外的东西。java

上面这图是去年的成都马拉松赛道上,摄影师抓拍的我。哎,真是阳光向上的 95 后帅小伙啊。程序员

今年因为疫情缘由,上半年的马拉松比赛所有停摆了。今年可能也没有机会再跑一次马拉松了。只有回味一下去年的成都马拉松了。面试

去年成都马拉松我跑的是半程,只有 21 千米,女友也报名跑了一个 5 千米的欢乐跑,因此前 5 千米都是陪着她边跑边玩。编程

过了 10 千米后,赛道两边的观众愈来愈多,成都的叔叔阿姨们特别的热情。老远看到我跑过来了,就用四川话大声的喊:帅哥,加油。segmentfault

还有不少老年人,手上拿着个小型国旗,在那里手舞足蹈的挥舞着。数组

固然还有不少成群结队的小朋友,伸长了手臂,极力张开着五指。那是他们要和你击掌的意思。oracle

每击一次,跑过以后都能听到小朋友那特有的一连串的笑声。他们收获了欢乐,而我收获了力量。jvm

有一个转弯的地方,路边站着的男女老幼都伸长着手臂,张开着五指,延绵几十米,每一个人嘴里喊着鼓劲的话。编程语言

我放慢脚步,一个个的轻轻击掌过去。这个时候耳机里面传来的是我循环播放的成都宣传曲《I love this city》。

我不知道应该怎样去描述那种氛围带给个人激励和感动,感受本身就是奔跑在星光大道上,我很怀恋。

每跑完一次马拉松,都能带给我爆棚的正能量。

固然了,成都马拉松的官方补给我也是吹爆的。可是给我印象深入的是大概在 16 千米的地方,有一处私人补给站,我竟然在这里喝了到几口乌苏啤酒,吃了几口豆花,几根凉面,几块冒烤鸭。逗留了大概 5 分钟的样子。

哎呀,那感受,难以忘怀,简直是巴适的板。

好了,说回文章。

阿里面试题

阿里巴巴出品的《码出高效 Java 开发手册》你知道吧?

前段时间我发现书的最后还有两道 Java 基础的面试题。其中有一道,很是的基础,能够说是入门级的题,可是都把我干懵了。

竟然经过眼神编译,看不出输出结果是啥。

最后猜了个答案,结果还错了。

这篇文章就带着你们一块儿看看这题,分析分析他背后的故事。

首先看题:

public class SwitchTest {  
    public static void main(String[] args) {  
      //当default在中间时,且看输出是什么?  
        int a = 1;  
        switch (a) {  
            case 2:  
                System.out.println("print 2");  
            case 1:  
                System.out.println("print 1");  
            default:  
                System.out.println("first default print");  
            case 3:  
                System.out.println("print 3");  
        }  
        
      //当switch括号内的变量为String类型的外部参数时,且看输出是什么?  
        String param = null;  
        switch (param) {  
            case "param":  
                System.out.println("print param");  
                break;  
            case "String":  
                System.out.println("print String");  
                break;  
            case "null":  
                System.out.println("print null");  
                break;  
            default:  
                System.out.println("second default print");  
        }  
    }  
}

这题主要是考的 switch 控制语句,你能经过眼神编译,在内心输出运行结果吗?

两个考点

先看看答案:

怎么样,这个答案是否是和你本身给出来的答案一致呢?

反正我以前是被它那个 default 写在中间的操做给迷惑了。

我寻思这玩意还有这种操做?能这样写吗?

至于下面那个空指针,问题不大,一眼看出问题。

因此在我看来,这题一共两个考点:

  • 前一个 switch 考的是其流程控制语言。
  • 后一个 switch 考的是其底层技术实现。

咱们一个个剥丝抽茧,扒光示众的说。一块儿把这个 switch 一顿爆学。

switch 执行流程

先看看考流程控制语句的:

这个程序的迷惑点在于第 5 行的注释,致使我主要关注这个 default 的位置了,忽略了每一个 case 并无 break。

没有 break 致使这个程序的输出结果是这样的:

那么 switch 是怎么控制流程的呢?

带着这个问题咱们去权威资料里面寻找答案。

什么权威资料呢?

https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.11

怎么样?

The Java® Language Specification,《Java 语言规范》,你就告诉我权不权威?

打开我上面给的连接,在这个页面那么轻轻的一搜:

这就是咱们要找的东西。

点击过去以后,在这个页面里面的信息量很是大。我一会都会讲到。

如今咱们先关注执行流程这块:

看到这么多英语,不要慌,why 哥这种暖男做者,确定是给你翻译的巴巴适适的。可是建议你们也看看英文原文,有的时候翻译出来的可能就差点意思。

接下来我就给你们翻译一下官方的话:

来,第一句:

当 switch 语句执行的时候,首先须要计算表达式。

等等,表达式(Expression)是什么?

表达式就是 switch 后面的括号里面的东西。好比说,这个东西能够是一个方法。

那么若是这个表达式的计算结果是 null,那么就抛出空指针异常。这个 switch 语句也就算完事了。

另外,若是这个表达式的结果是一个引用类型,那么还须要进行一个拆箱的处理。

好比就像这样式儿的:

test() 方法就是表达式,返回的是包装类型 Integer,而后 switch 会作拆箱处理。

这个场景下 test 方法返回了 null,因此会抛出空指针异常。

接着往下翻译:

若是表达式的计算或者随后的拆箱操做因为某些缘由忽然完成,那么这个 switch 语句也就完成了。

忽然完成,小样,说的还挺隐晦的。我以为这里就是在说表达式里面抛出了异常,那么 switch 语句也就不会继续执行了。

就像这样式儿的:

接下来就是流程了:

Otherwise,就是不然的意思。带入上下文也就是说前面的表达式是正常计算出来了一个东西了。

那么就拿着计算出来的这个东西(表达式的值)和每个 case 里面的常量来对比,会出现如下的状况:

  • 若是表达式的值和其中一个 case 语句中的常量相等了,那么咱们就说 case 语句匹配上了。switch 代码块中匹配的 case 语句以后的全部语句 (若是有)就按照顺序执行。若是全部语句都正常完成,或者在匹配的 case 语句以后没有语句,那么整个 switch 代码块就将正常完成。
  • 若是没有和表达式匹配的 case 语句,可是有一个 default 语句,那么 switch 代码块中 default 语句后面的全部语句(若是有)将按顺序执行。若是全部语句都正常完成,或者若是 default 标签以后没有语句了,则整个 switch 代码块就将正常完成。
  • 若是既没有 case 语句和表达式的值匹配上,也没有 default 语句,那就没有什么搞的了,switch 语句执行了个寂寞,也算是正常完成。

其实到这里,上面的状况一不就是阿里巴巴 Java 开发手册的面试题的场景吗?

你看着代码,再看着翻译,仔细的品一品。

为何那道面试题的输出结果是这样的:

没有为何,Java 语言规范里面就是这样规定的,按照规定执行就完事了。

除了上面这三种流程,官网上还接着写了三句话:

若是 switch 语句块里面包含任何的表示或者意外致使当即完成的语句,则按以下方式处理:

我先说一下我理解的官方文档中说的:“any statement immediately ... completes abruptly”。

表示当即完成的语句就是每一个 case 里面的 break、return。

意外致使忽然完成的语句就是在 switch 语句块里面任何会抛出异常的代码。

若是出现了这两种状况,switch 语句块怎么处理呢?

若是语句的执行因为 break 语句而完成,则不会采起进一步的操做(进一步操做是指若是没有 break 代码,则将继续执行后续语句),switch 语句块将正常完成。

若是语句的执行因为任何其余缘由忽然完成(好比抛出异常),switch 语句块也会因相同的缘由而立马完成。

上面就是 switch 语句的执行流程。因此你还别以为 switch 语句就必需要个 break,别人的设计就是如此,看场景的。

好比看官方给出的两个示例代码:

这是不带 break 的。需求就要求这样输出,你整个 break 干啥。

再看另一个带 break 的:

实现的又是另一个需求了。

因此,看场景。

另外,我以为官网上的这个例子给的很差。最后少了一个 default 语句。看看阿里 Java 开发手册上怎么说的:

这个地方见仁见智吧。

底层技术实现

第二个考点是底层技术实现。

也就下面这坨代码:

首先通过前面的一个小节,你知道为何运行结果是抛出空指针异常了不?

前面讲了哈,官方文档里面有这样的一句话:

规定如此。

因此,这小节的答案是这样的吗?确定不是的,咱们多想一步:

为何这样规定呢?

这才是这小节想要带你们寻找的东西。

首先你得知道 switch 支持 String 是 Java 的一颗语法糖。既然是语法糖, 咱们就看看它的 class 文件:

从 class 文件中,咱们尝到了这颗语法糖的味道。原来其实是有两个 switch 操做的。

switch 支持 String 类型的缘由是先取的 String 的 hashCode 进行 case 匹配,而后在每一个 case 里面给 var3 这个变量赋值。而后再对 var3 进行一次 switch 操做。

因此,上图中标记的 15 行,若是 String 是 null,那么对 null 取 hashCode ,那可不得抛出空指针异常吗?

因此,你看《Java开发手册》里面的这个建议:

明白为何这样写了吧?

因此,这小节的答案是这样的吗?确定不是的,咱们再多想一步呢:

为何要非得把 String 取 hashCode 才进行 switch/case 操做呢?

从 class 文件中咱们已经看不出什么有价值的东西了。只能在往下走。

class 再往下走就到哪里了?

对了,须要看看字节码了。

经过 javap 得到字节码文件:

这个字节码很长,你们本身编译后去看一下,我就不所有截取,浪费篇幅了。

在这个字节码里面,就算你什么都不太明白。可是只要你稍微注意一点点,你应该会注意到其中的这两个地方:

结合着 class 文件看:

奇怪了,一样的 switch 语言,却对应两个指令:lookupswitch 和 tableswitch。

因此这两个指令确定是关键突破点。

咱们去哪里找这个两个指令的信息呢?

确定是得找权威资料的:

怎么样?

The Java® Virtual Machine Specification,Java 虚拟机规范,你就大声的告诉我稳不稳?

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-3.html#jvms-3.10

在上面的连接中,咱们轻轻的那么一搜:

发现这两个指令,在 Compiling Switches 这一小节中是挨在一块儿的。

找到这里了,你就找到正确答案的门了。我带领你们看一下我经过这个门,看到的门后面的世界。

首先仍是给你们带着我本身的理解,翻译一下虚拟机规范里面是怎么介绍这两个指令的:

switch 语句的编译使用的是 tableswitch 和 lookupswitch 这两个指令。

咱们先说说 tableswitch 是干啥的。

当 switch 里面的 case 能够用偏移量进行有效表示的时候,咱们就用 tableswitch 指令。若是 switch 语句的表达式计算出来的值不在这个偏移量的有效范围内,那么就进入 default 语句。

看不太明白对不对?

不要紧,我第一次看的时候也不太明白。别急,咱们看看官方示例:

由于咱们 case 的条件是 0、一、2 这三个挨在一块儿的数据,挨在一块儿就是 near 。因此这个方法就叫作 chooseNear 。

而这个 0、一、2 就是三个连在一块儿的数字,因此咱们能够用偏移量直接找到其对应的下一个须要跳转的地址。

这个就有点相似于数组,直接经过索引下标就能定位到数据。而下标,是一串连续的数字。

这个场景下,咱们就能够用 tableswitch。

接着往下看:

当 switch 语句里面 case 的值比较“稀疏”(sparse)的时候,用 tableswitch 指令的话空间利用率就会很低下。因而咱们就用 lookupswitch 指令来代替 tableswitch。

你注意官网上用的这个词:sparse。

没想到吧,学技术的时候还能学个英语四级单词。

稀疏。翻译过来了,仍是读不懂是否是,没有关系。我给你搞个例子:

左边是 java 文件,里面的 case 只有 0、二、4。

右边是字节码文件, tableswitch 里面有0、一、二、三、4。

对应的 class 文件是这样的:

嘿,你说怎么着?莫名其妙多了个 1 和 3 的 case 。你说神奇不神奇?

这是在干吗?这不就是在填位置嘛。

填位置的目的是什么?不就是为了保证 java 文件里面的 case 对应的值恰好能和偏移量对上吗?

假设这个时候 switch 表达式的值是 2,我直接根据偏移量 2 ,就能够取到 2 对应的接下来须要执行的地方 47,而后接着执行输出语句了:

假设这个时候 switch 表达式的值是 3,我直接根据偏移量 3,就能够取到 3 对应的接下来须要执行的地方 69,而后接着执行 default 语句了:

因此,0,1,2 不叫稀疏,0,2,4 也不叫稀疏。

它们都不 sparse ,缺一点点的状况下,咱们能够补位。

因此如今你理解官网上的这句话了吗:

当 switch 语句里面 case 的值比较“稀疏”(sparse)的时候,用 tableswitch 指令的话空间利用率就会很低下。

比较稀疏的时候,假设三个 case 分别是 100,200,300。你不可能把 100 到 300 之间的数,除了 200 都补上吧?

那玩意补上了以后 case 得膨胀成什么样子?

空间占的多了,可是实际要用的就 3 个值,因此空间利用率低下。

那 tableswitch 指令不让用了怎么办呢?

别急,官方说能够用 lookupswitch 指令。

lookupswitch 指令拿着 switch 表达式计算出来的 int 值和一个表中偏移量进行配对(pairs)。

配对的时候,若是表里面一个 key 值与表达式的值配上了,就能够在这个 key 值关联的下一执行语句处继续执行。

若是表里面没有匹配上的键,则在 default 处继续执行。

你看明白了吗?迷迷糊糊的对不对?

什么玩意就出来一个表呢?

没事,别急,官方给了个例子:

此次的例子叫作 chooseFar 。由于 case 里面的值不是挨着的,0 到 100 之间隔得仍是有点距离。

我不能像 tableswitch 似的,拿着 100 而后去找偏移量为 100 的位置吧。这里就三个数,根本就找不到 100 。

只能怎么办?

就拿着我传进来的 100 一个个的去和 case 里面的值比了,这就叫 pairs。

其实官网上的这个例子没有给好,你看我给你一个例子:

你看左边的 java 代码,里面的 case 是乱序的,到字节码文件里面后就排好序了。

而官方文档里面说的这个“table”:

就是排好序的这个:

为何要排序呢?

答案就在虚拟机规范里面:

排序以后的查找比线性查找快。这个没啥说的吧。它这里虽然没有说,但其实它用的是二分查找,时间复杂度为O(log n)。

哦,对了。tableswitch 因为是直接根据偏移量定位,因此时间复杂度是 O(1)。

好了,到这里我就把 tableswitch 和 lookupswitch 这两个指令讲完了。

我不知道你在看的时候有没有产生什么疑问,反正我看到这个地方的时候我就在想:

虚拟机规范里面就说了个 sparse,那何时是稀疏,何时是不稀疏呢?

说实话,做为程序员,我对“稀疏”这个词仍是很敏感的,特别是前面再加上毛发两个字的时候。

不知道为何说到“稀疏”,我就想起了谢广坤。广坤叔你知道吧,这才叫“稀疏”:

怎么定义稀疏

因此,在 switch 里面,咱们怎么定义稀疏呢?

文档中没有写。

文档里没有写的,都在源码里面。

因而我搞了个 openJDK,我倒要看看源码里面到底什么是 TMD 稀疏。

通过一番探索,找到了这个方法:

com.sun.tools.javac.jvm.Gen#visitSwitch

这里我不作源码解读,我只是想单纯的知道源码里面到底什么 TMD 是 TMD 稀疏。

因此带你们直接看这个地方:

这里有个三目表达式。若是为真则使用 tableswitch ,为假则使用 lookupswitch。

咱们先拿着这个不稀疏的,加上断点调戏一番,呸,调试一番:

断点时候时候各个参数以下:

标号为 ① 的地方是表明咱们确实调试的是预期的程序。

标号为 ② 的地方咱们带入到上面的表达式中,能够求得最终值:

hi 是 case 里面的表达式对应的最大值,也就是 2。

lo 是 case 里面的表达式对应的最小值,也就是 0。

nlabels 表明的是 case 的个数,也就是 3。

因此带入到上面的代码中,最终算出来的值 16<=18,成立,使用 tablewitch。

这就叫不稀疏。

假设咱们把最后一个 case 改成 5:

Debug 时各个参数变成了这样:

最终算出来的值 19<=18,不知足,使用 lookupswitch 。

这叫作稀疏。

因此如今咱们知道了到底什么是 TMD 稀疏。

在源码里面有个公式能够知道是否是稀疏的,从而知道使用什么指令。

写到这里我以为其实我应该能够住手了。

可是我还在《Java 虚拟机规范》的文档里面挖到了一句话。我以为得讲一下。

switch表达式支持的类型

在《Java 虚拟机规范》文档中的这一部分,有这样的一句话:

就看第一句我圈起来的话。后面的描述都是围绕着这句话在展开描述。

Java 虚拟机的 tableswitch 和 lookupswitch 指令,只支持 int 类型。

好,那我如今来问你:switch 语句的表达式能够是哪些类型的值?注意我说的是表达式。

这个答案在《Java 语言规范》里面也写着的:

你看,8 种基本类型已经支持了char、byte、short、int 这4 种,而这 4 种都是能够转化为 int 类型的。

而剩下的 4 种:double、float、long、boolean 不支持。

为何?

你就想,你就结合我前面讲的内容,把你的小脑袋子动起来,为何这 4 种不支持?

由于 double、float 都是浮点类型的,tableswitch 和 lookupswitch 指令操做不了。

由于 long 类型 64 位了,而tableswitch 和 lookupswitch 指令只能操做 32 位的 int 。这两个指令对于 long 是搞不动的。

而至于 boolean 类型,还须要我说嘛?

你拿着 boolean 类型放到 switch 表达式里面去,你不以为害臊吗?

你就不能写个 if(boolean) 啥的?

而后你又发动你的小脑袋子想:对于 Character、Byte、Short、Integer 这 4 个包装类型是怎么支持的呢?

上个图,左上是 java 文件,右上是 jad 文件,下面是字节码:

拆了个箱,实际仍是用的 int 类型,这个不须要我细讲了吧?

因而你接着想对于 String 类型是怎么支持的呢?

它会先转 hashCode。hashCode 确定是稀疏的,因此用 lookupswitch。

而后在用 var3 这个变量去作一次 switch,通过转化后 var3 必定不是稀疏的,因此用 tableswitch:

你再多想一步,由于是用的 String 类型的 hashcode,那若是出现了哈希冲突怎么办?

看一下这个例子:

冲突了就再配一个 if-else 。

不用多说了吧。

最后,你再想,这个枚举又是怎么支持的呢?

好比下面这个例子,看字节码,只看到了使用了 tableswitch:

咱们再看一下 class 文件,javap 编译以后,变成了这样:

它们分别长这样的:

上面的 SwitchEnumTest.class 文件看不出来什么道道。

可是下面的 SwitchEnumTest$1.class 文件里面仍是有点东西的。

能够看到静态代码块里面有个数组,数组里面的参数是枚举的类型,而后调用了枚举的 ordinal 方法。这个方法的返回值是枚举的下标位置。

在 class 文件里面获取的信息有限,须要祭出 jad 文件来瞅一眼来:

上面就是 java 文件对应的 jad 文件。

标号为 ① 的地方是咱们传入的 switch 里面的表达式,线程状态枚举中的 RUNNABLE。

标号为 ② 的地方是给 int 数值中的位置赋值为 2。那么是哪一个位置呢?

RUNNABLE 在线程状态枚举中的下标位置,以下所示,下标位置是1:

编号为 ③ 的地方是把 int 数值中下标为 1 的元素取出来?

咱们前面刚刚放进去的。取出来是 2。

因而走到编号为 ④ 的逻辑中去。执行最终的输出语句。

因此写到这里,我想我更加能明白著名程序员沃·滋基索德的一句话:

相对于 String 类型而言,枚举简直天生就支持 Switch 操做。

奇怪的知识点

再送给你一个我在写这篇文章的时候学到的一个奇怪的知识点。

咱们知道 switch 的表达式和 case 里面都是不支持 null 的。

你有没有想过一个问题。case 里面为何不支持 null?若是表达式为 null ,咱们就拿着 null 去 case 里面匹配,这样理论上作也是能够作的。

好吧,应该也没有人想这个问题。固然,除了一些奇奇怪怪的面试官。

这个问题我在《Java 语言规范》里面找到了答案:

the designers of the Java programming language。

个人妈呀,这是啥啊。

Java 编程语言设计者,这是赏饭吃的祖师爷啊!

《Java 语言规范》里面说:根据 Java 编程语言设计者的判断,抛出空指针这样作比静默地跳过整个 switch 语句或选择在 default 标签(若是有)里面继续执行语句要好。

别问,问就是祖师爷以为这样写就是好的。

一个基本上用不到的知识点送给你们,没必要客气:

最后说一句(求关注)

这篇文章里面仍是不少须要翻译的地方。我发现有不少的程序猿比较惧怕英语。

以前还有人夸我英语翻译的好:

其实我大学的时候英语四级考了 4 次,最后一次才压线过的。

那为何如今看英文文档基本上没有什么障碍呢?

其实这个问题真的很好解决的。

你找一个英语六级 572 分,考研英语一考了 89 分的女友,她会督促你学英语的。

才疏学浅,不免会有纰漏,若是你发现了错误的地方,能够在留言区提出来,我对其加以修改。 感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。

我是 why,一个被代码耽误的文学创做者,不是大佬,可是喜欢分享,是一个又暖又有料的四川好男人。

还有,重要的事情说三遍:欢迎关注我呀。欢迎关注我呀。欢迎关注我呀。

相关文章
相关标签/搜索