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 :-)
}
}
javac
ou un outil de construction comme Gradle ou Maven?Réponses:
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:
with
il existe une substitution (certes vague) qui satisfait à toutes les exigences concernantR
:Serializable
withX
, l'introduction du paramètre de type supplémentaireF
oblige le compilateur à résoudre d'R
abord, sans tenir compte de la contrainteF extends Function<T,R>
.R
se résout en (beaucoup plus spécifique)String
ce qui signifie alors que l'inférence desF
é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:
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 leF
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'utiliseT
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 votrewithX
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:Cela peut ensuite être utilisé comme suit:
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:With
) qui définit le type en fonction de la référence de la méthode.of
) contraint le type de lavalue
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
etwithX
. 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
pourSupplier
, 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: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:
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 avecSupplier<R>
"Not a long"
est compatible avecR
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 (donnantSerializable
), 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 avecSupplier<R>
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:
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:Par conséquent, l'ensemble de contraintes initial pour l' inférence d'applicabilité , C , est:
"Also not a long"
est compatible avecR
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 (donnantString
), 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 avecF
F
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:Par la deuxième borne de B 2 , la résolution de
F
dépendR
doncV := {F, R}
.Nous choisissons un sous-ensemble de
V
selon la règle:Le seul sous-ensemble
V
qui satisfait cette propriété est{R}
.En utilisant la troisième borne (
String <: R
), nous instancionsR = String
et l'incorporons dans notre ensemble lié.R
est maintenant résolu, et la deuxième borne devient effectivementF <: 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 avecSupplier<String>
Long
est compatible avecString
... 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:
Integer <: Number
)Consumer
plutôt queSupplier
)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:
Le second de ces 3 passera exactement par le même processus d'inférence que
withX
ci-dessus (il suffit de le remplacerLong
parNumber
etString
parInteger
). 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:t::setNumber
compatible avecConsumer<R>
) se réduira àR <: Number
au lieu deNumber <: R
comme pourSupplier<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.
la source
TypeInference::getLong
pourrait imlementSupplier<Long>
ouSupplier<Serializable>
ouSupplier<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.