Skip to content

JEP 443: Unnamed Patterns and Variables (Preview) | 未命名模式与变量(预览)

摘要

使用 未命名模式未命名变量 增强 Java 语言。未命名模式在不声明记录组件的名称或类型的情况下匹配记录组件;未命名变量可以被初始化但不能被使用。两者都用下划线字符“_”表示。这是一个 预览语言特性

目标

  • 通过省略不必要的嵌套模式来提高记录模式的可读性。

  • 通过识别必须声明(例如在 catch 子句中)但不会被使用的变量来提高所有代码的可维护性。

非目标

  • 允许未命名的字段或方法参数不是目标。

  • 改变局部变量的语义不是目标,例如在 确定赋值分析 中。

动机

未使用的模式

记录(JEP 395)和记录模式(JEP 440)共同作用以简化数据处理。记录类将数据项的组件聚合到一个实例中,而接收记录类实例的代码使用带有记录模式的模式匹配将实例分解为其组件。例如:

java
record Point(int x, int y) { }
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) { }

... new ColoredPoint(new Point(3,4), Color.GREEN)...

if (r instanceof ColoredPoint(Point p, Color c)) {
   ... p.x()... p.y()...
}

在这段代码中,程序的一部分创建一个 ColoredPoint 实例,而另一部分使用带有 instanceof 的模式匹配来测试一个变量是否是 ColoredPoint,如果是,则提取它的两个组件。

ColoredPoint(Point p, Color c) 这样的记录模式非常具有描述性,但程序通常只需要一些组件进行进一步处理。例如,上面的代码在 if 块中只需要 p,不需要 c。每次进行这样的模式匹配时,写出记录类的所有组件都很费力。此外,从视觉上不清楚 Color 组件是无关的;这也使得 if 块中的条件更难阅读。当记录模式嵌套以提取组件中的数据时,这一点尤其明显,例如:

java
if (r instanceof ColoredPoint(Point(int x, int y), Color c)) {
   ... x... y...
}

我们可以使用 var 来减少不必要的组件 Color c 的视觉成本,例如 ColoredPoint(Point(int x, int y), var c),但如果完全省略不必要的组件,进一步降低成本会更好。这将通过从代码中去除杂乱内容,既简化编写记录模式的任务,又提高可读性。

随着开发人员在记录类的数据导向方法及其配套机制(密封类,JEP 409)方面获得经验,我们预计对复杂数据结构的模式匹配将变得司空见惯。通常,结构的形状与其内部的各个数据项一样重要。作为一个高度简化的示例,考虑以下 BallBox 类,以及一个探索 Box 内容的 switch

java
sealed abstract class Ball permits RedBall, BlueBall, GreenBall { }
final  class RedBall   extends Ball { }
final  class BlueBall  extends Ball { }
final  class GreenBall extends Ball { }

record Box<T extends Ball>(T content) { }

Box<? extends Ball> b =...
switch (b) {
    case Box(RedBall   red)   -> processBox(b);
    case Box(BlueBall  blue)  -> processBox(b);
    case Box(GreenBall green) -> stopProcessing();
}

每个 case 根据 Box 的内容处理一个 Box,但变量 redbluegreen 未被使用。由于这些变量未被使用,如果我们能省略它们的名称,这段代码会更具可读性。

此外,如果将 switch 重构为将前两个模式组合在一个 case 标签中:

java
case Box(RedBall red), Box(BlueBall blue) -> processBox(b);

那么命名组件将是错误的:由于左边的任何一个模式都可以匹配,所以右边都不能使用这两个名称。由于这些名称不可用,如果我们能省略它们会更好。

未使用的变量

转向传统的命令式代码,大多数开发人员都遇到过必须声明一个他们不打算使用的变量的情况。这通常发生在语句的副作用比其结果更重要的时候。例如,以下代码在循环中计算 total 作为副作用,而不使用循环变量 order

java
int total = 0;
for (Order order : orders) {
    if (total < LIMIT) {
       ... total++...
    }
}

考虑到 order 未被使用,它的声明却很突出,这很不幸。声明可以缩短为 var order,但没有办法避免给这个变量一个名称。名称本身可以缩短为例如 o,但这个语法技巧并没有传达变量将不被使用的语义意图。此外,静态分析工具通常会抱怨未使用的变量,即使开发人员有意不使用变量,并且可能没有办法消除警告。

这里有一个例子,表达式的副作用比其结果更重要,导致出现一个未使用的变量。以下代码出队数据,但每三个元素中只需要两个:

java
Queue<Integer> q =... // x1, y1, z1, x2, y2, z2..
while (q.size() >= 3) {
   int x = q.remove();
   int y = q.remove();
   int z = q.remove(); // z is unused
   ... new Point(x, y)...
}

remove() 的第三次调用具有所需的副作用——出队一个元素——无论其结果是否分配给一个变量,所以 z 的声明可以省略。然而,为了可维护性,开发人员可能希望通过声明一个变量来一致地表示 remove() 的结果,即使它当前未被使用,并且即使它会导致静态分析警告。不幸的是,在许多程序中,变量名称的选择不会像上面代码中的 z 那样容易。

