48 votes

Expression lambda et doutes de surcharge de méthode

D'accord, donc la surcharge de méthode est-une-mauvaise-chose™. Maintenant que cela a été réglé, supposons que je veuille vraiment surcharger une méthode comme ceci:

static void run(Consumer consumer) {
    System.out.println("consumer");
}

static void run(Function function) {
    System.out.println("function");
}

En Java 7, je pouvais les appeler facilement avec des classes anonymes non ambiguës en tant qu'arguments:

run(new Consumer() {
    public void accept(Integer integer) {}
});

run(new Function() {
    public Integer apply(Integer o) { return 1; }
});

Maintenant en Java 8, j'aimerais bien appeler ces méthodes avec des expressions lambda bien sûr, et je peux le faire!

// Consumer
run((Integer i) -> {});

// Function
run((Integer i) -> 1);

Étant donné que le compilateur devrait être capable d'inférer Integer, pourquoi ne pas omettre Integer, alors?

// Consumer
run(i -> {});

// Function
run(i -> 1);

Mais cela ne compile pas. Le compilateur (javac, jdk1.8.0_05) n'aime pas cela:

Test.java:63: erreur: référence à run est ambiguë
        run(i -> {});
        ^
  à la fois la méthode run(Consumer) dans Test et 
       la méthode run(Function) dans Test correspondent

Intuitivement pour moi, cela n'a pas de sens. Il n'y a absolument aucune ambiguïté entre une expression lambda qui renvoie une valeur ("compatible-valeur") et une expression lambda qui renvoie void ("compatible-void"), comme indiqué dans le JLS §15.27.

Mais bien sûr, le JLS est profond et complexe et nous héritons de 20 ans d'histoire de compatibilité ascendante, et il y a de nouvelles choses comme:

Certaines expressions d'argument contenant des expressions lambda implicitement typées (§15.27.1) ou des références de méthode inexacts (§15.13.1) sont ignorés par les tests de pertinence, car leur signification ne peut pas être déterminée avant qu'un type cible ne soit sélectionné.

du JLS §15.12.2

La limitation ci-dessus est probablement liée au fait que JEP 101 n'a pas été mise en œuvre jusqu'au bout, comme on peut le voir ici et ici.

Question:

Qui peut me dire exactement quelles parties du JLS spécifient cette ambiguïté de compilation (ou s'agit-il d'un bug du compilateur)?

Bonus: Pourquoi les choses ont-elles été décidées de cette manière?

20voto

Holger Points 13789

Je pense que vous avez trouvé ce bug dans le compilateur : JDK-8029718 (ou celui-ci similaire dans Eclipse : 434642).

Comparez à JLS §15.12.2.1. Identifier les méthodes potentiellement applicables:

  • Une expression lambda (§15.27) est potentiellement compatible avec un type d'interface fonctionnelle (§9.8) si toutes les conditions suivantes sont remplies :

    • L'arité du type de fonction du type cible est la même que celle de l'expression lambda.

    • Si le type de fonction du type cible a un retour void, alors le corps de la lambda est soit une expression d'instruction (§14.8) soit un bloc compatible avec le void (§15.27.2).

    • Si le type de fonction du type cible a un type de retour (non-void), alors le corps de la lambda est soit une expression, soit un bloc compatible avec la valeur (§15.27.2).

Remarquez la distinction claire entre les "blocs compatibles avec le void" et les "blocs compatibles avec la valeur". Bien qu'un bloc puisse être les deux dans certains cas, la section §15.27.2. Corps de la lambda indique clairement qu'une expression comme () -> {} est un "bloc compatible avec le void", car elle se termine normalement sans renvoyer de valeur. Et il devrait être évident que i -> {} est également un "bloc compatible avec le void".

Et selon la section citée ci-dessus, la combinaison d'une lambda avec un bloc qui n'est pas compatible avec la valeur et un type de cible avec un type de retour (non-void) n'est pas un candidat potentiel pour la résolution de surcharge de méthode. Donc votre intuition est correcte, il ne devrait y avoir aucune ambiguïté ici.

Les exemples de blocs ambigus sont

() -> { throw new RuntimeException(); }
() -> { while (true); }

car ils ne se terminent pas normalement, mais ce n'est pas le cas dans votre question.

3voto

Vicente Romero Points 295

