【问题标题】:Why do Consumers accept lambdas with statement bodies but not expression bodies?为什么消费者接受带有语句体而不是表达式体的 lambda?
【发布时间】:2018-01-09 16:22:57
【问题描述】:

下面的代码居然编译成功了:

Consumer<String> p = ""::equals;

这也是:

p = s -> "".equals(s);

但这是失败的错误boolean cannot be converted to void正如预期的那样:

p = s -> true;

用括号修改第二个例子也失败了:

p = s -> ("".equals(s));

这是 Java 编译器中的错误还是我不知道的类型推断规则?

【问题讨论】:

  • Eclipse 也会抱怨,所以可能不是编译器错误。
  • s -&gt; true;s -&gt; ("".equals(s)) 被解释为 s -&gt; {return true;}s -&gt; { return ("".equals(s));} 并且它们不是 Function&lt;String, Boolean&gt;p = s -&gt; true; 这没有意义,请改用p = s -&gt; {};
  • @DavidPérezCabrera 我明白为什么最后两行代码没有编译。不清楚后两个是否有效。
  • @Zefick 恕我直言,这是编译器的许可证,它必须解释您可以忽略返回值。为避免不必要的冗长,如果它不这样做,则不能使用p = ""::equals;,并且必须将p = s -&gt; "".equals(s); 定义为:p = s -&gt; {"".equals(s);};。恕我直言,这是一个很好的决定。

标签: java lambda java-8 type-inference


【解决方案1】:

首先,有必要看看 Consumer&lt;String&gt; 究竟是什么。 From the documentation:

表示一个接受单个输入参数并且 不返回任何结果。与大多数其他功能接口不同,Consumer 预计会通过副作用进行操作。

所以它是一个接受字符串并且不返回任何内容的函数。

Consumer<String> p = ""::equals;

编译成功,因为equals 可以接受一个字符串(实际上,任何对象)。 equals 的结果会被忽略。*

p = s -> "".equals(s);

这完全一样,但语法不同。编译器知道不要添加隐式return,因为Consumer 不应该返回值。如果 lambda 是 Function&lt;String, Boolean&gt;,它添加一个隐式的 return

p = s -> true;

这需要一个字符串 (s),但因为 true 是一个表达式而不是一个语句,所以不能以同样的方式忽略结果。编译器必须添加一个隐式的return,因为表达式不能单独存在。因此,这个确实有一个返回值:一个布尔值。因此它不是Consumer.**

p = s -> ("".equals(s));

同样,这是一个表达式,而不是一个语句。暂时忽略 lambda,你会看到 System.out.println("Hello"); 行如果你用括号括起来同样会编译失败。


*来自the spec:

如果 lambda 的主体是语句表达式(即,允许作为语句独立的表达式),则它与产生 void 的函数类型兼容;任何结果都会被丢弃。

**来自the spec(谢谢Eugene):

如果 ... lambda 主体是一个语句表达式 (§14.8) 或与 void 兼容的块。

【讨论】:

  • 所以关键时刻是equals 的结果可以被忽略。对于这种强类型语言来说,真是出乎意料。
  • @Zefick 是的,关键区别在于语句可能有也可能没有隐含的return(基于推断),而表达式总是有隐含的return。它会产生更好的语法。
  • @Michael 有一天我很想知道为什么语言中的语句和表达式之间存在区别。我真的不明白,就让一切都是陈述......
  • @FedericoPeraltaSchaffner 定义它的不是编译器,而是 JLS :) (编译器必须遵守)。如果语句编译器无法报告很多错误的所有表达式。如果必须使用非 void 函数的所有结果,则需要大量不必要的赋值/额外的局部变量,首先考虑 StringBuilder.append(..)。这种情况已经很久了,可能从 Java 1.0 开始
【解决方案2】:

我认为其他答案通过关注 lambda 使解释复杂化,而在这种情况下它们的行为类似于手动实现的方法的行为。这样编译:

new Consumer<String>() {
    @Override
    public void accept(final String s) {
        "".equals(s);
    }
}

而这不是:

new Consumer<String>() {
    @Override
    public void accept(final String s) {
        true;
    }
}

因为"".equals(s) 是一个声明,而true 不是。返回 void 的函数式接口的 lambda 表达式需要一个语句,因此它遵循与方法主体相同的规则。

请注意,通常 lambda 主体不遵循完全与方法主体相同的规则 - 特别是,如果主体为表达式的 lambda 实现了返回值的方法,则它具有隐式return。例如,x -&gt; true 将是Function&lt;Object, Boolean&gt; 的有效实现,而true; 不是有效的方法体。但在这种特殊情况下,功能接口和方法体是一致的。

【讨论】:

  • “这里的 lambda 没有什么特别之处”。你是对的,但是s -&gt; "".equals(s) 可以同时满足Consumer&lt;String&gt; Function&lt;String, Boolean&gt;,我认为这是造成混乱的根源。
  • 我认为还值得一提的是s -&gt; true 可以 是一个有效的 lambda(例如 Predicate&lt;String&gt; foo = s -&gt; true;),而 true; 本身永远不会编译。
【解决方案3】:
s -> "".equals(s)

s -> true

不要依赖相同的函数描述符。

s -&gt; "".equals(s) 可以引用 String-&gt;voidString-&gt;boolean 函数描述符。
s -&gt; true 仅引用 String-&gt;boolean 函数描述符。

为什么?

  • 当您编写 s -&gt; "".equals(s) 时,lambda 的主体:"".equals(s) 是一个产生值的语句
    编译器认为该函数可能返回voidboolean

所以写:

Function<String, Boolean> function = s -> "".equals(s);
Consumer<String> consumer = s -> "".equals(s);

有效。

当您将 lambda 主体分配给 Consumer&lt;String&gt; 声明的变量时,将使用描述符 String-&gt;void
当然,这段代码没有多大意义(您检查相等性并且不使用结果)但编译器并不关心。
编写语句时也是如此:myObject.getMyProperty() 其中getMyProperty() 返回一个boolean 值,但您不存储它的结果。

  • 当您编写 s -&gt; true 时,lambda 的主体:true 是单个表达式
    编译器认为该函数返回必然 boolean
    所以只能使用描述符String-&gt;boolean

现在,回到无法编译的代码。
你想做什么?

Consumer<String> p = s -> true;

你不能。您想为使用函数描述符 Consumer&lt;String&gt; 的变量分配一个带有 String-&gt;void 函数描述符的 lambda 主体。 不匹配!

【讨论】:

猜你喜欢
  • 2011-07-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-08
  • 1970-01-01
  • 2021-12-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多