Pourquoi un paramètre de type est-il plus fort qu'un paramètre de méthode

12

Pourquoi est-ce

public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {...}

plus strict alors

public <R> Builder<T> with(Function<T, R> getter, R returnValue) {...}

Il s'agit d'un suivi de la raison pour laquelle le type de retour lambda n'est pas vérifié au moment de la compilation . J'ai trouvé en utilisant la méthode withX()comme

.withX(MyInterface::getLength, "I am not a Long")

produit l'erreur de temps de compilation souhaitée:

Le type de getLength () du type BuilderExample.MyInterface est long, ce qui est incompatible avec le type de retour du descripteur: String

tout en utilisant la méthode with()ne fonctionne pas.

exemple complet:

import java.util.function.Function;

public class SO58376589 {
  public static class Builder<T> {
    public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {
      return this;
    }

    public <R> Builder<T> with(Function<T, R> getter, R returnValue) {
      return this;
    }

  }

  static interface MyInterface {
    public Long getLength();
  }

  public static void main(String[] args) {
    Builder<MyInterface> b = new Builder<MyInterface>();
    Function<MyInterface, Long> getter = MyInterface::getLength;
    b.with(getter, 2L);
    b.with(MyInterface::getLength, 2L);
    b.withX(getter, 2L);
    b.withX(MyInterface::getLength, 2L);
    b.with(getter, "No NUMBER"); // error
    b.with(MyInterface::getLength, "No NUMBER"); // NO ERROR !!
    b.withX(getter, "No NUMBER"); // error
    b.withX(MyInterface::getLength, "No NUMBER"); // error !!!
  }
}

javac SO58376589.java

SO58376589.java:32: error: method with in class Builder<T> cannot be applied to given types;
    b.with(getter, "No NUMBER"); // error
     ^
  required: Function<MyInterface,R>,R
  found: Function<MyInterface,Long>,String
  reason: inference variable R has incompatible bounds
    equality constraints: Long
    lower bounds: String
  where R,T are type-variables:
    R extends Object declared in method <R>with(Function<T,R>,R)
    T extends Object declared in class Builder
SO58376589.java:34: error: method withX in class Builder<T> cannot be applied to given types;
    b.withX(getter, "No NUMBER"); // error
     ^
  required: F,R
  found: Function<MyInterface,Long>,String
  reason: inference variable R has incompatible bounds
    equality constraints: Long
    lower bounds: String
  where F,R,T are type-variables:
    F extends Function<MyInterface,R> declared in method <R,F>withX(F,R)
    R extends Object declared in method <R,F>withX(F,R)
    T extends Object declared in class Builder
SO58376589.java:35: error: incompatible types: cannot infer type-variable(s) R,F
    b.withX(MyInterface::getLength, "No NUMBER"); // error
           ^
    (argument mismatch; bad return type in method reference
      Long cannot be converted to String)
  where R,F,T are type-variables:
    R extends Object declared in method <R,F>withX(F,R)
    F extends Function<T,R> declared in method <R,F>withX(F,R)
    T extends Object declared in class Builder
3 errors

Exemple étendu

L'exemple suivant montre le comportement différent de la méthode et du paramètre de type réduit à un fournisseur. En outre, il montre la différence par rapport au comportement d'un consommateur pour un paramètre de type. Et cela montre que cela ne fait aucune différence qu'il soit un consommateur ou un fournisseur pour un paramètre de méthode.

import java.util.function.Consumer;
import java.util.function.Supplier;
interface TypeInference {

  Number getNumber();

  void setNumber(Number n);

  @FunctionalInterface
  interface Method<R> {
    TypeInference be(R r);
  }

  //Supplier:
  <R> R letBe(Supplier<R> supplier, R value);
  <R, F extends Supplier<R>> R letBeX(F supplier, R value);
  <R> Method<R> let(Supplier<R> supplier);  // return (x) -> this;

  //Consumer:
  <R> R lettBe(Consumer<R> supplier, R value);
  <R, F extends Consumer<R>> R lettBeX(F supplier, R value);
  <R> Method<R> lett(Consumer<R> consumer);


