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

42次阅读

共计 9452 个字符,预计需要花费 24 分钟才能阅读完成。

这是 why 的第 65 篇原创文章

荒腔走板

大家好,我是 why,欢送来到我间断周更优质原创文章的第 65 篇。老规矩,先荒腔走板聊聊技术之外的货色。

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

往年因为疫情起因,上半年的马拉松较量全副停摆了。往年可能也没有机会再跑一次马拉松了。只有回味一下去年的成都马拉松了。

去年成都马拉松我跑的是半程,只有 21 公里,女朋友也报名跑了一个 5 公里的欢畅跑,所以前 5 公里都是陪着她边跑边玩。

过了 10 公里后,赛道两边的观众越来越多,成都的叔叔阿姨们特地的激情。老远看到我跑过来了,就用四川话大声的喊:帅哥,加油。

还有很多老年人,手上拿着个小型国旗,在那里载歌载舞的挥动着。

当然还有很多三五成群的小朋友,伸长了手臂,竭力张开着五指。那是他们要和你击掌的意思。

每击一次,跑过之后都能听到小朋友那特有的一连串的笑声。他们播种了欢畅,而我播种了力量。

有一个转弯的中央,路边站着的男女老少都伸长着手臂,张开着五指,延绵几十米,每个人嘴里喊着鼓劲的话。

我加快脚步,一个个的微微击掌过来。这个时候耳机外面传来的是我循环播放的成都宣传曲《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、1、2 这三个挨在一起的数据,挨在一起就是 near。所以这个办法就叫做 chooseNear。

而这个 0、1、2 就是三个连在一起的数字,所以咱们能够用偏移量间接找到其对应的下一个须要跳转的地址。

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

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

接着往下看:

当 switch 语句外面 case 的值比拟“稠密”(sparse)的时候,用 tableswitch 指令的话空间利用率就会很低下。于是咱们就用 lookupswitch 指令来代替 tableswitch。

你留神官网上用的这个词:sparse。

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

稠密。翻译过去了,还是读不懂是不是,没有关系。我给你搞个例子:

右边是 java 文件,外面的 case 只有 0、2、4。

左边是字节码文件,tableswitch 外面有 0、1、2、3、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,一个被代码耽搁的文学创作者,不是大佬,然而喜爱分享,是一个又暖又有料的四川好男人。

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

正文完
 0