未使用的变量经常出现在另外两种关注副作用的语句中:

  • try-with-resources 语句总是用于其副作用,即自动关闭资源。在某些情况下,资源代表一个上下文,try 块中的代码在该上下文中执行;代码不直接使用上下文,所以资源变量的名称是无关紧要的。例如,假设一个 ScopedContext 资源是 AutoCloseable,以下代码获取并自动释放一个上下文:

    java
    try (var acquiredContext = ScopedContext.acquire()) {
       ... acquiredContext not used...
    }

    名称 acquiredContext 只是杂乱内容,所以省略它会很好。

  • 异常是最终的副作用,处理异常经常会产生一个未使用的变量。例如,大多数 Java 开发人员都写过这种形式的 catch 块,其中异常参数的名称是无关紧要的:

    java
    String s =...;
    try {
        int i = Integer.parseInt(s);
       ... i...
    } catch (NumberFormatException ex) {
        System.out.println("Bad number: " + s);
    }

即使没有副作用的代码有时也必须声明未使用的变量。例如:

java
...stream.collect(Collectors.toMap(String::toUpperCase,
                                   v -> "NODATA"));

这段代码生成一个映射,将每个键映射到相同的占位符值。由于 lambda 参数 v 未被使用,它的名称是无关紧要的。

在所有这些变量未被使用且其名称无关紧要的场景中,如果我们能够简单地声明没有名称的变量会更好。这将使维护人员不必理解无关紧要的名称,并避免静态分析工具对未使用变量的误报。

可以合理地声明为没有名称的变量类型是那些在方法外部没有可见性的变量:局部变量、异常参数和 lambda 参数,如上面所示。这些类型的变量可以重命名或变为未命名而不会产生外部影响。相比之下,字段——即使它们是 private——在方法之间传达对象的状态,未命名的状态既无帮助也不可维护。

描述

未命名模式 用下划线字符“_”(U+005F)表示。它允许在模式匹配中省略记录组件的类型和名称;例如:

  • ... instanceof Point(int x, _)
  • case Point(int x, _)

当类型模式中的模式变量用下划线表示时,就声明了一个 未命名模式变量。它允许在类型模式中的类型或“var”后面的标识符被省略;例如:

  • ... instanceof Point(int x, int _)
  • case Point(int x, int _)

