2. Typy sparametryzowane (generics)
Pojęcie szablonu (template) z
języka C++ zrobiło oszałamiającą karierę. W różnych innych
językach są podobne konstrukcje. Również w Javie - od wersji 1.5
- mamy możliwość tworzenia czegoś na podobieństwo szablonów z
C++. Mechanizm generics z Javy różni się jednak znacząco
od szablonów znanych z innych języków. Czym są generic w
Javie? Jak je definiował? Jakie mają ograniczenia? I jakie użyteczne
zastosowania? To tematy niniejszego wykładu.
1. Szablony - 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 - w wersji 1.5 - pojawił się również odpowiednik szablonów tzw. generics.
Uwaga. Generics Javy bardzo różnią się od szablonow (templates) w C++
Przy wprowadzaniu koncepcji szablonów 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.
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 kłopoty:
ParaObj po = new ParaObj("Ala", new Integer(3));
System.out.println(po.getFirst() + " " + po.getLast());
// Problem 1
// konieczne konwersja
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 wykonaie
// późno, czasem w innym module
Integer n = (Integer) po.getLast(); // ClassCastException
Wydruk działania powyższego fragmentu:
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.
Parametryzowane mogą być:
2. Typy sparametryzowane i ich instancje. Typy surowe.
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 kluczowm 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:
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; }
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ą argumentami typu.
Para<String, String>, Para<String, Data> Para<Integer, Integer> nazywają się konkretnymi instancjami sparametryzowanej klasy Para<S, T>.
Zastosowanie typów sparametryzowanych pozwala na:
- unikania konwersji zawężających,
- tworzenie bardziej czytelnego kodu,
- wykrywanie błędów w fazie kompilacji i unikanie wyjątku ClassCastException.
Nawiązując do poprzedniego przykładu, testującego uogólnione pary:
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());
Wynik:
Ala 3
Ala Kowalska 4
a 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
W Javie - inaczej niż w C++ - po kompilacji dla każdego szablonu 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.*;
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();
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
Co oznacza ten wydruk:
- jest tylko jedna klasa Para dla wszystkich instancji klasy sparametryzowanej
Para<S, T>; typ wyznaczany przez tę klasę nazywa się typem surowym ("raw type"),
- 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 - 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).
Zobacz multimedialną prezentację (Para<S,T>, "raw type", "type erasure")
Jak widzieliśmy, możemy wykorzystywać w kodzie typy surowe. Ale zwróćmy uwagę,
że wykorzystanie typów surowych nie jest bezpieczne - kompilator nie jest
w stanie sprawdzić zgodności typów. Dlatego w fazie kompilacji wyda ostrzeżenie
(aby zobaczyć czego dotyczą ostrzeżenia należy przy kompilacji podać opcję -Xlint:unchecked):
GenTest2a.java:56: warning: [unchecked] unchecked call to Para(S,T) as a member
of the raw type Para
Para p = new Para("B", new Double(3.1));
^
1 warning
I oczywiście ma racje. W programie nie korzystamy z typu Para<String,
Double>, ale stworzyliśmy taki obiekt - zapewne przez pomyłkę.
Uwaga: użycie typów surowych może być zabronione w póżniejszych wersjach Javy.
Mamy więc odpowiedź na pytanie o typy fazy wykonania.
W najprostszym przypadku parametrów takich jak <S, T> parametry w skompilowanej klasie uzyskują jedyny możliwy typ
- ogólny nadtyp wszystkich innych - Object.
Czyli - praktycznie nasza klasa
Para<S,T> po kompilacji wygląda niemal tak samo jak wcześniej pokazana klasa ParaObj.
Uwaga: gdy będzie mowa o ograniczeniach parametrów typu z góry, zobaczymy,
że typy fazy wykonania mogą być bardziej specyficzne niż Object.
Jaka jest więc różnica pomiędzy kodem z generics i bez nich? Dzięki parametryzacji kompilator - przy tworzeniu instancji - może sprawdzić
zgodność typów i w kodzie, wykorzystującym te instancje automatycznie
dopisać zawężające konwersje.
Przyjęte w Javie rozwiązanie (jedna klasa w fazie wykonania, czyszczenie typów) ma swoje zalety i wady.
Zalety:
- mniej 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:
- 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.
Uwaga -
nie można parametryzować:
- enumeracji (bo generalnie są to typy statyczne, a parametrów typu -
jak wynika z omówionego mechanizmu kompilacji - nie można używać w kontekstach
statycznych),
-
klas anonimowych (bo nie można tworzyć ich obiektów przez new, a zatem nie
można podac konkretnych typów, które zastąpią parametry),
- klas
wyjątków (bo mechanizm wyjątków jest mechanizmem fazy wykonania, a JVM -
zgodnie z przyjętą koncepcją kompilacji - nie wie nic o generics).
Typy sparametryzowane mogą być używane prawie tak samo jak zwykłe typy.
Nie wolno jedynie:
- 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 elementow 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.
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 odwolania kompilator powinien był dopisać konwersje
do Integer, a faktycznie mamy String, a nie Integer.
Rozwiązanie problemu:
- 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,
- najlepsze - zastosowanie kolekcji (list) konkretnych instancji typu sparametryzowanego.
3. Argumenty i parametry typu
Parametry typu są symbolicznymi oznaczeniami typów, ale przy definiowaniu
szablonów - inaczej niż w C++ - nie możemy traktować ich dokładnie tak samo
jak normalne typy.
Wynika to z omówionego przed chwilą sposobu kompilacji "generics".
Przyjęte rozwiązanie oznacza, że nie możemy używać jako argumentów typu typów prostych
(int, double itp.), co jest jednak łagodzone (ale tylko na poziomie wykorzystania
instancji klas sparametryzowanych) autoboxingiem.
Możemy używać jako argumentów typu:
- nazw klas, w tym enumeracji (enum),
- nazw interfejsów,
- nazw typów sparametryzowanych.
Jak wspomniano, w definicji 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 sprametryzowanego),
- używac 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).
Ograniczenia te powodują, że np. funkcjonalność szablonu Para<S, T> nie może być zbyt wysoka.
Na pewno możemy dodać metody toString(), hashCode() i equals() - bo występują
w klasie Object i kompilator nie będzie protestował.
Na przykład:
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; }
public void setFirst(S f) { first = f; }
public void setLast(T l) { last = l; }
public boolean equals(Para<S,T> p) {
return first.equals(p.first) && last.equals(p.last);
}
public String toString() {
return first + " " + last;
}
}
public class GenTest3 {
public static void main(String[] args) {
Para<String, Integer> p1 = new Para<String, Integer>("Ala", 3);
Para<String, Integer> p2 = new Para<String, Integer>("Ala", 3);
System.out.println(p1 + "\n" + p2);
System.out.println(p1.equals(p2));
}
}
Wynik:
Ala 3
Ala 3
true
Możemy też dodać wszystko to co dotyczy przypisań np. metodę swap, która przestawia elementy pary.
class Para<S, T> {
// ...
public void swap() {
T temp = (T) first;
first = (S) last;
last = temp;
}
}
i teraz:
Para<String, String> p3 = new Para<String, String>("Ala", "Kowalska");
p3.swap();
System.out.println(p3);
da:
Kowalska Ala
Ale metoda swap() jest potencjalnie niebezpieczna, bo jeśli zastosujemy ją
wobec <String, Integer> nastąpi ClassCastException.
Kompilator ostrzega nas o takiej możliwości pisząc:
"unchecked unsafe operation"
co dotyczy właśnie - potrzebnych dla uniknięcia błędu w kompilacji - jawnych konwersji (S) i (T).
W tym przypadku aby uniknąć ew. błędu kończącego program możemy przy niezgodności typów generować
wyjątek UnsupportedOperationException (i obsługiwać go), albo zwracac wartość
boolean - czy przestawienie się udało.
Jednak pewnie lepiej będzie stworzyć inny szablon np.
class ParaSame<S> {
S first;
S last;
...
}
i tylko dla niego dopuścić operację swap, która tutaj będzie w pełni bezpieczna.
Mimo ograniczeń, wynikających ze sposobu kompilacji być może w naszym
szablonie
chcielibyśmy wprowadzić większą (ogólną) funkcjonalność,
np. mieć operacje kopiowania par oraz dodawania ich do siebie.
Czy
i jak można to zrobić?
4. Wykorzystanie refleksji w szablonach
Wszelkie uogólnienia klas sparametryzowanych, których nie można wprowadzić
bezpośrednio ze względu na ograniczenia związane z zastosowaniem parametrów
typu - można wprowadzić pośrednio za pomocą środków refleksji
Przykład: dodajemy do szablonu konstruktor kopiujący oraz metodę dodawania dwóch par.
import java.lang.reflect.*;
class Para<S, T> {
S first;
T last;
public Para() {}
public Para(S f, T l) {
first = f;
last = l;
}
// konstruktor kopiujący
public Para(Para<S,T> p) {
// nie możemy użyć new, ale możemy zastosować refleksję
try {
first = (S) getInstance(p.first); // unchecked, ale jest gwarancja
last = (T) getInstance(p.last); // że typy będą właściwe
} catch (Exception exc) {
throw new UnsupportedOperationException("Copy constructor not available",
exc.getCause());
}
}
private Object getInstance(Object o) throws Exception {
Class type = o.getClass();
Constructor c = null;
Object arg = null;
try { // czy jest konstruktor kopiujący?
c = type.getConstructor(type);
arg = o;
} catch (Exception exc) { // nie ma kopiującego
if (type.getSuperclass() == java.lang.Number.class) { // może to boxy?
c = type.getConstructor(java.lang.String.class);
arg = o.toString();
}
}
if (c == null) { // ani kopiujący, ani ze Stringa nie jest bezpiecznie
// utworzyć obiekt za pomocą konstruktora bezparametrowego (newInstance)
// pobrać od klasy Properties (PropertyDescriptor[])
// dla wszystkich setterów wywołać odpowiednie gettery na oryginale,
// a zwrócone wartości podać jako argumenty setterom
// (i wołać je po kolei na kopii)
throw new UnsupportedOperationException("Valid constructor not found in" +
type); // bo tego nie robimy
}
else return c.newInstance(arg);
}
// dodawanie par
public Para<S, T> add(Para<S, T> p) {
Para<S, T> wynik = new Para<S, T>(); // nie można new T(), ale można new X<T>!
try {
wynik.first = (S) addObjects(first, p.first); // unchecked,
wynik.last = (T) addObjects(last, p.last); // ale typ jest gwarantowany
return wynik;
} catch(Exception exc) {
throw new UnsupportedOperationException("Addition not allowed",
exc.getCause());
}
}
private Object addObjects(Object o1, Object o2) throws Exception {
if (o1 instanceof String) return (String) o1 + o2; // konkatenacja
if (o1 instanceof Number) { // działania na klasach opakowujących typy proste
double d = ((Number) o1).doubleValue() + ((Number) o2).doubleValue();
String s = String.valueOf(d);
Constructor c = null;
try { // wynik musi być specyficznego typu (np. Integer)
c = o1.getClass().getConstructor(java.lang.String.class);
return c.newInstance(s);
} catch(Exception exc) { // np. gdy new Integer("1.0");
int l = s.indexOf('.'); // bierzemy tylko cyfry przed kropką
s = s.substring(0, l);
return c.newInstance(s);
}
}
// Ani String ani Number - więc musi mieć metodę add(...)
Class typ = o1.getClass();
Method m = typ.getDeclaredMethod("add", typ);
return m.invoke(o1, o2);
}
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 String toString() {
return first + " " + last;
}
}
class Value {
int val;
Value(int n) { val = n; }
public Value add(Value v) {
return new Value(val + v.val);
}
public String toString() {
return "" + val;
}
}
public class GenTest4 {
public static void main(String[] args) {
Para<String, Integer> p1 = new Para<String, Integer>("A", 1);
Para<String, Integer> p2 = new Para<String, Integer>(p1);
System.out.println(p2);
Para<String, Integer> wynik = p1.add(p2);
System.out.println(wynik);
// Para <String, String> ps = new Para <String, String>("c", "d");
// wynik = p1.add(ps); <=== kompilator wykrywa błędy
Para<Value, Value> v1 = new Para<Value, Value>(new Value(1), new Value(2));
Para<Value, Value> v2 = new Para<Value, Value>(new Value(3), new Value(4));
Para<Value, Value> vv = v1.add(v2);
System.out.println(vv);
}
}
Wynik:
A 1
AA 2
4 6
Przy kompilacji tego przykładu dostaniemy sporo ostrzeżeń "unchecked cast".
Starannie napisany kod pozwala jednak ustrzec się błędów w fazie wykonania.
Zobacz demo programu
Refleksja daje nam mocne możliwości uogólnień. Połączenie jej z generics
umożliwia natomiast wykrywanie - w fazie kompilacji - wiele błędów przy
korzystaniu z klasy sparametryzowanej (przykład w kodzie: p1.add(ps)), no
i oczywiście - eliminuje żmudne konwersje zawężające.
5. Ograniczenia parametrów typu
Innym (od refleksji) sposobem zwiększania
funkcjonalności szablonow Javy jest użycie (jawnych) ograniczeń parametrów
typu. Dzięki temu w szablonach 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 parametrow 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.
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; }
}
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
6. Metody sparametryzowane
Oprócz klas i interfejsów możemy również parametryzować metody.
Definicja metody sparametryzowanej ma postać:
specDost [static] <ParametryTypu> typWyniku nazwa( lista parametrów) {
// ...
}
Argumenty typów (podstawiane do szablonu w fazie kompilacji, choćby po to
by zapewnić zgodność typów oraz automatyczne konwersje zawężające) są określane
(inferred) na podstawie faktycznych typów argumentów wywołania.
Przykład:
public class Metoda {
public static <T extends Comparable> 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);
Double[] da = {1.5, 231.7 };
double dmax = max(da);
System.out.println(imax + " " + dmax);
}
}
7. Parametry uniwersalne (wildcards)
Weżmy jakąś kolekcję sparametryzowaną np. listę:
List<Integer> list1 = new ArrayList<Integer>();
Czy List<Object> jest nadtypem dla typu List<Integer>?
Gdyby tak było, to moglibyśmy zrobić tak:
List<Object> list2 = list1; // hipotetyczna konwersja rozszerzająca
a wtedy kompilator nie mógłby protestowac 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 List<Integer> i List<String> nie są podtypami List<Object>-
to jak stworzyć metodę wypisującą zawartośc dowolnej listy?
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 List<?> oznacza wszystkie możliwe listy z dowolnym
parametrem T. Czyli List<?> jest nadtypem List<? extends Integer>
i nadtypem dla List<? super Integer>.
Kowariancja typów sparametryzowanych umożliwia pisanie uniwersalnych metod
(w rodzaju "wypisz dowolną kolekcję" albo "pokaż dowolną listę",
Np.
void showEmployee(List <? extends Pracownik>) {
// ...
}
Bez tego nie moglibyśmy jako argumentów przekazywać listy dyrektorów, kierowników, asystentów etc.
( bo między List<Pracownik> i List<Asystent> nie ma relacji podtyp - nadtyp).
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ć asystenci.
Nie możemy "podstawiać", ale możemy pobierać (dostajemy coś calkiem bezpiecznego
typu - np. typu wyznaczanego przez dolną granicę).
To samo dotyczy biwariancji, ale przy kontrawariancji mamy sytuację odwrotną.
8. Użyteczność szablonów (generics) w Javie
Czy użyteczność szablonów w Javie jest ograniczona wyłącznie do kontroli
typów w fazie kompilacji oraz unikania konieczności dokonywania konwersji
zawężających?
Zacznijmy od pułapek "cudu uogólnienia". Bardzo chcemy, by generics były
tym, czym szablony w językach bywają: środkiem uogólnienia kodu.
Myśląc w ten sposób możemy dojść do nieopatrznego wniosku: nareszcie - dzięki
generics - możemy pisać fragmenty kodu o uogólnionym działaniu, np. takie,
które wprowadzają jakieś nasze wymyślone ułatwienia do operowania na dowolnych
komponentach Swingu:
import java.awt.*;
import javax.swing.*;
import static java.awt.Color.*;
class MComp <T extends JComponent> {
private T comp;
public MComp(T c) { comp = c; }
public T getComponent() { return comp; }
public MComp setColors(Color f, Color b) {
comp.setForeground(f);
comp.setBackground(b);
return this;
}
public MComp setFont(String name, int ... args) {
int style = Font.PLAIN, size = 12;
if (args.length >= 1) size = args[0];
if (args.length >= 2) style = args[1];
comp.setFont(new Font(name, style, size));
return this;
}
public MComp setPreferredSize(int w, int h) {
comp.setPreferredSize(new Dimension(w, h));
return this;
}
}
public class Bounds0 extends JFrame {
public Bounds0() {
JButton b = new JButton("Generics");
MComp<JButton> bWrap = new MComp<JButton>( b );
bWrap.setColors(YELLOW, BLUE).setFont("Dialog",20).setPreferredSize(500,100);
setLayout(new FlowLayout());
add(b);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack();
setVisible(true);
}
public static void main(String[] args) {
new Bounds0();
}
}
Ale, zaraz! To samo mogliśmy napisać od zawsze, nie korzystając z generics.
Wystarczyło jako typ zmiennej comp w klasie MComp podać JComponent!
Ten fragment - z generics - przydaje się tylko do jednej rzeczy: kiedy z
"wrappera" pobieramy komponent (metodą getComponent()), to od razu uzyskamy
obiekt właściwego typu (np. JButton). W tym przypadku nie jest to wielka
korzyść w zamian za konieczność pisania:
class MComp<T extends JComponent>
a co gorsza (bo już dotyczy korzystania z klasy, a nie jej budowy, w trakcie
której utrudnienia, prowadzące do ułatwień w korzystaniu są usprawiedliwione):
MComp<JButton> bWrap = new MComp<JButton>( b );
Zdaje się, że nawet Bruce Eckel wpadł w pułapkę szukania sensu generics w uogólnianiu kodu.
(zob. Bruce Eckel. Thinking About Computing 10-8-04 Templates vs. Generics - weblog).
Zadając sobie pytanie, czy w Javie za pomocą generics można robić to samo
co w C++ za pomocą szablonów - odpowiada: w większości przypadków tak, a
często nawet krócej i bardziej elegancko (ale dzięki innym od generics środkom
językowym Javy).
Oto przykład napisanej przez niego metody statycznej apply (w klasie Apply),
która wobec wszystkich elementów przekazanego zestawu obiektów seq, wywołuje
podaną metodę z podanymi argumentami.
public class Apply {
public static <T, S extends Iterable<? extends T>>
void apply(S seq, Method f, Object... args) {
try {
for(T t: seq)
f.invoke(t, args);
} catch(Exception e) {
// Failures are programmer errors
throw new RuntimeException(e);
}
}
}
Bardzo ładna idea: automatyczne wywołanie dowolnej metody na rzecz zestawu dowolnych obiektow.
Tylko, że w Javie osiągalna nie dzięki generics, a dzięki refleksji i wprowadzonemu
w wersji 5 interfejsowi Iterable (który omawialiśmy przy okazji instrukcji
"for-each").
Więcej nawet - w tym przykładzie metody apply - generics nie mają żadnego sensu.
Należałoby to raczej napisać tak:
class Apply {
public static
void apply(Iterable seq, Method f, Object... args) {
try {
for(Object t : seq)
f.invoke(t, args);
} catch(Exception e) {
// Failures are programmer errors
throw new RuntimeException(e);
}
}
}
Osiągamy dokładnie ten sam efekt - włącznie z (ograniczoną w obu przypadkach)
kontrolą zgodności typów, a unikamy żmudnego "generyzowania", do tego trochę
zawikłanego:
<T, S extends Iterable<? extends T>>
Tutaj, oczywiście S musi mieć jako ograniczenie Iterable, abyśmy mogli sekwencję
seq zastosować w instrukcji for-each. Natomiast - nieco bardziej tajemnicze
zastosowanie uniwersalnego znaku ? do oznaczenia wszystkich podtypów T jest
wymuszone potrzebą zapewnienia możliwości zwracania przez iterator podtypów
T (a zatem możliwości zawarcia w sekwencji seq obiektów typu T jak również
ew.obiektów jego podtypów).
W oparciu o dwa w/w przykłady moglibyśmy wysnuć wniosek, że w istocie generics
niewiele dają. Ale już Bruce Eckel w dalszej części swoich przemyśleń pokazuje
dość subtelny i ładny przykład mocy generics.
Czyni to pod hasłem: jak można wybrnąć z problemu czyszczenia typów w generics
Javy przy próbie pisania kodów uogólnionych, w których nie ma gwarancji,
że stosujemy metody z istniejących interfejsów, a jednocześnie chcemy zachować
dużą dozę weryfikacji zgodności typów w fazie kompilacji i wynikające stąd
ułatwienia pomijania konwersji zawężających.
Problem:
napisać metodę uogólnioną która wypełnia kontener (=obiekt przechowujący
zestaw danych) referencjami do nowotworzonych obiektów danego typu.
Jak wypełniać taki kontener? Dodając do niego za pomocy metody add(...).
Ale w szablonie nie możemy użyć tej metody na rzecz różnych klas (nie będących
w jednej hierarchii dziedziczenia). Do kolekcji możemy dodawać za pomocą add (bo interfejs Collection
ma metodę add), ale kontenery to nie tylko kolekcje. Jak poszerzyć uniwersalność
ew. kodu?
Otóż można się umówić, że kontenery, do których możemy dodawać obiekty powinny
implementować interfejs Addable. I użyć tego interfejsu jako typu podstawowego
opisującego kontener w uogólnionej metodzie fill.
Taką metodę jest wtedy napisać dość łatwo.
// Metoda fill
// Autor: Bruce Eckel
// + moje rozszerzenia na konstruktor z argumentem
import java.util.*;
import java.lang.reflect.*;
// Dzięki implementacji tego interfejsu
// kontenery (obiekty-zestawy danych) czynić będziemy "wypełnialnymi"
interface Addable<T> { void add(T t); }
class ContainerOp {
// Dodaje do zestawu (kontenera) cont
// obiekty typu T (lub dowolnych jego podtypów)
// tworzone za pomocą konstruktora z argumentem typu V
// wywoływanego dla kolejnych argumentów args
public static <T, V> void fill(Addable<T> cont,
Class<? extends T> klasa, // aby wstawiać podtypy!
V ... args )
{
for(V arg : args)
try {
Constructor cons = klasa.getDeclaredConstructor(arg.getClass());
cont.add((T)cons.newInstance(arg));
} catch(Exception e) {
throw new RuntimeException(e);
}
}
// j.w. tylko n egzemplarzy stworzonych konstruktorem bezparemetrowym
public static <T> void fill(Addable<T> cont,
Class<? extends T> klasa,
int n )
{
for (int i=0; i<n; i++)
try {
cont.add(klasa.newInstance());
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}
Ale kto implementuje Addable? Nie ma takiej klasy!
Wobec tego należy użyć adapterów.
Adapter tworzymy po to - by dostosować gotowy interfejs klasy do jakichś nowych wymagań, zmodyfikować go ad hoc
Adaptery możemy tworzyć przez kompozycję i przez dziedziczenie.
// Użycie adapterów
// Autor: Bruce Eckel
// + moje drobne modyfikacje
// Kto implementuje Addable. Nie ma takiej klasy!!!!
// są jedenak klasy, w których występuje metoda add
// Aby uczynić każdą podklasę klasy X (w ktorej występuje metoda add) "addable"
// nalezy użyć kompozycji: pole oznaczające zestaw jest klasy X
// implememntujemy metodę add jako polimorficzne wywołanie na rzecz X
class AddableCollection<E> implements Addable<E> {
private Collection<E> c;
public AddableCollection(Collection<E> c) {
this.c = c;
}
public void add(E item) { c.add(item); }
}
// Ale mogą być przypadki, gdzie jest wiele konkretnych typow-kontenerow
// dla których dodawanie jest wyrażane w różny sposób (i oczywiście nie implementują
// one interfejsu Addable)
class SomeContainer<T> implements Iterable<T> {
private ArrayList<T> storage = new ArrayList<T>();
public void addElement(T item) { storage.add(item); } // tutaj "addElement"
public Iterator<T> iterator() { return storage.iterator(); }
}
// Nie warto dla każdego takiego przypadku tworzyć (wyraźnie)
// oddzielnej klasy poprzez dziedziczenie czy też kompozycję
// B. Eckel proponuje "abstract composition adapter"
abstract class AddableAdapter<S, T> implements Addable<T> {
private S seq;
public AddableAdapter(S sequence) { seq = sequence; }
public S getSequence() { return seq; }
public abstract void add(T item);
}
class Zwierz {}
class Pies extends Zwierz {}
class Kot extends Zwierz {}
class Test {
public static void main(String[] args) {
// Adaptacja kolekcji:
List<String> words = new ArrayList<String>();
ContainerOp.fill(new AddableCollection<String>(words),
String.class,
"Ala", "ma", "kota"
);
for (String w : words) System.out.println(w);
System.out.println("----------------------");
List<Integer> ints = new ArrayList<Integer>();
ContainerOp.fill(new AddableCollectionAdapter<Integer>(ints),
Integer.class,
"1", "2", "33"
);
for (int i : ints) System.out.println(i);
System.out.println("----------------------");
// Użycie abstrakcyjnego adaptera
SomeContainer<Zwierz> con = new SomeContainer<Zwierz>();
// I tu uwaga: generics pięknie zadziałały na klasie wewn.
// Naprawdę łatwo jest ad hoc tworzyć konkretne adaptery
// (kompilator wie jakiego typu jest wynik getSequence()
// i bez castingu możemy wołać metody!
Addable<Zwierz> zwierzeta =
new AddableAdapter<SomeContainer<Zwierz>, Zwierz>(con) {
public void add(Zwierz item) { getSequence().addElement(item); }
};
ContainerOp.fill(zwierzeta, Pies.class, 7);
ContainerOp.fill(zwierzeta, Kot.class, 3);
for (Zwierz z : con) System.out.println(z);
}
}
Wynik
Ala
ma
kota
----------------------
1
2
33
----------------------
Pies@1cd2e5f
Pies@19f953d
Pies@1fee6fc
Pies@1eed786
Pies@187aeca
Pies@e48e1b
Pies@12dacd1
Kot@1ad086a
Kot@10385c1
Kot@42719c
Tu już mamy trochę użyteczności z generics, rzecz do zastosowania na różne okazje - nie tylko Addable.
Następny (i na razie ostatni) przykład wymyśliłem zastanawiając się nad sposobami
ułatwienia pisania klas JavaBeans, gdzie występują właściwości związane i
ograniczane (natchnienie znalazłem w ciekawym artykule "A Generic MVC Model
in Java" autorstwa Arjan Vermeij. gdzie rzecz dotyczyła uogólniania modeli,
przy okazji było trochę o właściwościach; w sumie też b. dobry przykład praktycznej
użyteczności generics - zob. artykuł w O'Reilly Network).
A jak by ten pomysł nieco rozwinąć i zastosować do JavaBeans?
Wydaje mi się, ze tu będzie dość wyraźnie widać ile roboty generics mogą
nam zaoszczędzić, przynajmniej w niektórych przypadkach.
Co było żmudne:
a) prowadzenie słuchaczy (Change i Veto support + metody add/remove)
b) settery (trochę kodu do napisania)
Zróbmy więc klasę abstrakcyjnego ziarna, która jest gotowa do obsługi słuchaczy.
abstract class AbstractBean implements Serializable {
protected PropertyChangeSupport chg = new PropertyChangeSupport(this);
protected VetoableChangeSupport veto = new VetoableChangeSupport(this);
public void addPropertyChangeListener(PropertyChangeListener pcl) {
chg.addPropertyChangeListener(pcl);
}
public void removePropertyChangeListener(PropertyChangeListener pcl) {
chg.addPropertyChangeListener(pcl);
}
//... i vetoable też
// no i coś jeszcze, bo na razie nie wiele zaoszczędzamy
}
Kluczowe jest definiowanie właściwości.
Wprowadzimy więc do klasy AbstractBean - jako wewnętrzną - klasę sparametryzowaną opisującą właściwości ograniczane typu T:
abstract class AbstractBean implements Serializable {
protected PropertyChangeSupport chg = new PropertyChangeSupport(this);
protected VetoableChangeSupport veto = new VetoableChangeSupport(this);
public void addPropertyChangeListener(PropertyChangeListener pcl) {
chg.addPropertyChangeListener(pcl);
}
public void removePropertyChangeListener(PropertyChangeListener pcl) {
chg.addPropertyChangeListener(pcl);
}
// pominąłem dodawanie i usuwanie vetoableChangeListenerów
// Opis właściwości ograniczanej
class BoundedProperty<T> {
private String name;
private T value;
public BoundedProperty(String name) {
this.name = name;
}
public T getValue() { return value; }
public void setValue(T newValue) {
T old = value;
value = newValue;
chg.firePropertyChange(name, old, value);
}
}
// ...
}
Ta klasa jest wewnętrzna, aby móc odwoływać się do changeSupport
i propagować zdarzenie zmiany właściwości po wszystkich przyłączonych
słuchaczach.
Okaże się, że dość łatwo będziemy mogli teraz tworzyć klasy JavaBean zawierające właściwości ograniczane. Np.
class TestBean extends AbstractBean {
private BoundedProperty<Integer> count =
new BoundedProperty<Integer>("count");
int getCount() { return count.getValue(); }
void setCount(int n) { count.setValue(n); }
// void setCount(String s) { count.setValue(s); } błąd w kompilacji (i o to chodziło)
}
Tutaj dodajemy - zgodnie z konwencją JavaBeans - metody ustalania i pobierania
właściwości (akcesory); przy tym kompilator nie pozwoli nam popełnić pomyłki w argumentach.
Gdybyśmy chcieli i mogli zrezygnować z tej konwencji to możemy jeszcze bardziej
sobie uprościć życie. Cała nasza klasa JavaBean, opisująca dwie właściwości
ograniczane może wyglądać tak:
class TestBean1 extends AbstractBean { // Przyjemniejsza wersja
public final BoundedProperty<Integer> count =
new BoundedProperty<Integer>("count");
public final BoundedProperty<Color> color =
new BoundedProperty<Color>("color");
}
a korzystanie z niej jest dość intuicyjne i przyjemne:
TestBean1 b1 = new TestBean1();
b1.addPropertyChangeListener(this);
b1.count.setValue(1333);
System.out.println(b1.count.getValue());
b1.color.setValue(RED);
Color color = b1.color.getValue(); // mamy typy i automatyczne konwersje
System.out.println(color);
// b1.color.setValue(11); <-- mamy sprawdzanie typów w kompilacji
Tu uwaga: możemy spokojnie zmienne oznaczające właściwości uczynić publicznymi,
bo jednocześnie deklarujemy je jako final (co oczywiście nie wyklucza zmian
właściwości, zmienne oznaczają referencje do obiektów klasy BoundedProperty,
opakowujących właściwości i udostępniających właściwy interfejs operowania
na nich).
Dalsze ułatwienia (a nawet rozszerzenia) możemy wprowadzić odnośnie do obsługi
zdarzeń zmian właściwości.
Normalnie JavaBeans nie daje możliwości powiązania
danego słuchacza wyłącznie z obsługą zmian konkretnej właściwości. Teraz
to zmienimy, dodatkowo ułatwiając pisanie tej obsługi, bo wiele szczegółów
zapisując raz w klasie BoundedProperty.
Zobaczmy nową wersję, w której (oczywiście dla specyficznych przypadków,
ale pozostawiając także dowolną ogólność) upraszczamy życie użytkownika przez
to, że
nie musi zajmować się PropertyChnnaageListenerami i ich przyłączaniem; sam
obiekt klasy BoundedProperty jest słuchaczem swoich zmian i deleguje obsługę
tych zmian do ustalonej przez użytkownika metody.
import java.beans.*;
import java.io.*;
import java.util.*;
import java.lang.reflect.*;
abstract class AbstractBean implements Serializable {
protected PropertyChangeSupport chg = new PropertyChangeSupport(this);
protected VetoableChangeSupport veto = new VetoableChangeSupport(this);
public void addPropertyChangeListener(PropertyChangeListener pcl) {
chg.addPropertyChangeListener(pcl);
}
public void removePropertyChangeListener(PropertyChangeListener pcl) {
chg.addPropertyChangeListener(pcl);
}
class BoundedProperty<T> implements PropertyChangeListener {
private String name;
private T value;
private Object chgHandlerObject;
private Method changeHandler;
public BoundedProperty(String name) {
this.name = name;
}
public T getValue() { return value; }
public void setValue(T newValue) {
T old = value;
value = newValue;
chg.firePropertyChange(name, old, value);
}
public void propertyChange(PropertyChangeEvent e) {
if (!e.getPropertyName().equals(name)) return;
if (changeHandler == null) return;
try {
changeHandler.invoke(chgHandlerObject);
} catch(Exception exc) {
exc.printStackTrace();
}
}
public void setChangeHandler(Object handl, String mname) {
try {
Method m = handl.getClass().getDeclaredMethod(mname);
chgHandlerObject = handl;
changeHandler = m;
chg.addPropertyChangeListener(this);
} catch(Exception exc) {
exc.printStackTrace();
return;
}
}
public void setChangeHandler(Object ohandler) {
try {
Method m = ohandler.getClass().getDeclaredMethod(name+"Change");
chgHandlerObject = ohandler;
changeHandler = m;
} catch(Exception exc) {
exc.printStackTrace();
return;
}
chg.addPropertyChangeListener(this);
}
public void removeChangeHandler() {
changeHandler = null;
chgHandlerObject = null;
chg.removePropertyChangeListener(this);
}
}
}
I bardzo łatwe wykorzystanie:
import java.beans.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import static java.awt.Color.*;
class TestBean extends AbstractBean {
public final BoundedProperty<String> text =
new BoundedProperty<String>("text");
public final BoundedProperty<Color> color =
new BoundedProperty<Color>("color");
}
public class ExtProps extends JFrame {
JButton b = new JButton();
TestBean bean = new TestBean();
public ExtProps() {
bean.text.setValue("Dalej");
bean.color.setValue(BLACK);
b.setText(bean.text.getValue());
b.setForeground(bean.color.getValue());
b.addActionListener( new ActionListener() {
public void actionPerformed(ActionEvent e) {
bean.text.setValue(bean.text.getValue() + "+");
bean.color.setValue(bean.color.getValue().brighter());
}
});
bean.color.setChangeHandler(this);
bean.text.setChangeHandler(this);
add(b);
setSize(200,200);
setVisible(true);
}
public void colorChange() {
b.setForeground(bean.color.getValue().brighter().brighter());
}
public void textChange() {
b.setText(bean.text.getValue());
}
public static void main(String[] args) {
new ExtProps();
}
}
Zobacz demo programu
9. Podsumowanie
W tym wykładzie zapoznaliśmy się z mechanizmem generics w Javie.
W szczególności, dowiedzieliśmy się:
- jak budować klasy (i metody) sparametryzowane,
- jakie są możliwości i ograniczenia generics,
- jak stosować refleksję i generics razem, aby uzyskiwać uogólniony kod,
- co to jest "raw type",
- na czym polega "types erasure",
- co to są "wildcards",
- co to są "ograniczenia parametrów typu",
- na czym polega kowariancja, kontrawariancja i biwariancja.
Można powiedzieć, że generics w Javie nie zawsze są łatwe w użyciu. A
jednocześnie brakuje im mocy uogólniania kodu, znanej choćby z
szablonów C++. Mimo to, są przypadki, w których generics
Javy świetnie się sprawdzają, ułatwiając programowanie, czyniąc je
bardziej bezpiecznym, a kody - bardziej czytelnymi.
10. Zadania
Zadanie 1
Stworzyć klasę sparanetryzowaną, której obiekty reprezentują tablice liczb dowolnego typu.
Dostarczyć w niej operacji:
- wczytywania tablic z System.in (skaner),
- sumowania liczb w tablicy i zwracania sumy (z dbałościę o właściwy typ).
Klasę przetestować dla tablic: Integer, Double, BigDecimal.
Zadanie 2
Zgeneryzować JavaBean zawierającą właściwości ograniczane (constrained).
Zadanie 3
Wymyślić ciekawe (= oszczędzające kodowania, czytelne, efektywne) zastosowanie generics Javy.