web-dev-qa-db-fra.com

Expression lambda et méthode surchargeant les doutes

OK, donc la surcharge de méthode est une mauvaise chose ™. Maintenant que cela a été réglé, supposons que je - veux pour surcharger une méthode comme celle-ci:

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

static void run(Function<Integer, Integer> function) {
    System.out.println("function");
}

Dans Java 7, je pourrais les appeler facilement avec des classes anonymes non ambiguës comme arguments:

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

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

Maintenant dans Java 8, je voudrais bien sûr appeler ces méthodes avec des expressions lambda, et je peux!

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

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

Puisque le compilateur devrait pouvoir déduire Integer, pourquoi ne pas laisser Integer loin, alors?

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

// Function
run(i -> 1);

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

Test.Java:63: error: reference to run is ambiguous
        run(i -> {});
        ^
  both method run(Consumer<Integer>) in Test and 
       method run(Function<Integer,Integer>) in Test match

Pour moi, intuitivement, cela n'a pas de sens. Il n'y a absolument aucune ambiguïté entre une expression lambda qui donne une valeur de retour ("value-compatible") et une expression lambda qui donne void ("void-compatible"), 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é descendante, et il y a de nouvelles choses comme:

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

de JLS §15.12.2

La limitation ci-dessus est probablement liée au fait que JEP 101 n'a pas été implémenté complètement, comme on peut le voir ici et ici .

Question:

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

Bonus: Pourquoi les choses ont-elles été décidées de cette façon?

Mettre à jour:

Avec jdk1.8.0_40, ce qui précède se compile et fonctionne correctement

48
Lukas Eder

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

Comparer avec 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 l'arité de l'expression lambda.

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

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

Notez la distinction claire entre "void blocs compatibles" et "blocs compatibles avec les valeurs". Alors qu'un bloc peut être à la fois dans certains cas, la section §15.27.2. Lambda Body indique clairement qu'une expression comme () -> {} est un "void compatible block", car il se termine normalement sans renvoyer de valeur. Et il devrait être évident que i -> {} est également un "bloc compatible _ void".

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

Des 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.

19
Holger

Ce bogue a déjà été signalé dans le système de bogues JDK: https://bugs.openjdk.Java.net/browse/JDK-8029718 . Comme vous pouvez le vérifier, le bug a été corrigé. Cette correction synchronise javac avec la spécification dans cet aspect. À l'heure actuelle, javac accepte correctement la version avec des lambdas implicites. Pour obtenir cette mise à jour, vous devez cloner repo javac 8 .

Le correctif consiste à analyser le corps lambda et à déterminer s'il est compatible avec le vide ou la valeur. Pour déterminer cela, vous devez analyser toutes les déclarations de retour. Rappelons que d'après la spécification (15.27.2), déjà référencée ci-dessus:

  • Un corps lambda de bloc est compatible avec le vide si chaque instruction de retour dans le bloc a la forme return.
  • Un corps lambda de bloc est compatible avec les valeurs 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 lambda, vous pouvez savoir si le corps lambda est compatible avec le vide, mais pour déterminer s'il est compatible avec la valeur, vous devez également effectuer une analyse de flux sur celui-ci pour déterminer qu'il peut se terminer normalement ( 14,21 ).

Ce correctif introduit également une nouvelle erreur de compilation pour les cas où le corps n'est ni compatible avec les valeurs nulles ni les valeurs, 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 donnera cette sortie:

Test.Java:9: error: lambda body is neither value nor void compatible
    foo((x) -> {
        ^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error

J'espère que ça aide.

3
Vicente Romero

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

void run(Function<Integer, Integer> f)

run(i->i)

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

void run(BiFunction<Integer, Integer, Integer> f)
void run(Supplier<Integer> f)

Ici, l'arité des paramètres est différente, en particulier la partie i-> De i->i Ne correspond pas aux paramètres de apply(T,U) dans BiFunction ou get() dans Supplier. Donc, ici, toutes les ambiguïtés possibles sont définies par l'arité des paramètres, et non par les types, et non par le retour.


Quelles méthodes ne pouvons-nous pas ajouter?

void run(Function<Integer, String> f)

Cela donne une erreur de compilation comme run(..) and run(..) have the same erasure. Donc, 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 type Java 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 à la fois pour Function et IntUnaryOperator, mais cela refusera de compiler en raison de reference to run is ambiguous Car les deux fonctions correspondent à ce lambda. En effet, ils le font, et une erreur est à prévoir ici.

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

void run(Function<Y,String> f)
void run(Consumer<X> f)
run(i->i.thing())

Ici, cela ne se compile pas, 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.thing(). Nous admettons donc que cela est ambigu et ne parvient pas à juste titre à être compilé.


Dans votre exemple:

void run(Consumer<Integer> f)
void run(Function<Integer,Integer> f)
run(i->i)

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

Pourquoi ne fait-il pas cela? Je dirais que c'est un cas très spécifique, et inférer le type ici nécessite des mécanismes que nous n'avons vu pour aucun des autres cas ci-dessus, car dans le cas général, ils ne sont pas en mesure de déduire correctement le type et de choisir la bonne méthode .

0
ggovan