Java 10 可能对 Lambda 表达式进行升级

新的 JEP 对增强 Lambda 提出了修改建议,包括消除歧意、用下划线表示未使用的参数、隐藏外部变量 等。虽然这些改变会让 Java 的 Lambda 更接近于其它语言的 Lambda,不过最初的讨论只是想支持混合水平。这个 JEP 补充了一系列的提议来改善 Java 语言,包括局部变量的类型推断增强的枚举,这些内容都有可能包含在 Java 10 中。

尽管与 Lambda 相关的改变只有 3 个,但它们之间并没什么联系,它们中哪个更受欢迎完全取决于反馈。因此,我们将在本文中分别进行说明。

更好地消除歧义

Lambda 从 Java 8 开始加入 Java 语言,这需要修改类型推导来支持它们。但是之前的改变并没有达到它们的需求,而部分原因是那些改变可能会让新接触 Lambda 的开发者困惑。不过现在情况正在发生变化,在上下文提供了足够多信息的情况下,编译器仍然不能推导 Lambda 的类型会让开发者们沮丧。下面的例子说明了这个时候的 Lambda 类型推导:

// 情况 1: Lambda 被推导为 Predicate<String> 类型
//         第一个函数重载被调用。
private void m(Predicate<String> ps) { /* ... */ }
private void m(Function<String, String> fss) { /* ... */ }

private void callingM() {
    m((String s) -> s.isEmpty()); 
}

// 情况 2: 没有足够的信息来推导 Lambda 的类型,
//         不过 m2 没有重载。
//         方法参数的类型会用来推断 Lambda,
//         所以 s 是 String, s.length() 返回 Integer
private void m2(Function<String, Integer> fsi) { /* ... */ }
private void callingM2() {
    m2(s -> s.length());
}

// 情况 3: 没有足够的信息来推导 Lambda 的类型。
//         m3 有重载,不同的重载有不同的参数数量,
//         只有第一个重载匹配1个参数。
//         方法参数的类型会用于推断 Lambda, 
//         所以 s 是 String, s.length() 返回 Integer
private void m3(Function<String, Integer> fsi) { /* ... */ }
private void m3(Function<String, Integer> fsi, String s) { /* ... */ }

private void callingM3() {
    m3(s -> s.length());
}

// 情况 4: 没有足够的信息来推导 Lambda的类型
//         m4 的多个重载参数数量相同,
//         不清楚该调用哪一个,错误
private void m4(Predicate<String> ps)  { /* ... */ }
private void m4(Function<String, String> fss)  { /* ... */ }

private void callingM4() {
    m4(s -> s.isEmpty());
}

在最后一种情况下,有足够的信息可以推导出 m4 的第一个重载可用,然而当前的编译器不会使用这些信息。在新的提议下,编译器会通过以下步骤消除歧义:

  1. 两种可能的情况都需要 Lambda 的参数是 String,所以 s 可以认定为 String 类型
  2. 现在知道 s 是 String 了,那就会知道 String.isEmpty() 返回 boolean
  3. 既然 Labmda 返回 boolean,m4 的第二个重载就不能匹配,排除掉它
  4. 剩下唯一可选的是 m4 的第一个重载,它与根据 Lambda 推导的类型匹配,所以使用它

类似的论证可以应用于方法引用。

用下划线表示不用的参数

在某些情况下,预期 Lambda 会有多个参数,虽然代码块中不会完全用到,却要开发者为这些不用的参数命名。这个改变允许使用下划线来表示不使用的参数。

Function<String, Integer> noneByDefault = notUsed -> 0; // 当前
Function<String, Integer> noneByDefault = _ -> 0; // 提议

这个特性已经存在于其它一些语言,比如 Scala、Ruby 或 Prolog。不过到 Java 7,这都并不容易实现, 因为下划线是一个合法的标识符,所以代码中可能会用到。为了在不引起大量重写代码的前提下引入这个改变, 就不能操之过急:

  1. Java 8:如果下划线用作标识符,会产生一个警告,告诉开发者避免使用它;在 Lambda 中不允许使用下划线(这不会引起向后兼容的问题,因为 Lambda 是 Java 8 引入的)。
  2. Java 9:前面提到的警告已经转变为错误,这确保 Java 代码中不使用下划线作为标识符。
  3. Java 10 (及以后):下划线再次可用作标识符,但它只能作为 Lambda 表达式的参数使用。

从一开始就并非所有人都一致支持这个改变;有些用户喜欢新提议带来的简洁语法,而另外一些人则喜欢使用明确的名称。进一步讨论也许能达成共识。

影子参数

[译者注:shadow variable,通常译为隐藏变量。它是指在某个用域中定义的变量名与它直接的外部作用域的某个变量重名,那么在当前作用域,这个变量就隐藏外部作用域的同名变量。shadow parameter 意义近似。]

也许这是新提议中最有争议的一个功能。目前 Lambda 的参数不能隐藏外部变量,这就意味着在当前作用域内必须选择与其它可访问变量不同的名称;这与其它封装的作用域工作原理类似,比如 while 循环或 if 语句:

String s = "hello";

if(finished) {
    String s = "bye"; // 错误,s 已经定义了
}

Predicate<String> ps = s -> s.isEmpty(); // 错误,s 已经定义了

如果这个提议被接受,Lambda 的参数就可以隐藏外部已存在的标识符并再次使用它。它的好处是某些情况下不再需要一个意义不太明确的 Labmda 参数名(对上面的例子的一个典型的修正是 s2 -> s2.isEmpty())。不过它也可能带来像 Roy Van Rijn,国际有名的演说家,提到的潜在错误,它提到

Map<String, Integer> map = /* ... */
String key = "theInitialKey";

map.computeIfAbsent(key, _ -> {
   String key = "theShadowKey"; // 影子变量
   return key.length();
});

目前上面的代码还不是正确的代码,但在新提议下它就是正确的。如果注释“影子变量”那一行被删掉,代码仍然可以编译和运行,但它会做完全不同的事情。

仍然需要通过大量的讨论来评估是否将上面提到的东西引入 Java,以及以什么样的形式引入。不管怎么说,在 Java 8 中引入 Lambda 很明显是第一步,接下来还有一大批对 Java 的改进。



相关文章

发表评论

Comment form

(*) 表示必填项

还没有评论。

跳到底部
返回顶部