Ogólna granica górnego typu zwracanego typu - interfejs a klasa - zaskakująco poprawny kod

171

To jest prawdziwy przykład z interfejsu API biblioteki innej firmy, ale uproszczony.

Skompilowano za pomocą Oracle JDK 8u72

Rozważ te dwie metody:

<X extends CharSequence> X getCharSequence() {
    return (X) "hello";
}

<X extends String> X getString() {
    return (X) "hello";
}

Obaj zgłaszają ostrzeżenie o „niesprawdzonej obsadzie” - rozumiem dlaczego. Zdumiewa mnie to, dlaczego mogę zadzwonić

Integer x = getCharSequence();

i się kompiluje? Kompilator powinien wiedzieć, że Integernie implementuje CharSequence. Wezwanie do

Integer y = getString();

daje błąd (zgodnie z oczekiwaniami)

incompatible types: inference variable X has incompatible upper bounds java.lang.Integer,java.lang.String

Czy ktoś może wyjaśnić, dlaczego takie zachowanie miałoby zostać uznane za ważne? Jak by to było przydatne?

Klient nie wie, że to wywołanie jest niebezpieczne - kod klienta kompiluje się bez ostrzeżenia. Dlaczego kompilacja nie ostrzegłaby o tym / nie spowodowała błędu?

Czym różni się od tego przykładu:

<X extends CharSequence> void doCharSequence(List<X> l) {
}

List<CharSequence> chsL = new ArrayList<>();
doCharSequence(chsL); // compiles

List<Integer> intL = new ArrayList<>();
doCharSequence(intL); // error

Próba zaliczenia List<Integer>powoduje błąd, zgodnie z oczekiwaniami:

method doCharSequence in class generic.GenericTest cannot be applied to given types;
  required: java.util.List<X>
  found: java.util.List<java.lang.Integer>
  reason: inference variable X has incompatible bounds
    equality constraints: java.lang.Integer
    upper bounds: java.lang.CharSequence

Jeśli jest to zgłaszane jako błąd, dlaczego Integer x = getCharSequence();tak nie jest?

Adam Michalik
źródło
15
ciekawy! casting na LHS Integer x = getCharSequence();będzie się kompilował, ale casting na RHS Integer x = (Integer) getCharSequence();kończy się niepowodzeniem
flakes
Jakiej wersji kompilatora java używasz? Podaj te informacje w pytaniu.
Federico Peralta Schaffner
@FedericoPeraltaSchaffner nie widzi, dlaczego to ma znaczenie - to jest pytanie bezpośrednio o JLS.
Boris the Spider
@BoristheSpider Ponieważ mechanizm wnioskowania o typie uległ zmianie w java8
Federico Peralta Schaffner
1
@FedericoPeraltaSchaffner - już oznaczyłem pytanie jako [java-8], ale teraz dodałem wersję kompilatora w poście.
Adam Michalik

Odpowiedzi:

184

CharSequencejest interface. Dlatego nawet jeśli SomeClassnie implementuje CharSequence, byłoby doskonale możliwe utworzenie klasy

class SubClass extends SomeClass implements CharSequence

Dlatego możesz pisać

SomeClass c = getCharSequence();

ponieważ typ wywnioskowany Xjest typem skrzyżowania SomeClass & CharSequence.

Jest to trochę dziwne w przypadku, Integergdy Integerjest ostateczne, ale finalnie odgrywa żadnej roli w tych zasadach. Na przykład możesz pisać

<T extends Integer & CharSequence>

Z drugiej strony, Stringnie jest interface, więc niemożliwe byłoby rozszerzenie w SomeClasscelu uzyskania podtypu String, ponieważ java nie obsługuje wielokrotnego dziedziczenia dla klas.

W tym Listprzykładzie należy pamiętać, że typy ogólne nie są ani kowariantne, ani kontrawariantne. Oznacza to, że jeśli Xjest podtypem Y, List<X>nie jest ani podtypem, ani nadtypem List<Y>. Ponieważ Integernie implementuje CharSequence, nie możesz używać List<Integer>w swojej doCharSequencemetodzie.

