Programowanie uogólnione - klasy i metody generyczne

Programowanie uogólnione

Programowanie uogólnione często przydaje się gdy zechcesz napisać funkcjonalność działającą dla obiektów różnych klas. Wyobraź sobie sytuację, w której zechcesz napisać metodę przyjmującą listę elementów typu String, Long, Integer albo Double. Działanie tej metody ma się sprowadzać do posortowania malejąco zawartości listy. Oczywiście zawsze możesz napisać cztery osobne metody, po jednej dla każdego typu. Taki sposób programowania byłby bardzo niewydajny, ponadto musiałbyś tworzyć kolejną metodę za każdym razem gdy zechciałbyś obsługiwać kolejny typ.

Generyczność została wprowadzona w Javie 1.5

 

Klasy generyczne

Przyjrzyj się konstrukcji poniższej klasy:

 

public class Generyczna<A, B> {

private A a;

private B b;

public Generyczna(A a, B b) {

this.a = a;

this.b = b;

}

public void wypiszAiB(){

System.out.println("a="+a+" b="+b);

}

}

Mamy dwa pola typu A i B, konstruktor którzy przyjmuje dwie wartości typów pokrywających się z typami pól a następnie ustawiających wartość owych pól na podstawie wartości parametrów. W klasie znajduje się również metoda wypiszAiB której zadaniem jest wypisanie zawartości pól a i b.

Jakich typów są pola a i b? Typ ten jest nieznany do czasu gdy w owych polach umieścimy jakieś wartości. Obecność obu typów została zadeklarowana w nagłówku klasy:

 

public class Generyczna<A, B>

Jeśli chcemy takich generycznych typów użyć w odniesieniu do pól, musimy je właśnie w ten sposób wcześniej "obiecać". Typów tych może być oczywiście znacznie więcej, wystarczy dodawać je po kolejnych przecinkach.

Stworzymy teraz kawałek kodu testującego działanie naszej klasy generycznej. W innej klasie umieściłem taki oto kod:

 

Generyczna g = new Generyczna(1, "nietoperek");

g.wypiszAiB();

Jako parametry konstruktora podałem dwie wartości różnych typów. Od momentu inicjalizacji obiektu wiadomo już że typ A odnosi się liczb, typ B jest ciągiem tekstowym. Mimo braku jawnego zdeklarowania tego faktu kod działa i w efekcie na konsoli ukazał mi się wynik:

Metoda wypiszAiB wywołuje metodę toString obu obiektów podczas wypisywania. Ponieważ typami generycznymi nie mogą być typy proste (ewentualnie trzeba opakować typ prosty w jakiś typ obiektowy np int w Integer), a każdy obiekt pośrednio lub bezpośrednio dziedziczy po klasie Object, toteż każdy podany obiekt metodę toString będzie posiadał. Operacja arytmetyczna na obiektach a i b nie będzie możliwa nawet gdyby podać do metody dwie liczby, ponieważ na etapie kompilacji nie jest znany prawdziwy typ obiektów – a więc nie jesteśmy w stanie określić czy będą to akurat liczby (na których możnaby wykonać operację arytmetyczną).

Klasy przekazywanych do metody obiektów mogą być dowolne. Przykładowo stworzyłem osobną klasę :

 

public class Przykladowa {

String pole1;

String pole2;

public Przykladowa(String p1, String p2) {

pole1 = p1;

pole2 = p2;

}

public String toString() {

return "Przykladowa{" + "pole1=" + pole1 + ", pole2=" + pole2 + '}';

}

}

a następnie jej obiekt przekazałem do naszego "wypisywacza" :

 

Przykladowa p = new Przykladowa("hiszpańska", "inkwizycja");

Generyczna g = new Generyczna(p, "jakiś tekst");

g.wypiszAiB();

 

efekt:

Dla tego obiektu również została wywołana metoda toString.

 

Metody generyczne

Metody generyczne mogą przyjmować obiekty dowolnych klas. Metody generyczne nie muszą znajdować się w klasach generycznych, równie dobrze możemy umieścić je w zwykłej klasie. Za przykład posłuży nam metoda która przyjmuje listę obiektów dowolnej klasy, a następnie wypisuje ją na ekranie:

 

public class OperacjeNaListach {

public static <V> void wydrukujListe(List<V> lista) {

for(V v: lista){

System.out.println(v);

}

}

}

V jest tutaj niezdefiniowaną klasą. Przyjmując że V jest po prostu jakimś typem, możemy łatwo zorientować się co metoda wydrukujListe robi. Wszędzie gdzie użyłbym np klasy String (ale wtedy mógłbym iterować tylko po listach elementów klasy String) użyłem nazwy V określającą nie znany w momencie kompilacji typ. Zastanawiać może jedynie fragment:

 

