Typy generyczne
Typy generyczne w Javie pozwalają realizować programowanie uogólnione, i znacznie redukują ilość powtarzającego się kodu. Zostały wprowadzone w Javie 5 i stały się jednym z kluczowych elementów języka. Typowym przykładem ich wykorzystania są kolekcje wchodzące w skład Collections Framework, ale bez problemu można także spotkać ich zastosowanie we frameworkach oraz codziennym kodzie.
Po co nam typy generyczne?
Wyobraź sobie sytuację, w której chcesz przechować w swoim programie kilka obiektów tego samego typu, np. Book. Można do tego wykorzystać tablice i zapisać np.:
Book[] books = new Book[1000];
Tablice mają jednak swoje wady, nie są w stanie zwiększać swojego rozmiaru, gdy zabraknie w nich miejsca, usuwanie elementów jest kłopotliwe. Zamiast tego najczęściej korzystamy więc z kolekcji, np. list, albo zbiorów, które zostały wprowadzone do Javy w wersji 1.2 w postaci Collections Framework. Ze względu na to, że kolekcje powinny być uniwersalne, to pierwotnie operowały one na typie Object, który dawał możliwość wykorzystania polimorfizmu.
Tworząc więc np. obiekt ArrayList w taki sposób:
List books = new ArrayList();
do przechowywania obiektów w ramach kolekcji tworzona była tablica typu Object[]. Ma to oczywiście zaletę w postaci tego, że w tablicy takiej mogą być przechowywane obiekty dowolnego typu, ale ma równie duże wady. Przy pobieraniu obiektów z takiej kolekcji zwracane są referencje typu Object, co wymusza na nas późniejsze rzutowanie. Błędy jest więc zapis:
List books = new ArrayList();
books.add(new Book("Potop", "Henryk Sienkiewicz"));
Object book = books.get(0);
System.out.println(book.getTitle()); //błąd, w typie Object nie ma metody getTitle()
Konieczne jest rzutowanie:
List books = new ArrayList();
books.add(new Book("Potop", "Henryk Sienkiewicz"));
Book book = (Book) books.get(0);
System.out.println(book.getTitle()); //ok, referencja typu Book
To, co moglibyśmy zrobić w takiej sytuacji, to stworzenie własnych klas reprezentujących listy, dedykowane do odpowiednich typów, np. BookList, MagazineList, AuthorList itd. Zdecydowana większość kodu takich klas byłaby jednak powtarzalna, a różniłoby je tylko to, na jakich typach operują.
Typy generyczne
Znanym rozwiązaniem tego problemu jest programowanie uogólnione / generyczne. Problem polegał na tym, że wprowadzenie tego paradygmatu do Javy nie było takie proste, ponieważ od samego początku stawiano w niej na pierwszym miejscu kompatybilność wsteczną. Rozwiązaniem okazały się typy generyczne wprowadzone w Javie 1.5, które reprezentowane są przez ostre nawiasy. Od teraz powyższy kod można było zapisać w sposób, który nie wymaga rzutowania:
List<Book> books = new ArrayList<Book>();
books.add(new Book("Potop", "Henryk Sienkiewicz"));
Book book = books.get(0); //metoda get() zwraca referencję Book, a nie Object
System.out.println(book.getTitle()); //ok, referencja typu Book
W kolejnych wersjach Javy wprowadzano jeszcze pewne usprawnienia. W Javie 1.7 dodano automatyczne wnioskowanie typów (ang. type inference), dzięki czemu nie trzeba było już podawać typu generycznego dwukrotnie, tylko, zamiast tego można wykorzystać operator diamentu:
//przed Javą 7
List<Book> books = new ArrayList<Book>();
//od Javy 7
List<Book> books = new ArrayList<>();
Oczywiście kolekcje są najprostszym przykładem wykorzystania typów generycznych, ale wykraczają one znacząco poza to. Możemy definiować zarówno własne klasy jak i pojedyncze metody generyczne.
Ograniczenia
Typy generyczne wprowadzone w Javie nie są rozwiązaniem idealnym i posiadają istotne ograniczenia. Najważniejsze z nich jest to, że jako typów generycznych możemy używać wyłącznie typów obiektowych. Nie możemy natomiast używać typów prostych. Z tego powodu możemy stworzyć listę, która będzie przechowywała obiekty String:
List<String> words = new ArrayList<>(); //ok
ale nie możemy stworzyć listy do zapisywania wartości int:
List<int> numbers = new ArrayList<>(); //błąd
Z tego powodu wraz z typami generycznymi w Javie 1.5 wprowadzono jednocześnie typy opakowujące i w celu stworzenia listy przechowującej liczby całkowite należy zapisać:
List<Integer> numbers = new ArrayList<>(); //ok
Potencjalna przyszłość
Architekci Javy mocno pracują nad nowymi elementami języka i jedną z takich nowości mają być prymitywne obiekty rozwijane w ramach JEP 401: Primitive Objects. Kolejnym krokiem będzie zunifikowanie typów prostych oraz ich obiektowych odpowiedników w ramach JEP 402: Unify the Basic Primitives with Objects. Ostatecznie będzie to oznaczało, że List<int> i List<Integer> będą sobie równoważne.
Dyskusja i komentarze
Masz pytania do tego wpisu? Może chcesz się podzielić spostrzeżeniami? Zapraszamy dyskusji na naszej grupie na Facebooku.