Warum ist
public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {...}
dann strenger
public <R> Builder<T> with(Function<T, R> getter, R returnValue) {...}
Dies ist eine Nachverfolgung von Warum wird der Lambda-Rückgabetyp beim Kompilieren nicht überprüft? . Ich fand mit der MethodewithX()
wie
.withX(MyInterface::getLength, "I am not a Long")
erzeugt den gewünschten Fehler bei der Kompilierungszeit:
Der Typ von getLength () vom Typ BuilderExample.MyInterface ist lang. Dies ist nicht kompatibel mit dem Rückgabetyp des Deskriptors: String
während der Verwendung der Methode with()
nicht.
vollständiges Beispiel:
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
Erweitertes Beispiel
Das folgende Beispiel zeigt das unterschiedliche Verhalten von Methoden- und Typparametern, die auf einen Lieferanten reduziert wurden. Außerdem wird der Unterschied zu einem Consumer-Verhalten für einen Typparameter angezeigt. Und es zeigt, dass es keinen Unterschied macht, ob es sich um einen Verbraucher oder einen Lieferanten für einen Methodenparameter handelt.
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 :-)
}
}
quelle
javac
oder einem Build-Tool wie Gradle oder Maven?Antworten:
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:
with
es gibt eine (zugegebenermaßen vage) Substitution, die alle Anforderungen erfüllt anR
:Serializable
withX
die Einführung des zusätzlichen TypparametersF
zwingt den Compiler,R
zuerst aufzulösen , ohne die Einschränkung zu berücksichtigenF extends Function<T,R>
.R
löst sich auf das (viel spezifischere) auf,String
was dann bedeutet, dass die Folgerung vonF
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 aufwithX
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:
Ihr Anwendungsfall ist nicht anders.
Ein weiterer Grund, warum ich bei der Verwendung Ihrer
withX
Methode vorsichtig sein würde, ist derF
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 benutzeT
, der gleiche Typ ist.Logischerweise würden wir erwarten, dass jeder Typparameter mindestens zweimal in einer Methodensignatur erscheint, andernfalls "macht er nichts".
F
in IhrerwithX
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 :Dies kann dann wie folgt verwendet werden:
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:With
) ein, die den Typ basierend auf der Methodenreferenz definiert .of
) beschränkt den Typ des sovalue
, 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ürSupplier
, so gibt es wenige Typen und Parameter im Spiel. Hier ist ein vollständiger Ausschnitt, der das von Ihnen beschriebene Verhalten reproduziert:Lassen Sie uns nacheinander die Typanwendbarkeitsinferenz und die Typinferenzprozedur für jeden Methodenaufruf durcharbeiten:
with
Wir haben:
Die anfängliche gebundene Menge B 0 ist:
R <: Object
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 mitSupplier<R>
"Not a long"
ist kompatibel mitR
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 (GebenSerializable
) 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 mitSupplier<R>
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:
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:Daher ist der Anfangs - Zwang Satz für die Anwendbarkeit Inferenz , C ist:
"Also not a long"
ist kompatibel mitR
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 (GebenString
) 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 mitF
F
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:Durch die zweite Grenze in B 2
F
hängt die Auflösung vonR
also abV := {F, R}
.Wir wählen eine Teilmenge von
V
gemäß der Regel:Die einzige Teilmenge
V
davon erfüllt diese Eigenschaft ist{R}
.Mit der dritten Grenze (
String <: R
) instanziieren wir dieseR = String
und integrieren sie in unsere gebundene Menge.R
ist nun aufgelöst und die zweite Grenze wird effektivF <: 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 mitSupplier<String>
Long
ist kompatibel mitString
... 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:
Integer <: Number
) istConsumer
nichtSupplier
)Insbesondere 3 der angegebenen Aufrufe weisen möglicherweise auf ein anderes Compilerverhalten hin als in den Erläuterungen beschrieben:
Der zweite dieser 3 wird genau Prozess des gleiche Folgerung durchlaufen wie
withX
oben (nur ersetzenLong
mitNumber
undString
mitInteger
). 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 (dhwith
für die erste,withX
für die dritte). Es gibt nur eine kleine Änderung, die Sie beachten müssen:t::setNumber
ist kompatibel mitConsumer<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.
quelle
TypeInference::getLong
könnte imlementSupplier<Long>
oderSupplier<Serializable>
oderSupplier<Number>
etc, aber entscheidend ist, dass es nur eine von ihnen implementieren kann (genau wie jede andere Klasse)! Dies unterscheidet sich von allen anderen Ausdrücken, bei denen die implementierten Typen alle im Voraus bekannt sind und der Compiler nur herausfinden muss, ob einer von ihnen die Einschränkungsanforderungen erfüllt.