web-dev-qa-db-fra.com

Pourquoi les consommateurs acceptent-ils les lambdas avec des corps de déclaration mais pas de corps d'expression?

De façon surprenante, le code suivant se compile avec succès:

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

Cela aussi:

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

Mais cela échoue avec l'erreur boolean cannot be converted to void comme prévu:

p = s -> true;

La modification du deuxième exemple avec des parenthèses échoue également:

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

Est-ce un bogue dans le compilateur Java ou existe-t-il une règle d'inférence de type que je ne connais pas?

61
Zefick

Tout d'abord, il vaut la peine de regarder ce qu'est réellement un Consumer<String>. De la documentation :

Représente une opération qui accepte un seul argument d'entrée et ne renvoie aucun résultat . Contrairement à la plupart des autres interfaces fonctionnelles, Consumer devrait fonctionner via des effets secondaires.

C'est donc une fonction qui accepte une chaîne et ne renvoie rien.

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

Compile avec succès car equals peut prendre une chaîne (et, en fait, n'importe quel objet). Le résultat égal est simplement ignoré. *

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

C'est exactement la même chose, mais avec une syntaxe différente. Le compilateur ne sait pas ajouter un return implicite car un Consumer ne doit pas renvoyer de valeur. Il le ferait ajouterait un return implicite si le lambda était un Function<String, Boolean>.

p = s -> true;

Cela prend une chaîne (s) mais parce que true est une expression et non une instruction, le résultat ne peut pas être ignoré de la même manière. Le compilateur doit ajouter un return implicite car une expression ne peut pas exister seule. Ainsi, ce le fait a un retour: un booléen. Ce n'est donc pas un Consumer. **

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

Encore une fois, c'est une expression, pas une déclaration. Ignorant les lambdas pendant un moment, vous verrez que la ligne System.out.println("Hello"); échouera de la même manière à être compilée si vous la mettez entre parenthèses.


* De la spécification :

Si le corps d'un lambda est une expression d'instruction (c'est-à-dire une expression qui serait autorisée à être autonome en tant qu'instruction), elle est compatible avec un type de fonction produisant un vide; tout résultat est simplement rejeté.

** De la spécification (merci, Eugene ):

Une expression lambda est congruente avec un type de fonction [producteur de vides] si ... le corps lambda est soit une expression ( §14.8 ) soit un bloc compatible avec les vides.

80
Michael

Je pense que les autres réponses compliquent l'explication en se concentrant sur les lambdas alors que leur comportement dans ce cas est similaire au comportement des méthodes implémentées manuellement. Cela compile:

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

alors que cela ne:

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

car "".equals(s) est une instruction mais true ne l'est pas. Une expression lambda pour une interface fonctionnelle retournant void nécessite une instruction donc elle suit les mêmes règles que le corps d'une méthode.

Notez qu'en général les corps lambda ne suivent pas exactement les mêmes règles que les corps de méthode - en particulier, si un lambda dont le corps est une expression implémente une méthode renvoyant une valeur, il a un return. Ainsi, par exemple, x -> true Serait une implémentation valide de Function<Object, Boolean>, Tandis que true; N'est pas un corps de méthode valide. Mais dans ce cas particulier, les interfaces fonctionnelles et les corps de méthode coïncident.

11
s -> "".equals(s)

et

s -> true

ne vous fiez pas aux mêmes descripteurs de fonction.

s -> "".equals(s) peut faire référence au descripteur de fonction String->void ou String->boolean.
s -> true Fait uniquement référence au descripteur de fonction String->boolean.

Pourquoi ?

  • lorsque vous écrivez s -> "".equals(s), le corps du lambda: "".equals(s)est une instruction qui produit une valeur.
    Le compilateur considère que la fonction peut renvoyer void ou boolean.

Donc, en écrivant:

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

est valable.

Lorsque vous affectez le corps lambda à une variable déclarée Consumer<String>, Le descripteur String->void Est utilisé.
Bien sûr, ce code n'a pas beaucoup de sens (vous vérifiez l'égalité et vous n'utilisez pas le résultat) mais le compilateur s'en fiche.
C'est la même chose lorsque vous écrivez une instruction: myObject.getMyProperty()getMyProperty() renvoie une valeur boolean mais que vous ne stockez pas le résultat de il.

  • lorsque vous écrivez s -> true, le corps du lambda: trueest une expression unique.
    Le compilateur considère que la fonction renvoie nécessairementboolean.
    Ainsi, seul le descripteur String->boolean Peut être utilisé.

Maintenant, revenez à votre code qui ne compile pas.
Qu'essayez-vous de faire ?

Consumer<String> p = s -> true;

Vous ne pouvez pas. Vous souhaitez affecter à une variable qui utilise le descripteur de fonction Consumer<String> Un corps lambda avec le descripteur de fonction String->void. Ça ne correspond pas!

8
davidxxx