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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
Lássunk egy példát!
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Példa használatra:
1 2 | |
A használatkor egy típus megadásakor használhatunk egy további paraméterezett típust. Pl:
1 | |
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 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 | |
A fenti compare meghívása a következőképpen történik:
1 2 3 | |
1 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
Ez egy teljesen jó megoldás, viszont talán kicsit nehezebb olvasni, mint a következőt:
1 2 3 | |
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 | |
Ha ugyanezt szeretnénk wildcard-al:
1 2 3 | |
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 | |
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 | |
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 | |
Feladatok¶
- Í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.
- Í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¶
- 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 | |
- 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 | |