  public static void main(TypeInference t) {
    t.letBe(t::getNumber, (Number) 2); // Compiles :-)
    t.lettBe(t::setNumber, (Number) 2); // Compiles :-)
    t.letBe(t::getNumber, 2); // Compiles :-)
    t.lettBe(t::setNumber, 2); // Compiles :-)
    t.letBe(t::getNumber, "NaN"); // !!!! Compiles :-(
    t.lettBe(t::setNumber, "NaN"); // Does not compile :-)

    t.letBeX(t::getNumber, (Number) 2); // Compiles :-)
    t.lettBeX(t::setNumber, (Number) 2); // Compiles :-)
    t.letBeX(t::getNumber, 2); // !!! Does not compile  :-(
    t.lettBeX(t::setNumber, 2); // Compiles :-)
    t.letBeX(t::getNumber, "NaN"); // Does not compile :-)
    t.lettBeX(t::setNumber, "NaN"); // Does not compile :-)

    t.let(t::getNumber).be(2); // Compiles :-)
    t.lett(t::setNumber).be(2); // Compiles :-)
    t.let(t::getNumber).be("NaN"); // Does not compile :-)
    t.lett(t::setNumber).be("NaN"); // Does not compile :-)
  }
}
jukzi
la source
1
En raison de l'inférence avec ce dernier. Bien que les deux soient basés sur le cas d'utilisation à implémenter. Pour le vôtre, le premier pourrait être strict et bon. Pour plus de flexibilité, quelqu'un d'autre peut préférer ce dernier.
Naman
Essayez-vous de compiler cela dans Eclipse? La recherche de chaînes d'erreur du format que vous avez collé suggère qu'il s'agit d'une erreur spécifique à Eclipse (ecj). Avez-vous le même problème lors de la compilation avec raw javacou un outil de construction comme Gradle ou Maven?
user31601
@ user31601 j'ai ajouté un exemple complet avec sortie javac. Les messages d'erreur sont de format peu différent mais toujours éclipse et javac ont le même comportement
jukzi

Réponses:

12

C'est une question vraiment intéressante. La réponse, je le crains, est compliquée.

tl; dr

Trouver la différence implique une lecture assez approfondie de la spécification d' inférence de type Java , mais se résume essentiellement à ceci:

  • Toutes choses égales par ailleurs, le compilateur déduit le type le plus spécifique possible.
  • Cependant, s'il peut trouver une substitution pour un paramètre de type qui satisfait toutes les exigences, la compilation réussira, même si la substitution s'avère vague .
  • Car withil existe une substitution (certes vague) qui satisfait à toutes les exigences concernant R:Serializable
  • Pour withX, l'introduction du paramètre de type supplémentaire Foblige le compilateur à résoudre d' Rabord, sans tenir compte de la contrainte F extends Function<T,R>. Rse résout en (beaucoup plus spécifique) Stringce qui signifie alors que l'inférence des Féchecs.

Ce dernier point est le plus important, mais aussi le plus ondulé. Je ne peux pas penser à une manière plus concise de le formuler, donc si vous voulez plus de détails, je vous suggère de lire l'explication complète ci-dessous.

Est-ce le comportement voulu?

Je vais sortir sur un membre ici, et dire non .

Je ne suggère pas qu'il y ait un bug dans la spécification, plus que (dans le cas de withX) les concepteurs de langage ont levé la main et dit "il y a des situations où l'inférence de type devient trop difficile, donc nous échouerons" . Même si le comportement du compilateur par rapport à withXsemble être ce que vous voulez, je considérerais que c'est un effet secondaire fortuit de la spécification actuelle, plutôt qu'une décision de conception positive.

Cela est important, car il informe la question Dois-je me fier à ce comportement dans la conception de mon application? Je dirais que vous ne devriez pas, car vous ne pouvez pas garantir que les futures versions du langage continueront de se comporter de cette façon.

