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 |
|