跳到主要内容

JEP 443:未命名模式和变量(预览)

概括

使用未命名模式和__未命名变量(可以初始化但不能使用)增强 Java 语言,未命名模式匹配记录组件而不说明组件的名称或类型。两者都由下划线字符 表示_。这是预览语言功能

目标

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

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

非目标

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

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

动机

未使用的图案

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

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测试变量是否为 a ColoredPoint,如果是,则提取其两个组件。

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

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类,以及switch探索 a 内容的 a Box

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处理 a ,但不使用Box变量redblue和。green由于未使用变量,如果我们可以省略它们的名称,则该代码将更具可读性。

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

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

那么命名组件就是错误的:这两个名称在右侧均不可用,因为左侧的任何一个模式都可以匹配。由于这些名称无法使用,如果我们能够删除它们会更好。

未使用的变量

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

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

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

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

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上面的代码那么容易。

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

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

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

    这个名字acquiredContext只是很混乱,所以最好省略它。

  • 异常是最终的副作用,处理异常通常会产生未使用的变量。例如,大多数 Java 开发人员都编写了catch这种形式的块,其中异常参数的名称是不相关的:

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

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

...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 _)

当局部变量声明语句中的局部变量、子句中的异常参数或 lambda 表达式中的 lambda 参数用下划线表示时,就声明了_未命名变量_。它允许省略catch类型后面或语句或表达式中的标识符;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,使用下划线来声明没有名称的变量。

在长度为 2 或以上的标识符中使用下划线的能力没有改变,因为下划线仍然是 Java 字母和 Java 字母或数字。例如,诸如_ageandMAX_AGE__(两个下划线)之类的标识符仍然是合法的。

使用下划线作为数字分隔符的功能没有改变。例如,诸如123_456_789和 之类的数字文字0b1010_0101仍然是合法的。

未命名的模式

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

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

是合法的,但这些不是:

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

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

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

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

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

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

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

此代码提取x嵌套的坐标Point,同时省略yColor组件。

未命名的模式变量

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

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

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

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

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

前两种情况使用未命名模式变量,因为它们的右侧不使用Boxs 组件。第三种情况是新的,它使用未命名模式来将 aBoxnull组件进行匹配。

具有多个图案的标签case可以有一个防护罩。警卫管理整个案件,而不是个别模式。例如,假设有一个int变量x,则可以进一步约束上一个示例的第一种情况:

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

不允许将防护与每个模式配对,因此这是禁止的:

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),
  • -with-resources 语句的资源规范try(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具有副作用的增强循环:

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

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

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

    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) ...
    }

    如果程序只需要处理x1x2等坐标,则可以在多个赋值语句中使用未命名变量:

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

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

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

    try { ... } 
    catch (Exception _) { ... }
    catch (Throwable _) { ... }
  • try资源中:

    try (var _ = ScopedContext.acquire()) {
    ... no use of acquired resource ...
    }
  • 参数不相关的 lambda:

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

风险和假设

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

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

备择方案

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

  • JEP 302(Lambda Leftovers)检查了未使用的 lambda 参数的问题,并确定了下划线的作用来表示它们,但也涵盖了许多其他问题,这些问题通过其他方式得到了更好的处理。此 JEP 解决了 JEP 302 中探讨的未使用 lambda 参数的使用问题,但没有解决其中探讨的其他问题。