Typy generyczne w Javie ======================= Typy generyczne (po angielsku: generics) nazywane są również typami uogólnionymi, albo po prostu generykami. Typy generyczne to mechanizm, który został dodany do Javy stosunkowo późno. Przy dodawaniu generyków zdecydowano się na zachowanie wstecznej kompatybilności z wcześniejszymi wersjami Javy, przez co generyki w Javie mają dużo nienaturalnych ograniczeń. Inaczej: Generyki są dużo trudniejsze niż wyglądają na pierwszy rzut oka :) Po co typy generyczne? Piszemy często klasy, które w zasadzie wyglądają tak samo, różnią się tylko przechowywanym typem, np. - lista liczb całkowitych, lista napisów i lista obiektów Samochód zachowują się tak samo, - stos liczb całkowitych, stos napisów, stos obiektów Samochód zachowują się tak samo, - kolejka liczb całkowitych, kolejka napisów i kolejka obiektów Samochód zachowują się tak samo, - zbiór liczb całkowitych, zbiór napisów, zbiór obiektów Samochód zachowują się tak samo, - ... Typy generyczne służą do tego, żeby napisać jeden wzorzec takiej klasy, a potem go wielokrotnie używać. Składnia typów generycznych =========================== Definicję klasy możemy sparametryzować parametrem typowym. Parametr typowy umieszczamy w nawiasach kątowych po nazwie klasy. Na przykład jeśli chcemy mieć klasę Koszyk, do której wkładamy elementy tego samego typu, możemy ją zadeklarować w ten sposób: public class Koszyk { ... } Parametry typowe z reguły oznaczamy jednoliterowymi identyfikatorami, np. S, T, K, V, E, itp. Wewnątrz klasy możemy (w zasadzie) odwoływać się do parametru typowego, jak do zwykłego typu: public class Koszyk { // możemy zdefinować atrybut ... protected E pierwszyElement; ... // możemy zdefiniować metodę z parametrem typowym public void dodaj(E element) { ... } // możemy odwołać się do parametru w konstruktorze public Koszyk(E pierwszyElement) { ... } // możemy użyć parametru jako typu zwracanego: public E dajPierwszyElement() { ... } } Używanie typów generycznych =========================== Samo Koszyk to tylko szablon klasy (typu). Ten szablon staje się typem dopiero, gdy powiemy jakiego typu konkretnego chcemy użyć w miejscu parametru. Argument typowy podajemy w nawiasach kątowych po nazwie typu, np. typami są: Koszyk Koszyk Tworzenie obiektu typu generycznego: ... Koszyk koszyk = new Koszyk(); ... Od Javy 7 można nie powtarząć typu przy wywołaniu konstruktora, jeśli robimy proste przypisanie: Koszyk koszyk = new Koszyk<>(); Klasy opakowujące (wrappery) ============================ Argumentami typowymi do klas generycznych mogą być tylko typy referencyjne, tj. klasy i interfejsy. Nie mogą być nimi typy pierwotne. Na przykład typ Stos NIE jest poprawny. (UWAGA: to jest częsty błąd na egzaminie) Jeśli chcemy utworzyć klasę generyczną parametryzowaną typem liczb całkowitych, musimy użyć tzw. klas opakowujących. Każdy typ pierwotny w Javie ma odpowiadającą mu klasę opakowującą, czyli klasę która typ pierwotny podnosi do rangi klasy. Klasy opakowujące dla typów pierwotnych w Javie int --> Integer (Uwaga: nie Int) long --> Long byte --> Byte short --> Short float --> Float double --> Double char --> Character (Uwaga: nie Char) boolean --> Boolean Nowe wersje Javy umieją automatycznie skonwertować z typu pierwotnego na klasę opakowującą i odwrotnie, czyli takie przypisania będą działać. (Są sytuacje, kiedy to nie zadziała, ale są dosyć rzadkie.) int x = new Integer(7); // przypisujemy obiekt klasy Integer na typ pierwotny int Integer z = 7; // przypisanie typu pierwotnego int do klasy Integer Uwaga: automatyczna konwersja może prowadzić do nieoczywistych błędów, np. Integer a = null; int b = a; // będzie NullPointerException w czymś co wygląda na zwykłe przypisanie Uwaga: zmienna typu Boolean (pisany wielką literą) może mieć trzy wartości: true, false i null. Typ pierwotny boolean może mieć tylko dwie wartości: true i false. Parametryzowanie kilkoma parametrami ==================================== Klasy mogą być sparametryzowane więcej niż jednym parametrem: np. public class Para { } Przy użyciu takiego typu podajemy dwa argumenty typowe: Para p1 = new Para<>(); Para p2 = new Para<>(); Typy generyczne a dziedziczenie =============================== Dwie instancje typu generycznego to zawsze są różne typy, np. Koszyk i Koszyk to różne typy, nie ma między nimi żadnych związków dziedziczenia. Nie ma także związków dziedziczenia nawet jeśli argumenty typowe są w relacji dziedziczenia, np. String dziedziczy po Object, ale Koszyk nie dziedziczy po Koszyk. W deklaracji klasy generycznej możemy normalnie używać dziedziczenia, np. public class Koszyk extends ObiektWiklinowy { } // można dziedziczyć po klasie generycznej: public class Koszyk extends Collection { } Ograniczenia: extends, super ============================ Definiując klasę generyczną możemy powiedzieć, że można ją tworzyć tylko z podklasami danego typu: public class Koszyk // możemy tworzyć tylko koszyki owoców: Koszyk // OK Koszyk // OK Koszyk // ŹLE, Bułka nie jest Owocem Jako ograniczenie można podać także interfejs: public class Koszyk> // możemy tworzyć tylko koszyki obiektów implementujących interfejs Comparable Możemy podać kilka ograniczeń: public class Koszyk> // możemy tworzyć tylko koszyki owoców implementujących interfejs Comparable Tylko jedno ograniczenie może być klasą. Wycieranie typów ================ Uwaga: parametry typowe są tylko dekoracjami w kodzie źródłowym, widoczne są tylko w czasie kompilacji. W czasie kompilacji są wycierane i w czasie wykonania nie są dostępne. Taka klasa generyczna: public class Para { private S first; private T second; public Para(S f, T s){ this.first = f; this.second = s; } public S getFirst() { return first; } public T getSecond() { return second; } } w czasie wykonania wygląda tak: public class Para { private Object first; private Object second; public Para(Object f, Object s){ this.first = f; this.second = s; } public Object getFirst() { return first; } public Object getSecond() { return second; } } Najważniejsze konsekwencje wycierania typów: - nie można tworzyć obiektów parametrycznych: new T(); // nie wiadomo, czy jest taki konstruktor - nie można tworzyć tablic typu parametycznego new T[10]; // nie skompiluje się