8.06.2018
Typy i metody sparametryzowane (generics)
http://edu.pjwstk.edu.pl/wyklady/poj/scb/Generics/Generics.html
1/9
Typy i metody sparametryzowane (generics)
Przedstawiono tu najistotniejsze informacje o parametryzacji typów (generics). Są one ważne dla sprawnego posługiwania
się wieloma już sparametryzowanymi interfejsami i klasami ze standardu Javy, a także przydadzą się, gdy będziemy chcieli
tworzyć własne typy sprametryzowane.
1. Wprowadzenie
Często konstrukcje różnych klas oraz metod w klasach są funkcjonalnie podobne (czyli służą wykonaniu tych samych
czynności), różnią się natomiast tylko typami danych na których czynności te są wykonywane.
Po to by nie powielać tego samego kodu dla różnych przypadków do języków programowania wprowadzono szablony
(templates) - klasy oraz metody parametryzowane typami przetwarzanych danych.
Takie podejście stosowane jest w wielu językach (m.in. C++, Ada).
W Javie - poczynając od wersji 1.5 - pojawił się również odpowiednik szablonów tzw. generics.
Przy wprowadzaniu koncepcji generics do Javy w dużo mniejszym stopniu akcent położono na uogólnianie kodu (pierwotny
motyw szablonów C++). Dość wysoki stopień uogólniania kodu był i jest bowiem w Javie dostępny poprzez:
zagwarantowane dziedziczenie klasy Object,
implementację interfejsów,
mechanizmy refleksji (czyli np. dynamiczne, w fazie wykonania programu, odwołania do pól i metod klas) .
Do tych - od dawna dostępnych możliwości - wprowadzenie generics (typów sparametryzowanych) dodaje ułatwienia w
postaci:
unikania konwersji zawężających,
tworzenia bardziej czytelnego kodu,
wykrywania błędów w fazie kompilacji i unikania wyjątku ClassCastException.
Zobaczmy.
Można było np. zawsze napisać ogólną klasę reprezentującą dowolne pary:
class ParaObj {
Object first;
Object last;
public ParaObj(Object f, Object l) {
first = f;
last = l;
}
public Object getFirst() { return first; }
public Object getLast() { return last; }
public void setFirst(Object f) { first = f; }
public void setLast(Object l) { last = l; }
}
Ale przy takim podejściu mamy pewne problemy:
kompilator nie ma możliwości dokładnego sprawdzenia zgodności typów i błędy związane z użyciem niewłaściwego
typu pojawią się dopiero w fazie wykonania, być może w odległej przyszłości, w jakimś innym module systemu,
jesteśmy zmuszeni do stosowania konwersji zawężających, co czasem może być uciążliwe i zmniejsza czytelność
kodu.
Oba problemy obrazuje następujący kod.
8.06.2018
Typy i metody sparametryzowane (generics)
http://edu.pjwstk.edu.pl/wyklady/poj/scb/Generics/Generics.html
2/9
ParaObj po = new ParaObj("Ala", new Integer(3));
System.out.println(po.getFirst() + " " + po.getLast());
// Problem 1
// konieczne konwersje zawężające
String name = (String) po.getFirst();
int nr = (Integer) po.getLast();
po.setFirst(name + " Kowalska");
po.setLast( new Integer(nr + 1));
System.out.println(po.getFirst() + " " + po.getLast());
// Problem 2
// możliwe błędy
po.setLast("kot");
System.out.println(po.getFirst() + " " + po.getLast());
// Błąd może być wykryty w fazie wykonania
// późno, czasem w innym module
Integer n = (Integer) po.getLast(); // ClassCastException
Wynik:
Ala 3
Ala Kowalska 4
Ala Kowalska kot
Exception in thread "main"
java.lang.ClassCastException: java.lang.String
at GenTest1.main(GenTest1.java:62)
Zastosowanie generics (poprzez parametryzację typów) do dotychczasowych możliwości uogólniania kodu dodaje
rozwiązanie w/w problemów. Na czym polega parametryzacja typów?
Typ sparametryzowany - to typ (wyznaczany przez nazwę klasy lub interfejsu) z dołączonym jednym lub większą liczbą
parametrów.
Definicję typu sparametryzowanego wprowadzamy słowem kluczowym class lub interface podając po nazwie (klasy lub
interfejsu) parametry w nawiasach kątowych. Parametrów tych następnie używamy w ciele klasy (interfejsu) w miejscu
"normalnych" typów
Typ sparametryzowany - elementy składni
class | interface Nazwa < ParametrTypu1, ParametrTypu2, ... ParametrTypuN> {
//....
}
Przykład podano na wydruku:
class Para<S, T> {
S first;
T last;
public Para(S f, T l) {
first = f;
last = l;
}
public S getFirst() { return first; }
public T getLast() { return last; }
8.06.2018
Typy i metody sparametryzowane (generics)
http://edu.pjwstk.edu.pl/wyklady/poj/scb/Generics/Generics.html
3/9
public void setFirst(S f) { first = f; }
public void setLast(T l) { last = l; }
}
Możemy teraz tworzyć różne pary:
Para<String, String> p1 = new Para<String, String> ("Jan", "Kowalski");
Para<String, Data> p2 = new Para<String, Data> ("Jan Kowalski", new Data("2005-01-01"));
Para<Integer, Integer> p = new Para<Integer, Integer>(1,2); // autoboxing działa;
Tutaj <String, String>, <String, Data>, <String, Integer> oznaczają konkretne typy, które są podstawiane w miejscu
parametrów w klasie Para<S, T> (ale - jak zobaczymy zaraz - tylko chwilowo, w fazie kompilacji). Nazywane są one
argumentami typu.
Para<String, String>, Para<String, Data> Para<Integer, Integer> nazywają się konkretnymi instancjami sparametryzowanej
klasy Para<S, T>.
Nawiązując do poprzedniego przykładu, testującego uogólnione pary można podać kod używający sparametryzowanej klasy
Para<S, T> - zob. kod na listingu 5 oraz wynik jego działania na listingu 6.
Para<String, Integer> pg = new Para<String, Integer>("Ala", 3); //autoboxing
System.out.println(pg.getFirst() + " " + pg.getLast());
String nam = pg.getFirst(); // bez konwersji!
int m = pg.getLast(); // bez konwersji!
pg.setFirst(name + " Kowalska");
pg.setLast(m+1); // autoboxing
System.out.println(pg.getFirst() + " " + pg.getLast());
Listing 6 - wynik działania kodu z listingu 5
Ala 3
Ala Kowalska 4
Przy tym błędy są wykrywane w fazie kompilacji
pg.setLast("kot");
GenTest1.java:77: setLast(java.lang.Integer) in
Para<java.lang.String,java.lang
Integer> cannot be applied to (java.lang.String)
pg.setLast("kot");
^
1 error
Szczególnie użyteczna jest parametryzacja kolekcji (i był to niewątpliwie główny motyw wprowadzenia generics do Javy).
2. Typy surowe i czyszczenie typów
W Javie - inaczej niż w C++ - po kompilacji dla każdego "szablonu" (typu sparametryzowanego) powstaje tylko jedna klasa
(plik klasowy), współdzielona przez wszystkie instancje tego typu sparametryzowanego.
Skoro tak, to jaki - w fazie wykonania - będzie typ wyznaczany przez skompilowaną klasę sparametryzowaną i jaki
formalny typ uzyskają parametry typu używane w definicji tej klasy?
Aby się przekonać jak w fazie wykonania prezentują się klasy sparametryzowane możemy użyć następującego programiku.
import java.lang.reflect.*;
8.06.2018
Typy i metody sparametryzowane (generics)
http://edu.pjwstk.edu.pl/wyklady/poj/scb/Generics/Generics.html
4/9
class Para<S, T> {
static int nr;
S first;
T last;
public static int getNr() { return nr; }
public Para(S f, T l) {
first = f;
last = l;
nr++;
}
public S getFirst() { return first; }
public T getLast() { return last; }
public void setFirst(S f) { first = f; }
public void setLast(T l) { last = l; }
}
public class GenTest2 {
public static void main(String[] args) {
Para<String, Integer> p1 = new Para<String, Integer>("Ala", 3);
System.out.println(p1.getNr());
Para<String, Integer> p2 = new Para<String, Integer>("Ala", 3);
System.out.println(p2.getNr());
Para<String, String> p3 = new Para<String, String>("Ala", "Kowalska");
System.out.println(p3.getNr());
// Co jest - tylko klasa Para
// "Raw Type"
Class p1Class = p1.getClass();
System.out.println(p1Class);
// Metodami refleksji możemy się przekonać, że
// w definicji klasy Para typem fazy wykonania dla parametrów jest Object
// "type erasure"!!!
Method[] mets = p1Class.getDeclaredMethods(); // zwraca tablicę metod deklarwoanych w klasie
for (Method m : mets) System.out.println(m);
// Surowego typu ("Raw Type") możemy też używać
// ale czasem wiąże się to z niuansami
// i kompilator może nas ostrzegać o możliwych błędach
Para p = new Para("B", new Double(3.1));
String f = (String) p.getFirst();
double d = (Double) p.getLast();
System.out.println(f + " " + d);
}
}
Po jego uruchomieniu uzyskamy następujący wydruk.
1
2
3
class Para
public static int Para.getNr()
public java.lang.Object Para.getFirst()
public java.lang.Object Para.getLast()
public void Para.setFirst(java.lang.Object)
public void Para.setLast(java.lang.Object)
B 3.1
Wydruk ten oznacza, że:
jest tylko jedna klasa Para dla wszystkich instancji klasy sparametryzowanej Para<S, T>; typ wyznaczany przez tę
klasę nazywa się typem surowym
("raw type"
),
8.06.2018
Typy i metody sparametryzowane (generics)
http://edu.pjwstk.edu.pl/wyklady/poj/scb/Generics/Generics.html
5/9
z definicji klasy Para zniknęły wszystkie parametry typu i zostały zastąpione przez Object; ten mechanizm nazywa
się czyszczeniem typów (
"type erasure"
) ,
ponieważ jest tylko jedna klasa Para - zmienne reprezentowane przez pola statyczne są wspólne dla wszystkich
instancji typu sparametryzowanego; zmienna nr jest wspólna dla Para<String, Integer> i Para<String, String> -
dlatego zwiększa się w sposób ciągły (1, 2, 3).
3. Restrykcje
Przyjęte w Javie rozwiązanie (jedna klasa w fazie wykonania, czyszczenie typów) ma swoje zalety i wady.
Do zalet zaliczyć można:
mniejszą liczbę klas po kompilacji,
zgodność kodu binarnego z kodem nie używającym "generics" ("czyszczenie typów" stanowi właśnie o
kompatybilności kodów używających generics z kodami nie używającymi ich).
Wady wydają się jednak przeważać nad zaletami. Do wad zaliczymy:
ograniczenia na możliwości użycia parametrów typu i wynikające stąd:
znacznie mniejsze (niż np. w C++) możliwości prostego uogólniania klas i metod (ale funkcjonalnie
uogólnianie jest dostępne na takim samym poziomie jak w C++, choćby dzięki refleksji),
pewna trudność w definiowaniu typów sparametryzowanych - trzeba pamiętać o niuansach wprowadzanych
przez wybrany sposób kompilacji,
ograniczenia na możliwości parametryzacji typów.
Istotnie, w definicjach klas (i metod) sparametryzowanych nie do końca możemy traktować parametry typu jak zwykłe typy.
Możemy
:
podawać je jako typy pól i zmiennych lokalnych,
podawać je jako typy parametrów i wyników metod,
dokonywać jawnych konwersji do typów oznaczanych przez nie (ale to będzie tylko ważne na etapie kompilacji, po to
by uniknąć błędów niezgodności typów, natomiast nie uzyskamy w fazie wykonania faktycznych konwersji np.
zawężających, no bo jak?),
wywoływać na rzecz zmiennych oznaczanych typami sparametryzowanymi metody klasy Object (i ew. właściwe dla
klas i interfejsów, które stanowią tzw. górne ograniczenia danego parametru typu).
Nie możemy
(w definicjach sparametryzowanych klas i metod):
tworzyć obiektów typów sparametryzowanych (new T() jest niedozwolone, no bo na poziomie definicji generics nie
wiadomo co to konkretnie jest T),
używać operatora instanceOf ( z powodu j.w.),
używać ich w statycznych kontekstach (bo statyczny kontekst jest jeden dla wszystkich różnych instancji typu
sparametryzowanego),
używać ich w literałach klasowych,
wywoływać metod z konkretnych klas i interfejsów, które nie są zaznaczone jako górne ograniczenia parametru typu
(w najprostszym przypadku tą górną granicą jest Object, wtedy możemy używać tylko metod klasy Object).
Również użycie typów sparametryzowanych obarczone jest restrykcjami.
Nie wolno np.:
używać typów sparametryzowanych przy tworzeniu tablic (podając je jako typ elementu tablicy),
w obsłudze wyjątków (bo jest to mechanizm fazy wykonania),
w literałach klasowych (bo oznaczają typy fazy wykonania).
Dlaczego nie możemy mieć tablic elementów sparametryzowanego typu?
Wynika to z istoty pojęcia tablicy oraz ze sposobu kompilacji generics.
Tablica jest zestawem elementów tego samego typu (albo jego podtypu). Informacja o typie elementów tablicy jest
przechowywana i JVM korzysta z niej w fazie wykonania, aby zapewnić, że do tablicy nie jest wstawiany element
niewłaściwego typu (wtedy generowany jest wyjątek ArrayStoreException).
Gdyby dopuścić tablice elementów typów sparametryzowanych kontrakt ten zostałby zerwany (bowiem w fazie wykonania
nic nie wiadomo o konkretnych instancjach typów sparametryzowanych, zatem nie można zapewnić odpowiedniej
dynamicznej kontroli typów).
Zobaczmy.
8.06.2018
Typy i metody sparametryzowane (generics)
http://edu.pjwstk.edu.pl/wyklady/poj/scb/Generics/Generics.html
6/9
Para<String, Integer>[] pArr = new Para<String, Integer>[5];
// (1) niedozwolone
Object[] objArr = p;
objArr[0] = new Para<String, String>("A", "B"); // przejdzie, jeśli dopuścimy (1)
a błąd pojawi się (jako ClassCastException) kiedyś później, gdy np. sięgniemy po pierwszy element tablicy pArr i zapytamy
go o drugi składnik pary (pArr[0].getLast()). Skąd błąd? Bo do tego odwołania kompilator powinien był dopisać konwersję
do Integer, a faktycznie mamy String, a nie Integer.
Oczywiście, to ograniczenie można obejść, stosując następujące rozwiązania:
tablice typów surowych (niebezpieczne),
tablice uniwersalnych instancji typów sparametryzowanych (uniwersalna instancja wprowadzana jest z użyciem
parametru typu ?, co oznacza dowolny typ; zob. dalej); uniwersalne instancje też nie są dobrym rozwiązaniem - choć
semantycznie są zbliżone do typów surowych, to składniowa różnica powoduje, że są przez kompilator traktowane
inaczej niż typy surowe i w wielu przypadkach zamiast ostrzeżeń "unchecked cast" dostaniemy raczej błędy w
kompilacji,
i rozwiązanie najlepsze - zastosowanie kolekcji (list) konkretnych instancji typu sparametryzowanego.
4. Ograniczenia parametrów typu
Jednym ze sposobów zwiększania funkcjonalności generics Javy jest użycie (jawnych) ograniczeń parametrów typu. Dzięki
temu w klasach i metodach sparametryzowanych możemy korzystać z metod, specyficznych dla podanych ograniczeń.
Ograniczenie parametru typu określa zestaw typów, które mogą być używane jako argumenty typu (i
podstawiane w szablonie w miejscu parametrów typu), a w konsekwencji zestaw metod, które mogą być
wołane na rzecz zmiennych oznaczanych parametrami typu
Ograniczenia parametru typu wprowadzamy za pomocą składni:
ParametrTypu extends Typ1 & Typ2 & Typ3 & ... & TypN
gdzie:
Typ1 - nazwa klasy lub interfejsu
Typ2-TypN - nazwy interfejsów
Uwaga:
typy Typ1-TypN mogą być sparametryzowane,
typy ograniczające nie mogą się powtarzać, w tym nie mogą występować powtórzenia dla typów
sparametryzowanych TP<X> TP<Y> (ze względu na czyszczenie typów).
W przypadku ograniczanych parametrów typu "type erasure" daje typ pierwszego ograniczenia.
Np. w fazie wykonania, w kontekście class A <T extends Appendable>, T staje się Appendable.
Przykład wykorzystania ograniczeń typów pokazuje wydruk, zawierający uogólniony kod, pozwalający na określanie
maksymalnego i minimalnego elementu tablicy danych dowolnego typu, implementującego interfejs Comparable (z jedną
metodą compareTo pozwalająca na porównywanie obiektów).
class MinMax<T> {
private T min;
private T max;
public MinMax(T f, T l) {
min = f;
max = l;
}
public T getMin() { return min; }
public T getMax() { return max; }
}
8.06.2018
Typy i metody sparametryzowane (generics)
http://edu.pjwstk.edu.pl/wyklady/poj/scb/Generics/Generics.html
7/9
class GenArr<T extends Comparable<T>> {
private T[] arr;
private MinMax<T> minMax;
public GenArr(T[] a) { init(a); }
public void init(T[] a) {
minMax = null;
arr = a;
}
public MinMax<T> getMinMax() {
if (minMax != null) return minMax;
if (arr == null || arr.length == 0) return null;
T min = arr[0];
T max = arr[0];
for (int i=1; i<arr.length; i++) {
if (arr[i].compareTo(min) < 0) min = arr[i]; // dzięki T extends Comparable
if (arr[i].compareTo(max) > 0) max = arr[i];
}
minMax = new MinMax<T>(min, max);
return minMax;
}
}
public class Bounds {
public Bounds() {
Integer[] arr1 = { 1, 2 , 7, -3 };
Integer[] arr2 = { 1, 7 , 8, -10 };
String[] napisy = { "A", "Z", "C" };
GenArr<Integer> ga = new GenArr<Integer>(arr1);
MinMax<Integer> imx = ga.getMinMax();
System.out.println(imx.getMax() + " " + imx.getMin());
ga.init(arr2);
imx = ga.getMinMax();
System.out.println(imx.getMax() + " " + imx.getMin());
GenArr<String> gas = new GenArr<String>(napisy);
System.out.println(gas.getMinMax().getMax() + " " +
gas.getMinMax().getMin());
}
public static void main(String[] args) {
new Bounds();
}
}
Wynik:
7 -3
8 -10
Z A
5. Parametry uniwersalne (wildcards)
Weźmy jakąś kolekcję sparametryzowaną np. listę ArrayList:
ArrayList<Integer> list1 = new ArrayList<Integer>();
8.06.2018
Typy i metody sparametryzowane (generics)
http://edu.pjwstk.edu.pl/wyklady/poj/scb/Generics/Generics.html
8/9
Czy ArrayList<Object> jest nadtypem dla typu ArrayList<Integer>?
Gdyby tak było, to moglibyśmy zrobić tak:
ArrayList<Object> list2 = list1; // hipotetyczna konwersja rozszerzająca
a wtedy kompilator nie mógłby protestować przeciwko czemuś takiemu:
list2.add(new Object());
Co jednak doprowadziłoby do katastrofy:
Integer n = list1.get(0); // próba przypisania Object na Integer
I wobec tego konstrukcja: list2 = list1 jest zabroniona w fazie kompilacji (
"incompatible types"
), co oznacza, że w Javie
pomiędzy typami sparametryzowanymi za pomocą konkretnych parametrów nie zachodzą żadne relacje w rodzaju
dziedziczenia (typ-nadtyp itp.).
A jednak takie relacje są czasem potrzebne.
Jeśli ArrayList<Integer> i ArrayList<String> nie są podtypami ArrayList<Object>- to jak stworzyć metodę wypisującą
zawartośc dowolnej listy ArrayList?
Do tego służą parametry uniwersalne (wildcards) - oznaczenie "?".
Są trzy typy takich parametrów:
ograniczone z góry <? extends X> - oznacza "wszystkie podtypy X"
ograniczone z dołu <? super X> - oznacza "wszystkie nadtypy X"
nieograniczone <?> - oznacza "wszystkie typy"
Notacja ta wprowadza do Javy
wariancję
typów sparametryzowanych.
Typ sparametryzowany C<T> jest kowariantny względem parametru T, jeśli dla dowolnych typów A i B,
takich, że B jest podtypem A, typ sparametryzowany C<B> jest podtypem C<A> (kowariancja - bo
kierunek dziedziczenia typów sparametryzowanych jest zgodny z kierunkiem dziedziczenia parametrów
typu)
Kowariancję uzyskujemy za pomocą symbolu <? extends X>, co oznacza np. że
List<? extends Number> jest nadtypem wszystkich typów sparametryzowanych, gdzie parametrem typu jest Number albo
typ pochodny od Number.
Typ sparametryzowany C<T> jest kontrawariantny względem parametru T, jeżeli dla dowolnych typów A
i B, takich że B jest podtypem A, typ sparametryzowany C<A> jest podtypem typu sparametryzowanego
C<B> (kontra - bo kierunek dziedziczenia jest przeciwny).
Kontrawariancję uzyskujemy za pomocą symbolu <? super X>. Np. Integer jest podtypem Number, a List<Number> jest
podtypem List<? super Integer>, wobec czego możemy podstawiać:
List<? super Integer> list = new ArrayList<Number>;
Biwariancja oznacza rownoczesną kowariancję i kontrawariancję typu sparametryzowanego
Biwariancję uzyskujemy za pomocą symbolu <?>, który oznacza wszystkie typy. Faktycznie ArrayList<?> oznacza
wszystkie możliwe listy ArrayList z dowolnym parametrem typu T. Czyli ArrayList<?> jest nadtypem ArrayList<? extends
Integer> i nadtypem dla ArrayList<? super Integer>.
Kowariancja typów sparametryzowanych umożliwia pisanie uniwersalnych metod (w rodzaju "wypisz dowolną kolekcję"
albo "pokaż dowolną listę",
Np.
void showEmployee(ArrayList <? extends Pracownik>) {
8.06.2018
Typy i metody sparametryzowane (generics)
http://edu.pjwstk.edu.pl/wyklady/poj/scb/Generics/Generics.html
9/9
// ...
}
Bez tego nie moglibyśmy jako argumentów przekazywać listy dyrektorów, kierowników, asystentów etc.
( bo między ArrayList<Pracownik> i ArrayList<Asystent> nie ma relacji nadtyp - podtyp).
Ale jeśli mamy gdzieś dostęp do typu sparametryzowanego <? extends X>, to zabronione jest podstawianie na ten typ
konkretniejszych podtypów.
Inaczej mielibyśmy sytuację, w której do przekazanej listy dyrektorów dopisywani mogliby być np. asystenci.
Nie możemy "podstawiać", ale możemy pobierać (dostajemy coś calkiem bezpiecznego typu - np. typu wyznaczanego przez
dolną granicę).
6. Metody sparametryzowane i konkludowanie typów
Parametryzacji mogą podlegać nie tylko klasy czy interfejsy, ale również metody.
Definicja metody sparametryzowanej ma postać:
specyfikatorDostępu [static] <ParametryTypu> typWyniku nazwa( lista parametrów) {
// ...
}
Argumenty typów (podstawiane w fazie kompilacji w miejsce parametrów, choćby po to by zapewnić zgodność typów oraz
automatyczne konwersje zawężające) są określane na podstawie faktycznych typów użytych przy wywołaniu metody. Proces
wyznaczania aktualnych argumentów typów nazywa się konkludowaniem typów (ang. type inferring).
Poniższy program zawiera przykład sparametryzowanej metody wyznaczającej maksimum z tablicy elementów dowolnego
typu pochodnego od Comparable. Konkretne argumenty typu (odpowiadające parametrowi T użytemu zarówno na liście
parametrów metody, jak i jako typ jej wyniku) są konkludowane z wywołań.
public class Metoda {
public static <T extends Comparable<T>> T max(T[] arr) {
T max = arr[0];
for (int i=1; i<arr.length; i++)
if (arr[i].compareTo(max) > 0) max = arr[i];
return max;
}
public static void main(String[] args) {
Integer[] ia = { 1, 2, 77 };
int imax = max(ia); // w wyniku konkluzji T staje się Integer
Double[] da = {1.5, 231.7 };
double dmax = max(da); // w wyniku konkluzji T staje się Double
System.out.println(imax + " " + dmax);
}
}