与 Brian Goetz 聊 Java 的模式匹配

  • Michael Redlich
  • 薛命灯

2017 年 10 月 8 日

话题:JavaDevOps语言 & 开发架构AI

InfoQ 采访了来自 Oracle 的 Java 语言架构师Brian Goetz和编程语言研究员Gavin Bierman,谈论了有可能被集成到 Java 语言中的模式匹配

动机

之所有要研究是否有可能在 Java 中加入模式匹配,主要还是为了改进 Java 的语言特性。假如有这样的一段代码:

if (obj instanceof Integer) {
    int intValue = ((Integer) obj).intValue();
    // 使用 intValue
    }

这段代码做了三个操作:

  • 判断 obj 是否是一个 Integer 类型
  • 将 obj 转成 Integer 类型
  • 从 Integer 中抽取出 int

现在再来看看在 if...else 结构的语句中判断其他类型。

String formatted = "unknown";
if (obj instanceof Integer) {
    int i = (Integer) obj;
    formatted = String.format("int %d", i);
    }
else if (obj instanceof Byte) {
    byte b = (Byte) obj;
    formatted = String.format("byte %d", b);
    }
else if (obj instanceof Long) {
    long l = (Long) obj;
    formatted = String.format("long %d", l);
    }
else if (obj instanceof Double) {
    double d = (Double) obj;
    formatted = String.format("double %f", d);
    }
else if (obj instanceof String) {
    String s = (String) obj;
    formatted = String.format("String %s", s);
    }
...

虽然上述的代码是可运行的,也很好理解,但写起来很枯燥(太多重复的样板代码),也容易产生 bug。过多的样板代码会让业务逻辑变得含糊不清——如果 instanceof 方法已经判断出传入的实例是何种类型,那么就没必要重复进行转型了。

Goetz 和 Bierman 解释了他们想要做出的改进。

我们认为是时候让 Java 拥抱模式匹配了,而不仅仅是把它作为一种临时的解决方案。很多编程语言从 60 年代开始就已经采用了模式匹配,包括面向文本的编程语言SNOBOL4AWK、函数编程语言Haskell和 ML,以及最近的面向对象编程语言 Scala 和 C#。

模式由判断谓语(predicate)和一系列绑定变量(binding variable)组成,判断谓语被应用在目标上面,而绑定变量是从目标中抽取出来的。

if (x matches Integer i) {
    // 使用 i
    }

Goetz 和 Bierman 使用过各种模式匹配,它们使用了关键字 matches 和 exprswitch。

matches 操作符

matches 操作符可以用来代替 instanceof 操作。例如:

