为啥不能为 Java 中的 var 关键字分配 lambda 表达式?

Posted

技术标签:

【中文标题】为啥不能为 Java 中的 var 关键字分配 lambda 表达式?【英文标题】:Why can't the var keyword in Java be assigned a lambda expression?为什么不能为 Java 中的 var 关键字分配 lambda 表达式? 【发布时间】:2018-09-09 17:43:48 【问题描述】:

在 Java 10 中,var 可以用如下字符串赋值:

var foo = "boo";

虽然不允许使用 lambda 表达式对其进行赋值,例如:

var predicateVar = apple -> apple.getColor().equals("red");

当它可以推断出 StringArrayList、用户类等其余部分时,为什么它不能推断出 lambda 或方法引用类型?

【问题讨论】:

应该推断出var 的具体类型是什么?标准功能接口之一还是您自己的一个?该决定应基于什么? @lexicore 我希望Predicate<Apple>(假设appleApple 类型)。这就是为什么这是一个错误,而不是一个功能。 我从来没有暗示过任何关于性能的事情,我不知道你为什么这么问。我的评论的重点是你不能从它的初始化器推断你的变量的类型。正如我已经证明的那样,这是模棱两可的。 编码速度是次要考虑因素。你 90% 的时间将花在阅读和维护现有代码上,而不是编写新代码。正如a VP of Java development at Sun once put it, “Java 程序易于阅读编写更重要。” 有优秀答案的相关问题 - ***.com/questions/49134118/… 【参考方案1】:

这与var 无关。它与 lambda 是否具有独立类型有关。 var 的工作方式是计算 RHS 上初始化程序的独立类型,并推断。

自从在 Java 8 中引入以来,lambda 表达式和方法引用就没有独立的类型——它们需要一个目标类型,它必须是一个函数式接口。

如果你尝试:

Object o = (String s) -> s.length();

您还会收到类型错误,因为编译器不知道您打算将 lambda 转换为什么功能接口。

使用var 进行推理只会让事情变得更难,但由于无法回答更简单的问题,因此更难的问题也无法回答。

请注意,您可以通过其他方式(例如强制转换)提供目标类型,然后它会起作用:

var x = (Predicate<String>) s -> s.isEmpty();

因为现在 RHS 有一个独立的类型。但是你最好通过给x 一个清单类型来提供目标类型。

【讨论】:

【参考方案2】:

来自Local-Variable Type Inference JEP:

推理过程实质上只是为变量提供了其初始化表达式的类型。一些微妙之处:

初始化程序没有目标类型(因为我们还没有推断出它)。需要这种类型的 Poly 表达式(如 lambdas、方法引用和数组初始化器)将触发错误。

由于 lambda 表达式本身没有类型,因此无法推断为 var


...同样,可以设置默认规则。

当然,您可以想出一种方法来解决此限制。为什么开发人员决定不这样做实际上取决于猜测,除非参与决策的人可以在这里回答。 (更新:已回答 here。)如果您仍然感兴趣,可以在其中一个 openjdk 邮件列表中询问:http://mail.openjdk.java.net/mailman/listinfo

如果我猜的话,他们可能不想将var 上下文中的 lambda 推理与一组特定的功能接口类型联系起来,这将排除任何第三方功能接口类型。更好的解决方案是推断可以转换为兼容的功能接口类型的通用函数类型(即(Apple) -&gt; boolean)。但是 JVM 没有这样的函数类型,并且在创建 lambda 表达式的项目期间已经做出了不实现它们的决定。同样,如果您对具体原因感兴趣,请询问开发人员。

【讨论】:

考虑到 javac 所做的优化很少一般在编译时对实现进行潜在搜索,我猜多表达式会很疯狂 寻找兼容的功能接口不是性能问题;这是读心术的问题。仅JDK中就有多个功能接口与示例兼容;您希望编译器如何选择您想到的那个? @BrianGoetz 你没有。编译器定义一个类型列表(例如来自java.util.function 的类型,可能会重新排序),并选择列表中第一个兼容的类型,如果不兼容则抛出错误。因此,您基本上让编译器编写者为您决定(我假设这就是引用中“默认规则”的含义。)。恕我直言,这不是一个好主意,因为任何代码读者都必须记住确切的系统才能知道选择了哪种类型。 @JornVernee 你在两个方面错过了我的观点。 1.(String s) -&gt; s.isEmpty()已经兼容Function&lt;String,Boolean&gt;Predicate&lt;String&gt;等。要求选择就是要求读心术。如果我们添加新的,那么这可能会导致人们程序中的类型发生变化,或者现有程序不再编译。馊主意。 2.java.util.function中的功能接口不是魔法,也不是语言的一部分;它们只是普通的接口。让用户直接说出他们的意思要好得多。 @BrianGoetz 我错过了重点吗?我们似乎在提倡同样的事情。我试图在答案中说明的一点是,您总是可以想出某种方法来使其工作,但这并不意味着解决方案是一个的解决方案。但是像“他们为什么不只是做 X”(我知道你喜欢得到那些)这样的问题,一般来说,SO 社区并不能真正明确地回答,尽管我们可以推测。【参考方案3】:

对于所有说这是不可能、不希望或不希望的人,我只想指出 Scala 可以通过仅指定参数类型来推断 lambda 的类型:

val predicateVar = (apple: Apple) => apple.getColor().equals("red")

在 Haskell 中,因为 getColor 将是一个不附加到对象的独立函数,并且因为它执行完整的 Hindley-Milner 推理,所以您甚至不需要指定参数类型:

predicateVar = \apple -> getColor apple == "red"

这非常方便,因为让程序员显式指定烦人的不是简单类型,而是更复杂的类型。

换句话说,它不是 Java 10 中的一个特性。这是它们的实现和以前的设计选择的限制。

【讨论】:

因提到 HM 推理算法并提供上下文而被投票。【参考方案4】:

正如几个人已经提到的,var 应该推断什么类型,为什么要推断?

声明:

var predicateVar = apple -> apple.getColor().equals("red");

是模棱两可的,并且假设 lambda 中的 apple 标识符代表 Apple 不是,编译器应该选择 Function&lt;Apple, Boolean&gt; 而不是 Predicate&lt;Apple&gt; 或反之亦然,这是没有正当理由的。

另一个原因是 lambda 本身没有 speakable 类型,因此编译器无法推断它。

另外,“如果这是可能的” 想象一下开销,因为编译器必须遍历所有功能接口,并在每次将 lambda 分配给var 变量。

【讨论】:

它可以从它被引用的声明方法中推断出来,如果它没有被引用,那么它是死代码。 apples.stream.filter(predicateVar);当最终目标是提高编码速度时。虽然我同意你的观点,因为这很合乎逻辑。但换个角度想,如果它是双向引用的呢? var list = new ArrayList();,现在写 list=new LinkedList() 会出错。我们也以这种方式使用 final 关键字,无论是通过构造函数还是直接......辩论中的许多论点。 ..顺便说一句,var 始终是本地的,openjdk.java.net/jeps/286 兼容的功能接口不易互换的事实已经是一个更大的问题,而 var 也正好承载了这个问题【参考方案5】:

要回答这个问题,我们必须深入了解 lambda 是什么以及它是如何工作的。

首先我们应该了解什么是 lambda:

一个 lambda 表达式总是实现一个函数式接口,所以当你必须提供一个像 Runnable 这样的函数式接口时,不必创建一个实现该接口的全新类,你可以使用 lambda 语法来创建功能接口需要的方法。请记住,尽管 lambda 仍然具有它正在实现的功能接口的类型。

考虑到这一点,让我们更进一步:

这在 Runnable 的情况下非常有效,我可以创建一个像 new Thread(()-&gt;//put code to run here); 这样的新线程,而不是创建一个全新的对象来实现功能接口。这是因为编译器知道 Thread() 接受 Runnable 类型的对象,所以它知道 lambda 表达式必须是什么类型。

但是,在将 lambda 分配给局部变量的情况下,编译器不知道该 lambda 正在实现什么功能接口,因此它无法推断 var 应该是什么类型。由于它可能正在实现用户创建的功能接口,或者它可能是runnable 接口,因此无法知道。

这就是 lambda 不能与 var 关键字一起使用的原因。

【讨论】:

我不认为 lambda 是“只是实现功能接口的语法糖”。无论如何,这与这个原本不错的答案无关。考虑改写那段。【参考方案6】:

因为那是一个非特征:

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

http://openjdk.java.net/jeps/286

【讨论】:

【参考方案7】:

简而言之,var 和 lambda 表达式的类型都需要推理,但方式相反。 var 的类型由初始化器推断:

var a = new Apple();

lambda 表达式的类型由上下文设置。上下文期望的类型称为目标类型,通常由声明推断,例如

// Variable assignment
Function<Integer, Integer> l = (n) -> 2 * n;
// Method argument 
List<Integer> map(List<Integer> list, Function<Integer, Integer> fn)
    //...

map(List.of(1, 2, 3), (n) -> 2 * n);
// Method return 
Function<Integer, Integer> foo(boolean flag)
    //...
    return (n) -> 2 * n;

所以当var和lambda表达式一起使用时,前者的类型需要后者推断,后者的类型需要前者推断。

var a = (n) -> 2 * n;

这个困境的根源是Java不能唯一决定一个lambda表达式的类型,这进一步是由于Java的名义而不是结构类型系统造成的。即结构相同但名称不同的两种类型不认为是相同的,例如

class A
    public int count;
    int value()
        return count;
    


class B
    public int count;
    int value()
        return count;
    


Function<Integer, Boolean>
Predicate<Integer>

【讨论】:

以上是关于为啥不能为 Java 中的 var 关键字分配 lambda 表达式?的主要内容,如果未能解决你的问题,请参考以下文章

为啥不能将匿名方法分配给 var?

为啥直接将 Arrays.asList() 分配给 var 时会出现 AssertionError?

Java中的var关键字

为啥右值不能分配给 constexpr 引用变量

为啥数组不能分配给Iterable?

05-数组