sobota, 23 sierpnia 2014

Typy generyczne w kontekście kolekcji

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.