Java SE 12 扩展 Switch 语句 / 表达式完整指南

阅读数:5161 2019 年 2 月 24 日 08:00

Java SE 12扩展Switch语句/表达式完整指南

本文提供了 Java SE 12 扩展 Switch 语句 / 表达式的完整指南。文章详细介绍了扩展 Java switch 语句将其用作增强版 switch 语句或表达式。为帮助理解本文提供了具体案例。

本文要点

  • 现在 Java 的 switch 语句是遵循类似 C++ 这样的语言而设计的,在默认情况下支持 fall-through 语法。

  • 这种控制流对于编写低级代码非常有用。然而,因为 switch 是运用在高级别语言环境下的,容易出错的问题已经慢慢开始盖过它灵活的优势。

  • 随着 Java 构造者开始支持 Java 语言的模式匹配以来,现在的 switch 语句的不规则性变成了一个障碍。

  • 在 Java 12 中重新增强 switch 让它具备了新的能力,通过扩展现有的 switch 语句,可将其作为增强版的 switch 语句或是“switch 表达式”来简化代码。

  • 这篇文章探索了全新的 Java 12 switch 语句,并提供了基于 JShell 的正确使用和不正确使用的例子。

目前没有不使用 switch 语句的代码库,即使是要做性能调整,相比于 if/else 语句我们也更倾向于使用 switch 语句。自 Java 诞生以来,就已经有 Java switch 语句了,我们都非常习惯使用它了,甚至能够接受它的“怪癖”。

现在 Java 的 switch 语句是遵循类似 C++ 这样的语言而设计的,在默认情况下支持 fall-through 语法。这种控制流对于编写低级代码非常有用。然而,因为 switch 是运用在高级别语言环境下的,它容易出错的问题已经慢慢开始盖过它灵活的优势。

随着 Java 构造者开始支持 Java 语言的模式匹配以来,现在的 switch 语句的不规则性变成了一个障碍。问题包括 switch 块的默认控制流行为;switch 块的默认作用范围,在其中块被作为单个作用范围;以及 switch 只作为语句工作。

在 Java 12 中重新增强 switch 让它具备了新的能力,通过扩展现有的 switch 语句,可将其作为增强版的 switch 语句或是“switch 表达式”来简化代码。

这可以让“传统的”或是“简化的”switch 范围和控制流行为都可以实现。这些变化将会简化日常代码编写,为将来使用 switch 语句或表达式的模式匹配带来便利。

新功能实践

自 JDK 9 以来推出了全新的快速发布计划,每 6 个月发布一次的计划帮助 Java 变得更加有生产力和创新性,功能也会更快更频繁地发布。

从 JDK 9、10、11,到即将到来的 JDK 12(将会在 2019 年 3 月 19 日发布)及其下一个版本,近来的每次 JDK 发布都验证了这一点,它们总能给我们带来新的 Java 语言功能以及很多 API 的改进。

新项目

为了支持这样的改进和新功能,社区需要着重于关注语言某个方面的项目,让 Java 语言工程师专注于新的改进,而不是让所有的东西都在巨大的 JDK 环境下开发。

因此已经创造了很多项目,这些项目都集中在语言的某个特定方面。你可以在 OpenJDK.net 网站中的 project 菜单部分找到它们。这里仅举几个例子,Panama、Valhalla、Amber、Jigsaw、Loom等等。我们在这里详细介绍一下Amber 项目

Amber 项目

Amber 项目由 Compiler Group 发起的,我们可以从发起者的名字猜测到该项目的目标。是的,你猜的很对,这个项目的目标是探索并培育出更小的、生产力导向的 Java 语言功能,在 OpenJDK JEP 过程中被采纳为候选的 JEPs。

以下列出的是 Amber 项目正在开发或已经交付的 JEPs 列表以及其指定的 JDK 要求(如果有的话):

正在开发中的 JEPs:

已经交付的 JEPs:

  • JEP 286 Local-Variable Type Inference (var) (JDK 10)

  • JEP 323 Local-Variable Syntax for Lambda Parameters (JDK 11)

