https://mp.weixin.qq.com/s/LTT66W1Swhdir_2ecetBvA html
你们好,我是 why,欢迎来到我连续周更优质原创文章的第 65 篇。老规矩,先荒腔走板聊聊技术以外的东西。java
上面这图是去年的成都马拉松赛道上,摄影师抓拍的我。哎,真是阳光向上的 95 后帅小伙啊。
程序员
今年因为疫情缘由,上半年的马拉松比赛所有停摆了。从大二开始,我每一年至少报名跑一次马拉松,今年可能也没有机会在赛场上跑一次马拉松了。只有回味一下去年的成都马拉松了。web
去年成都马拉松我跑的是半程,只有 21 千米,女友也报名跑了一个 5 千米的欢乐跑,因此前 5 千米都是陪着她边跑边玩。面试
过了 10 千米后,赛道两边的观众愈来愈多,成都的叔叔阿姨们特别的热情。老远看到我跑过来了,就用四川话大声的喊:帅哥,加油。编程
还有不少老年人,手上拿着个小型国旗,在那里手舞足蹈的挥舞着。数组
固然还有不少成群结队的小朋友,伸长了手臂,极力张开着五指。那是他们要和你击掌的意思。微信
每击一次,跑过以后都能听到小朋友那特有的一连串的笑声。他们收获了欢乐,而我收获了力量。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 一顿爆学。
先看看考流程控制语句的:
这个程序的迷惑点在于第 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 得膨胀成什么样子?
补的 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 虚拟机规范》的文档里面挖到了一句话。我以为得讲一下。
在《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,里面的 case 必定是一串连续的数字,不稀疏,因此用 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 的。
你有没有想过一个问题。switch/case 里面为何不作成支持 null 的模式?
若是表达式为 null ,咱们就拿着 null 去 case 里面匹配,这样理论上作也是能够作的。
好吧,应该也没有人想这个问题。固然,除了一些奇奇怪怪的面试官。
这个问题我在《Java 语言规范》里面找到了答案:
the designers of the Java programming language。
个人妈呀,这是啥啊。
Java 编程语言设计者,这是赏饭吃的祖师爷啊!
《Java 语言规范》里面说:根据 Java 编程语言设计者的判断,抛出空指针这样作比静默地跳过整个 switch 语句或选择在 default 标签(若是有)里面继续执行语句要好。
别问,问就是祖师爷通过判断后,以为这样写就是好的。
这题就像我以前写的这个文章同样:《这道面试题我真不知道面试官想要的回答是什么》。
请问:ConcurrentHashMap中的key为何不能为null?
别问,问就是 Doug Lea 不喜欢 null。设计之初就没打算支持 null。
好了,又一个基本上用不到的知识点送给你们,没必要客气:
这篇文章里面仍是不少须要翻译的地方。我发现有不少的程序猿比较惧怕英语。
以前还有人夸我英语翻译的好:
其实我大学的时候英语四级考了 4 次,最后一次才压线过的。
那为何如今看英文文档基本上没有什么障碍呢?
其实这个问题真的很好解决的。
你找一个英语六级 572 分,考研英语一考了 89 分的女友,她会督促你学英语的。
哦,对了文章中提到的《阿里巴巴Java开发手册》能够在公众号后台回复关键字【java】便可得到电子版。
好了,看到了这里安排个“一键三连”(转发、在看、点赞)吧,周更很累的,不要白嫖我,须要一点正反馈。
才疏学浅,不免会有纰漏,若是你发现了错误的地方,能够在留言区提出来,我对其加以修改。
感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。
我是 why,一个被代码耽误的文学创做者,不是大佬,可是喜欢分享,是一个又暖又有料的四川好男人。
还有,重要的事情说三遍:欢迎关注我呀。欢迎关注我呀。欢迎关注我呀。