Typy generyczne
Nadszedł czas na zrozumienie pewnego zagadnienia. Już parę tygodnii temu pokazałem Ci listę – LinkedList oraz ArrayList, w których mogłeś bez problemu przechowywać obiekty.
Pamiętasz ten zapis?
List<String> names = new LinkedList<String>();
Wtedy Ci nie tłumaczyłem od czego są ostre nawiasy przy typie zmiennej, jednak dziś w końcu nadszedł czas… 😉
Ostre nawiasy
Ostre nawiasy właśnie np. w takiej linii kodu:
List<String> names = new LinkedList<String>();
Oznaczają właśnie wykorzystanie typów generycznych.
Co to jest…
Typy generyczne pozwalają na stworzenie jeszcze bardziej uniwersalnego szablonu z klasy, aniżeli z samej klasy.
Zwykła definicja klasy może wyglądać np. tak:
public class AnyClass {}
Jednak, gdy użyjemy typów generycznych wygląda ona tak:
public class AnyClass<T> {}
I znowu te nawiasy ostre…
Co nam to daje?
Typy generyczne dają nam możliwość tworzenia z klas szablonu dla obojętnie jakich typów danych lub konkretnych “rodzin” – czyli obiektów, które np. implementują dany interfejs. No ale o tym już kiedy indziej. 😉
Pamiętasz, jak tworzyłeś listę Stringów? Robiłeś to właśnie tak:
List<String> names = new LinkedList<String>();
Podczas tworzenia obiektu definiowałeś jaki typ będzie używany w obrębie listy. I jedyne co mogłeś wrzucać do tej listy to były własnie obiekty typu String.
Aby przejść jak najszybciej do przykładu (ponieważ w tej lekcji jest to konieczne) stwórzmy sobie trzy proste klasy:
Klasę Fruit
package pl.maniaq; public class Fruit { public Fruit() { } public String getName() { return "Owoc"; } }
Klasę Apple dziedziczącą po Fruit
package pl.maniaq; public class Apple extends Fruit { public Apple() { super(); } @Override public String getName() { return "Jabłko"; } }
Oraz klasę Strawberry dziedziczącą również po Fruit
package pl.maniaq; public class Strawberry extends Fruit { public Strawberry() { super(); } @Override public String getName() { return "Truskawka"; } }
Startujmy!
Mając już te trzy klasy bez problemu możemy użyć typów generycznych.
Stworzymy sobie kontener na owoce – gdzie przez konstruktor będziemy przekazywać jakiś owoc i będzie on zapisywany w prywatnym polu. Możemy zrobić to np. w taki sposób:
package pl.maniaq; public class FruitContainer { private Fruit fruit; public FruitContainer(Fruit fruit) { this.fruit=fruit; } public Fruit getFruit() { return fruit; } }
Wtedy z góry będziemy przyjmować wszystkie typy owoców, a my tego nie chcemy…
Musimy, więc użyć wcześniej wspomnianych typów generycznych!
package pl.maniaq; public class FruitContainer<T> { private T fruit; public FruitContainer(T fruit) { this.fruit=fruit; } public T getFruit() { return fruit; } }
Co oznacza litera T? Oznacza, że typ, który został przekazany do klasy podczas jego tworzenia jest podstawiony pod literę T.
W takim razie zamiast używania wszędzie gdzie tam to konieczne klasy Fruit, użyjemy litery T.
Oczywiście może to być jakaś dowolna inna litera lub slowo – jednak zazwyczaj jest to litera T.
Stwórzmy w klasie main sobie teraz trzy owoce i wrzućmy je wszystkie do naszego kontenera.
package pl.maniaq; public class Main { public static void main(String[] args) { Fruit fruit = new Fruit(); Apple apple = new Apple(); Strawberry strawberry = new Strawberry(); FruitContainer fruitContainer = new FruitContainer(fruit); System.out.println("Fruit container 1: " + ((Fruit) fruitContainer.getFruit()).getName()); FruitContainer<Fruit> fruitContainer1 = new FruitContainer<>(fruit); System.out.println("Fruit container 2: " + fruitContainer1.getFruit().getName()); FruitContainer<Apple> appleFruitContainer = new FruitContainer<>(apple); System.out.println("Apple container: " + appleFruitContainer.getFruit().getName()); FruitContainer<Strawberry> strawberryFruitContainer = new FruitContainer<>(strawberry); System.out.println("Strawberry container: " + strawberryFruitContainer.getFruit().getName()); } }
Podczas tworzenia obiektu FruitContainer w nawiasach ostrych podaliśmy typ obiektu, jaki będzie w nim przechowywany.
Z wyjątkiem jeden linii:
FruitContainer fruitContainer = new FruitContainer(fruit);
Jak widzisz w przypadku typów generycznych można utworzyć również obiekt bez nawiasów ostrych, jednak…
System.out.println("Fruit container 1: " + ((Fruit) fruitContainer.getFruit()).getName());
Jednak kompilator niestety nie ma pewności, czy przechowywany obiekt jest owocem i ma metodę getName.
I to jest duża przewaga typów generycznych – z góry wiadomo co będzie przechowywane w danej klasie. Dlatego w kolejnych przypadkach nie musimy robić tzw. rzutowania na obiekt Fruit.
FruitContainer<Fruit> fruitContainer1 = new FruitContainer<>(fruit); System.out.println("Fruit container 2: " + fruitContainer1.getFruit().getName());
Ograniczanie
Stworzyliśmy FruitContainer, który nie do końca jest kontenerem na owoce…
FruitContainer<String> stringFruitContainer = new FruitContainer<>("String");
W końcu możemy tam wrzucić co tylko chcemy…
Wtedy z pomocą przychodzi nam ograniczenia typów – czyli słowo extends.
public class FruitContainer<T extends Fruit> {
Dzięki temu możemy zawęzić zbiór obiektów, które pasują do naszego kontenera – czyli dziedziczą po danej klasie lub implementują dany interfejs. W naszym przypadku są to wszystkie owoce. 😉
Wincy typów!
Oczywiście można wprowadzać też do klasy więcej parametrów – tak naprawdę tyle ile tylko chcemy.
Możemy, więc zrefaktorować naszą klasę, aby przechowywała dwa typy owoców:
public class FruitContainer<T extends Fruit, S extends Fruit> {
A cała klasa wygląda teraz tak:
package pl.maniaq; public class FruitContainer<T extends Fruit, S extends Fruit> { private T firstFruit; private S secondFruit; public FruitContainer(T firstFruit, S secondFruit) { this.firstFruit = firstFruit; this.secondFruit = secondFruit; } public T getFirstFruit() { return firstFruit; } public S getSecondFruit() { return secondFruit; } }
Jeszcze szybka zmiana main:
package pl.maniaq; public class Main { public static void main(String[] args) { Fruit fruit = new Fruit(); Apple apple = new Apple(); Strawberry strawberry = new Strawberry(); FruitContainer<Strawberry, Apple> fruitContainer = new FruitContainer<>(strawberry, apple); System.out.println("Fruit container: " + fruitContainer.getFirstFruit().getName() + ", " + fruitContainer.getSecondFruit().getName()); } }
I po uruchomieniu otrzymujemy:
Fruit container: Truskawka, Jabłko
Podsumowanie
Typy generyczne największe zastosowanie mają w przypadku tworzenia kolekcji – czyli klas odpowiedzialnych za przechowywanie i operację na obiektach. Dzięki typom generycznym, z góry możemy ustalić jakie obiekt mają być dostarczane do klasy przez co obiekty nie zostaną pomieszczane – żaden inny typ nie dostanie się do naszej klasy. 😉
Choć generyki są głównie wykorzystywane dopiero podczas używania Springa to i tak nie zaszkodzi przećwiczyć tej lekcji. W tym celu wykonaj jedno zadanie – mówią, że każdy programista powinien je umieć wykonać. Nie poddawaj się i do dzieła! 😉
- Napisz klasę Stos – Stack. Stos jest strukturą danych, czyli kolekcją (podobnie jak lista), na którą można wkładać obiekt i zdejmować tylko z góry. Wygląda to trochę jak stos talerzy – możemy brać tylko z samej góry i kłaść na samą górę.
- W ramach podpowiedzi: W klasie Stack musisz mieć zdefiniowaną tablicę o jakimś początkowym rozmiarze – np. 10.
- W przypadku, gdy będziesz chciał dodać 11 element metodą add musisz przekopiować całą tablicę do nowej – większe np. o rozmiarze 20.
- Musisz pamiętać indeks ostatniego elementu w swojej tablicy – metoda pop zwraca element z pod tego indeksu i wyrzuca go z tablicy. Podczas usuwania elementu wstawiaj null na miejsce usuwanego obiektu.
- Pamiętaj o typach generycznych – na poczatku polecam Ci stworzyć Stack bez typu generycznego, a na sam koniec go dodać.
Moje rozwiązanie możesz zobaczyć tutaj.