Day714. switch表达式 -Java8后最重要新特性
Posted 阿昌喜欢吃黄桃
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day714. switch表达式 -Java8后最重要新特性相关的知识,希望对你有一定的参考价值。
switch表达式
Hi,我是阿昌
,今天学习记录的是关于 switch表达式
。
switch 表达式
这个特性,首先在 JDK 12 中以预览版的形式发布。在 JDK 13 中,改进的 switch 表达式再次以预览版的形式发布。最后,switch 表达式在 JDK 14 正式发布
。
不论你学习什么样的编程语言,合理地分析、判断、处理不同的情况都是必备的基本功。
比如我们使用的 if-else 语句,还有 switch 语句,都是用来处理种种不同的情况的。
我们都知道 switch 语句,那么 switch 表达式又是什么呢?switch 语句和 switch 表达式又有什么不同呢?
如果你了解了 Java 的语句和表达式这两个基本概念,你的困扰也许会少一点。
Java 规范里,表达式完成对数据的操作。一个表达式的结果可以是一个数值(i * 4);或者是一个变量(i = 4);或者什么都不是(void 类型)。
Java 语句是 Java 最基本的可执行单位
,它本身不是一个数值,也不是一个变量。
Java 语句的标志性符号是分号(代码)和双引号(代码块),比如 if-else 语句,赋值语句等。这样再来看,就很简单了:switch 表达式就是一个表达式,而 switch 语句就是一个语句。
switch 表达式是什么样子的?为什么需要 switch 表达式?
一、阅读案例
该怎么用代码计算一个月有多少天?
生活中,我们熟悉这样的顺口溜,“一三五七八十腊,三十一天永不差,四六九冬三十整,平年二月二十八,闰年二月把一加”。
下面的这段代码,就是按照这个顺口溜的逻辑来计算了一下,今天所在的这个月,一共有多少天。
class DaysInMonth
public static void main(String[] args)
Calendar today = Calendar.getInstance();
int month = today.get(Calendar.MONTH);
int year = today.get(Calendar.YEAR);
int daysInMonth;
switch (month)
case Calendar.JANUARY:
case Calendar.MARCH:
case Calendar.MAY:
case Calendar.JULY:
case Calendar.AUGUST:
case Calendar.OCTOBER:
case Calendar.DECEMBER:
daysInMonth = 31;
break;
case Calendar.APRIL:
case Calendar.JUNE:
case Calendar.SEPTEMBER:
case Calendar.NOVEMBER:
daysInMonth = 30;
break;
case Calendar.FEBRUARY:
if (((year % 4 == 0) && !(year % 100 == 0))
|| (year % 400 == 0))
daysInMonth = 29;
else
daysInMonth = 28;
break;
default:
throw new RuntimeException(
"Calendar in JDK does not work");
System.out.println(
"There are " + daysInMonth + " days in this month.");
这段代码里,我们使用了 switch 语句。代码本身并没有什么错误,但是,至少有两个容易犯错误的地方。
第一个容易犯错的地方,就是在 break 关键字的使用上。
上面的代码里,如果多使用一个 break 关键字,代码的逻辑就会发生变化;
同样的,少使用一个 break 关键字也会出现问题。
int daysInMonth;
switch (month)
case Calendar.JANUARY:
case Calendar.MARCH:
case Calendar.MAY:
break; // WRONG BREAK!!!
case Calendar.JULY:
case Calendar.AUGUST:
case Calendar.OCTOBER:
case Calendar.DECEMBER:
daysInMonth = 31;
break;
// snipped
int daysInMonth;
switch (month)
// snipped
case Calendar.APRIL:
case Calendar.JUNE:
case Calendar.SEPTEMBER:
case Calendar.NOVEMBER:
daysInMonth = 30;
// WRONG, NO BREAK!!!
case Calendar.FEBRUARY:
if (((year % 4 == 0) && !(year % 100 == 0))
|| (year % 400 == 0))
daysInMonth = 29;
else
daysInMonth = 28;
break;
// snipped
break 语句
的遗漏或者冗余,这样的错误如此得常见,甚至于被单列成了一个常见软件安全漏洞。
凡是使用 switch 语句的代码,都有可能成为黑客们重点关注的对象。由于逻辑的错误和黑客的特殊关照,我们在编写代码的时候,需要十二分的小心;
阅读代码的时候,也需要反复地查验 break 语句的前后语境。
毫无疑问,这增加了代码维护的成本,降低了生产效率。为什么 switch 语句里需要使用 break 呢?
最主要的原因,就是希望能够在不同的情况下,共享部分或者全部的代码片段。比如上面的例子中,四月、六月、九月、十一月这四种情景,可以共享每个月都是 30 天这样的代码片段。
这个代码片段只需要写在十一月情景的后面,前面的四月、六月和九月这三个情景都会顺次执行下面的操作(fall-through),直到遇到下一个 break 语句或者 switch 语句终结。
现在我们都知道了,这样是一个弊大于利的设计。
但很遗憾,Java 初始的设计就是采用了这样的设计思想。
如果要新设计一门现代的语言,我们需要更多地使用 switch 语句,但是就不要再使用 break 语句了。
不过,不同的情景共享代码片段,仍然是一个真实的需求。在废弃掉 break 语句之前,我们要找到在不同的情景间共享代码片段的新规则。第二个容易犯错的地方,是反复出现的赋值语句。
在上面的代码中,daysInMonth 这个本地变量的变量声明和实际赋值是分开的。赋值语句需要反复出现,以适应不同的情景。
如果在 switch 语句里,daysInMonth 变量没有被赋值,编译器也不会报错,缺省的或者初始的变量值就会被使用。
int daysInMonth = 0;
switch (month)
// snipped
case Calendar.APRIL:
case Calendar.JUNE:
case Calendar.SEPTEMBER:
case Calendar.NOVEMBER:
break; // WRONG, INITIAL daysInMonth value IS USED!!!
case Calendar.FEBRUARY:
// snipped
在上面的例子里,初始的变量值不是一个合适的数据;
当然,在另外一个例子里,缺省的或者初始的变量值也可能就是一个合适的数据了。
为了判断这个本地变量有没有合适的值,我们需要通览整个 switch 语句块,确保赋值没有遗漏,也没有多余。
这增加了编码出错的几率,也增加了阅读代码的成本。那么,能不能让多情景处理的代码块拥有一个数值呢?
或者换个说法,多情景处理的代码块能不能变成一个表达式?这个想法,就催生了 Java 语言的新特性:“switch 表达式”。
二、switch 表达式
switch 表达式是什么样子的呢?
下面的这段代码,使用的就是 switch 表达式,它改进了上面阅读案例里的代码。
你可以带着上面遇到的问题,来阅读这段代码。这些问题包括:
- switch 表达式是怎么表示一个数值,从而可以给变量赋值的?
- 在不同的情景间,switch 表达式是怎么共享代码片段的?
- 使用 switch 表达式的代码,有没有变得更简单、更皮实、更容易理解?
class DaysInMonth
public static void main(String[] args)
Calendar today = Calendar.getInstance();
int month = today.get(Calendar.MONTH);
int year = today.get(Calendar.YEAR);
int daysInMonth = switch (month)
case Calendar.JANUARY,
Calendar.MARCH,
Calendar.MAY,
Calendar.JULY,
Calendar.AUGUST,
Calendar.OCTOBER,
Calendar.DECEMBER -> 31;
case Calendar.APRIL,
Calendar.JUNE,
Calendar.SEPTEMBER,
Calendar.NOVEMBER -> 30;
case Calendar.FEBRUARY ->
if (((year % 4 == 0) && !(year % 100 == 0))
|| (year % 400 == 0))
yield 29;
else
yield 28;
default -> throw new RuntimeException(
"Calendar in JDK does not work");
;
System.out.println(
"There are " + daysInMonth + " days in this month.");
我们最先看到的变化,就是 switch 代码块出现在了赋值运算符的右侧。这也就意味着,这个 switch 代码块表示的是一个数值,或者是一个变量。换句话说,这个 switch 代码块是一个表达式。
int daysInMonth = switch (month)
// snipped
我们看到的第二个变化,是多情景的合并。也就是说,一个 case 语句,可以处理多个情景。这些情景,使用逗号分隔开来,共享一个代码块。而传统的 switch 代码,一个 case 语句只能处理一种情景。
case Calendar.JANUARY,
Calendar.MARCH,
// snipped
多情景的合并的设计,满足了共享代码片段的需求。而且,由于只使用一个 case 语句,也就不再需要使用 break 语句来满足这个需求了。所以,break 语句从 switch 表达式里消失了。
不同之处在于,传统的 switch 代码,不同的 case 语句之间可以共享部分的代码片段;
而 switch 表达式里,需要共享全部的代码片段。
这看似是一个损失,但其实,共享部分代码片段的能力给代码的编写者带来的困惑远远多于它带来的好处。如果需要共享部分的代码片段,我们总是可以找到替换的办法,比如把需要共享的代码封装成更小的方法。
所以,我们没有必要担心 switch 表达式不支持共享部分代码片段。
下一个变化,是一个新的情景操作符
,“->”
,它是一个箭头标识符
。这个符号使用在 case 语句里,一般化的形式是“case L ->”。
这里的 L,就是要匹配的一个或者多个情景。如果目标变量和情景匹配,那么就执行操作符右边的表达式或者代码块。如果要匹配的情景有两个或者两个以上,就要使用逗号“,”用分隔符把它们分割开来。
case Calendar.JANUARY,
// snipped
Calendar.DECEMBER -> 31;
传统的 switch 代码,这个一般化的形式是“case L :”,也就是使用冒号标识符。
为什么不延续使用传统的情景操作符呢?这主要是出于简化代码的考虑。
我们依然可以在 switch 表达式里使用冒号标识符,使用冒号标识符的一个 case 语句只能匹配一个情景,这种情况我们稍后再讨论。
下一个我们看到的变化,是箭头标识符右侧的数值。这个数值,代表的就是该匹配情景下,switch 表达式的数值。需要注意的是,箭头标识符右侧可以是表达式、代码块或者异常抛出语句,而不能是其他的形式。
如果只需要一个语句,这个语句也要以代码块的形式呈现出来。
case Calendar.JANUARY,
// snipped
Calendar.DECEMBER -> // CORRECT, enclosed with braces.
yield 31;
没有以代码块形式呈现的代码,编译的时候,就会报错。这是一个很棒的约束。
代码块的形式,增强了视觉效果,减少了编码的失误。
case Calendar.JANUARY,
// snipped
Calendar.DECEMBER -> // WRONG, not a block.
yield 31;
另外,箭头标识符右侧需要一个表达 switch 表达式的数值,这是一个很强的约束。
如果一个语句破坏了这个需要,它就不能出现在 switch 表达式里。
比如,下面的代码里的 return 语句,意图退出该方法,而没有表达这个 switch 表达式的数值。
这段代码就不能通过编译器的审查。
int daysInMonth = switch (month)
// snipped
case Calendar.APRIL,
// snipped
Calendar.NOVEMBER ->
// yield 30;
return; // WRONG, return outside of enclosing switch expression.
// snipped
最后一个我们能够看到的变化,是出现了一个新的关键字“yield”。
用JDK8中的lambda表达式中的return去理解他。
大多数情况下,switch 表达式箭头标识符的右侧是一个数值或者是一个表达式。
如果需要一个或者多个语句,我们就要使用代码块的形式。这时候,我们就需要引入一个新的 yield 语句来产生一个值,这个值就成为这个封闭代码块代表的数值。为了便于理解,我们可以把 yield 语句产生的值看成是 switch 表达式的返回值。
所以,yield 只能用在 switch 表达式里,而不能用在 switch 语句里。
case Calendar.FEBRUARY ->
if (((year % 4 == 0) && !(year % 100 == 0))
|| (year % 400 == 0))
yield 29;
else
yield 28;
其实,这里还有一个我们从上述的代码里看不到的变化。在 switch 表达式里,所有的情景都要列举出来,不能多、也不能少(这也就是我们常说的穷举)。
比如说,在上面的例子里,如果没有最后的 default 情景分支,编译器就会报错。
这是一个影响深远的改进,它会使得 switch 表达式的代码更加健壮,大幅度降低维护成本,如果未来需要增加一个情景分支的话,就更是如此了。
int daysInMonth = switch (month)
case Calendar.JANUARY,
// snipped
Calendar.DECEMBER -> 31;
case Calendar.APRIL,
// snipped
Calendar.NOVEMBER -> 30;
case Calendar.FEBRUARY ->
// snipped
// WRONG to comment out the default branch, 'switch' expression
// MUST cover all possible input values.
//
// default -> throw new RuntimeException(
// "Calendar in JDK does not work");
;
三、改进的 switch 语句
那这些变化有没有影响 switch 语句呢?
比如说,我们能够在 switch 语句里使用箭头标识符吗?
我们前面说过,yield 语句是来产生一个 switch 表达式代表的数值的,因此 yield 语句只能用在 switch 表达式里,不能用在 switch 语句。
其他的变化呢?我们还是先来看下面一段代码。
private static int daysInMonth(int year, int month)
int daysInMonth = 0;
switch (month)
case Calendar.JANUARY,
Calendar.MARCH,
Calendar.MAY,
Calendar.JULY,
Calendar.AUGUST,
Calendar.OCTOBER,
Calendar.DECEMBER ->
daysInMonth = 31;
case Calendar.APRIL,
Calendar.JUNE,
Calendar.SEPTEMBER,
Calendar.NOVEMBER ->
daysInMonth = 30;
case Calendar.FEBRUARY ->
if (((year % 4 == 0) && !(year % 100 == 0))
|| (year % 400 == 0))
daysInMonth = 29;
break;
daysInMonth = 28;
// default -> throw new RuntimeException(
// "Calendar in JDK does not work");
return daysInMonth;
在这段代码里,我们看到了箭头标识符,看到了 break 语句,看到了注释掉的 default 语句。
这是一段合法的、能够工作的代码。
换个说法,switch 语句可以使用箭头标识符,也可以使用 break 语句,也不需要列出所有的情景。
表面上看起来,switch 语句的改进不是那么显而易见。
其实,switch 语句的改进主要体现在 break 语句的使用上。
我们应该也看到了,break 语句没有出现在下一个 case 语句之前。
这也就意味着,使用箭头标识符的 switch 语句不再需要 break 语句来实现情景间的代码共享了。
虽然我们还可以这样使用 break 语句,但是已经不再必要了。
switch (month)
// snipped
case Calendar.APRIL,
// snipped
Calendar.NOVEMBER ->
daysInMonth = 30;
break; // UNNECESSARY, could be removed safely.
// snipped
有没有 break 语句,使用箭头标识符的 switch 语句都不会顺次执行下面的操作(fall-through)。
这样,我们前面谈到的 break 语句带来的烦恼也就消失不见了。
不过,使用箭头标识符的 switch 语句并没有禁止 break 语句,而是恢复了它本来的意义:
从代码片段里抽身,就像它在循环语句里扮演的角色一样。
switch (month)
// snipped
case Calendar.FEBRUARY ->
if (((year % 4 == 0) && !(year % 100 == 0))
|| (year % 400 == 0))
daysInMonth = 29;
break; // BREAK the switch statement
daysInMonth = 28;
// snipped
四、怪味的 switch 表达式
我们前面说过,switch 表达式也可以使用冒号标识符。
使用冒号标识符的一个 case 语句只能匹配一个情景,而且支持 fall-through。
和箭头标识符的 switch 表达式一样,使用冒号标识符 switch 表达式也不支持 break 语句,取而代之的是 yield 语句。这是一个充满了怪味道的编码形式,我并不推荐使用这种形式,但我可以带你略作了解。
下面的这段代码,就是我们试着把箭头标识符替换成冒号标识符的一个例子。
你可以比较一下使用冒号标识符和箭头标识符的两段代码,想一想两种不同形式的优劣。
毫无疑问,使用箭头标识符的代码更加简洁。
class DaysInMonth
public static void main(String[] args)
Calendar today = Calendar.getInstance();
int month = today.get(Calendar.MONTH);
int year = today.get(Calendar.YEAR);
int daysInMonth = switch (month)
case Calendar.JANUARY:
case Calendar.MARCH:
case Calendar.MAY:
case Calendar.JULY:
case Calendar.AUGUST:
case Calendar.OCTOBER:
case Calendar.DECEMBER:
yield 31;
case Calendar.APRIL:
case Calendar.JUNE:
case Calendar.SEPTEMBER:
case Calendar.NOVEMBER:
yield 30;
case Calendar.FEBRUARY:
if (((year % 4 == 0) && !(year % 100 == 0))
|| (year % 400 == 0))
yield 29;
else
yield 28;
default:
throw new RuntimeException(
"Calendar in JDK does not work");
;
System.out.println(
"There are " + daysInMonth + " days in this month.");
有了使用箭头标识符的 switch 语句和 switch 表达式之后,不再推荐使用冒号标识符的 switch 语句和 switch 表达式。
学习并使用箭头标识符的 switch 语句和 switch 表达式,会使代码更简洁、更健壮。
五、总结
了解了 switch 表达式和改进的 switch 语句。
讨论了 switch 表达式带来的新概念和新的关键字,了解了这些基本概念以及它们的适用范围。
新的 switch 形式、语句和表达式,不同的使用范围,这些概念交织在一起,让 switch 的学习和使用都变成了一件有点挑战性的事情。
箭头标识符的引入,简化了代码,提高了编码效率。
可是,学习这么多种 switch 的表现形式,也增加了我们的学习负担。
不同的 switch 表达形式,以及它们支持的特征,放在了下面这张表格里。
或者,你也可以记住下面的总结:
- break 语句只能出现在 switch 语句里,不能出现在 switch 表达式里;
- yield 语句只能出现在 switch 表达式里,不能出现在 switch 语句里;
- switch 表达式需要穷举出所有的情景,而 switch 语句不需要情景穷举;
- 使用冒号标识符的 swtich 形式,支持情景间的 fall-through;
- 而使用箭头标识符的 swtich 形式不支持 fall-through;
- 使用箭头标识符的 swtich 形式,一个 case 语句支持多个情景;
- 而使用冒号标识符的 swtich 形式不支持多情景的 case 语句。
使用箭头标识符的 swtich 形式,废止了容易出问题的 fall-through 这个特征。
因此,我们推荐使用箭头标识符的 swtich 形式,逐步废止使用冒号标识符的 swtich 形式。在 switch 表达式和 switch 语句之间,我们应该优先使用 switch 表达式。
这些选择,都可以帮助我们简化代码逻辑,减少代码错误,提高生产效率。
如果你要丰富你的代码评审清单,你可以加入下面这一条:
使用冒号标识符的 swtich 形式,是不是可以更改为使用箭头标识符?
使用 switch 语句赋值的操作,是不是可以更改为使用 switch 表达式?
另外,我还拎出了几个今天讨论过的技术要点,这些都可能在你们面试中出现哦。
通过这一次学习,你应该能够:
- 知道 switch 表达式,并且能够使用 switch 表达式;
- 面试问题:你知道 switch 表达式吗?该怎么处理 switch 表达式里的语句?
- 了解 switch 表达式要解决的问题,并且知道解决掉这些问题的办法;
- 面试问题:使用 switch 表达式有哪些好处?
- 了解不同的 switch 的表现形式,能够看得懂不同的表现形式,并且给出改进意见。
- 面试问题:你更喜欢使用箭头标识符还是冒号标识符?
如果你能够有意识地使用箭头标识符的 switch 表达式,应该可以大幅度提高编码的效率和质量;
如果你能够了解不同的 switch 表现形式,并且对每种形式都有自己的见解,你就能帮助你的同事提高编码的效率和质量。
毫无疑问,在面试的时候,有意识地在代码里使用 switch 表达式,是一个能够展现你的学习能力、理解能力和对新知识的接受能力的一个好机会。
以上是关于Day714. switch表达式 -Java8后最重要新特性的主要内容,如果未能解决你的问题,请参考以下文章