跳到主要内容

JEP 286:局部变量类型推断

概括

增强 Java 语言,将类型推断扩展到带有初始值设定项的局部变量声明。

目标

我们寻求通过减少与编写 Java 代码相关的仪式来改善开发人员的体验,同时通过允许开发人员省略通常不必要的局部变量类型的清单声明来保持 Java 对静态类型安全的承诺。例如,此功能允许声明如下:

var list = new ArrayList<String>();  // infers ArrayList<String>
var stream = list.stream(); // infers Stream<String>

这种处理仅限于带有初始化器的局部变量、增强型for循环中的索引以及传统for循环中声明的局部变量;它不适用于方法形式、构造函数形式、方法返回类型、字段、catch 形式或任何其他类型的变量声明。

成功标准

从数量上讲,我们希望实际代码库中很大一部分局部变量声明可以使用此功能进行转换,从而推断出适当的类型。

定性地,我们希望典型用户能够理解局部变量类型推断的限制以及这些限制的动机。 (当然,这通常是不可能实现的;我们不仅无法为所有局部变量推断出合理的类型,而且一些用户将类型推断视为一种读心术,而不是一种约束求解算法,在这种情况下,没有解释似乎是明智的。)但是我们试图以这样一种方式来划清界限,以便可以清楚地说明为什么特定的构造超出了界限——并且以这样的方式编译器诊断可以有效地将它连接起来用户代码的复杂性,而不是语言的任意限制。

动机

开发人员经常抱怨 Java 所需的样板编码程度。当地人的清单类型声明通常被认为是不必要的,甚至是碍事的;有了良好的变量命名,通常就可以清楚地知道发生了什么。

为每个变量提供清单类型的需要也无意中鼓励开发人员使用过于复杂的表达式;使用较低仪式的声明语法,将复杂的链式或嵌套表达式分解为更简单的表达式的阻碍性较小。

几乎所有其他流行的静态类型“大括号”语言,无论是在 JVM 上还是在 JVM 之外,都已经支持某种形式的局部变量类型推断:C++ (auto)、C# (var)、Scala (var/val)、Go (声明与:=)。 Java 几乎是唯一一种不支持局部变量类型推断的流行静态类型语言;至此,这应该不再是一个有争议的功能了。

Java SE 8 中类型推断的范围显着扩大,包括嵌套和链式泛型方法调用的扩展推断以及 lambda 形式的推断。这使得构建为调用链设计的 API 变得更加容易,并且此类 API(例如 Streams)非常流行,这表明开发人员已经习惯了推断中间类型。在如下调用链中:

int maxWeight = blocks.stream()
.filter(b -> b.getColor() == BLUE)
.mapToInt(Block::getWeight)
.max();

没有人会担心(甚至没有注意到)中间类型Stream<Block>IntStream以及 lambda 形式的类型b没有明确出现在源代码中。

局部变量类型推断可以在结构不太紧密的 API 中实现类似的效果;局部变量的许多用途本质上都是链,并且同样受益于推理,例如:

var path = Paths.get(fileName);
var bytes = Files.readAllBytes(path);

描述

对于带有初始值设定项的局部变量声明、增强for型循环索引以及在传统 for 循环中声明的索引变量,允许var接受保留类型名称来代替清单类型:

var list = new ArrayList<String>(); // infers ArrayList<String>
var stream = list.stream(); // infers Stream<String>

标识符var不是关键字;相反,它是一个_保留的类型名称_。这意味着用作var变量、方法或包名称的代码不会受到影响;var用作类或接口名称的代码将受到影响(但这些名称在实践中很少见,因为它们违反了通常的命名约定)。

不允许使用缺少初始化器、声明多个变量、具有额外数​​组维数括号或引用正在初始化的变量的局部变量声明形式。在没有初始化器的情况下拒绝局部变量会缩小功能的范围,避免“远距离动作”推理错误,并且仅排除典型程序中的一小部分局部变量。

本质上,推理过程只是为变量提供其初始值设定项表达式的类型。一些微妙之处:

  • 初始化器没有目标类型(因为我们还没有推断出它)。需要此类类型的 Poly 表达式(例如 lambda、方法引用和数组初始值设定项)将触发错误。
  • 如果初始化器具有 null 类型,则会发生错误 - 就像没有初始化器的变量一样,该变量可能打算稍后初始化,但我们不知道需要什么类型。
  • 捕获变量和具有嵌套捕获变量的类型将_投影_到未提及捕获变量的超类型。此映射将捕获变量替换为其上限,并将提及捕获变量的类型参数替换为有界通配符(然后重复)。这保留了传统上有限的捕获变量范围,这些变量仅在单个语句中考虑。
  • 除了上述例外之外,还可以推断不可表示类型,包括匿名类类型和交集类型。编译器和工具需要考虑这种可能性。

适用性和影响

扫描 OpenJDK 代码库中的局部变量声明,我们发现 13% 不能使用 来编写var,因为没有初始化器、初始化器具有 null 类型,或者(很少)初始化器需要目标类型。其余局部变量声明中:

  • 94% 的初始化器具有与源代码中存在的确切类型(63% 的情况具有参数化类型)
  • 5% 的初始化器具有一些更清晰的可表示类型(29% 的情况具有参数化类型)
  • 1% 的初始化器的类型提到了捕获变量(7% 的情况具有参数化类型)
  • <1% 具有匿名类类型或交集类型的初始值设定项(对于参数化类型的情况相同)

备择方案