Możesz jednak zmusić to do kompilacji

<T extends Integer & CharSequence> void foo(List<T> list) {
    doCharSequence(list);
}  

Jeśli masz metodę, która zwraca następującą wartośćList<T> :

static <T extends CharSequence> List<T> foo() 

możesz to zrobić

List<? extends Integer> list = foo();

Dzieje się tak, ponieważ typem wywnioskowanym jest Integer & CharSequencei jest to podtyp Integer.

Typy przecięć występują niejawnie, gdy określisz wiele granic (np <T extends SomeClass & CharSequence>.).

Więcej informacji można znaleźć w części JLS, w której wyjaśniono, jak działają ograniczenia typów. Możesz dołączyć wiele interfejsów, np

<T extends String & CharSequence & List & Comparator>

ale tylko pierwsza granica może nie być interfejsem.

Paul Boddington
źródło
62
Nie miałem pojęcia, że ​​możesz umieścić &w ogólnej definicji. +1
płatki
13
@flkes Możesz wstawić więcej niż jeden, ale tylko pierwszy argument może nie być interfejsem. <T extends String & List & Comparator>jest ok, ale <T extends String & Integer>nie jest, ponieważInteger nie jest interfejsem.
Paul Boddington
7
@PaulBoddington Istnieje kilka praktycznych zastosowań tych metod. Na przykład, jeśli typ nie jest faktycznie używany dla przechowywanych danych. Przykładami tego są Collections.emptyList()również Optional.empty(). Te zwracają implementacje ogólnego interfejsu, ale niczego nie przechowują.
Stefan Dollase
6
I nikt nie mówi, że klasa będąca finalw czasie kompilacji będzie finalw czasie wykonywania.
Holger
7
@Federico Peralta Schaffner: chodzi o to, że metoda getCharSequence()obiecuje zwrócić wszystko, Xczego potrzebuje wywołujący, w tym zwracanie typu rozszerzającego Integeri implementującego, CharSequencejeśli wywołujący tego potrzebuje i zgodnie z tą obietnicą, prawidłowe jest zezwolenie na przypisanie wyniku do Integer. Jest to metoda, getCharSequence()która jest zepsuta, ponieważ nie dotrzymuje obietnicy, ale to nie wina kompilatora.
Holger
59

Typ, który jest wywnioskowany przez kompilator przed przypisaniem, Xto Integer & CharSequence. Ten typ wydaje się dziwny, ponieważ Integerjest ostateczny, ale jest to całkowicie poprawny typ w Javie. Następnie jest rzucany na Integer, co jest całkowicie OK.

Istnieje dokładnie jedna możliwa wartość dla Integer & CharSequencetypu: null. Z następującą implementacją:

<X extends CharSequence> X getCharSequence() {
    return null;
}

Następujące zadanie będzie działać:

Integer x = getCharSequence();

Z powodu tej możliwej wartości nie ma powodu, dla którego przypisanie powinno być błędne, nawet jeśli jest oczywiście bezużyteczne. Przydałoby się ostrzeżenie.

Prawdziwym problemem jest interfejs API, a nie witryna wywołań

W rzeczywistości niedawno pisałem na blogu o tym wzorcu anty projektowania API . Nie należy (prawie) nigdy projektować ogólnej metody zwracania dowolnych typów, ponieważ (prawie) nigdy nie można zagwarantować, że wywnioskowany typ zostanie dostarczony. Wyjątkiem są metody takie jak Collections.emptyList(), w przypadku których pustka listy (i wymazanie typu ogólnego) jest powodem, dla którego wszelkie wnioskowanie for <T>zadziała:

public static final <T> List<T> emptyList() {
    return (List<T>) EMPTY_LIST;
}
Lukas Eder
źródło