Typy generyczne w javie są powszechnie znane i używane. Praktycznie każdy programista, który używa javy, używa typów generycznych. Znaczna większość z nas wykorzystuje je świadomie, ale znajdą się też tacy, którzy używają ich przez przypadek, bo po prostu skopiowali przykład użycia z jakiegoś forum :). Dziś krótkie przypomnienie jak korzystać z typów generycznych w kontekście kolekcji.
Zaczniemy od najprostszego przykładu, czyli listy w której określamy jakie obiekty będą się tam znajdować. Taka konstrukcja sprawdza się w 99,9% sytuacj z jakimi mamy do czynienia na co dzień. Lista
persons jest w pełni użyteczna, można do niej dodawać elementy, usuwać, pobierać i modyfikować. Jedyne na co nie pozwoli nam kompilator, to przypisanie jednej listy do innej, która zawiera typ pokrewny, np. listy
teachers nie możemy przypisać do listy
persons, mimo tego, że obiekt
Teacher dziedziczy po
Person. Poniżej kawałek kodu, który pokazuje to zachowanie.
List<Person> persons = createList();
persons.add(new Person());
Person p = persons.get(0);
List<Teacher> teachers = createList();
// persons = teachers; error
Oczywiście jeśli będziemy potrzebowali takiego przypisania, możemy lekko zmodyfikować nasz kodzik poprzez powiedzenie kompilatorowi, że nasza lista będzie zawierać obiekty, które dziedziczą po obiekcie
Person. Po modyfikacji nasz kod prezentuje się następująco.
List<? extends Person> persons = createList();
// persons.add(new Person()); error
Person p = persons.get(0);
List<Teacher> teachers = createList();
persons = teachers;
Możemy teraz przypisywać listę do listy i nie mamy błędu, ale w momencie, gdy chcemy coś wstawić do listy
persons dostajemy błąd kompilacji. Na początku może nam się wydawać, że jest to dziwne zachowanie, ale po dłuższym zastanowieniu uświadamiamy sobie, że jest to nawet logiczne :). Kompilator zgłosi błąd w tym przypadku dlatego, że nie może jednoznacznie stwierdzić, jaki typ znajduje się w liście. Wiadomo tylko, że lista zawiera obiekty, które będą dziedziczyć po
Person. Żeby lepiej zrozumieć ten przypadek, napiszmy sobie przykładową implementację metody
createList().
private List<Teacher> createList() {
List<Teacher> teachers = new ArrayList<>();
teachers.add(new Teacher());
return teachers;
}
Metoda zwraca listę obieków
Teacher i taką listę możemy z powodzeniem przypisać do listy
persons. Teraz jeśli kompilator pozwoliłby nam dodać do listy jakikolwiek obiekt, który dziedziczy po
Person to mielibyśmy niespójność, bo na liście, w której powinny znajdować się obiekty
Teacher znalazłby się obiekt innego typu, np.
Person.
Typy generyczne posiadają jeszcze jedną magiczną odsłonę, a mianowicie możemy zadeklarować listę z nadtypami pewnej klasy. Wygląda to mniej więcej tak:
List<? super Person> persons = createList();
persons.add(new Person());
// Person p = persons.get(0); error
Do listy, która jest zadeklarowana w ten sposób, możemy dodawać obiekty, które dziedziczą po
Person, ale nie możemy z takiej listy pobierać elementów. Tutaj znowu musimy się chwilę zastanowić, ponieważ cała sytuacja jest dość dziwna i niecodzienna. Dla lepszego jej zrozumienia napiszmy sobie kolejną implementację metody
createList().
private List<Base> createList() {
List<Base> bases = new ArrayList<>();
bases.add(new Base());
bases.add(new Teacher());
return bases;
}
Załóżmy, że klasa
Teacher dziedziczy po
Person, a klasa
Person dziedziczy po
Base. Metoda zwraca listę obiektów
Base, w naszym przykładzie jest jeden obiekt
Base i jeden
Teacher. Taka lista może być przypisana do listy
persons, ponieważ konstrukcja
List<? super Person> zakłada, że możemy tutaj przypisać każdą listę, na której są obiekty znajdujące się w hierarchii dziedziczenia nad
Person (
Base znajduje się nad
Person). Jeśli wstawimy do takiej listy obiekt
Person to lista będzie cały czas spójna. Niestety taka lista nie nadaje się do odczytu elementów, ponieważ nie wiemy jakiego typu obiekty się tam znajdują, wiemy tylko, że typ
Person jest nadtypem do typów na liście.
Jeśli macie jakieś przemyślenia albo pytania, to zachęcam do umieszczania ich w komentarzach.