Pourquoi ce lambda Java 8 ne parvient-il pas à se compiler?

85

Le code Java suivant ne parvient pas à se compiler:

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

Le compilateur rapporte:

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

La chose étrange est que la ligne marquée "OK" se compile correctement, mais la ligne marquée "Erreur" échoue. Ils semblent essentiellement identiques.

Brian Gordon
la source
5
est-ce une faute de frappe ici que la méthode d'interface fonctionnelle renvoie void?
Nathan Hughes
6
@NathanHughes Non. Il s'avère être au cœur de la question - voir la réponse acceptée.
Brian Gordon
devrait-il y avoir du code à l'intérieur { }de takeBiConsumer... et si oui, pourriez-vous donner un exemple ... si je lis ceci correctement, bcest une instance de la classe / interface BiConsumer, et devrait donc contenir une méthode appelée acceptpour correspondre à la signature de l'interface. .. ... et si c'est vrai, alors la acceptméthode doit être définie quelque part (par exemple, une classe qui implémente l'interface) ... c'est donc ce qui devrait être dans le {}?? ... ... ... merci
dsdsdsdsd
Les interfaces avec une seule méthode sont interchangeables avec les lambdas en Java 8. Dans ce cas, (String s1, String s2) -> "hi"est une instance de BiConsumer <String, String>.
Brian Gordon

Réponses:

100

Votre lambda doit être en accord avec BiConsumer<String, String>. Si vous vous référez au JLS # 15.27.3 (Type of a Lambda) :

Une expression lambda est congruente avec un type de fonction si toutes les conditions suivantes sont vraies:

  • [...]
  • Si le résultat du type de fonction est void, le corps lambda est soit une expression d'instruction (§14.8), soit un bloc compatible void.

Ainsi, le lambda doit être une expression d'instruction ou un bloc compatible void:

assylies
la source
31
@BrianGordon Un littéral String est une expression (une expression constante pour être précis) mais pas une expression d'instruction.
assylias
44

Fondamentalement, new String("hi")est un morceau de code exécutable qui fait réellement quelque chose (il crée une nouvelle chaîne et la renvoie). La valeur retournée peut être ignorée et new String("hi")peut toujours être utilisée dans void-return lambda pour créer une nouvelle chaîne.

Cependant, "hi"c'est juste une constante qui ne fait rien par elle-même. La seule chose raisonnable à faire avec un corps lambda est de le retourner . Mais la méthode lambda devrait avoir le type de retour Stringou Object, mais elle retourne void, d'où l' String cannot be casted to voiderreur.

Kajacx
la source
6
Le terme formel correct est Expression Statement , une expression de création d'instance peut apparaître aux deux endroits, là où une expression ou là où une instruction est requise, tandis qu'un Stringlittéral est simplement une expression qui ne peut pas être utilisée dans un contexte d' instruction .
Holger
2
La réponse acceptée peut être formellement correcte, mais celle-ci est une meilleure explication
edc65
3
@ edc65: c'est pourquoi cette réponse a également été votée. Le raisonnement des règles et l'explication intuitive non formelle peuvent en effet aider, cependant, chaque programmeur doit être conscient qu'il y a des règles formelles derrière cela et dans le cas où le résultat de la règle formelle n'est pas intuitivement compréhensible, la règle formelle l'emportera toujours. . Par exemple, ()->x++est légal, alors que ()->(x++), fondamentalement, faire exactement la même chose, n'est pas…
Holger
21

Le premier cas est correct car vous invoquez une méthode "spéciale" (un constructeur) et vous ne prenez pas réellement l'objet créé. Juste pour être plus clair, je vais mettre les accolades optionnelles dans vos lambdas:

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

Et plus clair, je vais traduire cela dans l'ancienne notation:

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

Dans le premier cas, vous exécutez un constructeur, mais vous ne retournez PAS l'objet créé, dans le second cas, vous essayez de renvoyer une valeur String, mais votre méthode dans votre interface BiConsumerrenvoie void, d'où l'erreur du compilateur.

Morgano
la source
12

Le JLS précise que

Si le résultat du type de fonction est void, le corps lambda est soit une expression d'instruction (§14.8), soit un bloc compatible void.

Voyons maintenant cela en détail,

Puisque votre takeBiConsumerméthode est de type void, la réception lambda new String("hi")l'interprétera comme un bloc comme

{
    new String("hi");
}

qui est valide dans le vide, d'où la première compilation de cas.

Cependant, dans le cas où le lambda est -> "hi", un bloc tel que

{
    "hi";
}

n'est pas une syntaxe valide en java. Par conséquent, la seule chose à faire avec "hi" est d'essayer de le renvoyer.

{
    return "hi";
}

qui n'est pas valide dans le vide et expliquer le message d'erreur

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

Pour une meilleure compréhension, notez que si vous changez le type de takeBiConsumeren String, -> "hi"sera valide car il essaiera simplement de renvoyer directement la chaîne.


Notez qu'au début, j'ai pensé que l'erreur était due au fait que le lambda était dans un mauvais contexte d'invocation, je vais donc partager cette possibilité avec la communauté:

JLS 15.27

Il s'agit d'une erreur de compilation si une expression lambda se produit dans un programme dans un endroit autre qu'un contexte d'affectation (§5.2), un contexte d'appel (§5.3) ou un contexte de transtypage (§5.5).

Cependant dans notre cas, nous sommes dans un contexte d'invocation qui est correct.

Jean-François Savard
la source