public static <V>

Co to takiego? Jeżeli chcemy użyć typów generycznych w metodzie, musimy uprzedzić kompilator o tym fakcie – podobnie jak przy klasach generycznych. Podajemy tutaj nazwę V – taką jaką później używamy do odniesienia się do typu. Fragment <V> musi znaleźć się po ewentualnym słówku static, ale zawsze przed nazwą typu zwracanego bądź void.

Przyszedł czas na przetestowanie metody. W innej klasie Stworzyłem dwie listy dwóch różnych typów – jedna lista to lista elementów klasy String, druga elementów klasy Integer. Obie listy przekazałem do metody wydrukuj listę:

 

ArrayList<String> lista = new ArrayList<>();

lista.add("ala");

lista.add("ma");

lista.add("kota");

OperacjeNaListach.wydrukujListe(lista);

ArrayList<Integer> lista2 = new ArrayList<>();

lista2.add(1);

lista2.add(2);

lista2.add(3);

OperacjeNaListach.wydrukujListe(lista2);

 

Efekt działania:

Nasza generyczna metoda zadziałała dla obu list.

 

Klasy i metody generyczne a dziedziczenie i interfejsy

Utrudnijmy sobie nieco zadanie. Chciałbym przygotować metodę która przyjmie obiekty dowolnych klas dziedziczących po X lub implementujących interfejs Y i dla przyjętego obiektu wywoła jakąś metodę. Nie możemy zastosować poprzedniej notacji, próba wywołania metody (innej niż te które mamy dzięki dziedziczeniu po Object) jeśli typ nie jest w ogóle określony nie powiedzie się. Kompilator nie wie czy obiekt który mu podamy na pewno będzie posiadał taką metodę.

Tworzę klasę PojazdJezdzacy posiadającą metodę jedz:

 

public class PojazdJezdzacy {

public void jedz(){

System.out.println("BRUM BRUM");

}

}

Następnie tworzę inną klasę dziedziczącą po PojazdJezdzacy, a więc również posiadającą metodę jedz():

 

public class Samochod extends PojazdJezdzacy{

}

Dodaję teraz dodatkową klasę. Posiada ona metodę odpal(V v) ktora na rzecz przyjętego przez parametr obiektu wywoła metodę jedz(). Przekazany przez parametr obiekt nie może być obiektem jakiejkolwiek klasy, poniewaz musi posiadać metodę jedz(). Z tego powodu typ parametru został określony tym razem w taki sposób:

<V extends PojazdJezdzacy>

Oznaczający że przekazywany obiekt może być obiektem dowolnej klasy, byleby ta klasa dziedziczyła po klasie PojazdJezdzacy – bo tylko to zapewnia posiadanie metody jedz() przez obiekt.

 

public class Uruchamiacz {

public static <V extends PojazdJezdzacy> void odpal(V v) {

v.jedz();

}

}

Notacja bez "extends PojazdJezdzacy" spowodowałaby błąd kompilacji z omówionych przed momentem powodów.

Wystarczy teraz przetestować działanie:

 

Samochod s = new Samochod();

PojazdJezdzacy p = new PojazdJezdzacy();

Uruchamiacz.odpal(s);

Uruchamiacz.odpal(p);

Dla obu obiektów metoda zadziałała prawidłowo wykonując metodę jedz() na każdym z nich. Próba przekazania do metody obiektu klasy nie dziedziczącej po PojazdJezdzacy np:

 

String x = ":)";

Uruchamiacz.odpal(x);

 

Zakończy się niepowodzeniem:

W podobny sposób możemy zadeklarować typ elementów otrzymywanej przez parametr listy:

 

public <V extends PojazdJezdzacy> void odpalWszystkie(List<V> lista) {

for (V v : lista) {

v.jedz();

}

}

Istnieje też analogiczna metoda deklaracji typu – za pomocą tak zwanego symbolu wieloznacznego. Konstrucja z jego użyciem analogiczna do powyższej:

 

public void odpalWszystkie2(List<? extends PojazdJezdzacy> lista) {

for (int i =0;i<lista.size();i++) {

lista.get(i).jedz();

}

}

W tym jednak przypadku nie będziemy mogli posłużyć się taką pętlą jak wcześniej, ponieważ do zastosowania pętli for wymagane jest podanie typu. Próba zrobienia czegoś w stylu :

 

for(? x : lista){}

nie zadziała :)

 

Kod źródłowy do pobrania:

jsystems.pl/static/download/blog/java/GenericDev.zip

Ten artykuł jest elementem poniższych kursów: