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
with
il existe une substitution (certes vague) qui satisfait à toutes les exigences concernant R
:Serializable
- Pour
withX
, l'introduction du paramètre de type supplémentaire F
oblige le compilateur à résoudre d' R
abord, sans tenir compte de la contrainte F extends Function<T,R>
. R
se résout en (beaucoup plus spécifique) String
ce 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 à withX
semble ê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 withX
méthode est le F
paramè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 T
c'est, mais je veux être sûr que partout où j'utilise T
c'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". F
dans votre withX
n'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 with
mé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 value
pour ê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 with
et 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 Function
pour 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:
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 R
succè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 m
est 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 R
succè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 .
Nous déterminons un sous-ensemble V
comme suit:
Étant donné un ensemble de variables d'inférence à résoudre, V
soit 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 F
dépend R
donc V := {F, R}
.
Nous choisissons un sous-ensemble de V
selon la règle:
laissez { α1, ..., αn }
un sous - ensemble non vide de variables non dans V
telle que i) pour tous i (1 ≤ i ≤ n)
, si αi
dépend de la résolution d'une variable β
, alors soit β
a une instanciation ou il y a une j
telle que β = αj
; et ii) il n'existe aucun sous-ensemble approprié non vide de { α1, ..., αn }
cette propriété.
Le seul sous-ensemble V
qui satisfait cette propriété est {R}
.
En utilisant la troisième borne ( String <: R
), nous instancions R = String
et l'incorporons dans notre ensemble lié. R
est maintenant résolu, et la deuxième borne devient effectivement F <: Supplier<String>
.
En utilisant la deuxième borne (révisée), nous instancions F = Supplier<String>
. F
est maintenant résolu.
Maintenant que F
c'est résolu, nous pouvons procéder à la réduction , en utilisant la nouvelle contrainte:
TypeInference::getLong
est compatible avec Supplier<String>
- ... réduit à
Long
est compatible avec String
- ... 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
Consumer
plutô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 withX
ci-dessus (il suffit de le remplacer Long
par Number
et String
par 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 Consumer
vous 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, withX
pour 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 <: Number
au lieu de Number <: R
comme 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.