String formatted;switch (obj) {
    case Integer i: formatted = String.format("int %d", i); break;
    case Byte b:    formatted = String.format("byte %d", b); break;
    case Long l:    formatted = String.format("long %d", l); break;
    case Double d:  formatted = String.format(“double %f", d); break;
    default:        formatted = String.format("String %s", s);
    }...

只有当变量 x 与某个 Integer 实例匹配时,变量 i 才能被使用。如果把 Integer 类型扩展到其他类型,那么 if...else 结构里的类型转换就可以省掉了。

switch 的改进

Goetz 和 Bierman 解释说,“switch 语句就是一种最完美的模式匹配”。例如:

String formatted;switch (obj) {
    case Integer i: formatted = String.format("int %d", i); break;
    case Byte b:    formatted = String.format("byte %d", b); break;
    case Long l:    formatted = String.format("long %d", l); break;
    case Double d:  formatted = String.format(“double %f", d); break;
    default:        formatted = String.format("String %s", s);
    }...

上面的代码清晰易懂。不过,Goetz 和 Bierman 也指出了 switch 的一个局限——“它只是一个语句,所以分支也必须是语句。我们希望可以把它们变成三元操作符那样的表达式,这样就可以保证只对其中的一个表达式求值”。

他们建议引入一种新的表达式语句——exprswtich。

String formatted =
    exprswitch (obj) {
        case Integer i -> String.format("int %d", i);
        case Byte b    -> String.format("byte %d", b);
        case Long l    -> String.format("long %d", l);
        case Double d  -> String.format(“double %f", d);
        default        -> String.format("String %s", s);
        };
...

Goetz 和 Bierman 建议的模式如下所述。

  • 类型检测模式(将被转型的目标绑定到变量)
  • 解构模式(解构目标并进行递归匹配)
  • 常量模式(等值匹配)
  • 变量模式(任意匹配并绑定目标)
  • 下划线模式(任意匹配)

以下是 Goetz 与 InfoQ 的谈话内容。

InfoQ:在你发布论文后,社区都有哪些反馈?

Goetz:我们收到了非常积极的反馈。在其他语言里使用过模式匹配的人都很喜欢这个特性,他们也希望能够在 Java 中使用它。对于那些之前没有使用模式匹配的人,我们希望他们能够学会使用这个特性,我们认为很有必要在 Java 里添加这一特性。

InfoQ:Scala 的匹配操作符对 Java 模式匹配的设计有多大的影响?有什么事情是 Scala 的匹配操作能做的而 Java 却做不到的吗?

Goetz:Scala 只是众多启发我们在 Java 中加入模式匹配的语言之一。为一门语言添加特性不外乎从其他语言那里“移植”,但实际上,我们并不希望做得跟 Scala 完全一样,我们只要能够做到 Scala 的一部分,同时也能做 Scala 做不到的。

我们认为我们更有可能将模式匹配深度集成到对象模型中,比 Scala 有过之而无不及。Scala 的模式是静态的,难以重载或覆盖。虽说能够做到这样已经很好了,但我们希望能够做得更好。

解构(deconstruction)是构造(construction)的另一面,面向对象编程语言让我们可以构造对象(构造器、工厂、构建器),而解构将给我们带来更丰富的 API。虽说模式匹配与面向函数语言有一定的历史渊源,但我们相信它在面向对象语言里将会得到更好的发扬。

人们对语言特性津津乐道,不过我们认为语言特性真正的作用应该是为软件库提供更好的服务,而模式匹配将帮助我们写出更简单、更安全的软件库。

以 java.lang.Class 的两个方法为例:

    public boolean isArray() { ... }
    public Class getComponentType() { ... }

第二个方法需要以第一个方法返回 true 作为前提。对于 API 提供者(需要些更多代码,也需要更多的测试)和用户(容易出错)来说,涉及多 API 的逻辑操作就意味着复杂性。从逻辑上看,这两个方法就是一个模式,融合了“判断这个类是否是一个数组类”和“根据条件抽取组件类型”。如果能够通过以下的表达式来表达,那么代码写起来就更简单了,而且不容易出错。

if (aClass matches Class.arrayClass(var componentType)) { ... }

InfoQ:这次是否把让 Scala rebase 模式匹配作为目标(比如 Scala 2.12 就基于接口对 trait 进行了 rebase)?

Goetz:与 Lambda 表达式一样,我们希望在设计这一语言特性的过程中,能够找到一些构建块,并把它们集成到底层的平台中,让其他语言也能从中获益,并为多个语言提供更好的互操作性。

InfoQ:Scala 在实现这一特性时添加了很多额外的字节码用于支持解构 case 类型,这样会造成负面影响吗?

Goetz:这样做最大的问题在于,编译器不再只是个编译器了,它往类成员里添加了语义。虽然这样做很方便,但可能不是用户想要的,比如,比较数组要用 Arrays.equals(),而不能用 Object.equals()。

InfoQ:解构是否仅限于数据类(data class)?

Goetz:我们计划分几次来发布模式匹配功能,最开始先发布类型检测,然后是数据类的解构模式,最后是用户自定义的解构模式。虽说解构不会仅限于数据类,但这一过程还是需要一些时间。

InfoQ:你能够解释一下数据类和值类型(value type)之间的关系吗?

Goetz:它们之间是一种正交关系。值就是一种合体,没有标识。通过显式地拒用标识,运行时可以优化内存布局,扁平化对象头部,更自由地跨同步点缓存值组件。数据类简化了类表示和 API 协定之间复杂的关系,编译器就可以注入常用的类成员,如构造器、模式匹配器、equals 方法、hashCode 方法和 toString 方法。有些类可以被声明成值类(value class),有些则适合被声明成数据类,或者都不声明,或者都声明,这些情况都有可能。

InfoQ:Sealing 特性是否需要源码编译器的支持?

Goetz:Sealing 特性不仅仅需要编译器的支持,也需要 JVM 的支持,这样语言层面的约束——比如“X 不能继承 Y”——就可以在 JVM 层面得到加强。

InfoQ:Sealing 是否意味着“只能在当前模块内扩展出子类”?

Goetz:Sealing 可以有多种说法,最简单的就是“这个类只能在同一个源码文件中被扩展”——这是最常见的情况,也是最简单的。Sealing 也可以被定义成“同一个包中”或“同一个模块中”,甚至可以是“友联(friend)”或复杂的运行时判断。

InfoQ:Java 的新发布周期有助于模式匹配被集成到 Java 中吗?

Goetz:我们希望如此。我们已经将模式匹配分为几个小块,这样就可以快速地推出最简单的部分,然后继续开发其他部分。

InfoQ:什么时候可以看到原型?

Goetz:现在就有了,尝鲜者可以直接从源代码编译 JDK。“Amber”上有一个分支已经可以支持类型检测模式和“matches”判断谓语。

InfoQ:你们将会怎样继续关于模式匹配的研究工作?

Goetz:我们会继续探究如何将匹配器作为类成员,以及如何实现重载和继承。我们还有很多工作要做。

更多资源

查看英文原文:Brian Goetz Speaks to InfoQ on Pattern Matching for Java

JavaDevOps语言 & 开发架构AI