S'il est vrai que les concepteurs de langage s'efforcent de ne pas casser les applications existantes lorsqu'ils mettent à jour leur spécification / conception / compilateur, le problème est que le comportement sur lequel vous souhaitez vous fier est celui où le compilateur échoue actuellement (c'est-à-dire pas une application existante ). Les mises à jour de Langauge transforment le code non compilable en code compilateur tout le temps. Par exemple, le code suivant pourrait être garanti de ne pas compiler en Java 7, mais se compilerait en Java 8:

static Runnable x = () -> System.out.println();

Votre cas d'utilisation n'est pas différent.

Une autre raison pour laquelle je serais prudent à propos de l'utilisation de votre withXméthode est le Fparamètre lui-même. En règle générale, un paramètre de type générique sur une méthode (qui n'apparaît pas dans le type de retour) existe pour lier les types de plusieurs parties de la signature. Ça dit:

Peu m'importe ce que Tc'est, mais je veux être sûr que partout où j'utilise Tc'est le même type.

Logiquement, nous nous attendrions donc à ce que chaque paramètre de type apparaisse au moins deux fois dans une signature de méthode, sinon "il ne fait rien". Fdans votre withXn'apparaît qu'une seule fois dans la signature, ce qui me suggère une utilisation d'un paramètre de type non conforme à l' intention de cette fonctionnalité du langage.

Une implémentation alternative

Une façon d'implémenter cela d'une manière légèrement plus "comportementale" serait de diviser votre withméthode en une chaîne de 2:

public class Builder<T> {

    public final class With<R> {
        private final Function<T,R> method;

        private With(Function<T,R> method) {
            this.method = method;
        }

        public Builder<T> of(R value) {
            // TODO: Body of your old 'with' method goes here
            return Builder.this;
        }
    }

    public <R> With<R> with(Function<T,R> method) {
        return new With<>(method);
    }

}

Cela peut ensuite être utilisé comme suit:

b.with(MyInterface::getLong).of(1L); // Compiles
b.with(MyInterface::getLong).of("Not a long"); // Compiler error

Cela n'inclut pas un paramètre de type étranger comme le vôtre withX. En décomposant la méthode en deux signatures, elle exprime également mieux l'intention de ce que vous essayez de faire, du point de vue de la sécurité des types:

  • La première méthode définit une classe ( With) qui définit le type en fonction de la référence de la méthode.
  • La méthode scond ( of) contraint le type de la valuepour être compatible avec ce que vous avez précédemment configuré.

Le seul moyen pour une future version du langage de compiler cela est de mettre en œuvre le typage complet du canard, ce qui semble peu probable.

Une dernière remarque pour rendre tout cela non pertinent: je pense que Mockito (et en particulier sa fonctionnalité de stubbing) pourrait déjà faire ce que vous essayez de réaliser avec votre "constructeur générique sûr de type". Peut-être pourriez-vous simplement l'utiliser à la place?

L'explication complète (ish)

Je vais suivre la procédure d'inférence de type pour withet withX. C'est assez long, alors prenez-le lentement. En dépit d'être long, j'ai encore laissé pas mal de détails. Vous pouvez vous référer à la spécification pour plus de détails (suivez les liens) pour vous convaincre que j'ai raison (j'ai peut-être bien fait une erreur).

Aussi, pour simplifier un peu les choses, je vais utiliser un exemple de code plus minimal. La principale différence est qu'il permute sur Functionpour Supplier, donc il y a moins de types et les paramètres en jeu. Voici un extrait complet qui reproduit le comportement que vous avez décrit:

public class TypeInference {

    static long getLong() { return 1L; }

    static <R> void with(Supplier<R> supplier, R value) {}
    static <R, F extends Supplier<R>> void withX(F supplier, R value) {}

    public static void main(String[] args) {
        with(TypeInference::getLong, "Not a long");       // Compiles
        withX(TypeInference::getLong, "Also not a long"); // Does not compile
    }

}

Examinons tour à tour l' inférence d'applicabilité de type et la procédure d' inférence de type pour chaque appel de méthode:

with

On a:

with(TypeInference::getLong, "Not a long");

L'ensemble de bornes initial, B 0 , est:

  • R <: Object

Toutes les expressions de paramètres sont pertinentes pour l'applicabilité .

Par conséquent, l'ensemble de contraintes initial pour l' inférence d'applicabilité , C , est:

  • TypeInference::getLong est compatible avec Supplier<R>
  • "Not a long" est compatible avec R

Cela se réduit à l'ensemble lié B 2 de:

  • R <: Object(à partir de B 0 )
  • Long <: R (à partir de la première contrainte)
  • String <: R (à partir de la deuxième contrainte)

Étant donné que cela ne contient pas la borne « false » et (je suppose) la résolution de Rsuccès (donnant Serializable), alors l'invocation est applicable.

Nous passons donc à l' inférence de type d'invocation .

Le nouvel ensemble de contraintes, C , avec les variables d' entrée et de sortie associées , est:

  • TypeInference::getLong est compatible avec Supplier<R>
    • Variables d'entrée: aucune
    • Variables de sortie: R

Cela ne contient pas d'interdépendances entre les variables d' entrée et de sortie , donc peut être réduit en une seule étape, et le jeu de bornes final, B 4 , est le même que B 2 . Par conséquent, la résolution réussit comme avant, et le compilateur pousse un soupir de soulagement!

withX

On a:

withX(TypeInference::getLong, "Also not a long");

L'ensemble de bornes initial, B 0 , est:

  • R <: Object
  • F <: Supplier<R>

Seule la deuxième expression de paramètre est pertinente pour l'applicabilité . Le premier ( TypeInference::getLong) ne l'est pas, car il remplit la condition suivante:

Si mest une méthode générique et que l'appel de méthode ne fournit pas d'arguments de type explicites, une expression lambda explicitement typée ou une expression de référence de méthode exacte pour laquelle le type cible correspondant (dérivé de la signature de m) est un paramètre de type de m.

Par conséquent, l'ensemble de contraintes initial pour l' inférence d'applicabilité , C , est:

  • "Also not a long" est compatible avec R

Cela se réduit à l'ensemble lié B 2 de:

  • R <: Object(à partir de B 0 )
  • F <: Supplier<R>(à partir de B 0 )
  • String <: R (de la contrainte)

Encore une fois, puisque cela ne contient pas la borne " false " et la résolution de Rsuccès (donnant String), alors l'invocation est applicable.

Inférence de type d'invocation une fois de plus ...

Cette fois, le nouvel ensemble de contraintes, C , avec les variables d' entrée et de sortie associées , est:

  • TypeInference::getLong est compatible avec F
    • Variables d'entrée: F
    • Variables de sortie: aucune

Encore une fois, nous n'avons aucune interdépendance entre les variables d' entrée et de sortie . Cependant, cette fois, il existe une variable d'entrée ( F), nous devons donc résoudre ce problème avant de tenter une réduction . Donc, nous commençons avec notre ensemble lié B 2 .

  1. Nous déterminons un sous-ensemble Vcomme suit:

    Étant donné un ensemble de variables d'inférence à résoudre, Vsoit l'union de cet ensemble et de toutes les variables dont dépend la résolution d'au moins une variable de cet ensemble.

    Par la deuxième borne de B 2 , la résolution de Fdépend Rdonc V := {F, R}.

  2. Nous choisissons un sous-ensemble de Vselon la règle:

    laissez { α1, ..., αn }un sous - ensemble non vide de variables non dans Vtelle que i) pour tous i (1 ≤ i ≤ n), si αidépend de la résolution d'une variable β, alors soit βa une instanciation ou il y a une jtelle que β = αj; et ii) il n'existe aucun sous-ensemble approprié non vide de { α1, ..., αn }cette propriété.

    Le seul sous-ensemble Vqui satisfait cette propriété est {R}.

  3. En utilisant la troisième borne ( String <: R), nous instancions R = Stringet l'incorporons dans notre ensemble lié. Rest maintenant résolu, et la deuxième borne devient effectivement F <: Supplier<String>.

  4. En utilisant la deuxième borne (révisée), nous instancions F = Supplier<String>. Fest maintenant résolu.

