Sélection de signature de méthode pour l'expression lambda avec plusieurs types de cibles correspondants

11

Je répondais à une question et suis tombé sur un scénario que je ne peux pas expliquer. Considérez ce code:

interface ConsumerOne<T> {
    void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
}

class A {
    private static CustomIterable<A> iterable;
    private static List<A> aList;

    public static void main(String[] args) {
        iterable.forEach(a -> aList.add(a));     //ambiguous
        iterable.forEach(aList::add);            //ambiguous

        iterable.forEach((A a) -> aList.add(a)); //OK
    }
}

Je ne comprends pas pourquoi taper explicitement le paramètre du lambda (A a) -> aList.add(a)fait compiler le code. De plus, pourquoi est-il lié à la surcharge dans Iterableplutôt qu'à celle dedans CustomIterable?
Y a-t-il une explication à cela ou un lien vers la section pertinente de la spécification?

Remarque: iterable.forEach((A a) -> aList.add(a));compile uniquement lors de l' CustomIterable<T>extension Iterable<T>(une surcharge plate des méthodes CustomIterableentraîne une erreur ambiguë)


Obtenir ceci sur les deux:

  • openjdk version "13.0.2" 2020-01-14
    Compilateur Eclipse
  • openjdk version "1.8.0_232"
    compilateur Eclipse

Edit : le code ci-dessus ne parvient pas à compiler lors de la construction avec maven tandis qu'Eclipse compile la dernière ligne de code avec succès.

ernest_k
la source
3
Aucun des trois ne se compile sur Java 8. Maintenant, je ne sais pas si c'est un bug qui a été corrigé dans une version plus récente, ou un bug / fonctionnalité qui a été introduit ... Vous devriez probablement spécifier la version Java
Sweeper
@Sweeper J'ai d'abord obtenu ceci en utilisant jdk-13. Les tests ultérieurs dans java 8 (jdk8u232) montrent les mêmes erreurs. Je ne sais pas pourquoi le dernier ne compile pas non plus sur votre machine.
ernest_k
Impossible de reproduire sur deux compilateurs en ligne non plus ( 1 , 2 ). J'utilise 1.8.0_221 sur ma machine. Cela devient de plus en plus bizarre ...
Sweeper
1
@ernest_k Eclipse a sa propre implémentation de compilateur. Cela pourrait être une information cruciale pour la question. En outre, le fait qu'un maven propre génère des erreurs sur la dernière ligne doit également être mis en évidence dans la question à mon avis. En revanche, concernant la question liée, l'hypothèse selon laquelle l'OP utilise également Eclipse pourrait être clarifiée, car le code n'est pas reproductible.
Naman
3
Bien que je comprenne la valeur intellectuelle de la question, je ne peux que déconseiller de créer des méthodes surchargées qui ne diffèrent que par des interfaces fonctionnelles et en s'attendant à ce que cela puisse être appelé en toute sécurité en passant un lambda. Je ne crois pas que la combinaison de l'inférence de type lambda et de la surcharge soit quelque chose que le programmeur moyen sera proche de comprendre. C'est une équation avec de très nombreuses variables que les utilisateurs ne contrôlent pas. À ÉVITER, s'il vous plaît :)
Stephan Herrmann

Réponses:

8

TL; DR, il s'agit d'un bogue du compilateur.

Il n'y a aucune règle qui donnerait la priorité à une méthode applicable particulière lorsqu'elle est héritée ou à une méthode par défaut. Fait intéressant, lorsque je change le code en

interface ConsumerOne<T> {
    void accept(T a);
}
interface ConsumerTwo<T> {
  void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
    void forEach(ConsumerTwo<? super T> c); //another overload
}

l' iterable.forEach((A a) -> aList.add(a));instruction produit une erreur dans Eclipse.

Puisqu'aucune propriété de la forEach(Consumer<? super T) c)méthode de l' Iterable<T>interface n'a changé lors de la déclaration d'une autre surcharge, la décision d'Eclipse de sélectionner cette méthode ne peut pas (de manière cohérente) être basée sur une propriété de la méthode. C'est toujours la seule méthode héritée, toujours la seule defaultméthode, toujours la seule méthode JDK, etc. Aucune de ces propriétés ne devrait de toute façon affecter la sélection de la méthode.

Notez que la modification de la déclaration en

interface CustomIterable<T> {
    void forEach(ConsumerOne<? super T> c);
    default void forEach(ConsumerTwo<? super T> c) {}
}

produit également une erreur «ambiguë», donc le nombre de méthodes surchargées applicables n'a pas d'importance non plus, même lorsqu'il n'y a que deux candidats, il n'y a pas de préférence générale vers les defaultméthodes.

Jusqu'à présent, le problème semble se poser lorsqu'il existe deux méthodes applicables et qu'une defaultméthode et une relation d'héritage sont impliquées, mais ce n'est pas le bon endroit pour creuser davantage.