用模式匹配增强 Java

模式匹配技术从 20 世纪 60 年代开始就已经适用于不同风格的编程语言,包括面向文本的语言如SNOBOL4AWK,函数式语言如HaskellML,最近扩展到面向对象的语言如Scala(甚至最近还到了 Microsoft C#语言)。

Amber 项目添加了 Java 语言支持的模式匹配。模式匹配使程序中的通用逻辑可以有条件地从对象中提取组件,来让它们更简洁和安全地表达,这会增强许多还在构建中的语言,比如说JEP 305增强了 instanceof 运算符(还没有指定任何 JDK 版本),以及JEP 325的 switch 表达式(目标是 JDK 12)。

使用全新的 switch 语句

自 Java SE 9 发布以来,在交互式环境工具(REPL)JShell 中试验新的 Java 语言或 API 功能已经成为了传统。它不需要安装任何 IDE 或其他的软件,只需要 JDK 就足够了。

由于每个 JDK 都有这个功能,我们能很容易地试验新的 Java SE 12 语言功能,比如说新扩展的 switch 语句和新的 switch 表达式。我们只需要下载并安装 JDK 12 早期访问版本,可以通过这个链接安装。

成功下载后,解压文件到任意选择的位置,让 JDK 的 bin 文件夹可以从你系统的任何位置访问。

为什么是 JShell?*

我通常喜欢使用交互式编程环境工具,就像大多数现代语言使用的工具一样,方便快速学习 Java 语言语法,了解全新 Java API 和其功能,甚至原型化复杂的代码。

这并不是冗长的编辑、编译和执行代码的循环过程,通常会包括以下几个过程:

  1. 编写完整的代码。

  2. 编译它并修复任何错误。

  3. 运行程序。

  4. 了解发生了什么错误。

  5. 编辑它。

  6. 重复这个过程。

什么是 JShell?

现在 Java 有了 JShell 工具之后拥有了丰富的REPL(读取、求值、输出、循环)实现,这称为 Java Shell,是交互式编程环境。那么,它的神奇之处在哪里呢?其实非常简单。JShell 提供了快速友好的环境,帮助你快速探索、发现并试验 Java 语言功能以及其丰富的库。

使用 JShell,你可以一次输入一个程序元素,就可以立即看到结果,并根据需要进行调整。在 REPL 中你不需要编写完整的代码,你可以写 JShell 指令和 Java 代码片段。

InfoQ最近发表的对于 JShell 的介绍可以帮助你全面了解该工具。想要深入了解学习 JShell 的所有功能,我录制了名为“通过 JShell 实现 Java 10 编程”的视频,可以帮助你掌握这个主题,可以从 Packt 网站查看。

开始 JShell 会话

与该工具交互的第一件事就是打开新的 JShell 会话,如下所示:

  1. 在 Microsoft Windows 系统下,仅需打开命令提示符,输入 jshell 后回车。

  2. 在 Linux 系统下,打开 shell 窗口,输入 jshell 后回车。

  3. 如果你用的是 macOS(之前的 OS X)系统,则打开 Terminal 窗口,输入”jshell”指令,然后回车。

哦耶!该命令打开了新的 JShell 会话,并在 jshell> 提示符显示如下信息:

复制代码
mohamed_taman:~$ jshell --enable-preview
| Welcome to JShell -- Version 12-ea
| For an introduction type: /help intro
jshell>

在上面的第一行中,“Version 12-ea”表示你在使用 Java SE JDK 12 抢先体验版。JShell 的提示性消息用竖线(|)标识,现在你就可以准备输入任何 Java 代码或 JShell 指令和片段。

我相信你已经注意到了 enable-preview 选项。这个选项是干嘛的?听着我的朋友,这个选项帮助你解锁现在在“预览”阶段的任何新语言功能,它们还没有正式成为 JDK 的一部分。这些功能在默认情况下是无法使用的,因为它们还在试验和反馈阶段。

使用这个选项可以帮助开发人员试用预览功能,收集你的反馈来持续改进,你还可以反馈你觉得不好用的部分。在这篇文章中,我们将继续探索新的 switch 表达式功能,这就是为什么我们在打开 JShell 会话的时候要引入该选项。

注意:由于这还是一个预览功能,你还可以在 Amber邮件列表中给出你的反馈。

理论部分已经介绍的足够多了,接下来让我们尝试一些新的 switch 语句或表达式的片段,来帮助理清概念,学习如何使用这个新功能。

剖析 switch 语句和表达式

首先,我们要搞清楚现状,了解当前版本的 switch 语句,甚至了解我们在日常写代码中的奇怪用法。

让我们简单的把 Day 作为变量,枚举 Day(周六、周日、周一等等),基于此返回“周末”还是“工作日”。

在前面打开的 JShell 会话中,让我们开始定义 Day enum:

复制代码
jshell> enum Day{
...> NONE,
...> SAT,
...> SUN,
...> MON,
...> TUS,
...> WED,
...> THU,
...> FRI;
...> }
| created enum Day

接下来,定义我们的方法 String whatIsToday (Day day) 来切换(我们使用的是普通的 switch)day 的值并返回它是工作日还是周末,如下所示:

复制代码
jshell> String whatIsToday(Day day){
...> var today = "";
...> switch(day){
...> case SAT:
...> case SUN: today = "Weekend day";
...> break;
...> case MON:
...> case TUS:
...> case WED:
...> case THU:
...> case FRI: today = "Working day";
...> break;
...> default: today = "N/A";
...> }
...> return today;
...> }
| created method whatIsToday(Day)
jshell> whatIsToday(Day.SUN)
$3 ==> "Weekend day"

尽管这个方法可以正常运作,如果我们使用每个情况都有一个 break 语句的原始版本,就会给视觉带来紊乱,让错误调试变得困难。还会让代码变得冗长,这根本没必要,但少一个 break 语句就会导致发生fall-through

在上面的例子中我们通过 fall-through 机制归类有相同值的几个日子来减少 break 语句的使用,但代码还是很长。

传统 switch 语句的改进

感谢 JEP 325 中提出了全新的”箭头”(switch 标签规则)形式,写为”case L ->”来表示如果标签匹配,仅执行标签右边的代码,而且它可以和 switch 语句或表达式一起使用。

同样的,传统的”colon”语法(switch 标签语句组)也可以用于这两个情况。

尽管colon和全新的箭头语法都适用于这两种情况,colon(:) 的出现并代表着这就是 switch 语句,箭头(->)的出现也代表着这就是 switch 表达式。

现在让我们实践一下之前所讲的理论,比如说,之前的 String whatIsToday(Day day) 可以简化为下面的简单版本:

复制代码
jshell> /edit whatIsToday

通过运行之前的 JShell /edit 指令,它将打开 JShell 编辑板,更方便地修改大块代码段,比如这个方法,这样我们就在 JShell 编辑板中修改代码,显示如下:

复制代码
String whatIsToday(Day day){
var today = "";
switch(day){
case SAT, SUN -> today = "Weekend day";
case MON, TUS, WED, THU, FRI -> today = "Working day";
default -> throw new IllegalArgumentException("Invalid day: " + day.name());
}
return today;
}

当你结束修改之后,点击 Exit 按钮来保存修改并返回当前的 JShell> 会话,输入一些值进行试验:

复制代码
jshell> /edit whatIsToday
| modified method whatIsToday(Day)
jshell> whatIsToday(Day.SUN)
$5 ==> "Weekend day"
jshell> whatIsToday(Day.MON)
$6 ==> "Working day"
jshell> whatIsToday(Day.NONE)
| Exception java.lang.IllegalArgumentException: Invalid day: NONE
| at whatIsToday (#11:6)
| at (#12:1)
jshell>

如果你仔细研究一下之前新的 switch 语句结构,你会注意到这里有很多变化,首先,它更加清晰、简洁并且没有”break”语句,不是吗?

另外,你会发现 switch 语句利用新的”箭头”语法(标签规则)形式来完成切换,而不需要显式地指定 break,这就避免了我们担心的 switch fall-through情况。

需要注意的是”case L ->”switch 标签右边的代码严格规定是表达式、块或(为了方便)throw 语句,就像我们在前面的方法中将识别到的非法的日期抛出 IllegalArgumentException。

单个 switch 情况下的多个逗号分隔标签

在传统的 switch 语句中,当我们需要将多个情况执行相同的一组语句的时候,我们会使用fall-through机制。

但这里,我们要提醒大家需要使用全新的“单个 switch 情况下的多个逗号分隔标签”,只需要用逗号分隔就可以执行相同的语句。

Switch 标签语句组

根据 JEP 325 的说法,传统的”colon”语法(switch 标签语句组)还可以正常使用,我们用 colon 的形式重写之前的方法如下所示:

复制代码
String whatIsToday(Day day){
var today = "";
switch(day){
case SAT, SUN: today = "Weekend day"; break;
case MON, TUS, WED, THU, FRI: today = "Working day"; break;
default: throw new IllegalArgumentException("Invalid day: " + day.name());
}
return today;
}

我会把这部分留给你自己做小练习。你可以自己尝试一下,运行 JShell 指令 /edit whatIsToday,将现在的方法转变为箭头符号语法,使用colon/break语法改写之前的方法,输入几个值试一试,这样你能更深入地了解它。

新的 switch 表达式

在我们深入研究之前,你可能会想在 switch 作为表达式之前是什么样的。在 Java SE 12 之前,switch 一直是语句,它永远都是控制流的结构,不会赋给什么东西。另外,表达式总是能精确得到一个值的结果。因为计算的最终目的都是结果,将值赋给某个目的地。

我们讨论过了语句和表达式的区别,让我们看看新的switch 表达式是如何工作的。实际上,很多日常生活中使用的现有 switch 语句,甚至是上面的代码,都是 switch 表达式的模拟,每个分支要么赋值给某个公共目标变量,要么返回值。

现在我们可以用新的 switch 表达式将值直接返回给要赋值的目标变量,我们来修改一下 StringwhatIsToday(Day day) 方法:

复制代码
String whatIsToday(Day day){
var today = switch(day){
case SAT, SUN -> "Weekend day";
case MON, TUS, WED, THU, FRI -> "Working day";
default -> throw new IllegalArgumentException("Invalid day: " + day.name());
};
return today;
}

仔细来看一下修改之后的方法,我们会发现我们将 switch 语句作为表达式来写:首先,switch 是写在等号之后的;其次,它是语句的一部分,需要以分号结尾,但是传统的 switch 语句并不需要;第三,箭头符号之后是返回值。

就像我之前所说的一样,我们可以使用传统的”colon”语法”case L:”在新的 switch 表达式中,我们可以用 value 语句重写之前使用 break 的方法,如下所示:

复制代码
String whatIsToday(Day day){
var today = switch(day){
case SAT, SUN: break "Weekend day";
case MON, TUS, WED, THU, FRI: break "Working day";
default: throw new IllegalArgumentException("Invalid day: " + day.name());
};
return today;
}

两种形式的 break(有或没有值)都类似于方法中两种形式的返回。

语句块

尽管我们在日常工作中使用新的 switch 语句 / 表达式会使用”colon”或”箭头”语法,在大多数情况下我们还是会使用两种形式任意一个右侧的单个表达式。

然而会有情况需要同时处理多个语句。我们可以简单地使用块{}来实现,我们可以多次在一个 switch 语句 / 表达式中创造并使用相同的变量名,如下所示:

复制代码
String whatIsToday(Day day){
var today = switch(day){
case SAT, SUN: break "Weekend day";
case MON, TUS, WED, THU, FRI:{
var kind = "Working day";
break kind;
}
default: {
var kind = day.name();
System.out.println(kind);
throw new IllegalArgumentException("Invalid day: " + kind);
}
};
return today;
}

它是 poly 表达式

Switch 语句是“poly 表达式”,如果目标类型已知,这个类型可以下推到每个 case 分支中,否则,会综合考虑每个 case 分支推算出一个独立的类型。

想想以下的 switch 表达式赋值:

复制代码
jshell> var day = Day.SUN
day ==> SUN
jshell> var today = switch(day){
...> case SAT, SUN -> "Weekend day";
...> case MON, TUS, WED, THU, FRI -> "Working day";
...> default -> {
...> var len = day.name().length();
...> break len;
...> }
...> };
today ==> "Weekend day"
jshell> var day = Day.NONE
day ==> NONE
jshell> var today = switch(day){
...> case SAT, SUN -> "Weekend day";
...> case MON, TUS, WED, THU, FRI -> "Working day";
...> default -> {
...> var len = day.name().length();
...> break len;
...> }
...> };
4
today ==> 4

如果我们进一步分析上面的 switch 表达式,我们会发现两个分支返回 String,默认分支返回 int 变量 len。综合这两种情况,指定目的变量为 var 类型。

现在,我们显式地声明目标赋值类型是 int 而不是 var,这会让编译器想方设法满足“如果目标类型已知,这种类型会下推到每个 case 分支”这种情况:

复制代码
jshell> int today = switch(day){
...> case SAT, SUN -> "Weekend day";
...> case MON, TUS, WED, THU, FRI -> "Working day";
...> default -> {
...> var len = day.name().length();
...> System.out.println(len);
...> break len;
...> }
...> };
| Error:
| incompatible types: bad type in switch expression
| java.lang.String cannot be converted to int
| case SAT, SUN -> "Weekend day";
| ^-----------^
| Error:
| incompatible types: bad type in switch expression
| java.lang.String cannot be converted to int
| case MON, TUS, WED, THU, FRI -> "Working day";
| ^-----------^

结果如上所示,会报出错误: 不兼容的类型,说 java.lang.String 不能转换为 int 类型,并给出了返回值是 String 类型的两个 case 分支。

新的 switch 特性做不了什么

现在让我们来想一下,我们写什么样的 switch 语句或表达式代码会让编译器停止运作,以及怎么才能避免这样的情况,保证编译器正常运转。

Break 不能返回 switch 语句中的值

如果你尝试在一个 break 语句中使用 switch 语句返回的值,这就会造成编译时错误:

复制代码
jshell> String whatIsToday(Day day){
...> var today = "";
...> switch(day){
...> case SAT, SUN: break "Weekend day";
...> case MON, TUS, WED, THU, FRI: break "Working day";
...> default: throw new IllegalArgumentException("Invalid day: " + day.name());
...> }
...> return today;
...> }
| Error:
| unexpected value break
| case SAT, SUN: break "Weekend day";
| ^------------------^
| Error:
| unexpected value break
| case MON, TUS, WED, THU, FRI: break "Working day";
| ^------------------^
| Error:
| unreachable statement
| return today;
| ^-----------^
jshell>

箭头语法只指向 switch 语句中的语句

同样,如果你尝试使用 switch 语句和箭头语法来返回值,也会造成编译时错误。在这种情况下,箭头语法应该只指向语句而返回值:

复制代码
jshell> String whatIsToday(Day day){
...> var today = "";
...> switch(day){
...> case SAT, SUN -> "Weekend day";
...> case MON, TUS, WED, THU, FRI -> "Working day";
...> default -> throw new IllegalArgumentException("Invalid day: " + day.name());
...> }
...> return today;
...> }
| Error:
| not a statement
| case SAT, SUN -> "Weekend day";
| ^-----------^
| Error:
| not a statement
| case MON, TUS, WED, THU, FRI -> "Working day";

不允许混合使用 colon 和 break 语法

如果你尝试混合使用“箭头”和传统的colon/break语法,编译器就会报错:

复制代码
jshell> String whatIsToday(Day day){
...> var today = "";
...> switch(day){
...> case SAT, SUN -> today = "Weekend day";
...> case MON, TUS, WED, THU, FRI: today = "Working day";break;
...> default -> throw new IllegalArgumentException("Invalid day: " + day.name());
...> }
...> return today;
...> }
| Error:
| different case kinds used in the switch
| case MON, TUS, WED, THU, FRI: today = "Working day";break;
| ^--------------------------------------------------------^

所有匹配情况都要包含在 switch 表达式中

最后,如果特定测试变量的 switch 表达式中没有包含所有的 switch 情况,会产生编译时错误。如果你没有涵盖所有的情况,会发生这样的错误:

复制代码
jshell> String whatIsToday(Day day){
...> var today = switch(day){
...> case SAT, SUN -> "Weekend day";
...> case MON, TUS, WED, THU, FRI -> "Working day";
...> };
...> return today;
...> }
| Error:
| the switch expression does not cover all possible input values
| var today = switch(day){
| ^-----------...

在 switch 表达式中涵盖所有情况并不容易,这一点会让人很郁闷,但是要修复它却不难,只需要添加一个默认 case,它就能作为一个分支正常编译了。你可以亲自尝试将上面的代码修改正确。

Switch 的未来

和之前的 switch 一样,新改进的 switch 语句switch 表达式都能很好地完成枚举。所以,其他类型呢?新的增强的“传统”和“表达式”形式很类似,它们都可以切换 String、int、short、byte、char 和封装类型,到目前为止并没有变化。

在将来的 Java 版本中扩展 switch 是一个目标,比如说允许切换 float、double 和 long(和它们的封装类型),这是之前做不到的。

总结

在本文中,你了解了 Amber 项目,模式匹配以及 JEP 325 是如何扩展 switch 语句的,帮助它作为语句和表达式进行使用,这两种形式都可以使用“传统”或“简化的”范围和控制流行为。

这些变更还能简化日常代码工作,可以避免你忘记添加相关的 break 语句导致的 fall-through 的错误。在上文中,我已经列出了开发人员开始使用新的 switch 语句 / 表达式是可能会犯的错误。

另外,这个语言的变更也是为了 switch 语句 / 表达式的模式匹配(JEP 305)做准备,当前的语言进行了扩展以支持更多的原始类型,比如 float、double 和 long 以及其封装类型。

这篇文章接近尾声,写文章是一种快乐,我希望你也会喜欢读这篇文章!如果你喜欢这篇文章,请点击“喜欢”按钮,并向朋友或在社交媒体中传播这篇文章!

资源

Project Amber

Pattern Matching for Java

JEP 325: Switch Expressions

JEP 305: Pattern Matching for instanceof

Hands-on Java 10 Programming with JShell

Getting Started with Clean Code Java SE 9

作者简介

Mohamed Taman是 @Comtrade 数字服务高级企业架构师、Java Champion、Oracle Groundbreaker Ambassador、采用 Java SE.next()、JakartaEE.next()、是 JCP 成员。曾经是 JCP Executive Committee 成员、JSR 354, 363 & 373 Expert Group 成员、EGJUG 领导人、Oracle Egypt Architects Club 董事会成员、写 Java、喜爱移动、大数据、云、区块链和 DevOps 领域。他是国际讲师,“JavaFX essentials”、“Getting Started with Clean Code, Java SE 9”、"Hands-On Java 10 Programming with JShell" 等书和视频的作者、编写了新书“Secrets of a Java Champions”、获得了 Duke’s choice 2015, 2014 大奖、以及 JCP outstanding adopt-a-jar participant 2013 大奖。

查看英文原文: The Complete Guide to the Java SE 12 Extended Switch Statement/Expression

评论

发布
用户头像
感觉这样搞反倒将switch语句搞复杂了,switch现在有了fall through和非fall through模式,我觉得还是拆成两个关键字好理解一些。
2019 年 03 月 03 日 10:36
回复
用户头像
谢谢
2019 年 02 月 28 日 22:33
回复
没有更多了