Dlaczego jest
public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {...}
bardziej rygorystyczne
public <R> Builder<T> with(Function<T, R> getter, R returnValue) {...}
Jest to kontynuacja Dlaczego nie jest sprawdzany typ zwrotu lambda w czasie kompilacji . Znalazłem metodę withX()
podobną
.withX(MyInterface::getLength, "I am not a Long")
produkuje żądany błąd czasu kompilacji:
Typ getLength () z typu BuilderExample.MyInterface jest długi, co jest niezgodne z typem zwracanym przez deskryptor: String
podczas korzystania z metody with()
nie.
pełny przykład:
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
Rozszerzony przykład
Poniższy przykład pokazuje różne zachowanie metody i parametru typu sprowadzone do Dostawcy. Ponadto pokazuje różnicę w zachowaniu konsumenta dla parametru typu. I pokazuje, że nie robi to różnicy, skoro jest parametrem metody konsumenta lub dostawcy.
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 :-)
}
}
java
generics
lambda
type-inference
jukzi
źródło
źródło
javac
pomocą narzędzia raw lub narzędzia do budowania, takiego jak Gradle lub Maven?Odpowiedzi:
To naprawdę interesujące pytanie. Obawiam się, że odpowiedź jest skomplikowana.
tl; dr
Wypracowanie różnicy wymaga dogłębnego odczytania specyfikacji wnioskowania o typie Java , ale w zasadzie sprowadza się do tego:
with
istnieje (co prawda niejasne) substytucja, która spełnia wszystkie wymagania dotycząceR
:Serializable
withX
wprowadzenie dodatkowego parametru typuF
zmusza kompilator doR
pierwszego rozwiązania , bez uwzględnienia ograniczeniaF extends Function<T,R>
.R
odnosi się do (znacznie bardziej szczegółowego),String
co następnie oznacza, że wnioskowanie oF
niepowodzeniu.Ten ostatni punkt jest najważniejszy, ale także najbardziej falisty. Nie mogę wymyślić lepszego zwięzłego sposobu sformułowania go, więc jeśli chcesz uzyskać więcej szczegółów, sugeruję przeczytanie pełnego wyjaśnienia poniżej.
Czy to jest zamierzone zachowanie?
Pójdę w opałach tutaj i powiedzieć nie .
Nie sugeruję, że w specyfikacji jest błąd, a ponadto (w przypadku
withX
) projektanci języka podnieśli ręce i powiedzieli: „w niektórych sytuacjach wnioskowanie o typie staje się zbyt trudne, więc po prostu się nie powiedziemy” . Nawet jeśli zachowanie kompilatora w odniesieniu dowithX
wydaje się być tym, czego chcesz, uważam, że jest to uboczny efekt uboczny obecnej specyfikacji, a nie pozytywnie zaplanowana decyzja projektowa.Ma to znaczenie, ponieważ informuje o pytaniu. Czy powinienem polegać na tym zachowaniu w projekcie aplikacji? Twierdziłbym, że nie powinieneś, ponieważ nie możesz zagwarantować, że przyszłe wersje języka będą się zachowywać w ten sposób.
Chociaż prawdą jest, że projektanci języków bardzo starają się nie uszkodzić istniejących aplikacji podczas aktualizacji specyfikacji / projektu / kompilatora, problem polega na tym, że zachowanie, na którym chcesz polegać, polega na tym, że kompilator obecnie zawodzi (tzn. Nie jest aplikacją istniejącą ). Aktualizacje Langauge przez cały czas przekształcają kod niekompilujący w kod kompilujący. Na przykład, następujący kod może zostać zagwarantowane nie skompilować w Java 7, ale będzie skompilować w Java 8:
Twój przypadek użycia nie jest inny.
Innym powodem, dla którego byłbym ostrożny w używaniu tej
withX
metody, jestF
sam parametr. Ogólnie rzecz biorąc, parametr typu ogólnego w metodzie (który nie pojawia się w typie zwracanym) istnieje w celu powiązania typów wielu części podpisu razem. Mówi:Nie dbam o to
T
, co jest, ale chcę mieć pewność, że gdziekolwiek używamT
, jest tego samego typu.Logicznie więc spodziewalibyśmy się, że każdy parametr typu pojawi się co najmniej dwa razy w sygnaturze metody, w przeciwnym razie „nic nie robi”.
F
w swojejwithX
pojawia się tylko raz w podpisie, co sugeruje mi się użycie parametru typu nie inline z intencją tej funkcji języka.Alternatywne wdrożenie
Jednym ze sposobów na wdrożenie tego w nieco bardziej „zamierzony sposób” byłoby podzielenie
with
metody na łańcuch 2:Można to następnie wykorzystać w następujący sposób:
Nie obejmuje to zewnętrznego parametru typu, jak Twój
withX
. Dzieląc metodę na dwie sygnatury, lepiej wyraża zamiar tego, co próbujesz zrobić, z punktu widzenia bezpieczeństwa typu:With
), która definiuje typ na podstawie odwołania do metody.of
) ogranicza typ,value
aby był zgodny z tym, co wcześniej skonfigurowałeś.Jedynym sposobem, w jaki przyszła wersja języka byłaby w stanie to skompilować, jest zaimplementowanie pełnego wpisywania kaczych znaków, co wydaje się mało prawdopodobne.
Ostatnia uwaga, żeby to wszystko nie miało znaczenia: Myślę, że Mockito (a w szczególności jego funkcja stubowania) może już zasadniczo zrobić to, co próbujesz osiągnąć za pomocą „ typowego generatora bezpiecznego generycznego”. Może mógłbyś po prostu tego użyć?
Pełne (ish) wyjaśnienie
Idę do pracy przez procedury rodzaj wnioskowania zarówno dla
with
iwithX
. To jest dość długie, więc weź to powoli. Pomimo tego, że byłem długi, wciąż pozostawiłem sporo szczegółów. Możesz zapoznać się ze specyfikacją, aby uzyskać więcej informacji (skorzystaj z linków), aby przekonać się, że mam rację (być może popełniłem błąd).Ponadto, aby trochę uprościć, użyję bardziej minimalnej próbki kodu. Główną różnicą jest to, że zamienia się
Function
naSupplier
, więc jest mniej typy i parametry w grze. Oto pełny fragment, który odtwarza opisywane zachowanie:Przeanalizujmy wnioskowanie o zastosowaniu typu i wnioskowanie o typie procedurę o dla każdego wywołania metody:
with
Mamy:
Początkowy zestaw związany, B 0 , to:
R <: Object
Wszystkie wyrażenia parametrów są stosowalności .
Stąd początkowy zestaw ograniczeń dla wnioskowania o zastosowaniu , C , wynosi:
TypeInference::getLong
jest kompatybilny zSupplier<R>
"Not a long"
jest kompatybilny zR
To zmniejsza się związanego zestaw B 2 z:
R <: Object
(od B 0 )Long <: R
(od pierwszego ograniczenia)String <: R
(od drugiego ograniczenia)Ponieważ nie zawiera związany „ false ”, a (zakładam) uchwałę o
R
powiedzie (dajeSerializable
), a następnie wezwanie to dotyczy.Zatem przechodzimy do wnioskowania o typie wywołania .
Nowy zestaw ograniczeń C z powiązanymi zmiennymi wejściowymi i wyjściowymi to:
TypeInference::getLong
jest kompatybilny zSupplier<R>
R
Nie zawiera żadnych współzależności między zmiennymi wejściowymi i wyjściowymi , więc można je zmniejszyć w jednym kroku, a ostateczny zestaw wiązań, B 4 , jest taki sam jak B 2 . Stąd rozdzielczość kończy się tak jak poprzednio, a kompilator odetchnął z ulgą!
withX
Mamy:
Początkowy zestaw związany, B 0 , to:
R <: Object
F <: Supplier<R>
Jedynie wyrażenie drugiego parametru dotyczy zastosowania . Pierwszy (
TypeInference::getLong
) nie jest, ponieważ spełnia następujący warunek:Stąd początkowy zestaw ograniczeń dla wnioskowania o zastosowaniu , C , wynosi:
"Also not a long"
jest kompatybilny zR
To zmniejsza się związanego zestaw B 2 z:
R <: Object
(od B 0 )F <: Supplier<R>
(od B 0 )String <: R
(z ograniczenia)Ponownie, ponieważ nie zawiera związany „ false ”, a rozdzielczość z
R
uda (dajeString
), a następnie wezwanie to dotyczy.Wnioskowanie o typie wywołania jeszcze raz ...
Tym razem nowy zestaw ograniczeń C z powiązanymi zmiennymi wejściowymi i wyjściowymi to:
TypeInference::getLong
jest kompatybilny zF
F
Ponownie nie mamy zależności między zmiennymi wejściowymi i wyjściowymi . Jednak tym razem, nie jest zmienna wejściowa (
F
), więc musimy rozwiązać ten przed próbą redukcji . Zaczynamy więc od naszego zestawu B 2 .Podzbiór określamy w
V
następujący sposób:W drugim ograniczeniu w B 2 rozdzielczość
F
zależy odR
, więcV := {F, R}
.Wybieramy podzbiór
V
według reguły:Jedyny podzbiór,
V
który spełnia tę właściwość, to{R}
.Za pomocą trzeciego bound (
String <: R
) tworzymy instancjęR = String
i włączamy to do naszego zestawu związanego.R
jest teraz rozwiązany, a druga granica faktycznie staje sięF <: Supplier<String>
.Korzystając z (poprawionej) drugiej granicy, tworzymy instancję
F = Supplier<String>
.F
jest teraz rozwiązany.Teraz, gdy
F
problem został rozwiązany, możemy kontynuować redukcję , stosując nowe ograniczenie:TypeInference::getLong
jest kompatybilny zSupplier<String>
Long
jest kompatybilny zString
... i pojawia się błąd kompilatora!
Dodatkowe uwagi na temat „Rozszerzonego przykładu”
Rozszerzony przykład w wyglądzie pytanie na kilku interesujących przypadków, które nie są bezpośrednio objęte wyrobisk powyżej:
Integer <: Number
)Consumer
ZamiastSupplier
)W szczególności 3 z podanych wywołań wyróżniają się jako potencjalnie sugerujące „inne” zachowanie kompilatora niż opisane w objaśnieniach:
Drugi z tych 3 przejdzie dokładnie ten sam proces wnioskowania, jak
withX
wyżej (wystarczy wymienićLong
zNumber
iString
zInteger
). To ilustruje jeszcze jeden powód, dla którego nie powinieneś polegać na tym błędnym wnioskowaniu typu w projekcie klasy, ponieważ niepowodzenie kompilacji tutaj prawdopodobnie nie jest pożądanym zachowaniem.W przypadku pozostałych 2 (i rzeczywiście wszystkich innych wywołań, w
Consumer
których chcesz przeprowadzić pracę) zachowanie powinno być widoczne, jeśli przejdziesz przez procedurę wnioskowania typu określoną dla jednej z powyższych metod (tj.with
Dla pierwszejwithX
dla trzeci). Jest tylko jedna niewielka zmiana, na którą musisz zwrócić uwagę:t::setNumber
jest kompatybilny zConsumer<R>
) będzie zmniejszać sięR <: Number
zamiastNumber <: R
, jak to robi dlaSupplier<R>
. Jest to opisane w powiązanej dokumentacji dotyczącej redukcji.Pozostawiam to czytelnikowi jako ćwiczenie, aby ostrożnie przepracować jedną z powyższych procedur, uzbrojoną w tę dodatkową wiedzę, aby zademonstrować sobie dokładnie, dlaczego dane wywołanie się kompiluje, czy nie.
źródło
TypeInference::getLong
mógłby imlementSupplier<Long>
lubSupplier<Serializable>
lubSupplier<Number>
itp, ale co najważniejsze może realizować tylko jeden z nich (podobnie jak każdej innej klasie)! Różni się to od wszystkich innych wyrażeń, w których zaimplementowane typy są znane z góry, a kompilator musi tylko sprawdzić, czy jeden z nich spełnia wymagania ograniczenia.