Mais il est compréhensible que les constructions de votre exemple puissent être gérées par différents codes d'implémentation dans le compilateur, l'un présentant un bogue tandis que l'autre ne le fait pas.
a -> aList.add(a)est une expression lambda typée implicitement , qui ne peut pas être utilisée pour la résolution de surcharge. En revanche, (A a) -> aList.add(a)est une expression lambda explicitement typée qui peut être utilisée pour sélectionner une méthode correspondante parmi les méthodes surchargées, mais cela n'aide pas ici (ne devrait pas aider ici), car toutes les méthodes ont des types de paramètres avec exactement la même signature fonctionnelle .

À titre de contre-exemple, avec

static void forEach(Consumer<String> c) {}
static void forEach(Predicate<String> c) {}
{
  forEach(s -> s.isEmpty());
  forEach((String s) -> s.isEmpty());
}

les signatures fonctionnelles diffèrent, et l'utilisation d'une expression lambda de type explicite peut en effet aider à sélectionner la bonne méthode tandis que l'expression lambda implicitement typée n'aide pas, donc forEach(s -> s.isEmpty())produit une erreur de compilation. Et tous les compilateurs Java sont d'accord là-dessus.

Notez qu'il aList::adds'agit d'une référence de méthode ambiguë, car la addméthode est également surchargée, elle ne peut donc pas aider à sélectionner une méthode, mais les références de méthode peuvent de toute façon être traitées par un code différent. Passer à un sans ambiguïté aList::containsou passer Listà Collection, pour rendre l' addambiguïté, n'a pas changé le résultat dans mon installation Eclipse (j'ai utilisé 2019-06).

Holger
la source
1
@howlger votre commentaire n'a aucun sens. La méthode par défaut est la méthode héritée et la méthode est surchargée. Il n'y a pas d'autre méthode. Le fait que la méthode héritée soit une defaultméthode n'est qu'un point supplémentaire. Ma réponse montre déjà un exemple où Eclipse ne donne pas la priorité à la méthode par défaut.
Holger
1
@howlger il y a une différence fondamentale dans nos comportements. Vous avez seulement découvert que la suppression defaultmodifiait le résultat et supposiez immédiatement trouver la raison du comportement observé. Vous êtes tellement confiant à ce sujet que vous appelez mal d'autres réponses, malgré le fait qu'elles ne sont même pas contradictoires. Parce que vous projetez votre propre comportement sur d'autres, je n'ai jamais prétendu que l'héritage était la raison . J'ai prouvé que non. J'ai démontré que le comportement est incohérent, car Eclipse sélectionne la méthode particulière dans un scénario mais pas dans un autre, où trois surcharges existent.
Holger
1
@howlger en plus de cela, j'ai déjà nommé un autre scénario à la fin de ce commentaire , créer une interface, pas d'héritage, deux méthodes, defaultl'autre abstract, avec deux arguments de type consommateur et l'essayer. Eclipse dit à juste titre qu'elle est ambiguë, bien que l'une soit une defaultméthode. Apparemment, l'héritage est toujours pertinent pour ce bogue Eclipse, mais contrairement à vous, je ne deviens pas fou et je ne me trompe pas sur les autres réponses, simplement parce qu'ils n'ont pas analysé le bogue dans son intégralité. Ce n'est pas notre travail ici.
Holger
1
@howlger non, le fait est que c'est un bug. La plupart des lecteurs ne se soucient même pas des détails. Eclipse peut lancer des dés à chaque fois qu'elle sélectionne une méthode, peu importe. Eclipse ne doit pas sélectionner une méthode lorsqu'elle est ambiguë, donc peu importe pourquoi elle en sélectionne une. Cette réponse prouve que le comportement est incohérent, ce qui est déjà suffisant pour indiquer fortement qu'il s'agit d'un bogue. Il n'est pas nécessaire de pointer la ligne du code source d'Eclipse où les choses tournent mal. Ce n'est pas le but de Stackoverflow. Peut-être confondez-vous Stackoverflow avec le traqueur de bogues d'Eclipse.
Holger
1
@howlger, vous prétendez à nouveau à tort que j'ai fait une déclaration (erronée) expliquant pourquoi Eclipse a fait ce mauvais choix. Encore une fois, je ne l'ai pas fait, car l'éclipse ne devrait pas faire de choix du tout. La méthode est ambiguë. Point. La raison pour laquelle j'ai utilisé le terme « hérité », c'est parce que la distinction des méthodes portant le même nom était nécessaire. J'aurais pu dire « méthode par défaut » à la place, sans changer la logique. Plus correctement, j'aurais dû utiliser l'expression « la méthode même Eclipse mal sélectionnée pour une raison quelconque ». Vous pouvez utiliser l'une ou l'autre des trois phrases de manière interchangeable et la logique ne change pas.
Holger
2

Le compilateur Eclipse se résout correctement à la defaultméthode , car il s'agit de la méthode la plus spécifique selon la spécification de langage Java 15.12.2.5 :

Si exactement l'une des méthodes maximales spécifiques est concrète (c'est-à-dire non abstractou par défaut), c'est la méthode la plus spécifique.

javac(utilisé par Maven et IntelliJ par défaut) indique que l'appel de méthode est ambigu ici. Mais selon la spécification du langage Java, ce n'est pas ambigu car l'une des deux méthodes est ici la méthode la plus spécifique.

