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:
- Wszystkie inne rzeczy są takie same, kompilator określa najbardziej konkretny typ, jaki może.
- Jednakże, jeśli można go znaleźć na podstawienie dla parametru typu, który spełnia wszystkie wymagania, to kompilacja będzie odnieść sukces, jednak niejasne podstawienie okazuje się być.
- Ponieważ
with
istnieje (co prawda niejasne) substytucja, która spełnia wszystkie wymagania dotyczące R
:Serializable
- Ponieważ
withX
wprowadzenie dodatkowego parametru typu F
zmusza kompilator do R
pierwszego rozwiązania , bez uwzględnienia ograniczenia F extends Function<T,R>
. R
odnosi się do (znacznie bardziej szczegółowego), String
co następnie oznacza, że wnioskowanie o F
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:
static Runnable x = () -> System.out.println();
Twój przypadek użycia nie jest inny.
Innym powodem, dla którego byłbym ostrożny w używaniu tej withX
metody, jest F
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żywam T
, 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 swojej withX
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:
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);
}
}
Można to następnie wykorzystać w następujący sposób:
b.with(MyInterface::getLong).of(1L); // Compiles
b.with(MyInterface::getLong).of("Not a long"); // Compiler error
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:
- Pierwsza metoda ustawia klasę (
With
), która definiuje typ na podstawie odwołania do metody.
- Metoda scond (
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 dlawith
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
na Supplier
, więc jest mniej typy i parametry w grze. Oto pełny fragment, który odtwarza opisywane zachowanie:
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
}
}
Przeanalizujmy wnioskowanie o zastosowaniu typu i wnioskowanie o typie procedurę o dla każdego wywołania metody:
with
Mamy:
with(TypeInference::getLong, "Not a long");
Początkowy zestaw związany, B 0 , to:
Wszystkie wyrażenia parametrów są stosowalności .
Stąd początkowy zestaw ograniczeń dla wnioskowania o zastosowaniu , C , wynosi:
TypeInference::getLong
jest kompatybilny z Supplier<R>
"Not a long"
jest kompatybilny z R
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łę oR
powiedzie (daje Serializable
), 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 z Supplier<R>
- Zmienne wejściowe: brak
- Zmienne wyjściowe:
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:
withX(TypeInference::getLong, "Also not a long");
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:
Jeśli m
jest to metoda ogólna, a wywołanie metody nie dostarcza argumentów typu jawnego, jawnie typowanego wyrażenia lambda lub dokładnego wyrażenia referencyjnego metody, dla którego odpowiedni typ docelowy (wyprowadzony z podpisu m
) jest parametrem typu m
.
Stąd początkowy zestaw ograniczeń dla wnioskowania o zastosowaniu , C , wynosi:
"Also not a long"
jest kompatybilny z R
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 (daje String
), 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 z F
- Zmienne wejściowe:
F
- Zmienne wyjściowe: brak
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:
Biorąc pod uwagę zestaw zmiennych wnioskowania do rozwiązania, pozwól V
będzie sumą tego zbioru i wszystkich zmiennych, od których zależy rozdzielczość co najmniej jednej zmiennej w tym zestawie.
W drugim ograniczeniu w B 2 rozdzielczość F
zależy od R
, więc V := {F, R}
.
Wybieramy podzbiór V
według reguły:
niech { α1, ..., αn }
będzie niepustym podzbiorem niezainicjowanych zmiennych w V
taki sposób, że i) dla wszystkich i (1 ≤ i ≤ n)
, jeśli αi
zależy od rozdzielczości zmiennej β
, wówczas β
ma instancję lub istnieje j
taka możliwość β = αj
; oraz ii) nie istnieje niepusty pusty podzbiór { α1, ..., αn }
tej właściwości.
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 z Supplier<String>
- ... redukuje do
Long
jest kompatybilny z String
- ... co sprowadza się do fałszu
... 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:
- Gdzie typ wartości jest podtypem zwracanego typu metody (
Integer <: Number
)
- Gdzie interfejs funkcjonalny jest sprzeczny w wywnioskowanym typie (tj.
Consumer
Zamiast Supplier
)
W szczególności 3 z podanych wywołań wyróżniają się jako potencjalnie sugerujące „inne” zachowanie kompilatora niż opisane w objaśnieniach:
t.lettBe(t::setNumber, "NaN"); // Does not compile :-)
t.letBeX(t::getNumber, 2); // !!! Does not compile :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)
Drugi z tych 3 przejdzie dokładnie ten sam proces wnioskowania, jak withX
wyżej (wystarczy wymienić Long
z Number
i String
z Integer
). 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 pierwszej withX
dla trzeci). Jest tylko jedna niewielka zmiana, na którą musisz zwrócić uwagę:
- Ograniczenie pierwszego parametru (
t::setNumber
jest kompatybilny z Consumer<R>
) będzie zmniejszać się R <: Number
zamiast Number <: R
, jak to robi dla Supplier<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.