Ce bogue a déjà été signalé dans le système de bogue JDK : https://bugs.openjdk.java.net/browse/JDK-8029718. Comme vous pouvez le vérifier, le bogue a été corrigé. Cette correction synchronise javac avec la spécification à cet égard. Actuellement, javac accepte correctement la version avec des lambdas implicites. Pour obtenir cette mise à jour, vous devez cloner le dépôt javac 8.

La correction analyse le corps du lambda et détermine s'il est compatible avec void ou valeur. Pour déterminer cela, vous devez analyser toutes les instructions return. Rappelons que selon la spécification (15.27.2), déjà référencée ci-dessus :

  • Un corps de lambda de bloc est compatible avec void si chaque instruction return dans le bloc a la forme return.
  • Un corps de lambda de bloc est compatible avec valeur s'il ne peut pas se terminer normalement (14.21) et chaque instruction return dans le bloc a la forme return Expression.

Cela signifie qu'en analysant les retours dans le corps du lambda, vous pouvez savoir si le corps du lambda est compatible avec void, mais pour déterminer s'il est compatible avec valeur, vous devez également effectuer une analyse de flot dessus pour déterminer s'il peut se terminer normalement (14.21).

Cette correction introduit également une nouvelle erreur de compilation pour les cas où le corps n'est ni compatible avec void ni avec valeur, par exemple si nous compilons ce code :

class Test {
    interface I {
        String f(String x);
    }

    static void foo(I i) {}

    void m() {
        foo((x) -> {
            if (x == null) {
                return;
            } else {
                return x;
            }
        });
    }
}

le compilateur affichera cette sortie :

Test.java:9: erreur : le corps du lambda n'est ni compatible avec une valeur ni avec void
    foo((x) -> {
        ^
Note : Certains messages ont été simplifiés ; recompilez avec -Xdiags:verbose pour obtenir la sortie complète
1 erreur

J'espère que cela vous aidera.

0voto

ggovan Points 1178

Supposons que nous avons une méthode et un appel de méthode

void run(Fonction f)

run(i->i)

Quelles méthodes pouvons-nous légalement ajouter?

void run(BiFunction f)
void run(Fournisseur f)

Ici, l'arité des paramètres est différente, spécifiquement la partie i-> de i->i ne correspond pas aux paramètres de apply(T,U) dans BiFunction, ou get() dans Supplier. Ainsi ici, toute ambiguïté possible est définie par l'arité des paramètres, pas par les types, et pas par le retour.


Quelles méthodes ne pouvons-nous pas ajouter?

void run(Fonction f)

Ceci génère une erreur de compilation car run(..) et run(..) ont la même gomme. Comme la JVM ne peut pas prendre en charge deux fonctions avec le même nom et les mêmes types d'arguments, cela ne peut pas être compilé. Ainsi, le compilateur n'a jamais à résoudre les ambiguïtés dans ce type de scénario car elles sont explicitement interdites en raison des règles préexistantes dans le système de types Java.

Cela nous laisse donc avec d'autres types fonctionnels avec une arité de paramètre de 1.

void run(IntUnaryOperator f)

Ici, run(i->i) est valide pour à la fois Fonction et IntUnaryOperator, mais cela refusera de se compiler en raison de référence à run est ambiguë car les deux fonctions correspondent à ce lambda. En effet, elles le font, et une erreur ici est à prévoir.

interface X { void chose();}
interface Y { String chose();}

void run(Fonction f)
void run(Consommateur f)
run(i->i.chose())

Ici, cela échoue à la compilation, encore une fois en raison d'ambiguïtés. Sans connaître le type de i dans ce lambda, il est impossible de connaître le type de i.chose(). Nous acceptons donc que cela soit ambigu et qu'il échoue logiquement à être compilé.


Dans votre exemple :

void run(Consommateur f)
void run(Fonction f)
run(i->i)

Ici, nous savons que les deux types fonctionnels ont un seul paramètre Integer, donc nous savons que le i dans i-> doit être un Integer. Donc nous savons que c'est run(Fonction) qui est appelé. Mais le compilateur ne tente pas de le faire. C'est la première fois que le compilateur fait quelque chose que nous n'attendons pas.

Pourquoi ne le fait-il pas? je dirais que c'est parce que c'est un cas très spécifique, et inférer le type ici nécessite des mécanismes que nous n'avons pas vus pour aucun des autres cas ci-dessus, car dans le cas général ils ne peuvent pas correctement inférer le type et choisir la bonne méthode.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X