Kihagyás

Generikus típusok

Az előadás videója elérhető a itt.

A generikusok megjelenése a nyelvben a Java 5 nyelvi bővítése. Ahogy láttuk már korábban, nagyon nagy szerepük lesz abban, hogy az objektumokat tároló kollekciókat és leképezéseket hatékonyan és biztonságosan tudjuk használni. Kicsit olyanok a Java generikusok, mint a C++-os template-ek. De csak kicsit, valójában számos technikai különbség van a kettő között, amikre most nem fogunk kitérni, célunk csak egy általános ismertetőt adni, hogyan is kell ezeket a generikus dolgokat létrehozni, kezelni.

Motiváló példa

A motiváló példa épp az előbb említett kollekciók használata lesz. A Java 5 előtti kollekciók legnagyobb hátránya az volt, hogy elfelejtették a típust, azaz bármilyen típusú elemet is tettünk bele, az Objectként volt eltárolva. Ahhoz, hogy az eredeti elemet visszakapjuk, az Objectet downcastolni kellett a megfelelő típusra. Ezzel szemben generikus kollekció osztályokat használva, adott kollekcióba csak meghatározott típusú elemek kerülhetnek be, és amikor azokat kivesszük a tárolóból, konvertálni már nem kell őket a megfelelő típusra:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import java.util.*;

class GenericsMotivacio {
  public static void main(String[] args) {
    // hagyományos
    List s1 = new LinkedList();
    s1.add(new Integer(0));
    // ...
    Integer i1 = (Integer)s1.iterator().next();
    System.out.println(i1);

    // generics
    List<Integer> s2 = new LinkedList<Integer>();
    s2.add(new Integer(0));
    // ...
    Integer i2 = s2.iterator().next();
    System.out.println(i2);
  }
}

Alap szintaxis

Típus paramétert osztályok, interface-ek és metódusok kaphatnak. Az alap szintaxis nagyon egyszerű, a típus paramétert a az osztály, interface neve után, vagy a metódus visszatérési értékének típusa előtt kell feltűntetni "kacsacsőrök" közé zárva:

  • interface InterfeszNev<T> {...}
  • class OsztalyNev<T> {...}
  • <T> T fuggvenyNev(T p);

Használat során a konkrét típust, amit adott osztálynak / interface-nek szeretnénk átadni, a deklarációkor szintén az típus neve után tett "kacsacsőrök" között adhatjuk meg. Amire figyelünk kell, hogy a generikus paraméter csak referencia típus lehet, primitív típus megadása fordítási hibához vezetne:

1
OsztalyNev<Long> i = new OsztalyNev<Long>(new Long(1));

Generikus metódus paraméterének típusát a hívás aktuális paramétere határozza meg:

1
String s = x.fuggvenyNev("valami");

Példák

Egy példán keresztül nézzük meg egy kicsit alaposabban is:

 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