当局部变量声明语句中的局部变量、catch 子句中的异常参数或 lambda 表达式中的 lambda 参数用下划线表示时,就声明了一个 未命名变量。它允许在语句或表达式中的类型或“var”后面的标识符被省略;例如:

  • int _ = q.remove();
  • ... } catch (NumberFormatException _) {...
  • (int x, int _) -> x + x

在单参数 lambda 表达式的情况下,如“_ -> "NODATA"”,作为参数的未命名变量不应与未命名模式混淆。

单个下划线是表示没有名称的最轻量级合理语法。由于它在 Java 1.0 中作为标识符是有效的,我们在 2014 年启动了一个长期过程,以将其用于未命名模式和变量。我们在 Java 8(2014 年)中开始在使用下划线作为标识符时发出编译时警告,并在 Java 9(2017 年,JEP 213)中将这些警告变为错误。许多其他语言,如 Scala 和 Python,使用下划线来声明一个没有名称的变量。

在长度为两个或更多的 标识符 中使用下划线的能力保持不变,因为下划线仍然是 Java 字母和 Java 字母或数字。例如,像“_age”、“MAX_AGE”和“__”(两个下划线)这样的标识符仍然是合法的。

使用下划线作为 数字分隔符 的能力保持不变。例如,像“123_456_789”和“0b1010_0101”这样的数字字面量仍然是合法的。

未命名模式

未命名模式是一个无条件模式,不绑定任何内容。它可以在嵌套位置代替类型模式或记录模式使用。例如:

  • ... instanceof Point(_, int y)

以下是不合法的:

  • r instanceof _
  • r instanceof _(int x, int y)

因此,前面的例子可以完全省略 Color 组件的类型模式:

java
if (r instanceof ColoredPoint(Point(int x, int y), _)) {... x... y... }

同样,我们可以提取 Color 组件,同时省略 Point 组件的记录模式:

java
if (r instanceof ColoredPoint(_, Color c)) {... c... }

在深度嵌套的位置,使用未命名模式可以提高进行复杂数据提取的代码的可读性。例如:

java
if (r instanceof ColoredPoint(Point(int x, _), _)) {... x... }

这段代码在省略 yColor 组件的同时提取嵌套的 Pointx 坐标。

未命名模式变量

未命名模式变量可以出现在任何类型模式中,无论类型模式是在顶级出现还是嵌套在记录模式中。例如,以下两种出现都是合法的:

  • r instanceof Point _
  • r instanceof ColoredPoint(Point(int x, int _), Color _)

通过允许我们省略名称,未命名模式变量使基于类型模式的运行时数据探索在视觉上更加清晰,特别是在 switch 语句和表达式中使用时。

switch 对多个 case 执行相同的操作时,未命名模式变量特别有用。例如,前面的 BoxBall 代码可以重写为:

java
switch (b) {
    case Box(RedBall _), Box(BlueBall _) -> processBox(b);
    case Box(GreenBall _)                -> stopProcessing();
    case Box(_)                          -> pickAnotherBox();
}

前两个 case 使用未命名模式变量,因为它们的右侧不使用 Box 的组件。第三个 case(新添加的)使用未命名模式以匹配具有 null 组件的 Box

具有多个模式的 case 标签可以有一个 保护条件。保护条件管理整个 case,而不是单个模式。例如,假设存在一个 int 变量 x,前面例子的第一个 case 可以进一步约束:

java
case Box(RedBall _), Box(BlueBall _) when x == 42 -> processBox(b);

不允许为每个模式配对一个保护条件,所以以下是禁止的:

java
case Box(RedBall _) when x == 0, Box(BlueBall _) when x == 42 -> processBox(b);

未命名模式是类型模式“var _”的简写。未命名模式和“var _”都不能在模式的顶级使用,所以以下所有都是禁止的:

  • ... instanceof _
  • ... instanceof var _
  • case _
  • case var _

未命名变量

以下类型的声明可以引入命名变量(用标识符表示)或未命名变量(用下划线表示):

  • 块中的局部变量声明语句(JLS 14.4.2),
  • try-with-resources 语句的资源规范(JLS 14.20.3),
  • 基本 for 语句的头部(JLS 14.14.1),
  • 增强 for 循环的头部(JLS 14.14.2),
  • catch 块的异常参数(JLS 14.20),以及
  • lambda 表达式的形式参数(JLS 15.27.1)。

(上面已经涵盖了通过模式声明未命名局部变量的可能性,即模式变量(JLS 14.30.1)。)

声明未命名变量不会将名称放入作用域中,因此在初始化后不能对该变量进行写入或读取。在上述每种声明中,都必须为未命名变量提供一个初始化器。

未命名变量永远不会遮蔽任何其他变量,因为它没有名称,所以可以在同一个块中声明多个未命名变量。

以下是上面的例子,修改为使用未命名变量。

  • 带有副作用的增强 for 循环:

    java
    int acc = 0;
    for (Order _ : orders) {
        if (acc < LIMIT) {
           ... acc++...
        }
    }

    基本 for 循环的初始化也可以声明未命名局部变量:

    java
    for (int i = 0, _ = sideEffect(); i < 10; i++) {... i... }
  • 赋值语句,其中不需要右侧表达式的结果:

    java
    Queue<Integer> q =... // x1, y1, z1, x2, y2, z2,...
    while (q.size() >= 3) {
       var x = q.remove();
       var y = q.remove();
       var _ = q.remove();
      ... new Point(x, y)...
    }

    如果程序只需要处理“x1”、“x2”等坐标,那么可以在多个赋值语句中使用未命名变量:

    java
    while (q.size() >= 3) {
        var x = q.remove();
        var _ = q.remove();
        var _ = q.remove();
       ... new Point(x, 0)...
    }
  • catch 块:

    java
    String s =...
    try {
        int i = Integer.parseInt(s);
       ... i...
    } catch (NumberFormatException _) {
        System.out.println("Bad number: " + s);
    }

    未命名变量可以在多个 catch 块中使用:

    java
    try {... }
    catch (Exception _) {... }
    catch (Throwable _) {... }
  • try-with-resources 中:

    java
    try (var _ = ScopedContext.acquire()) {
       ... no use of acquired resource...
    }
  • lambda 表达式,其参数无关紧要:

    java

...stream.collect(Collectors.toMap(String::toUpperCase, _ -> "NODATA"))
```

风险和假设

  • 我们假设很少有现有和维护的代码使用下划线作为变量名。这样的代码几乎肯定是为 Java 7 或更早版本编写的,并且不能用 Java 9 或更高版本重新编译。对于这样的代码,风险是在读取或写入名为“”的变量以及声明任何其他类型的实体(类、字段等)名为“”时出现编译时错误。我们假设开发人员可以通过例如将“_”重命名为“_1”来修改这样的代码,以避免使用下划线作为变量或任何其他类型实体的名称。

  • 我们期望静态分析工具的开发人员意识到下划线对于未命名变量的新作用,并避免在现代代码中标记此类变量的未使用情况。

替代方案

  • 可以定义一个类似的 未命名方法参数 概念。然而,这与规范(例如,如何为未命名参数编写 JavaDoc?)和重写(例如,重写具有未命名参数的方法意味着什么?)有一些交互。我们在这个 JEP 中不会追求它。

  • JEP 302(Lambda Leftovers)研究了未使用的 lambda 参数的问题,并确定了下划线用于表示它们的作用,但也涵盖了许多其他以其他方式更好处理的问题。这个 JEP 解决了 JEP 302 中探索的未使用 lambda 参数的使用问题,但不解决那里探索的其他问题。