在Java中酝酿模式:一本非正式的入门书
关于模式匹配、记录和密封的想法。
不久前,Java爱好者们被一个巨大的新增功能吞噬了--是的,我们都知道,对吗?- Java 1.8中的Lambda表达式!在Lambda面前黯然失色的,是另一条信息--"不应作为标识符使用,因为从源码级1.8开始,它是一个保留关键字"--被勤奋和迂腐的人看在眼里;等等,我错过了什么吗? 嗯......是的,就像我们中的一些人错过了本文以_开头,一个下划线。下划线正被悄悄地从合法的标识符中移除--它正被提升(嘶......下划线还不知道)到一个更复杂的角色--这只是时代到来的另一个标志--在Java中匹配模式的时代已经到来了
模式匹配的ABC
那么什么是模式匹配呢?所有那些Unix的粉丝都会突然想起古老的awk;维基百科说"AWK ... 用作数据提取的......工具;......对程序中的每个模式进行一行扫描,对于每个匹配的模式,执行相关的动作。"--一个给定的模式被匹配,数据被提取,一些行动被采取--无论是否有数据。如果这听起来太复杂,我们会惊讶地发现,我们一直都在使用这个方法--我们所有人都非常熟悉的查找和替换选项。
让我们以 "千里之行 "为例,在这个例子中,我们要找到并将 "英里 "替换成 "公里"。因此,我们要寻找的模式是 "英里",我们要提取的数据是 "英里 "本身--在这些情况下,模式和数据是一样的--然后我们对数据采取一个行动,即用 "公里 "替换数据--"英里"。这些首要原则在Java模式匹配中也是一样的,尽管在支持语法和语义方面增加了一些花哨的东西。
第一步
我们所有人都会使用instanceof检查,在大多数情况下,我们要做的是将其类型化为我们要检查的类型;即
public void foo(Object obj) {
if (obj instanceof Integer) { // Match
Integer myInteger = (Integer) obj; // Extract
System.out.println(myInteger); // Action
}
}
如果我们仔细观察上面的内容,我们可以清楚地看到一个模式的出现--匹配-提取-行动序列。毫不奇怪,模式匹配通过一个看似简单的改进进入了Java--如下图所示的Pattern Instanceof。
if (obj instanceof Integer myInteger) { // Match and Extract.
System.out.println(myInteger); // Action
}
模式实例将匹配和提取操作合二为一,为程序员提供了更紧凑的代码。这已经成为Java 16中的一个标准功能。
少即是多
随着instanceof模式在 "if语句 "中的初步成功,自然而然的进展就是要找出如何在switch语句中把它自然化。术语 "归化 "是有意使用的,因为在switch中使用同样的结构,直截了当地说,看起来很难看,因此在Java 17中,测试了一个名为 "模式切换 "的预览功能。在Java 17中,这处于第一个预览阶段,让我们看看它看起来如何。
switch (obj) {
case Integer myInteger -> System.out.println(myInteger);
case String myString -> System.out.println(myString);
default -> System.out.println("Object");
}
上面的内容应该是不言自明的,对吗?我们不需要为每一个if语句调用一个pattern instanceof,而只需要在switch语句中得到这个结构,然后我们称之为 "Pattern Switch"。虽然语法和语义对程序员来说很简单,但考虑到我们在Eclipse写的Eclipse Compiler for Java (ecj)就是为了支持这一点(当然,javac的朋友也一样),我可以自信地说,实现这一点的重任并不是一帆风顺的;将其改造成现有的switch结构而不出现退步,经历了许多风暴总之,这一切都完成了,现在我们有了这个漂亮的开关模式。
传统上,switch总是有常量的大小写标签--现在有了Pattern Switches,大小写标签包含类型而不是常数。我们能不能以某种方式把 "常量 "的含义带到类型中去呢? 嗯......好想法......看来我们已经在酝酿这方面的东西了--进入记录和密封类型。这些都是相当复杂的话题,我打算很快就写出自己的博文,但尽管如此,现在让我们简单地浏览一下它们的表面。
记录--一个恒定类的案例
记录的概念是在Java 16中引入的;事实上,它是前两个版本的预览功能,在16中成为标准。现在只需要说,记录实际上是具有特定语法的常量类--一种类似于构造函数的语法--其定义如下所示。
Java
record R(String name, Integer age) {}
现在,请记住,这可以编译成一个 "常量 "类,或只是一个有两个私有最终字段 "姓名 "和 "年龄 "的R类,这两个字段的值需要在实例化该类时给出。我们还有两个编译器 "内置 "的访问器--即 "name() "和 "age()",分别返回name和age的值。因此,随着记录的出现,我们正在实现 "数据 "的恒定性。
密封类型
类型中的恒定性还有一个层面。你可以继续从一个类型派生出子类型。从编译器的角度来看,特别是从模式切换的角度来看,这意味着应该总是有一个 "默认 "的案例臂来做模式的 "全面 "滑移。如果我们真的想实现 "不变性",即在编译时就知道所有的子类型,这样我们就可以列举代码中的所有类型,那么我们应该能够以某种方式 "密封 "层次结构,只 "允许 "我们 "允许 "的那些子类型。而从17日起,这可以通过 "密封"和 "允许"的组合来实现,如下图所示。
sealed interface I permits Y, Z, R {}
final class Y implements I {}
final class Z implements I {}
record R(String name, Integer age) implements I {}
"sealed "和 "permits "是限制性的标识符,它们在类或接口的定义中具有特殊的意义--我们在编译时为层次结构密封接口I,只允许类Y和Z以及记录R实现该接口--从而为编译时检查详尽分析提供额外的力量。注意,R中没有 "final",因为根据定义,记录是final的,因此修饰词final是可选的。
把这一切结合起来
现在,让我们在我们的模式切换中使用这些新知识,看看如果我们在界面I上进行切换会是什么样子。
Java
public void foo(I myInterface) {
switch (myInterface) {
case Y y -> System.out.println(y);
case Z z -> System.out.println(z);
case R r -> System.out.println(r.name());
}
}
细心的读者会发现,由于我们在列举所有的情况,所以缺省的臂膀不见了--类似于我们涵盖所有枚举值的情况--本质是一样的。
未来--不那么遥远
我们满意吗?不错的尝试--我们想要更多,不!我们想要更少--在提取时更少;那么我们能做什么?让我们看看上面的代码--我们看到在记录的情况下,我们没有使用记录的R,因为它是,但我们正在提取名字--我们可以做得更好吗?我们可以,如果我们能够把 "名字和年龄 "放在案例标签本身,如下所示:
case R(name, age) -> System.out.println(name);
现在,提取更简单了;它只是使用了 "名字"--但等等--这只是在讨论阶段,作为记录和数组模式JEP[Java增强建议]的一部分--在此提及,以便我们了解模式匹配的未来发展情况。
哦,UnderScore!你为何而来?
我们已经到了这篇文章的结尾,但正如前面提到的,这些功能中的每一个都值得单独写一篇文章来论证它们的细微差别。我保证很快就会写出这些文章来增加你的无聊感。在你进入深度睡眠之前,让我们看看我们被遗忘的英雄,下划线(_)。这个小家伙在整个故事中的地位如何?
任何尝试过Python的人都会知道,下划线可以用来表示我们对该变量不感兴趣--现在,如果你不知道Python,没关系--这只是Yours Truly不加掩饰地试图展示他比自己知道的更多;鉴于他仍然在学习Java本身,近十年来的进步速度让最懒惰的蜗牛也望尘莫及那么,废话少说......这里的_可能有什么用呢?在上一节的代码中,我们知道我们只使用了 "name";"age "的加入只是为了使语法完整。
case R(name, _) -> System.out.println(name);
如果我们可以用下划线来代替那些我们不使用或不关心的变量--就像上面的代码一样,会怎么样?同样,这也是未来的预期。在类似的情况下,请谨慎对待,在该功能被纳入标准之前,我们不能声称它是一个真正的头像。然而,我只想说,当你看到下划线的时候,请记住它背后有一个完整的故事--永远不要低估"_"的力量。