Kihagyás

Java generikusok

Motiváció

Nagyobb alkalmazások fejlesztésekor elkerülhetetlen, hogy hibákba ütközzünk. Hiába a gondos tervezés és a sok-sok tesztelés, valahogy úgyis útat találnak maguknak. Szerencsére néhány hibát egyszerűbb megtalálni, mint másokat. Ilyen hibák például a fordítási időben észlelhető hibák. A generikusok használatával próbálunk minél több futási idejű hibát fordítás közben detektálhatóvá, így könnyeben javíthatóvá tenni.

Bevezetés

A generikusok használatával típus paramétereket adhatunk meg osztályokhoz, interface-ekhez, illetve metódusokhoz. Típus paraméter...

Típus paraméter...

Típus paraméter...

Ez a kulcs mondat. Jól jegyezzük meg.

Az előny amelyet behoznak az, hogy erős típus ellenőrzésket tud végezni a fordító, így már ő tudja jelezni, ha a típusokkal valami nincs rendben. Ezen felül rengeteg castolástól szabadít meg minket, mely igencsak rontja a kód olvashatóságát.

Vegyünk egy egyszerű példát:

1
2
3
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);

Ez a jól ismert List, melyet remélem, hogy senki nem használ így... A listába beletesszük a "hello" szöveget. Amikor el akarjuk kérni a listából a 0. elemet, azaz az imént berakott String-et akkor át kell alakítanunk String-é, hiszen a List nem tud semmit arról, hogy milyen objektumokat szeretnénk belepakolni (egyszerűen Object-eket tárol).

Ugyanez a kód újraírva, immár generikusok használatával.

1
2
3
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); 

Mivel a List-nek megmondtuk a '<>' jelek között, hogy String-eket akarunk tárolni, ezért tudja, hogy amikor a 0. elemet kérem el akkor az egy String típusú objektum lesz. Továbbá a hozzáadáskor nem is engedi, hogy String helyett valami más elemet rakjak bele.

A generikusok használatával általános érvényű algoritmusokat is implementálhatunk, melyek különböző típusú elemekre egyaránt működnek, nem kell az algoritmust mindre megírni. Például: Collections.sort().

Generikus típusok

Egy generikus típus olyan osztály vagy interface, mely generikus típusparaméterrel rendelkezik.

Egyszerű doboz osztály példa. Először generikusok nélkül:

1
2
3
4
5
6
public class Box {
    private Object object;

    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}

A doboz bármilyen objektumot képes eltárolni. Ez menet közben hibákhoz vezethet, ha valamilyen feltételezésekkel elünk arról, hogy milyen típusú elem van éppen a dobozban.

Generikus osztályként:

1
2
3
4
5
6
public class Box<T> {
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

Az osztály definícióját lecseréltük a következőre: public class Box<T>. Itt jelenik meg először a T típus paraméter. Ezen a ponton mondjuk azt, hogy a Box osztály vár egy típus paramétert is. A T típus paraméter ezek után bárhol használható az osztályon belül. A fenti példában az Object minden előfordulását T-re cseréljük, azaz nem Object-eket fog tárolni az osztály, hanem olyan elemet melyet megadnak a Box konstruktorában.

A típus paramétereket általában egy nagy betűvel szoktuk jelölni. A T a type szóból jön, de ezen felül használatos például a K (Key), E (Element), V (Value), N (Number).

A fenti osztály használata igen egyszerű:

1
Box<Integer> integerBox = new Box<Integer>();

Ez deklarál egy integerBox nevű változót, mely ahogy neve mutatja, Integer elemet képes tárolni. A konstruktor hívásával le is példányosítun egy ilyen objektumot.

A 7-es Java verzióval megjelent a diamond operátor, melynek köszönhetően nem kell kiírni a típus paramétert a konstruktor hívásba, ha a típus paraméter a környezet alapján kikövetkeztethető. A fenti példányosítás pontosan egy ilyen helyzet, így használjuk a diamond operátort:

1
Box<Integer> integerBox = new Box<>();

Amennyiben több típus paramétert szeretnénk használni egy osztályban, interface-ben, akkor a típus paramétereket vesszővel válasszuk el egymástól. Általános megadási mód:

1
class name<T1, T2, ..., Tn> { /* ... */ }

Lássunk egy példát!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class Pair<K, V> {

    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey()   { return key; }
    public V getValue() { return value; }
}

Példa használatra:

1
2
    Pair<String, Integer> p1 = new Pair<>("Even", 8);
    Pair<Integer, Integer> p1 = new Pair<>(42, 999);

A használatkor egy típus megadásakor használhatunk egy további paraméterezett típust. Pl:

1
Pair<String, Box<Integer>> p1 = new Pair<>("key", new Box<Intger>());

Raw type

Amikor nem adunk meg egy generikusan megírt osztálynak típusparamétert, akkor úgynvezett raw type-ot kapunk eredményül. Pl. a Box típusparaméter nélkül:

1
Box b = new Box();
Ebben az esetben a Box Object-eket fog tárolni. Ez leginkább a 1.5-ös Java előtti Java verziókkal való kompatibilitás miatt van. Ha lehet akkor kerüljük ennek használatát!

Generikus metódusok

Hasonló, mint amikor egy osztályra adjuk meg a generikus paramétert, itt viszont csak a metóduson belül fog élni a típusparaméterünk. Használhatjuk statikus és nem statikus metódusokra is egyaránt. A típusparamétereket a visszatérési típus megadása előtt kell megadni kisebb, nagyobb jelek között:

1
2
3
4
5
6
public class Util {
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
}

A fenti compare meghívása a következőképpen történik:

1
2
3
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);
Azonban a p1, p2 paraméter típusából a fordító ki tudja következtetni a compare típusparamétereit, így ezeket el is hagyhatjuk:
1
boolean same = Util.compare(p1, p2);

