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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章