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

79

在Java 10中,允许像这样使用字符串赋值var

var foo = "boo";

虽然不允许将其分配给lambda表达式,例如:

var predicateVar = apple -> apple.getColor().equals("red");
为什么它可以推断出StringArrayList、用户类等,却无法推断出lambda或方法引用类型呢?

14
var应该推断哪种特定类型?标准函数接口中的一种还是您自己创建的一种?这个决定基于什么? - Pshemo
10
@lexicore 我期望的是 Predicate<Apple>(假设 appleApple 类型)。这就是为什么这是一个错误而不是一个特性。 - erickson
7
我从未暗示任何关于性能的事情,也不确定你为什么问这个问题。我的评论重点是,你无法从初始化程序推断变量的类型;正如我所展示的那样,它是模棱两可的。 - erickson
5
编码速度是一个次要的考虑因素。你将花费90%的时间来阅读和维护现有代码,而不是编写新代码。正如“Sun公司的一位Java开发副总裁所说的那样,更重要的是Java程序易于阅读,而不是编写。”。 - VGR
4
相关问题,有很棒的答案 - https://dev59.com/B1UM5IYBdhLWcg3wOeTz - Oleksandr Pyrohov
显示剩余17条评论
7个回答

76

这与var无关,而是与lambda是否具有独立类型有关。 var的工作方式是计算右侧初始化程序的独立类型,并推断出该类型。

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

如果您尝试:

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

你还会得到类型错误,因为编译器不知道你想将lambda转换成什么函数接口。

使用var进行推断只会让问题更难,但是由于更容易的问题无法回答,更难的问题也无法得到解决。

请注意,你可以通过其他方式(比如强制类型转换)提供目标类型,那样它就会起作用:

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

因为现在RHS有一个独立的类型。但最好通过为x提供显式类型来指定目标类型。


64

来自局部变量类型推断JEP

推断过程实质上只是给变量赋予其初始化表达式的类型。一些微妙之处:

  • 初始化器没有目标类型(因为我们尚未推断出它)。需要此类类型的多态表达式,如lambda、方法引用和数组初始化器,将触发错误。

因为lambda表达式本身没有类型,所以无法为var推断类型。


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

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

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


3
考虑到通常情况下javac做了很少的优化,在编译时对多态表达式进行潜在搜索实现将是疯狂的,我想。 - Eugene
14
寻找兼容的函数接口并不是关于性能的问题,它是心灵感应的问题。在JDK中,有多个与示例兼容的函数接口;你如何期望编译器选择你想要的接口呢? - Brian Goetz
1
@BrianGoetz 你不需要选择。编译器定义了一系列类型(例如来自“java.util.function”的类型,可能会稍微重排序),然后选择与之兼容的列表中的第一个类型,如果没有匹配的类型,则抛出错误。所以基本上就是让编译器的作者为你决定(我认为这就是引用中所指的“默认规则”)。在我看来,这并不是一个好主意,因为任何代码读者都必须记住确切的系统才能知道选择了哪种类型。 - Jorn Vernee
4
@JornVernee,你在两个方面都没有理解我的观点。首先,(String s) -> s.isEmpty() 已经兼容 Function<String,Boolean>Predicate<String> 等多种类型。要求选择一种类型等于要求心灵感应,如果我们添加新类型,则可能会导致人们程序中的类型发生变化,或现有程序无法编译。这样做不太好。其次,在java.util.function中的函数式接口不是魔法或语言的一部分,它们只是普通的接口。更好的方式是让用户直接声明他们所需的类型。 - Brian Goetz
2
@BrianGoetz 我错过了什么吗?我们似乎在为同一件事情进行辩护。我在回答中试图表达的观点是,你总是可以想出某种方法使其工作,但这并不意味着解决方案是一个好的解决方案。但是像“为什么他们不只是做X”这样的问题(我知道你喜欢得到这些问题)通常不能被SO社区整体明确地回答,尽管我们可以推测。 - Jorn Vernee
显示剩余3条评论

36

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

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

在 Haskell 中,由于 getColor 是一个独立的函数而不是附加到对象上的,而且它执行完整的 Hindley-Milner 推理,因此你甚至不需要指定参数类型:

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

这非常方便,因为对于程序员来说,繁琐的不是指定简单类型,而是更复杂的类型。

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


9
因为提到了HM推理算法并提供了背景信息,所以给予了赞。 - fps

5

正如其他人已经提到的那样,var应该推断哪种类型,为什么呢?

这个语句:

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

这段代码含义不够明确,编译器在lambda表达式中的apple标识符代表一个Apple实例时,没有理由为什么要选择Function<Apple, Boolean>而不是Predicate<Apple>或者反过来。

另一个原因是,lambda表达式本身没有一个可说的类型,因此编译器无法推断它。

此外,如果这是可能的话,想象一下编译器会有多大的开销,每次将lambda分配给var变量时,它都必须遍历所有函数接口并确定哪个函数接口最合适。


1
可以从声明的方法中推断出它被引用的位置,如果没有被引用,则是死代码。 apples.stream.filter(predicateVar);当最终目标是提高编码速度时。虽然我同意你的观点很有道理。但是换个角度思考一下,如果两种方式都被引用怎么办?var list = new ArrayList();,现在写list=new LinkedList()会报错。我们也可以通过构造函数或直接使用final关键字来这样使用。辩论中有许多争论。另外,var始终是局部变量,http://openjdk.java.net/jeps/286 - hi.nitish
2
可兼容的函数接口并不是很容易互换,这已经是一个更大的问题了,而 var 关键字只是将这个问题带了进来。 - njzk2

5
要回答这个问题,我们需要深入了解Lambda是什么以及它是如何工作的。
首先,我们应该了解Lambda是什么:
Lambda表达式总是实现一个函数接口,因此当您需要提供一个像 Runnable 这样的函数接口时,您可以使用Lambda语法创建函数接口所需的方法,而不必创建一个实现该接口的全新类。但请记住,Lambda仍然具有它正在实现的函数接口的类型。
有了这个想法,让我们更深入地了解一下:
在Runnable的情况下非常适用,我可以像这样创建一个新线程 new Thread(() -> { //放这里要运行的代码 }); 而不是创建一个实现函数接口的全新对象。这可以工作,因为编译器知道Thread()需要一个Runnable类型的对象,因此它知道Lambda表达式必须是什么类型的函数接口。
然而,在将Lambda分配给局部变量的情况下,编译器无法确定这个Lambda实现的函数接口的具体类型,因此它无法推断出 var 变量应该是什么类型。由于它可能是用户创建的功能性接口或者是 runnable 接口,因此没有办法知道。
这就是为什么Lambda不能与var关键字一起使用的原因。

4
我认为 "lambda 只是一种语法糖来实现函数式接口" 这种说法并不完全正确。不管怎样,这与本来很好的回答无关。考虑重新表述该段落。 - erickson

2
因为这不是一个特性:
该处理仅限于具有初始化程序的局部变量,增强型for循环中的索引和传统for循环中声明的局部变量;它不适用于方法形式参数、构造函数形式参数、方法返回类型、字段、catch形式参数或任何其他类型的变量声明。 http://openjdk.java.net/jeps/286

0
简而言之,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;
}

当变量和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>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接