Expression de Lambda et doutes de la méthode de surcharge

OK, donc la méthode de surcharge est-une-mauvaise-chose™. Maintenant que c'est réglé, supposons que je veuille 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");
}

en 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 appeler ces méthodes avec des expressions lambda bien sûr, et je peux!

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

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

depuis le le compilateur devrait être capable de déduire Integer , pourquoi ne pas laisser Integer loin, alors?

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

// Function
run(i -> 1);

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

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 ("compatible avec la valeur") et une expression lambda qui donne void ("compatible avec le vide"), 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é à rebours, et il y a de nouvelles choses comme:

certaines expressions d'argument qui contiennent implicitement dactylographiées lambda expressions ( §15.27.1 ) ou inexacte références à la méthode ( §15.13.1 ) sont ignorées par les tests d'applicabilité, parce que 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é mis en œuvre dans son intégralité, comme on peut le voir ici et ici .

Question:

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

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

mise à jour:

avec jdk1.8.0_40, le ci-dessus compiles et fonctionne très bien

45
demandé sur Lukas Eder 2014-05-02 18:43:14

3 réponses

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

comparer à 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 sont vrais:

    • 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 de vide, alors le corps lambda est soit une expression de déclaration (§14.8) soit un bloc compatible avec un vide (§15.27.2).

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

Note la distinction claire entre " void compatible blocs" et "valeur compatible avec les blocs". 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", comme il complète normalement sans retourner une valeur. Et il devrait être évident que i -> {} est "un 151910920" compatible bloc".

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

exemples de blocs Ambigus:

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

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

18
répondu Holger 2014-05-12 12:19:38

ce bug a déjà été signalé dans le système de Bug JDK: https://bugs.openjdk.java.net/browse/JDK-8029718 . Comme vous pouvez vérifier que le bug a été corrigé. Cette correction synchronise javac avec le spec dans cet aspect. Pour l'instant javac accepte correctement la version implicite avec lambdas. Pour obtenir cette mise à jour, vous devez cloner javac 8 repo .

ce que le fix fait est d'analyser le corps lambda et de déterminer si elle est vide ou compatible avec la valeur. Pour déterminer cela, vous devez analyser tous les déclarations de retour. Rappelons que de la spécification (15.27.2), déjà référencée ci-dessus:

  • Un bloc lambda corps est nulle-compatible si chaque instruction de retour dans le bloc a le formulaire de retour.
  • un corps lambda de bloc est compatible avec la valeur s'il ne peut pas être complet normalement ( 14.21 ) et chaque déclaration de retour dans le bloc a le forme Expression de retour.

cela signifie qu'en analysant les retours dans le corps lambda vous pouvez savoir si le corps lambda est compatible vide mais pour déterminer si sa valeur compatible vous devez également faire une analyse de flux sur elle pour déterminer qu'il peut compléter normalement ( 14.21 ).

cette correction introduit également une nouvelle erreur de compilateur pour les cas où le corps n'est ni nul ni compatible avec 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 cela aidera.

3
répondu Vicente Romero 2014-05-07 02:22:56

laisse supposer que nous avons méthode et appel de méthode

void run(Function<Integer, Integer> f)

run(i->i)

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

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

ici le paramètre arity est différent, spécifiquement la i-> partie 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 le paramètre arity, pas les types, et pas le retour.


quelles méthodes ne pouvons-nous pas ajouter?

void run(Function<Integer, String> f)

cela donne une erreur de compilateur comme run(..) and run(..) have the same erasure . Donc, comme la JVM ne peut pas supporter deux fonctions avec le même nom et le même type d'argument, 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 rejetées en raison des règles préexistantes dans le système de type Java.

Alors que nous laisse avec d'autres types fonctionnels avec un paramètre arité de 1.

void run(IntUnaryOperator f)

ici run(i->i) est valable à la fois pour Function et IntUnaryOperator , mais cela refusera de compiler en raison de reference to run is ambiguous car les deux fonctions correspondent à cette lambda. En effet, ils le font, et qu'une erreur est possible 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 compile pas, encore une fois en raison d'ambiguïtés. Sans connaître le type de i dans cette lambda, il est impossible de connaître le type de i.thing() . Nous par conséquent accepter que cela est ambigu et à juste titre échoue à compiler.


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 , donc nous savons 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 faire ça. C'est la première fois que le compilateur ne quelque chose que nous ne nous attendons pas.

pourquoi ne fait-il pas cela? Je dirais 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 vu pour aucun des autres cas ci-dessus, parce que dans le cas général, ils sont incapables d'inférer correctement le type et de choisir la bonne méthode.

0
répondu ggovan 2014-05-05 13:11:56