Les expressions lambda typées implicitement sont traitées différemment des expressions lambda typées explicitement en Java. Les expressions lambda typées implicitement, contrairement aux expressions explicitement typées, passent par la première phase pour identifier les méthodes d'invocation stricte (voir Java Language Specification jls-15.12.2.2 , premier point). Par conséquent, l'appel de méthode ici est ambigu pour les expressions lambda typées implicitement .

Dans votre cas, la solution de contournement pour ce javacbogue consiste à spécifier le type de l'interface fonctionnelle au lieu d'utiliser une expression lambda explicitement typée comme suit:

iterable.forEach((ConsumerOne<A>) aList::add);

ou

iterable.forEach((Consumer<A>) aList::add);

Voici votre exemple encore minimisé pour les tests:

class A {

    interface FunctionA { void f(A a); }
    interface FunctionB { void f(A a); }

    interface FooA {
        default void foo(FunctionA functionA) {}
    }

    interface FooAB extends FooA {
        void foo(FunctionB functionB);
    }

    public static void main(String[] args) {
        FooAB foo = new FooAB() {
            @Override public void foo(FunctionA functionA) {
                System.out.println("FooA::foo");
            }
            @Override public void foo(FunctionB functionB) {
                System.out.println("FooAB::foo");
            }
        };
        java.util.List<A> list = new java.util.ArrayList<A>();

        foo.foo(a -> list.add(a));      // ambiguous
        foo.foo(list::add);             // ambiguous

        foo.foo((A a) -> list.add(a));  // not ambiguous (since FooA::foo is default; javac bug)
    }

}
hurler
la source
4
Vous avez manqué la condition préalable juste avant la phrase citée: « Si toutes les méthodes au maximum spécifiques ont des signatures équivalentes à la substitution » Bien sûr, deux méthodes dont les arguments sont des interfaces totalement indépendantes n'ont pas de signatures équivalentes à la substitution. En plus de cela, cela n'expliquerait pas pourquoi Eclipse cesse de sélectionner la defaultméthode lorsqu'il existe trois méthodes candidates ou lorsque les deux méthodes sont déclarées dans la même interface.
Holger
1
@Holger Votre réponse affirme: "Il n'y a aucune règle qui donnerait la priorité à une méthode applicable particulière lorsqu'elle est héritée ou à une méthode par défaut." Dois-je bien comprendre que vous dites que la condition préalable à cette règle inexistante ne s'applique pas ici? Veuillez noter que le paramètre ici est une interface fonctionnelle (voir JLS 9.8).
hurlement le
1
Vous avez arraché une phrase hors de son contexte. La phrase décrit la sélection de méthodes équivalentes à la substitution , en d'autres termes, un choix entre des déclarations qui invoqueraient toutes la même méthode au moment de l'exécution, car il n'y aura qu'une seule méthode concrète dans la classe concrète. Cela n'est pas pertinent dans le cas de méthodes distinctes comme forEach(Consumer)et forEach(Consumer2)qui ne peuvent jamais aboutir à la même méthode de mise en œuvre.
Holger
2
@StephanHerrmann Je ne connais pas de JEP ou JSR, mais le changement ressemble à un correctif pour être conforme à la signification de «concret», c'est-à-dire comparer avec JLS§9.4 : « Les méthodes par défaut sont distinctes des méthodes concrètes (§8.4. 3.1), qui sont déclarés dans les classes. », Qui n'a jamais changé.
Holger
2
@StephanHerrmann oui, il semble que la sélection d'un candidat à partir de méthodes équivalentes est devenue plus compliquée et il serait intéressant d'en connaître la justification, mais cela n'est pas pertinent pour la question posée. Il devrait y avoir un autre document expliquant les changements et les motivations. Dans le passé, il y en avait, mais avec cette politique "une nouvelle version tous les ½ ans", garder la qualité semble impossible ...
Holger
2

Le code dans lequel Eclipse implémente JLS §15.12.2.5 ne trouve aucune méthode comme plus spécifique que l'autre, même pour le cas du lambda explicitement typé.

Idéalement, Eclipse s'arrêterait ici et rapporterait une ambiguïté. Malheureusement, l'implémentation de la résolution de surcharge a un code non trivial en plus d'implémenter JLS. D'après ma compréhension, ce code (qui date de l'époque où Java 5 était nouveau) doit être conservé pour combler certaines lacunes dans JLS.

J'ai déposé https://bugs.eclipse.org/562538 pour suivre cela.

Indépendamment de ce bug particulier, je ne peux que déconseiller fortement ce style de code. La surcharge est bonne pour un bon nombre de surprises en Java, multipliée par l'inférence de type lambda, la complexité est tout à fait disproportionnée par rapport au gain perçu.

Stephan Herrmann
la source
Je vous remercie. Avait déjà enregistré bugs.eclipse.org/bugs/show_bug.cgi?id=562507 , peut-être pourriez-vous aider à les lier ou à en fermer un en double ...
ernest_k