class Generikus<E> {
  private E x;
  public Generikus(E x) {
    this.x = x;
  }
  public E getX() {
    return x;
  }
  public void setX(E x) {
    this.x = x;
  }
}
class GenMetodus {
  public String toString() {
    return "GenMetodus";
  }
  <T> T f(T p) {return p;}
}
class GenericsPelda {
  public static void main(String[] args) {
    //Generikus<long> a1 = new Generikus<long>(1);  // forditasi hiba
    Generikus<Long> a1 = new Generikus<Long>(new Long(1));
    Generikus<String> a2 = new Generikus<String>("egy");
    GenMetodus b = new GenMetodus();
    Generikus<GenMetodus> a3 = new Generikus<GenMetodus>(b);
    System.out.println(b.f("valami"));
    System.out.println(b.f(9));
  }

A Generikus osztálynak van egy E típus paramétere. Ez az E az osztályon belül bárhol, ahol típust kell megadni, szerepelhet. A példában a Generikus osztálynak van egy adattagja, aminek a típusa az E lesz. A konstruktor beállítja ezt az adattagot, így paraméterben egy szintén E típusú paramétert vár. Az adattaghoz lesz egy getter, aminek így a visszatérési típusa is az E típus lesz, illetve egy setter, aminek pedig a paramétere lesz E típusú. Amikor a GenericsPelda main metódusában példányosítjuk ezt a Generikus osztályt, akkor egyszer E helyére Long kerül, és a konstruktor hívás paramétere is egy egész szám lesz (22. sor), míg a másik példányosítás során String lesz (23. sor). Valamennyi helyen, ahol a Generikus osztályban az E szerepelt, az az első esetben Longra, a másodikban Stringre "cserélődik", és ennek megfelelően kell a Generikus osztály metódusait paraméterezni.

A példában a GenMetodus osztálynak az f metódusa lesz generikus. A generikus típus paramétert T jelöli. Ahogy látjuk, ez a T paraméter jelenik meg a metódus paraméterlistájában is. Amikor meghívjuk a mainben az f metódust (26. és 27. sorok), akkor az aktuális paraméterek fogják meghatározni, hogy adott kontextusban a T értéket mire kell cseréljük.

Generikusok fejlődése

A generikusok megjelenésével számos olyan újítás jelent meg a nyelvben, ami kihasználja a generikusok lehetőségeit.

A Java 5 újítása a for each szintaxis, amely lehetővé teszi a kollekciók egyszerűsített bejárását. Mivel a kollekciók már nem Object típusú elemeket tárolnak, hanem azon típusú elemeket, amivel az adott kollekciót példányosítottuk, így a for each ezen típusú elemeket adja vissza:

1
2
3
4
5
6
7
List<String> strings = new ArrayList<String>();

//... add String instances to the strings list...

for (String aString : strings) {
    System.out.println(aString);
}

A fenti lista deklarációnál láthatjuk, hogy mind a változó deklarálásánál meg kell adni, hogy a lista milyen típusú elemeket tárol, mind a konstruktor hívásnál, hogy az ArrayListet String típusokkal példányosítjuk. Tulajdonképpen ez egy kicsit redundáns, más típussal nem is lehetne ezt példányosítani. Így a Java 7-től a típus következtetés (type inference) bevezetésével a ArrayList konstruktor hívásnál már nem kell megadni a generikus típus paramétert, mivel a fordító kikövetkezteti a példányosított kollekció típusát a hozzárendelt változó típusából, elég a diamond operátort használni:

1
List<String> strings = new ArrayList<>();

Saját tároló osztály bejárása

Készítsünk egy saját MyStack osztályt, amiben a generikus típusnak megfelelő típusú elemeket tudunk tárolni. Ehhez a tárolóhoz definiáljunk egy MyIterator osztályt is, ami képes bejárni a tároló elemeinket, és visszaadni a tárolt elemeket.

 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
import java.lang.Iterable;
import java.util.Iterator;

class MyStack<E> implements Iterable<E> {
    class Link {
        E item;
        Link next;
        Link(E item, Link next) {this.item = item; this.next = next;}
    }
    Link top;

    void push(E item) {
        top = new Link(item, top);
    }
    public Iterator<E> iterator() {return new MyIterator<E>(top);}
}

class MyIterator<E> implements Iterator<E> {
    MyStack<E>.Link cur;