Típus paraméter megszorítások (Bound Type Parameters)

Van amikor korlátozni akarjuk a típus paraméterek lehetséges értékeit. Például lehet hogy egy függvény csak számokon dolgozik, ezért le akarjuk korlátozni, hogy csak Number-ből származó oszályt adhassunk át. Ez egy felső korlátot határoz meg. Ennek megadásához az extends kulcsszót kell használnunk! Típus paraméter megszorításnál az extends általánosan használadndó class-ra és interface-re is (interface-re nem az implements-et kell használni).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Box<T> {

    private T t;          

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }

    public <U extends Number> void inspect(U u){
        System.out.println("T: " + t.getClass().getName());
        System.out.println("U: " + u.getClass().getName());
    }

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<Integer>();
        integerBox.set(new Integer(10));
        integerBox.inspect("some text"); // Hiba
    }
}

Az inspect egyszerűen kiírja a t és az u paraméter típusait, viszont megszorítjuk, hogy az u nem lehet csak a Number osztály leszármazott osztályának objektuma. Ez a jellegű megszorítás azért is lehet jó, mert ha tudjuk a korlátot, akkor tudjuk használni a felső korlát metódusait. A fenti példában használhatnánk az intValue() metódust például, hiszen azt minden Number típusú objektum tudja.

Egy típusparaméternek lehet több megszorítása is:

1
<T extends B1 & B2 & B3>

Ha a típus paraméter megszorításai között van osztály, akkor azt kell elsőnek megadni.

Példa típusmegszorításra

A feladatunk, hogy egy T típusú tömbben megszámoljuk, hogy hány olyan elem van, amelyik nagyobb a egy bizonyos megadott értéknél (szintén T típusú).

1
2
3
4
5
6
7
8
9
public static <T> int countIfGreater(T[] array, T threshold){
    int count = 0;
    for(T item : array){
        if(item > threshold){ // error
            count++;
        }
    }
    return count;
}

A fenti példában hibát kapunk az összehasonlításnál, mivel a > logikai operátor nincs értelmezve csak a primitív típusokra (int, double, float, ...). Emiatt valami mást kellene használnunk. Pontosan erre van kitalálva a Comparable interface, ami valahogy így néz ki:

1
2
3
public interface Comparable<T> {
    public int compareTo(T o);
}

A compareTo-nak az implementáló osztályokban -1-et kell visszadniuk, ha saját értéke kisebb, mint a paraméterül kapott, 0-t ha megegyezik azzal, és +1-et ha nagyobb annál. A Comparable interfészt a fenti példában tökéletesen felhasználhatjuk:

1
2
3
4
5
6
7
8
9
public static <T extends Comparable<T>> int countIfGreater(T[] array, T threshold){
    int count = 0;
    for(T item : array){
        if(item.compareTo(threshold) > 0){
            count++;
        }
    }
    return count;
}

Wildcards

Java generikus kódban a ?-et hívják wildcard-nak, ami egy ismeretlen típust reprezentál.

Felső korlátos wildcard

Ahhoz, hogy megértsük ezt a részt, elengedhetetlen a következőt tudni: Például a Number osztály gyereke az Integer, de a List<Number>-nek nem gyereke a List<Integer>. Ezt nagyon véssük az agyunkba. Tekintsük például a következő kódot:

1
2
3
4
5
6
7
8
public static void print(List<Number> list){
    list.forEach(item -> System.out.println( item ) );
}

public static void main(String[] args) {
    List<Integer> test = new ArrayList<>();
    print(test); // error
}

A print meghívása ebben az esetben hibát fog dobni. Ennek megoldására már láttunk egy lehetőséget, használjunk generikus metódust:

1
2
3
public static <T extends Number> void print(List<T> list){
    list.forEach(item -> System.out.println( item ) );
}

Ez egy teljesen jó megoldás, viszont talán kicsit nehezebb olvasni, mint a következőt:

1
2
3
public static void print(List<? extends Number> list){
    list.forEach(item -> System.out.println( item ) );
}

