Warum kann dieses Java 8-Lambda nicht kompiliert werden?

84

Der folgende Java-Code kann nicht kompiliert werden:

@FunctionalInterface
private interface BiConsumer<A, B> {
    void accept(A a, B b);
}

private static void takeBiConsumer(BiConsumer<String, String> bc) { }

public static void main(String[] args) {
    takeBiConsumer((String s1, String s2) -> new String("hi")); // OK
    takeBiConsumer((String s1, String s2) -> "hi"); // Error
}

Der Compiler meldet:

Error:(31, 58) java: incompatible types: bad return type in lambda expression
    java.lang.String cannot be converted to void

Das Seltsame ist, dass die mit "OK" gekennzeichnete Zeile einwandfrei kompiliert wird, die mit "Fehler" gekennzeichnete Zeile jedoch fehlschlägt. Sie scheinen im Wesentlichen identisch zu sein.

Brian Gordon
quelle
5
Ist es hier ein Tippfehler, dass die Methode der funktionalen Schnittstelle void zurückgibt?
Nathan Hughes
6
@ NathanHughes Nein. Es stellt sich als zentral für die Frage heraus - siehe die akzeptierte Antwort.
Brian Gordon
sollte es Code innerhalb der sein { }von takeBiConsumer... und wenn ja, könnten Sie ein Beispiel geben ... wenn ich das richtig gelesen, bcist eine Instanz der Klasse / Schnittstelle BiConsumerund somit ein Verfahren enthalten sollte , genannt acceptdie Schnittstelle Signatur übereinstimmen. .. ... und wenn das richtig ist, muss die acceptMethode irgendwo definiert werden (zB eine Klasse, die die Schnittstelle implementiert) ... also sollte das in der {}?? ... ... ... danke
dsdsdsdsd
Schnittstellen mit einer einzelnen Methode sind in Java 8 mit Lambdas austauschbar. In diesem Fall (String s1, String s2) -> "hi"handelt es sich um eine Instanz von BiConsumer <String, String>.
Brian Gordon

Antworten:

100

Ihr Lambda muss kongruent sein mit BiConsumer<String, String>. Wenn Sie sich auf JLS # 15.27.3 (Typ eines Lambda) beziehen :

Ein Lambda-Ausdruck stimmt mit einem Funktionstyp überein, wenn alle der folgenden Bedingungen erfüllt sind:

  • [...]
  • Wenn das Ergebnis des Funktionstyps ungültig ist, ist der Lambda-Körper entweder ein Anweisungsausdruck (§14.8) oder ein void-kompatibler Block.

Das Lambda muss also entweder ein Anweisungsausdruck oder ein void-kompatibler Block sein:

  • Ein Konstruktoraufruf ist ein Anweisungsausdruck , der kompiliert wird.
  • Ein String-Literal ist kein Anweisungsausdruck und nicht void-kompatibel (siehe Beispiele in 15.27.2 ), sodass es nicht kompiliert wird.
Assylien
quelle
31
@BrianGordon Ein String-Literal ist ein Ausdruck (genauer gesagt ein konstanter Ausdruck), aber kein Anweisungsausdruck.
Assylias
44

Grundsätzlich new String("hi")handelt es sich um einen ausführbaren Code, der tatsächlich etwas tut (er erstellt einen neuen String und gibt ihn dann zurück). Der zurückgegebene Wert kann ignoriert werden und new String("hi")kann weiterhin in void-return Lambda verwendet werden, um einen neuen String zu erstellen.

Es "hi"ist jedoch nur eine Konstante, die nichts alleine macht. Das einzig Vernünftige im Lambda-Körper ist die Rückgabe . Aber die Lambda-Methode müsste den Rückgabetyp Stringoder haben Object, aber sie gibt zurück void, daher der String cannot be casted to voidFehler.

kajacx
quelle
6
Der korrekte formale Begriff lautet Ausdrucksanweisung . Ein Ausdruck zur Instanzerstellung kann an beiden Stellen erscheinen, an denen ein Ausdruck oder eine Anweisung erforderlich ist, während ein StringLiteral nur ein Ausdruck ist, der in einem Anweisungskontext nicht verwendet werden kann .
Holger
2
Die akzeptierte Antwort mag formal korrekt sein, aber diese ist eine bessere Erklärung
edc65
3
@ edc65: Deshalb wurde auch diese Antwort positiv bewertet. Die Begründung der Regeln und die nicht formale intuitive Erklärung können in der Tat hilfreich sein. Jeder Programmierer sollte sich jedoch darüber im Klaren sein, dass formale Regeln dahinter stehen. Falls das Ergebnis der formalen Regel nicht intuitiv verständlich ist, gewinnt die formale Regel dennoch . ZB ()->x++ist legal, während ()->(x++)im Grunde genau das gleiche nicht ist ...
Holger
21

Der erste Fall ist in Ordnung, da Sie eine "spezielle" Methode (einen Konstruktor) aufrufen und das erstellte Objekt nicht tatsächlich übernehmen. Um es klarer zu machen, werde ich die optionalen Klammern in Ihre Lambdas stecken:

takeBiConsumer((String s1, String s2) -> {new String("hi");}); // OK
takeBiConsumer((String s1, String s2) -> {"hi"}); // Error

Und klarer, ich werde das in die ältere Notation übersetzen:

takeBiConsumer(new BiConsumer<String, String>(String s1, String s2) {
    public void accept(String s, String s2) {
        new String("hi"); // OK
    }
});

takeBiConsumer(new BiConsumer<String, String>(String s1, String s2) {
    public void accept(String s, String s2) {
        "hi"; // Here, the compiler will attempt to add a "return"
              // keyword before the "hi", but then it will fail
              // with "compiler error ... bla bla ...
              //  java.lang.String cannot be converted to void"
    }
});

Im ersten Fall führen Sie einen Konstruktor aus, aber Sie geben das erstellte Objekt NICHT zurück. Im zweiten Fall versuchen Sie, einen String-Wert zurückzugeben, aber Ihre Methode in Ihrer Schnittstelle BiConsumergibt void zurück, daher der Compilerfehler.

Morgano
quelle
11

Die JLS geben das an

Wenn das Ergebnis des Funktionstyps ungültig ist, ist der Lambda-Körper entweder ein Anweisungsausdruck (§14.8) oder ein void-kompatibler Block.

Nun wollen wir das im Detail sehen,

Da Ihre takeBiConsumerMethode vom Typ void ist, wird sie vom Lambda-Empfang new String("hi")als blockartig interpretiert

{
    new String("hi");
}

was in einer Leere gültig ist, daher der erste Fall kompilieren.

In dem Fall, in dem sich das Lambda befindet -> "hi", kann jedoch ein Block wie z

{
    "hi";
}

ist keine gültige Syntax in Java. Daher ist das einzige, was mit "hi" zu tun ist, zu versuchen, es zurückzugeben.

{
    return "hi";
}

Dies ist nichtig und erklärt die Fehlermeldung

incompatible types: bad return type in lambda expression
    java.lang.String cannot be converted to void

Beachten Sie zum besseren Verständnis, dass, wenn Sie den Typ eines Strings ändern takeBiConsumer, -> "hi"dieser gültig ist, da einfach versucht wird, den String direkt zurückzugeben.


Beachten Sie, dass ich zuerst dachte, der Fehler sei darauf zurückzuführen, dass sich das Lambda in einem falschen Aufrufkontext befindet. Daher teile ich diese Möglichkeit mit der Community:

JLS 15.27

Es ist ein Fehler zur Kompilierungszeit, wenn ein Lambda-Ausdruck in einem Programm an einem anderen Ort als einem Zuweisungskontext (§5.2), einem Aufrufkontext (§5.3) oder einem Casting-Kontext (§5.5) auftritt.

In unserem Fall befinden wir uns jedoch in einem Aufrufkontext, der korrekt ist.

Jean-François Savard
quelle