我们可以继续要求局部变量类型的清单声明。

var我们可以限制对变量声明中使用 Diamond 的支持,而不是支持;这将解决 所解决的案例的子集var

上述设计包含了有关范围、语法和不可指示类型的多项决策;此处记录了也考虑过的那些选择的替代方案。

范围选择

我们还可以通过其他几种方式来确定此功能的范围。我们考虑将该功能限制为有效的最终局部变量 ( val)。然而,我们放弃了这一立场,因为:

  • 无论如何,大多数(在 JDK 和更广泛的语料库中超过 75%)具有初始值设定项的局部变量实际上已经是不可变的,这意味着该功能可能提供的任何远离可变性的“推动”都会受到限制;

  • lambdas/内部类的捕获能力已经为有效的最终局部变量提供了重要的推动力;

  • 在具有(例如)7 个有效最终局部变量和 2 个可变局部变量的代码块中,可变变量所需的类型在视觉上会很不协调,从而破坏了该功能的大部分好处。

另一方面,我们可以扩展此功能以包括“空白”finals 的本地等效项(即,不需要初始值设定项,而是依赖于明确的赋值分析。)我们选择对“仅具有初始值设定项的变量”的限制,因为它覆盖了很大一部分候选人,同时保持了功能的简单性并减少了“远距离操作”错误。

同样,我们也可以在推断类型时考虑所有赋值,而不仅仅是初始化器;虽然这将进一步增加可以利用此功能的当地人的百分比,但它也会增加“远距离行动”错误的风险。

语法选择

关于语法有不同的意见。这里的两个主要自由度是使用什么关键字(varauto等),以及是否为不可变局部变量使用单独的新形式(vallet)。我们考虑了以下语法选项:

  • var x = expr仅(如 C#)
  • var,加上val不可变的局部变量(例如 Scala、Kotlin)
  • var,加上let不可变的本地变量(如 Swift)
  • auto x = expr(如 C++)
  • const x = expr(已经是保留字)
  • final x = expr(已经是保留字)
  • let x = expr
  • def x = expr(如 Groovy)
  • x := expr(如围棋)

在收集了大量意见后,var显然优于 Groovy、C++ 或 Go 方法。对于不可变局部变量的第二种语法形式存在很大的分歧(val, let);这将是额外仪式与额外捕捉设计意图的权衡。最终我们选择只支持var。有关基本原理的一些详细信息可以在此处找到。

不可表示类型

有时初始化器的类型是不可表示的类型,例如捕获变量类型、交集类型或匿名类类型。在这种情况下,我们可以选择是否 i) 推断类型,ii) 拒绝表达式,或 iii) 推断可指示的超类型。

编译器(和细心的程序员!)必须已经能够轻松地推理不可指示的类型。然而,将它们用作局部变量的类型将显着增加它们的暴露程度,揭示编译器/规范错误并迫使程序员更频繁地面对它们。从教学上来说,在显式类型和隐式类型声明之间进行简单的语法转换是很好的。

也就是说,简单地拒绝具有不可指示类型的初始值设定项是无益的学究气(通常会让程序员感到惊讶,例如在声明中var c = getClass())。到超类型的映射可能是意外的和有损的。

这些考虑导致我们得出不同的答案:

  • 类型null变量实际上是无用的,并且对于推断类型没有好的替代方案,因此我们拒绝这些。
  • 允许捕获变量流入后续语句为语言增加了新的表现力,但这不是此功能的目标。相反,我们无论如何都需要使用所提议的_投影_操作来解决类型系统中的各种错误(例如,参见JDK-8016196),并且在这里应用它是合理的。
  • 交集类型特别难以映射到超类型 - 它们没有排序,因此交集的一个元素本质上并不比其他元素“更好”。超类型的稳定选择是所有元素的_lub_,但这通常Object或同样无益。所以我们允许他们。
  • 匿名类类型无法命名,但它们很容易理解——它们只是类。允许变量具有匿名类类型引入了用于声明本地类的单例实例的有用简写。我们允许他们。

风险和假设

风险:由于 Java 已经在 RHS 上进行了重要的类型推断(lambda 形式、泛型方法类型参数、菱形),因此存在尝试在此类表达式的 LHS 上使用会失败的风险var,并且可能会导致难以阅读错误消息。

我们通过在推断 LHS 时使用简化的错误消息来缓解这一问题。

例子:

Main.java:81: error: cannot infer type for local
variable x
var x;
^
(cannot use 'val' on variable without initializer)

Main.java:82: error: cannot infer type for local
variable f
var f = () -> { };
^
(lambda expression needs an explicit target-type)

Main.java:83: error: cannot infer type for local
variable g
var g = null;
^
(variable initializer is 'null')

Main.java:84: error: cannot infer type for local
variable c
var c = l();
^
(inferred type is non denotable)

Main.java:195: error: cannot infer type for local variable m
var m = this::l;
^
(method reference needs an explicit target-type)

Main.java:199: error: cannot infer type for local variable k
var k = { 1 , 2 };
^
(array initializer needs an explicit target-type)

风险:源不兼容(有人可能使用了var类型名称。)

通过保留类型名称来缓解;像这样的名称var不符合类型的命名约定,因此不太可能用作类型。名称var _通常_用作标识符;我们继续允许这样做。

风险:可读性降低、重构时出现意外。

与任何其他语言功能一样,局部变量类型推断可用于编写清晰和不清楚的代码;最终,编写清晰代码的责任在于用户。请参阅使用风格指南var常见问题