Das ist eine wirklich interessante Frage. Ich fürchte, die Antwort ist kompliziert.
tl; dr
Das Herausarbeiten des Unterschieds erfordert ein ziemlich gründliches Lesen der Typinferenzspezifikation von Java, läuft jedoch im Wesentlichen auf Folgendes hinaus:
- Wenn alle anderen Dinge gleich sind, leitet der Compiler den spezifischsten Typ ab, den er kann.
- Wenn jedoch eine Substitution für einen Typparameter gefunden werden kann, der alle Anforderungen erfüllt, ist die Kompilierung erfolgreich , auch wenn die Substitution vage ist.
- Denn
with
es gibt eine (zugegebenermaßen vage) Substitution, die alle Anforderungen erfüllt an R
:Serializable
- Denn
withX
die Einführung des zusätzlichen Typparameters F
zwingt den Compiler, R
zuerst aufzulösen , ohne die Einschränkung zu berücksichtigen F extends Function<T,R>
. R
löst sich auf das (viel spezifischere) auf, String
was dann bedeutet, dass die Folgerung von F
fehlschlägt.
Dieser letzte Punkt ist der wichtigste, aber auch der handgewellteste. Ich kann mir keine präzisere Formulierung vorstellen. Wenn Sie also mehr Details wünschen, empfehlen wir Ihnen, die vollständige Erklärung unten zu lesen.
Ist das beabsichtigtes Verhalten?
Ich werde hier auf die Beine gehen und nein sagen .
Ich schlage nicht , dass es einen Fehler in der Spezifikation, mehr , dass (im Falle withX
) die Sprache Designer ihre Hände gelegt haben und sagte : „Es gibt einige Situationen , in denen Typinferenz zu hart wird, so werden wir nur scheitern“ . Auch wenn das Verhalten des Compilers in Bezug auf withX
das zu sein scheint, was Sie wollen, würde ich dies eher als zufälligen Nebeneffekt der aktuellen Spezifikation als als eine positiv beabsichtigte Entwurfsentscheidung betrachten.
Dies ist wichtig, da es die Frage informiert. Soll ich mich bei meinem Anwendungsdesign auf dieses Verhalten verlassen? Ich würde argumentieren, dass Sie dies nicht tun sollten, da Sie nicht garantieren können, dass sich zukünftige Versionen der Sprache weiterhin so verhalten.
Zwar bemühen sich Sprachdesigner sehr, vorhandene Anwendungen nicht zu beschädigen, wenn sie ihre Spezifikation / ihr Design / ihren Compiler aktualisieren. Das Problem besteht jedoch darin, dass das Verhalten, auf das Sie sich verlassen möchten, ein Verhalten ist, bei dem der Compiler derzeit ausfällt (dh keine vorhandene Anwendung ). Langauge-Updates verwandeln nicht kompilierten Code ständig in Kompilierungscode. Zum Beispiel könnte der folgende Code wird garantiert nicht in Java 7, zu kompilieren , aber würde in Java 8 kompilieren:
static Runnable x = () -> System.out.println();
Ihr Anwendungsfall ist nicht anders.
Ein weiterer Grund, warum ich bei der Verwendung Ihrer withX
Methode vorsichtig sein würde, ist der F
Parameter selbst. Im Allgemeinen ist ein generischer Typparameter für eine Methode vorhanden (der nicht im Rückgabetyp enthalten ist), um die Typen mehrerer Teile der Signatur miteinander zu verbinden. Es heißt:
Es ist mir egal, was T
ist, aber ich möchte sicher sein, dass es überall dort, wo ich es benutze T
, der gleiche Typ ist.
Logischerweise würden wir erwarten, dass jeder Typparameter mindestens zweimal in einer Methodensignatur erscheint, andernfalls "macht er nichts". F
in Ihrer withX
erscheint nur einmal in der Signatur, was mir die Verwendung eines Typparameters nahe legt, der nicht mit der Absicht dieser Funktion der Sprache übereinstimmt.
Eine alternative Implementierung
Eine Möglichkeit, dies in einem etwas "beabsichtigteren Verhalten" zu implementieren, besteht darin, Ihre with
Methode in eine Kette von 2 aufzuteilen :
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);
}
}
Dies kann dann wie folgt verwendet werden:
b.with(MyInterface::getLong).of(1L); // Compiles
b.with(MyInterface::getLong).of("Not a long"); // Compiler error
Dies beinhaltet keinen fremden Typparameter wie Ihren withX
. Indem Sie die Methode in zwei Signaturen aufteilen, drückt sie auch die Absicht dessen, was Sie versuchen, unter dem Gesichtspunkt der Typensicherheit besser aus:
- Die erste Methode richtet eine Klasse (
With
) ein, die den Typ basierend auf der Methodenreferenz definiert .
- Die scond-Methode (
of
) beschränkt den Typ des so value
, dass er mit dem kompatibel ist, was Sie zuvor eingerichtet haben.
Die einzige Möglichkeit, wie eine zukünftige Version der Sprache dies kompilieren kann, besteht darin, die vollständige Enten-Typisierung zu implementieren, was unwahrscheinlich erscheint.
Ein letzter Hinweis, um diese ganze Sache irrelevant zu machen: Ich denke, Mockito (und insbesondere seine Stubbing-Funktionalität) könnte im Grunde schon das tun, was Sie mit Ihrem "typsicheren generischen Builder" erreichen wollen. Vielleicht könnten Sie das stattdessen einfach verwenden?
Die vollständige (ish) Erklärung
Ich werde das Typinferenzverfahren für with
und durcharbeitenwithX
. Das ist ziemlich lang, also nimm es langsam. Obwohl ich lang bin, habe ich immer noch viele Details ausgelassen. Weitere Informationen finden Sie in der Spezifikation (folgen Sie den Links), um sich davon zu überzeugen, dass ich Recht habe (möglicherweise habe ich einen Fehler gemacht).
Um die Dinge ein wenig zu vereinfachen, werde ich ein minimaleres Codebeispiel verwenden. Der wesentliche Unterschied besteht darin , dass es auslagert Function
für Supplier
, so gibt es wenige Typen und Parameter im Spiel. Hier ist ein vollständiger Ausschnitt, der das von Ihnen beschriebene Verhalten reproduziert:
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
}
}
Lassen Sie uns nacheinander die Typanwendbarkeitsinferenz und die Typinferenzprozedur für jeden Methodenaufruf durcharbeiten:
with
Wir haben:
with(TypeInference::getLong, "Not a long");
Die anfängliche gebundene Menge B 0 ist:
Alle Parameterausdrücke sind für die Anwendbarkeit relevant .
Daher ist der Anfangs - Zwang Satz für die Anwendbarkeit Inferenz , C ist:
TypeInference::getLong
ist kompatibel mit Supplier<R>
"Not a long"
ist kompatibel mit R
Dies reduziert sich auf die gebundene Menge B 2 von:
R <: Object
(von B 0 )
Long <: R
(von der ersten Einschränkung)
String <: R
(aus der zweiten Einschränkung)
Da dies nicht die gebundene ' falsche ' und (ich nehme an) Auflösung von R
Erfolg (Geben Serializable
) enthält, ist der Aufruf anwendbar.
Also fahren wir mit der Inferenz des Aufruftyps fort .
Der neue Einschränkungssatz C mit zugehörigen Eingabe- und Ausgabevariablen lautet:
TypeInference::getLong
ist kompatibel mit Supplier<R>
- Eingangsvariablen: keine
- Ausgabevariablen:
R
Dies enthält keine Abhängigkeiten zwischen Eingabe- und Ausgabevariablen , kann also in einem einzigen Schritt reduziert werden , und die endgültige gebundene Menge B 4 ist dieselbe wie B 2 . Daher ist die Auflösung nach wie vor erfolgreich und der Compiler atmet erleichtert auf!
withX
Wir haben:
withX(TypeInference::getLong, "Also not a long");
Die anfängliche gebundene Menge B 0 ist:
R <: Object
F <: Supplier<R>
Nur der zweite Parameterausdruck ist für die Anwendbarkeit relevant . Das erste ( TypeInference::getLong
) ist nicht, weil es die folgende Bedingung erfüllt:
Wenn m
es sich um eine generische Methode handelt und der Methodenaufruf keine expliziten Typargumente, einen explizit typisierten Lambda-Ausdruck oder einen genauen Methodenreferenzausdruck enthält, für den der entsprechende Zieltyp (abgeleitet aus der Signatur von m
) ein Typparameter von ist m
.
Daher ist der Anfangs - Zwang Satz für die Anwendbarkeit Inferenz , C ist:
"Also not a long"
ist kompatibel mit R
Dies reduziert sich auf die gebundene Menge B 2 von:
R <: Object
(von B 0 )
F <: Supplier<R>
(von B 0 )
String <: R
(von der Einschränkung)
Da dies wiederum nicht die gebundene ' falsche ' und die Auflösung von R
Erfolg (Geben String
) enthält, ist der Aufruf anwendbar.
Inferenz des Aufruftyps noch einmal ...
Diesmal lautet der neue Einschränkungssatz C mit den zugehörigen Eingabe- und Ausgabevariablen :
TypeInference::getLong
ist kompatibel mit F
- Eingabevariablen:
F
- Ausgabevariablen: keine
Auch hier bestehen keine Abhängigkeiten zwischen Eingabe- und Ausgabevariablen . Diesmal gibt es jedoch eine Eingabevariable ( F
), daher müssen wir diese beheben, bevor wir versuchen, sie zu reduzieren . Wir beginnen also mit unserer gebundenen Menge B 2 .
Wir bestimmen eine Teilmenge V
wie folgt:
Bei einer Menge von Inferenzvariablen, die aufgelöst werden sollen, sei V
die Vereinigung dieser Menge und aller Variablen, von denen die Auflösung mindestens einer Variablen in dieser Menge abhängt.
Durch die zweite Grenze in B 2F
hängt die Auflösung von R
also ab V := {F, R}
.
Wir wählen eine Teilmenge von V
gemäß der Regel:
Sei { α1, ..., αn }
eine nicht leere Teilmenge von nicht fundierten Variablen, V
so dass i) für alle i (1 ≤ i ≤ n)
, wenn dies αi
von der Auflösung einer Variablen abhängt β
, entweder β
eine Instanziierung hat oder es eine j
solche gibt, die β = αj
; und ii) es gibt keine nicht leere richtige Teilmenge von { α1, ..., αn }
mit dieser Eigenschaft.
Die einzige Teilmenge V
davon erfüllt diese Eigenschaft ist {R}
.
Mit der dritten Grenze ( String <: R
) instanziieren wir diese R = String
und integrieren sie in unsere gebundene Menge. R
ist nun aufgelöst und die zweite Grenze wird effektiv F <: Supplier<String>
.
Mit der (überarbeiteten) zweiten Grenze instanziieren wir F = Supplier<String>
. F
ist jetzt gelöst.
Nachdem dies F
behoben ist, können wir mit der Reduzierung fortfahren , indem wir die neue Einschränkung verwenden:
TypeInference::getLong
ist kompatibel mit Supplier<String>
- ... reduziert auf
Long
ist kompatibel mit String
- ... was sich auf false reduziert
... und wir bekommen einen Compilerfehler!
Zusätzliche Hinweise zum 'Extended Example'
Das erweiterte Beispiel in der Frage befasst sich mit einigen interessanten Fällen, die von den obigen Arbeiten nicht direkt abgedeckt werden:
- Wobei der Werttyp ein Subtyp der Methode return type (
Integer <: Number
) ist
- Wenn die funktionale Schnittstelle im abgeleiteten Typ kontravariant ist (dh
Consumer
nicht Supplier
)
Insbesondere 3 der angegebenen Aufrufe weisen möglicherweise auf ein anderes Compilerverhalten hin als in den Erläuterungen beschrieben:
t.lettBe(t::setNumber, "NaN"); // Does not compile :-)
t.letBeX(t::getNumber, 2); // !!! Does not compile :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)
Der zweite dieser 3 wird genau Prozess des gleiche Folgerung durchlaufen wie withX
oben (nur ersetzen Long
mit Number
undString
mit Integer
). Dies zeigt einen weiteren Grund, warum Sie sich bei Ihrem Klassendesign nicht auf dieses fehlgeschlagene Typinferenzverhalten verlassen sollten, da das Nichtkompilieren hier wahrscheinlich kein wünschenswertes Verhalten ist.
Für die anderen 2 (und in der Tat alle anderen Aufrufe, die a Consumer
Sie arbeiten möchten) sollte das Verhalten offensichtlich sein, wenn Sie das für eine der oben genannten Methoden beschriebene Typinferenzverfahren durcharbeiten (dh with
für die erste, withX
für die dritte). Es gibt nur eine kleine Änderung, die Sie beachten müssen:
- Die Einschränkung für den ersten Parameter (
t::setNumber
ist kompatibel mit Consumer<R>
) wird auf statt reduziertR <: Number
Number <: R
wie fürSupplier<R>
. Dies ist in der verknüpften Dokumentation zur Reduzierung beschrieben.
Ich überlasse es dem Leser als Übung, eines der oben genannten Verfahren, das mit diesem zusätzlichen Wissen ausgestattet ist, sorgfältig durchzuarbeiten, um sich selbst genau zu demonstrieren, warum ein bestimmter Aufruf kompiliert wird oder nicht.