A Java nyelvhez nem terveztek genericet, template-et (a C++-tól eltérően), mivel arra gyakorlatilag csak hatékonysági okokból van szükség. De ügyeltek arra, hogy szimulálni lehessen a generic-et. A változó típusokat az osztályhierarchia csúcsával helyettesíthetjük; megtehetjük, hogy az Object osztályt használjuk, ami az összes osztály közös őse, és azon a helyen emiatt bármely osztálytípus használható. A standard Java könyvtárakban végig ezt az módszert használták a generic reprezentálására, ide értve a vektorokat, a hash táblákat és az enumerációkat. Ahogy a JDK fejlődött, a generic egyre több szerepet kapott. A JDK 1.1-ben bevezettek egy observer mintát (design pattern; lásd a Gamma könyvet) ami a generikusságon alapszik ugyanúgy, mint a kollekció osztályok az 1.2-ben. Generic eszközök sok nyelvben léteznek (Ada, Eiffel, Modula3). Ha egy adott nyelvben nincsenek meg, a generic eszközök megtervezése ma már rutinmunka. A Java-ban már a Commumity Process része a generic hozzáadása a nyelvhez, és a Pizza az első megoldások között volt. A szerzők azóta a Generic Java-val (GJ) továbbléptek kifejezetten a generic irányába.
A fenti bekezdésben leírtak időközben érvényüket vesztették. A Sun Java implementációja 2004 óta, a J2SE 5.0 -val kezdve támogatja a genericeket. A Pizza utolsó verziója 2002-ben került kiadásra.Nézzük most bővebben, miért van szükség genericre:
A Pizza lehetővé teszi, hogy paraméterezett osztályokat és interfészeket írjunk. A paraméter bármilyen Java/Pizza típus lehet (Java osztályok, Pizza osztályok, beleértve a generikus Pizza osztályokat, felhasználói típusok, és elemi típusok (például az int). Van azonban néhány megkötés, ezekről lásd később (homogén átírás). Alprogramok is lehetnek a generic paraméterei, ezeket függvényparaméterekként lehet átadni. A generic formális típus paraméterére adhatunk megszorításokat (megkövetelhetjük, hogy a paraméter osztály bizonyos interfészt implementáljon). A példányosítás fordítási időben történik, és a fordító elvégzi a megfelelő típusellenőrzéseket. Nem azt csinálja, mint C++-ban, hogy előbb generálja a példánynak a kódját és utána ellenőriz. Így értelmes hibaüzeneteket kapunk.
Az osztály/interfész paramétere bármilyen Java/Pizza típus lehet. A típusparamétereket az osztály/interfész név után soroljuk fel < és > jelek között. Érdekesség: a Pizzában a < és > jelek határoló jelként viselkednek típusparaméter részben, így nyugodtan írhatjuk őket egybe, szemben a C++-al ahol a >>-t operátornak tekinti.
Nézzünk egy egyszerű példát egy olyan osztályra, ami egy meghatározott típusú elemet tárol. Az tárolt elem típusa az A típusparaméterben példányosításkor megadott típus lesz.
A generic átfordítása című fejezetben két módszert is láthatunk amivel szimulálni tudjuk Java-ban a paraméterezett osztályokat. Mennyivel szebb a Pizza megoldása.
A példányosítás úgy történik, hogy a típus neve után < és > jelek között felsoroljuk az aktuális típusparamétereket. Vegyük észre, hogy amikor allokálunk egy paraméterezett osztálybeli objektumot, akkor nem kell kiírni a típusparamétereket. Ezt a fordító a konstruktor argumentumainak típusából kideríti. Eltérően a Sun Java szabványba bevezetett genericektől, új objektumpéldány létrehozásakor a Pizza fordító hibát jelez, ha típus paramétert próbálunk megadni.
Lehetőségünk van polimorf osztályból örököltetni is:
Néha szükségünk van arra, hogy csak bizonyos feltételeket kielégítő típusokkal lehessen paraméterezni egy polimorf osztályt. Ez Pizza-ban úgy jelenik meg, hogy a típusparaméterre előírhatjuk, hogy milyen interfészt (sajnos csak egyet) implementáljon (ill. milyen osztálynak legyen a leszármazottja). A paraméter típusunkat a következő interfészhez kötjük:
Ezek után a halmazunk elemeit erre korlátozzuk:
Ez a deklaráció azt jelenti, hogy a Set típusparamétere csak olyan típus lehet, ami implementálja a Comparable interfészt. Ezután írhatunk például ilyesmit:
De ezt csak olyan osztállyal példányosíthatjuk, ami implementálja a Comparable-t. Ha nincs ilyen, akkor nekünk kell ilyet írni:
Mint láthattuk a ComparableString-ben van néhány ronda típusellenőrzés. Ezt elkerülhetnénk, ha tudnánk paraméterezni a Comparable interfészt. És valóban, a kötött változó előfordulhat a megszorító kifejezésben. A típusra vonatkozó megszorítás tehát rekurzív is lehet. Például:
Most a ComparableString osztályt definiálhatjuk az alábbi módon:
Az F-kötött polimorfizmust ritkán használják a gyakorlatban.
A típusváltozókat csak dinamikus függvényekhez, blokkokhoz és inicializálókhoz használhatjuk. Statikushoz azért nem, mert ott nincs aktuális példány. Néha szükséges azonban a polimorf statikus függvények használata is. Például ha olyan dinamikus függvényt szeretnénk, ami az aktuális példánytól függetlenül polimorf. Ezt úgy érhetjük el, hogy a függvény visszatérési típusa elé beírjuk a típusváltozókat:
A paraméteres típusokat kétféleképpen lehet implementálni:
Sajnos a homogén átfordításnál felmerültek bizonyos problémák. A generikus tömböknél gondot okozna, ha primitív típussal próbálnánk példányosítani. Wrapper osztályt kellene létrehozni ilyen esetekben, viszont ekkor a castolások válnak lehetetlenné konstans időben, ezért a Pizza tervezői elvetették ezt a lehetőséget. Tehát generikus tömböt nem lehet létrehozni.
Ez nem működik. Továbbá castolni se lehet parametrikus típusra. Azaz (Tree<long>)intTree-t nem lehet használni.
A Pizza tervezői eredetileg közvetlenül a JVM-re akartak fordítani, de mivel a JVM és a Java szorosan kapcsolódik egymáshoz, ezért úgy döntöttek, hogy inkább első lépésben Java kódra átfordítják a Pizza kódot, majd onnan tovább a JVM-re. Ezzel ugyan vesztettek a kód hatékonyságából, de sokat nyertek az érthetőség terén. Két módja is van az átfordításnak: a heterogén és a homogén. Heterogén átfordítás esetén minden típushoz (paraméterezett) külön kódot generál a fordító. Így nagy, de hatékony kódot kapunk. Homogén átfordítás esetén egyetlen kód felel az általános reprezentációért, éppen ezért kicsi, de kevésbé hatékony kód lesz az eredmény. A két módszer keverése lehetséges, így a programozó egyensúlyozni tud a méret és a hatékonyság között. A legjobb választás, ha a heterogén átfordítást választjuk a primitív típusokra (int, char, ...), és a homogént a referencia típusokra. Persze csak a fordító paraméterezésével lehet választani a két módszer között.
Alappélda: A Pair osztálynak van egy típusparamétere, az elem. Ez az osztály tartalmaz két elem típusú tagot, x és y. A konstruktora két paraméterével inicializálja őket. A swap() függvény megcseréli a két értéket. A példa végén található egy kis teszt programrészlet, amely példányosítja a Pair osztályt egy String és egy int típussal. Az eredmény:
Hello, world!
42
Most lássuk milyen kódot kapunk a két módszerrel:
A Pair osztály minden példánynak generálunk egy új osztályt. A Pair<String> és a Pair<int> példányosításból a Pair_String és a Pair_int osztályok keletkeznek. Valójában nem így generálódnak a példányosított nevek, de hasonlóan. Ha nem használunk $ jelet, akkor nem fordul elő névütközés saját osztály és egy példány között. Az új osztályok törzsében az elem típusváltozókat lecseréljük az aktuális paraméterre, vagyis String-re és int-re. A forráskódban a példányosításokat pedig az új osztálynevekre.
Ennél a módszernél csak egy osztályt hozunk létre egy parametrikus osztályhoz, ez fogja az összes példányosítást kezelni. Az egésznek a kulcsa az, hogy minden típus konvertálható Object-re és vissza. A típus paraméterek előfordulásait a generic osztály kódrészében lecseréljük Object-re és a törzsében minden paraméterezett típusú változót Object-re konvertálunk. A konkrét példányoknál meg a megfelelő típusra konvertálnunk vissza. Referencia típus esetén a konvertálás egy egyszerű cast. Primitív típus esetén egy kis trükköt kell alkalmazni. Ha például a v változó int típusú, akkor Object-re (Object)(new Integer(v))-vel lehet konvertálni, visszafele pedig ((Integer)o).intValue-val.
Kicsit módosítjuk az előző példánkat. Meg akarjuk találni egy pár elem közül a kisebbiket. Az Ord interfésznek van egy típus paramétere elem és definiál egy less műveletet. A Pair osztályunknak szintén van egy típusparamétere (elem), de itt meg van szorítva olyan típusokra amik implementálják az Ord<elem> interfészt. Mint látjuk ez egy F-megszorított polimorfizmus.
Láthatjuk a példában, hogy a Pair<OrdInt> és az Ord<OrdInt> példányosítás fordul elő, ezért létrehozunk nekik egy új osztályt Pair_OrdInt, illetve egy interfészt Ord_OrdInt. Az új példányokat hasonlóan generáljuk, mint a parametrikus polimorfizmus heterogén átfordításánál. Mint látjuk az Ord_OrdInt interfésznek semmi különleges szerepe nincs az új kódban, így azt eltávolíthatjuk a hivatkozásaival együtt.
Az Ord interfészben a megszorítatlan típusparaméter Object-re cserélődik. A Pair osztályban a megszorított típusparaméter a megszorításra cserélődik, Ord-ra. Az Ord interfészben a less függvény Object típusú argumentumot vár, míg az OrdInt osztályban OrdInt-et. Ezt úgy kerüljük el, hogy az Ord interfészben a less függvényt átnevezzük less_Ord-ra, az OrdInt osztályban pedig felvesszük a less_Ord függvényt.