Maintenant que Fc'est résolu, nous pouvons procéder à la réduction , en utilisant la nouvelle contrainte:

  1. TypeInference::getLong est compatible avec Supplier<String>
  2. ... réduit à Long est compatible avec String
  3. ... qui se réduit à faux

... et nous obtenons une erreur de compilation!


Notes supplémentaires sur «l'exemple étendu»

L' exemple étendu de la question examine quelques cas intéressants qui ne sont pas directement couverts par le fonctionnement ci-dessus:

  • Où le type de valeur est un sous - type de la méthode return type ( Integer <: Number)
  • Lorsque l'interface fonctionnelle est contravariante dans le type déduit (c'est-à-dire Consumerplutôt que Supplier)

En particulier, 3 des invocations données se distinguent comme suggérant potentiellement un comportement de compilateur «différent» de celui décrit dans les explications:

t.lettBe(t::setNumber, "NaN"); // Does not compile :-)

t.letBeX(t::getNumber, 2); // !!! Does not compile  :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)

Le second de ces 3 passera exactement par le même processus d'inférence que withXci-dessus (il suffit de le remplacer Longpar Numberet Stringpar Integer). Cela illustre encore une autre raison pour laquelle vous ne devriez pas vous fier à ce comportement d'inférence de type échoué pour la conception de votre classe, car l'échec de la compilation ici n'est probablement pas un comportement souhaitable.

Pour les 2 autres (et en fait pour toutes les autres invocations impliquant un que Consumervous souhaitez utiliser), le comportement devrait être apparent si vous suivez la procédure d'inférence de type définie pour l'une des méthodes ci-dessus (c'est- withà- dire pour la première, withXpour le troisième). Il y a juste un petit changement dont vous devez prendre note:

  • La contrainte sur le premier paramètre ( t::setNumber compatible avec Consumer<R> ) se réduira à R <: Numberau lieu de Number <: Rcomme pour Supplier<R>. Ceci est décrit dans la documentation liée sur la réduction.

Je laisse comme un exercice pour le lecteur de travailler avec précaution à travers l'une des procédures ci-dessus, armé de ce morceau de connaissances supplémentaires, pour se démontrer exactement pourquoi une invocation particulière compile ou ne compile pas.

user31601
la source
Très en profondeur, bien documenté et formulé. Merci!
Zabuzard
@ user31601 Pouvez-vous s'il vous plaît indiquer où la différence entre fournisseur et consommateur entre en jeu. J'ai ajouté un exemple étendu dans la question d'origine pour cela. Il montre un comportement covariant, contravariant et invariant pour les différentes versions de letBe (), letBeX () et let (). Be () selon le fournisseur / consommateur.
jukzi
@jukzi J'ai ajouté quelques notes supplémentaires, mais vous devriez avoir suffisamment d'informations pour travailler vous-même sur ces nouveaux exemples.
user31601
C'est intéressant: autant de cas particuliers en 18.2.1. pour les lambdas et les références de méthode où je ne m'attendais pas du tout à un cas particulier pour eux de ma compréhension naïve. Et probablement aucun développeur ordinaire ne pourrait s'y attendre.
jukzi
Eh bien, je suppose que la raison en est qu'avec les lambdas et les références de méthode, le compilateur doit décider quel type approprié le lambda doit implémenter - il doit faire un choix! Par exemple, TypeInference::getLongpourrait imlement Supplier<Long>ou Supplier<Serializable>ou Supplier<Number>etc, mais surtout, il ne peut implémenter l'un d'eux (comme n'importe quelle autre classe)! Ceci est différent de toutes les autres expressions, où les types implémentés sont tous connus à l'avance, et le compilateur n'a qu'à déterminer si l'une d'entre elles satisfait aux exigences de contrainte.
user31601