    MyIterator(MyStack<E>.Link top) {cur = top;}
    public boolean hasNext() {return cur != null;}
    public E next() {
        E item = cur.item;
        cur = cur.next;
        return item;
    }
}

public class GenericsForEach {
    public static void main(String[] args) {
        MyStack<Double> collection = new MyStack<>();
        collection.push(1.5);
        collection.push(2.5);
        collection.push(3.5);
        for(Double d : collection) {
            System.out.println(d);
        }
    }
}

Kimenet

3.5

2.5

1.5

A MyStack osztály implementálja az Iterable interface-t, ez teszi lehetővé majd, hogy a main metódusban majd a tároló elemeit a for each szerkezettel járjuk be. Bővebben: azzal, hogy az osztályunk implementálja ezt az interface-t, meg kell valósítsa a Iterator<E> iterator () metódust, amely visszaad egy olyan iterátort, amely ismeri az adott típus szerkezetét, és amely így be tudja járni a tárolót és egyesével vissza tudja adni annak elemeit az által, hogy az Iterator hasNext metódusa megválaszolja, hogy van e még olyan eleme a tárolónak, amit nem értintettünk, a next metódus pedig ezekből visszaad egyet. A MyStack generikus paramétere és a MyIterator paramétere összhangban van egymással, a konkrét iterátor, amit a MyStack példányosít, ugyanolyan típusú elemet ad vissza, amely típusú elemeket eltárolunk a vermünkben.

Típus paraméterek elnevezési konvenciói

Eddigi példáinkban a típus paraméterek neve vagy T, vagy E volt. A Java arra törekszik, hogy a kódban használt elnevezések is segítsék a kód érthetőségét. Ezekhez a konvenciókhoz igazodnak a típus paraméterek elnevezési lehetőségei is. Általában az E, az az element szóra utal, akkor használjuk, ha valaminek az elemeit, illetve azok típusát szeretnénk helyettesíteni. A T betű a szimpla típusokat helyettesítik, K betűt ott használunk, ahol egy kulcs érték típusát szeretnénk helyettesíteni, V, azaz value ennek az érték párja, N betű akkor kell, ha a helyettesített típusról elvárjuk, hogy szám legyen.

Raw type-ok

Látjuk, hogy a JAVA fejlődésével egy csomó típus (például a tároló típusok) különböző alakban vannak jelen. Ahhoz, hogy az újabb kódok kompatibilisek maradhassanak a régebbi kódokkal, a generikus osztály, vagy interfész nevét használhatjuk a típus argumentumok nélkül is. Ezek a raw type-ok, vagy nyers típusok a generikus osztályok típus paraméterek nélküli változatai lesznek, pont az az alak, ami a korábbi JAVA verziókban is elérhető volt.

1
2
3
4
5
public void set(T t) { /* ... */ }
// ...
}
Box<String> stringBox = new Box<>(); // paraméterezett típus
Box rawBox = new Box(); // raw type

Bounded type-ok

Néha szükség lehet, hogy a típus paraméterre valamilyen megszorítást tegyünk. Például felső korlátot adhatunk neki:

1
public class NaturalNumber<T extends Integer>

Azaz a NaturalNumber olyan osztály lesz, ahol a T típus az Integer osztály valamely specializációjával helyettesíthető.

Elképzelhető, hogy nem minden esetben akarjuk konkrétan rögzíteni, hogy milyen típus paraméterrel akarjuk használni adott esetben a generikus típusunkat. Például csak azért akarjuk a kollekciónkat bejárni, hogy az elemeinek meg tudjuk hívni a toString metódusát. Ha általánosak akarunk maradni, akkor használhatunk ilyen helyzetben úgynevezett wildcardot is, ami helyettesíti az ismeretlen típus paramétert:

1
2
3
4
5
void printCollection(Collection<?> c) {
    for (Object e: c) {
        System.out.println(e);
    }
}

Ilyenkor a ? helyére tetszőleges típus kerülhet (azaz ? az Object és valamennyi leszármazottja lehet)

Ha mégis van valami megszorításunk az ismeretlen típusparaméterre, akkor azt korlátozhatjuk is akár felülről, akár alulról.

Felülről korlátos wildcard:

1
public void process(List<? extends Foo> list)

minden olyan listára, ami vagy a Foo, vagy annak leszármazottaiból áll.

Alulról korlátos wildcard:

1
public void addNumbers(List<? super Foo> list)

minden olyan listára, ami vagy a Foo, vagy annak őseiből áll.


Utolsó frissítés: 2021-05-04 07:53:32