A fenti kódban a ? jelenti az ismeretlen típust. Itt maga a metódus nem lesz generikus, csak a paraméterre teszünk megkötéseket, hogy amit listát kapunk, annak az elemeinek bizony Number-nek vagy annak leszármazottainak kell lennie. Még egyszer felhívnám a figyelmet, hogy a List<Number>-nek nem leszármazottja a List<Integer>.

De akkor mikor használjak generikus metódust és mikor wildcardot?

Vegyük a következő problémát: át akarunk másolni egy listából elemeket egy másik listába és biztosítani szeretnénk, hogy a forrás és a cél lista típusa megegyezik, hogy közben megszorítást is szeretnénk tenni (mondjuk a lista elemei a Number-ből származnak). Ekkor a következőt írhatjuk típusparaméterek használatával:

1
2
3
public static <T extends Number> void copy(List<T> dest, List<T> src){
    ...
}

Ha ugyanezt szeretnénk wildcard-al:

1
2
3
public static void copy(List<? extends Number> dest, List<? extends Number> src){
    ...
}
Azonban ez hibás, hiszen átpasszolhatok dest-nek egy List<Integer>-t, mag src-nek egy List<Float>-ot. Az első esetben ezt nem tehetem meg.

Alapszabály: Ha a paraméterek típusai és vagy a visszatérés típusa között valamilyen függőség van, akkkor ebben az esetben csak a típusparaméteres (generikus metódus) megoldás használható.

További különbségek: - Ha egy paraméteres típusú argumentumunk van, akkor használjunk Wildcard-ot! (Persze lehetne generikus típus paramétert is használni) - A generikus típus paraméterek támogatják a többszörös megszorítás (<T extends B1 & B2 & B3>) - A wildcard-ok támogatják az alsó és felső típusmegszorításokat is, míg a generikus típus paraméterek csak a felső korlátot támogatják. Az alsó korlátról hamarosan olvashatunk többet is.

Megszorítás nélküli wildcard-ok

Ebben az esetben csak a ?-et adjuk meg, pl: List<?>. Ezt úgy hívjuk hogy ismeretlen típusú lista. Hasznos lehet, ha olyan metódust írunk, melyet meg lehet valósítani az Object osztály funkcióival. Például:

1
2
3
4
5
public static void printList(List<Object> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}

A fenti csak kiírná a lista elemeit, viszont megintcsak azért, amiért a List<Object> nem ősosztálya a List<Integer>-nek, ezért ezekre az elemekre nem fog működni, viszont a következő megvalósítás már lehetővé fogja ezt tenni:

1
2
3
4
5
public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + " ");
    System.out.println();
}

Tanulság: a List<Object> és a List<?> nem ugyanaz.

Alsó korlátos wildcard-ok

Hasonlóan a felső korlátos wildcardhoz (pl: <? extends Number>), használhatunk alsó korlátot is, pl: <? super Number>. Ebben az esetben a megadott típus csak Number és annak ősosztályai lehetnek.

Fontos: egy wildcard-ra nem tudunk alsó és felső korlátot is megadni egyszerre.

Példa:

1
2
3
4
5
public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

Feladatok

  1. Írjunk generikus osztályt egy verem (Stack) reprezentálására! Belül használhatunk akár List-et is. Legyen push és pop metódus illetve legyen egy print, ami a stackben található aktuális értékeket írja ki.
  2. Írjunk generikus metódust, mely megszámolja, hogy az adott collection-ben hány darab páros szám van, vagy hány palindróma van, stb.! Figyeljünk a megszorításokra! A kritériumot paraméterként lehessen megadni!

Megoldások

  1. feladat megoldása
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Stack<T>{
    private List<T> items = new ArrayList<>();

    public void push(T newItem){
        items.add(newItem);
    }

    public T pop(){
        if(items.size() > 0){
            return items.remove(items.size() - 1);
        }
        else{
            return null;
        }
    }

    public void print(){
        if(items.size() == 0){
            System.out.println("Stack is empty.");
            return;
        }

        for(int i = items.size() - 1; i >= 0; i--){
            System.out.println(items.get(i));
        }
    }
}

public class DemoApplication {

    public static void main(String[] args) {
        Stack<Integer> stack = new Stack<>();

        stack.push(10);
        stack.push(20);
        stack.push(30);

        stack.print();

        System.out.println(stack.pop() + " removed ");
        System.out.println(stack.pop() + " removed ");
        System.out.println(stack.pop() + " removed ");

        stack.print();

    }

}
  1. feladat megoldása
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.Predicate;

public class Main {

    public static <T> int countIf(Collection<T> collection, Predicate<T> pred){
        int count = 0;
        for(T item : collection){
            if(pred.test(item)){
                count++;
            }
        }

        return count;
    }

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);

        int result = countIf(list, e -> e % 2 == 0);

        System.out.println(result);
    }
}

Utolsó frissítés: 2020-03-19 16:02:02