Dlaczego nie można skompilować tej lambdy Java 8?

85

Następujący kod Java nie może się skompilować:

@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
}

Kompilator raportuje:

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

Dziwne jest to, że linia oznaczona jako „OK” kompiluje się dobrze, ale linia oznaczona jako „Error” zawodzi. Wydają się zasadniczo identyczne.

Brian Gordon
źródło
5
czy to literówka, że ​​metoda interfejsu funkcjonalnego zwraca void?
Nathan Hughes
6
@NathanHughes Nie. Okazuje się, że jest to kluczowe dla pytania - zobacz zaakceptowaną odpowiedź.
Brian Gordon
czy powinien znajdować się kod wewnątrz { }of takeBiConsumer... a jeśli tak, czy mógłbyś podać przykład ... jeśli przeczytałem to poprawnie, bcjest to instancja klasy / interfejsu BiConsumer, a zatem powinna zawierać metodę wywoływaną w acceptcelu dopasowania sygnatury interfejsu. .. ... a jeśli to prawda, to acceptmetoda musi być gdzieś zdefiniowana (np. klasa implementująca interfejs) ... więc czy to powinno być w {}?? ... ... ... dzięki
dsdsdsdsd
Interfejsy z jedną metodą są zamienne z lambdami w Javie 8. W tym przypadku (String s1, String s2) -> "hi"jest to instancja BiConsumer <String, String>.
Brian Gordon

Odpowiedzi:

100

Twoja lambda musi być zgodna z BiConsumer<String, String>. Jeśli odwołujesz się do JLS # 15.27.3 (typ Lambda) :

Wyrażenie lambda jest zgodne z typem funkcji, jeśli spełnione są wszystkie poniższe warunki:

  • […]
  • Jeśli wynikiem typu funkcji jest void, treść lambda jest albo wyrażeniem instrukcji (§14.8), albo blokiem zgodnym z void.

Zatem lambda musi być wyrażeniem instrukcji lub blokiem zgodnym z void:

asylias
źródło
31
@BrianGordon Literał typu String jest wyrażeniem (dokładniej wyrażeniem stałym), ale nie wyrażeniem instrukcji.
assylias
44

Zasadniczo new String("hi")jest to wykonywalny fragment kodu, który faktycznie coś robi (tworzy nowy ciąg, a następnie zwraca go). Zwracana wartość może zostać zignorowana i new String("hi")nadal może być używana w lambdzie void-return do tworzenia nowego ciągu.

Jednak "hi"jest to po prostu stała, która sama z siebie nic nie robi. Jedyną rozsądną rzeczą, jaką można z tym zrobić w ciele lambda, jest zwrócenie jej. Ale metoda lambda musiałaby mieć zwracany typ Stringlub Object, ale zwraca void, stąd String cannot be casted to voidbłąd.

kajacx
źródło
6
Prawidłowy termin formalny to Expression Statement , wyrażenie tworzenia instancji może pojawić się w obu miejscach, w których wymagane jest wyrażenie lub instrukcja, podczas gdy Stringliterał jest po prostu wyrażeniem, którego nie można użyć w kontekście instrukcji .
Holger
2
Przyjęta odpowiedź może być formalnie poprawna, ale to jest lepsze wyjaśnienie
edc65
3
@ edc65: dlatego też ta odpowiedź została pozytywnie oceniona. Rozumowanie reguł i nieformalne intuicyjne wyjaśnienie może rzeczywiście pomóc, jednak każdy programista powinien być świadomy, że stoją za tym formalne reguły i w przypadku, gdy formalna reguła nie jest intuicyjnie zrozumiała, formalna reguła nadal wygrywa. . Np. ()->x++Jest legalne, podczas gdy ()->(x++)zasadniczo robiąc dokładnie to samo, nie jest…
Holger
21

Pierwszy przypadek jest w porządku, ponieważ wywołujesz „specjalną” metodę (konstruktora) i nie bierzesz w rzeczywistości utworzonego obiektu. Aby było to bardziej zrozumiałe, wstawię opcjonalne nawiasy klamrowe do twoich lambd:

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

I dokładniej, przetłumaczę to na starszą notację:

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"
    }
});

W pierwszym przypadku wykonujesz konstruktor, ale NIE zwracasz utworzonego obiektu, w drugim przypadku próbujesz zwrócić wartość typu String, ale Twoja metoda w interfejsie BiConsumerzwraca void, stąd błąd kompilatora.

morgano
źródło
12

JLS to określa

Jeśli wynikiem typu funkcji jest void, treść lambda jest albo wyrażeniem instrukcji (§14.8), albo blokiem zgodnym z void.

Zobaczmy teraz szczegółowo,

Ponieważ twoja takeBiConsumermetoda jest typu void, odbierająca lambda new String("hi")zinterpretuje ją jako blok

{
    new String("hi");
}

który jest ważny w void, stąd pierwszy przypadek kompilacji.

Jednak w przypadku, gdy lambda jest -> "hi", blok taki jak

{
    "hi";
}

nie jest poprawną składnią w java. Dlatego jedyną rzeczą do zrobienia z „cześć” jest próba zwrócenia go.

{
    return "hi";
}

który nie jest ważny w przypadku unieważnienia i wyjaśnij komunikat o błędzie

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

Dla lepszego zrozumienia zwróć uwagę, że jeśli zmienisz typ takeBiConsumerna String, -> "hi"będzie to poprawne, ponieważ po prostu spróbuje bezpośrednio zwrócić ciąg.


Zauważ, że na początku myślałem, że błąd jest spowodowany tym, że lambda znajduje się w niewłaściwym kontekście wywołania, więc podzielę się tą możliwością ze społecznością:

JLS 15.27

Jest to błąd czasu kompilacji, jeśli wyrażenie lambda występuje w programie w miejscu innym niż kontekst przypisania (§5.2), kontekst wywołania (§5.3) lub kontekst rzutowania (§5.5).

Jednak w naszym przypadku znajdujemy się w kontekście wywołania, który jest poprawny.

Jean-François Savard
źródło