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:
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:
Generikus metódus paraméterének típusát a hívás aktuális paramétere határozza meg:
Példák¶
Egy példán keresztül nézzük meg egy kicsit alaposabban is:
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:
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:
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.
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 érintettü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.
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:
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:
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:
minden olyan listára, ami vagy a Foo, vagy annak leszármazottaiból áll.
Alulról korlátos wildcard:
minden olyan listára, ami vagy a Foo, vagy annak őseiből áll.