Pisanje Java generika sopstvenim rukama — savladajte ih potpuno!
„Erge, zašto su uopšte dizajnirani generici?” upitala je Sanmei bez uvijanja.
„Sanmei, slušaj brata, polako ću ti objasniti”, rekao sam.
Java je u verziji 1.5 dodala mehanizam generika; kažu da su stručnjaci na tome proveli oko pet godina (zvuči prilično teško). Sa genericima, naročito korišćenje kolekcija postaje mnogo disciplinovanije.
Pogledajte sledeći jednostavan kod.
ArrayList<String> list = new ArrayList<String>();
list.add("Chenmo Wang Er");
String str = list.get(0);„Sanmei, možeš li da zamisliš kako se to radilo pre generika?”
„Ne mogu, neki ti kaži, Erge.”
Pa, možemo iskoristiti Object niz da dizajniramo klasu Arraylist.
class Arraylist {
private Object[] objs;
private int i = 0;
public void add(Object obj) {
objs[i++] = obj;
}
public Object get(int i) {
return objs[i];
}
}Zatim u Arraylist ubacujemo i čitamo podatke.
Arraylist list = new Arraylist();
list.add("Chenmo Wang Er");
list.add(new Date());
String str = (String)list.get(0);„Sanmei, da li primećuješ ova dva problema?”
- Arraylist može da čuva bilo koju vrstu podataka (i stringove i datume istovremeno), jer sve klase nasleđuju klasu Object.
- Pri čitanju iz Arraylist-a potrebna je eksplicitna konverzija tipa, jer kompajler ne zna da li čitate string ili datum.
„Da, da, tako je”, rekla je Sanmei.
Uporedite i odmah ćete osetiti prednost generika: korišćenje parametra tipa rešava neodređenost elemenata — u kolekciji čiji je parametar tipa String ne dozvoljava se ubacivanje elemenata drugog tipa, a pri čitanju podataka više nije potrebna eksplicitna konverzija tipa.
Dizajnirajmo sopstveni generik
„Erge, a kako se ono dizajnira generik?”
„Sanmei, ti si početnik, treba samo da znaš da koristiš generike, a već hoćeš i da ih dizajniraš?! Ali dobro, pošto želiš da razumeš, brat je ne stidi.”
Prvo, redizajnirajmo klasu Arraylist prema standardima generika.
class Arraylist<E> {
private Object[] elementData;
private int size = 0;
public Arraylist(int initialCapacity) {
this.elementData = new Object[initialCapacity];
}
public boolean add(E e) {
elementData[size++] = e;
return true;
}
E elementData(int index) {
return (E) elementData[index];
}
}Generička klasa je klasa koja ima jednu ili više promenljivih tipa. Klasa Arraylist uvodi promenljivu tipa E (Element, prvo slovo reči „element”), koja se navodi u uglastim zagradama <> iza imena klase.
Zatim možemo konkretan tip (na primer String) zameniti za promenljivu tipa kako bismo instancirali generičku klasu.
Arraylist<String> list = new Arraylist<String>();
list.add("Chenmo Wang San");
String str = list.get(0);I tip Date je moguć.
Arraylist<Date> list = new Arraylist<Date>();
list.add(new Date());
Date date = list.get(0);Dalje, možemo definisati i generičke metode unutar ne-generičke klase (ili generičke klase).
class Arraylist<E> {
public <T> T[] toArray(T[] a) {
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
}
}Iskreno, definicija generičke metode deluje pomalo nepregledno. Pogledajte sliku (napomena: potrebna je barem jedna oznaka tipa među povratnim tipom metode i tipovima parametara).

Sada da pozovemo tu generičku metodu.
Arraylist<String> list = new Arraylist<>(4);
list.add("Chen");
list.add("mo");
list.add("Wang");
list.add("Er");
String [] strs = new String [4];
strs = list.toArray(strs);
for (String str : strs) {
System.out.println(str);
}Ograničavajući kvalifikatori generika
Sada da progovorimo o kvalifikatoru extends za promenljive tipa.
Pre nego što ga objasnimo, pretpostavimo da imamo tri klase čije je međusobno nasleđivanje sledeće.
class Wanglaoer {
public String toString() {
return "Wang Lao'er";
}
}
class Wanger extends Wanglaoer{
public String toString() {
return "Wang Er";
}
}
class Wangxiaoer extends Wanger{
public String toString() {
return "Wang Xiao'er";
}
}Iskoristimo kvalifikator extends da redizajniramo klasu Arraylist.
class Arraylist<E extends Wanger> {
}Kada u Arraylist pokušamo da dodamo element tipa Wanglaoer, kompajler prijavljuje grešku: Arraylist dozvoljava samo Wanger i njegovu podklasu Wangxiaoer, a ne dozvoljava nadklasu Wanglaoer.
Arraylist<Wanger> list = new Arraylist<>(3);
list.add(new Wanger());
list.add(new Wanglaoer());
// The method add(Wanger) in the type Arraylist<Wanger> is not applicable for the arguments
// (Wanglaoer)
list.add(new Wangxiaoer());Drugim rečima, kvalifikator extends sužava opseg tipova za generik.
Brisanje tipova
„Aha, razumem”, klimnula je Sanmei zamišljeno. „Erge, čula sam da virtuelna mašina nema generike?”
„Sanmei, lepo si pripremila gradivo. Mogu ti potvrditi — virtuelna mašina zaista nema generike.”
„A kako se uopšte utvrdi da li virtuelna mašina ima generike?” pitala je Sanmei.
„Dovoljno je dekompilirati bajtkod generičke klase pa ćeš videti!” Upotrebio sam alat za dekompilaciju (dok sam pisao ovaj tekst koristio sam jad, a možeš i nekim drugim alatom), dekompilirao class fajl i rekao: „Sanmei, vidi.”
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: Arraylist.java
package com.cmower.java_demo.fanxing;
import java.util.Arrays;
class Arraylist
{
public Arraylist(int initialCapacity)
{
size = 0;
elementData = new Object[initialCapacity];
}
public boolean add(Object e)
{
elementData[size++] = e;
return true;
}
Object elementData(int index)
{
return elementData[index];
}
private Object elementData[];
private int size;
}Promenljiva tipa <E> je nestala, a na njenom mestu je Object!
„Ako je tako, šta onda bude kada generička klasa koristi kvalifikator extends?” Sanmejino pitanje je veoma domišljato.
Pogledajmo ovaj kod.
class Arraylist2<E extends Wanger> {
private Object[] elementData;
private int size = 0;
public Arraylist2(int initialCapacity) {
this.elementData = new Object[initialCapacity];
}
public boolean add(E e) {
elementData[size++] = e;
return true;
}
E elementData(int index) {
return (E) elementData[index];
}
}Rezultat dekompilacije je sledeći.
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: Arraylist2.java
package com.cmower.java_demo.fanxing;
// Referenced classes of package com.cmower.java_demo.fanxing:
// Wanger
class Arraylist2
{
public Arraylist2(int initialCapacity)
{
size = 0;
elementData = new Object[initialCapacity];
}
public boolean add(Wanger e)
{
elementData[size++] = e;
return true;
}
Wanger elementData(int index)
{
return (Wanger)elementData[index];
}
private Object elementData[];
private int size;
}„Vidiš, promenljiva tipa <E extends Wanger> je nestala, a E je zamenjeno sa Wanger”, rekao sam. „Iz ova dva primera se vidi da Java virtuelna mašina briše promenljive tipa generika i zamenjuje ih ograničavajućim tipom (ako nema ograničenja, koristi Object).”
„Erge, da li brisanje tipova stvara neke probleme?” Sanmei je postavila još jedno pametno pitanje.
„Sanmei, da znaš, brisanje tipova zaista može izazvati neke probleme”, rekao sam. „Pogledaj ovaj kod.”
public class Cmower {
public static void method(Arraylist<String> list) {
System.out.println("Arraylist<String> list");
}
public static void method(Arraylist<Date> list) {
System.out.println("Arraylist<Date> list");
}
}Na prvi pogled pomislićemo da su Arraylist<String> list i Arraylist<Date> list dva različita tipa, jer su String i Date različite klase.
Ali zbog brisanja tipova, gornji kod neće proći kompilaciju — kompajler će prijaviti grešku (upravo jedan od onih „problema” koje izaziva brisanje tipova):
>Erasure of method method(Arraylist<String>) is the same as another method in type
Cmower
>
>Erasure of method method(Arraylist<Date>) is the same as another method in type
CmowerU prevodu, tipovi parametara ove dve metode su nakon brisanja isti.
Drugim rečima, method(Arraylist<String> list) i method(Arraylist<Date> list) su metode sa istim tipom parametra, pa ne mogu postojati istovremeno. Promenljive tipa String i Date se nakon brisanja automatski gube, pa je stvarni parametar metode Arraylist list.
Postoji izreka: „Bolje jednom videti nego sto puta čuti”, ali čak i kada vidimo, to ne mora da znači da je istina — problem brisanja generika to dobro ilustruje.
Džokeri generika (wildcards)
„Aha, razumem. Erge, čula sam da generici imaju i džokere (wildcards)?”
„Sanmei, tako odjednom pomislim da si rođena za dragu programerku! Zaista si temeljno pripremila gradivo — čak i za džokere znaš!”
Džoker se označava znakom pitanja (?). Prilikom kreiranja generičkog objekta možemo ključnom rečju extends ograničiti podklase, a ključnom rečju super ograničiti nadklase.
Pogledajmo sledeći kod.
// Definišemo generičku klasu Arraylist<E>, gde E označava tip elementa
class Arraylist<E> {
// Privatne članice — niz za čuvanje elemenata i brojač elemenata
private Object[] elementData;
private int size = 0;
// Konstruktor: prima početni kapacitet initialCapacity i kreira Object niz zadate dužine
public Arraylist(int initialCapacity) {
this.elementData = new Object[initialCapacity];
}
// Dodaje element na kraj niza i vraća indikator uspeha
public boolean add(E e) {
elementData[size++] = e;
return true;
}
// Dohvata element na zadatom indeksu
public E get(int index) {
return (E) elementData[index];
}
// Traži prvo pojavljivanje zadatog elementa; ako ga nema, vraća -1
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
// Proverava da li se zadati element nalazi u nizu
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
// Pretvara elemente niza u tekstualni prikaz
public String toString() {
StringBuilder sb = new StringBuilder();
for (Object o : elementData) {
if (o != null) {
E e = (E)o;
sb.append(e.toString());
sb.append(',').append(' ');
}
}
return sb.toString();
}
// Vraća broj elemenata u nizu
public int size() {
return size;
}
// Menja element na zadatom indeksu i vraća prethodnu vrednost
public E set(int index, E element) {
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
}Dodata metoda
indexOf(Object o)koja određuje poziciju elementa uArraylist. Obratite pažnju da je parametar tipaObject, a ne generikE.Dodata metoda
contains(Object o)koja proverava da li se element nalazi uArraylist. Obratite pažnju da je parametar tipaObject, a ne generikE.Dodata metoda
toString()koja olakšava ispisArraylist-a.Dodata metoda
set(int index, E element)koja olakšava izmenu elemenataArraylist-a.
Zbog brisanja tipova, izjava poput Arraylist<Wanger> list = new Arraylist<Wangxiaoer>(); ne može proći kompilaciju, iako je Wangxiaoer podklasa klase Wanger. Ako nam zaista treba ovakav „uzlazni prelaz” (upcasting), šta onda? Tu na scenu stupaju džokeri.
Pomoću džokera oblika <? extends Wanger> može se ostvariti uzlazni prelaz generika — pogledajmo primer.
Arraylist<? extends Wanger> list2 = new Arraylist<>(4);
list2.add(null);
// list2.add(new Wanger());
// list2.add(new Wangxiaoer());
Wanger w2 = list2.get(0);
// Wangxiaoer w3 = list2.get(1);Tip promenljive list2 je Arraylist<? extends Wanger>, što u prevodu znači: list2 je Arraylist čiji je tip Wanger ili neka od njegovih podklasa.
I sada, „ključni” trenutak! list2 ne dozvoljava dodavanje objekata tipa Wanger ili Wangxiaoer preko metode add(E e) — jedini izuzetak je null.
„Onda je čudno — ako ne dozvoljava ubacivanje elemenata, čemu onda služi ovakav list2 tipa Arraylist<? extends Wanger>?” radoznalo je pitala Sanmei.
Iako elemente ne možemo dodavati u list2 metodom add(E e), možemo joj dodeliti vrednost.
Arraylist<Wanger> list = new Arraylist<>(4);
Wanger wanger = new Wanger();
list.add(wanger);
Wangxiaoer wangxiaoer = new Wangxiaoer();
list.add(wangxiaoer);
Arraylist<? extends Wanger> list2 = list;
Wanger w2 = list2.get(1);
System.out.println(w2);
System.out.println(list2.indexOf(wanger));
System.out.println(list2.contains(new Wangxiaoer()));Izraz Arraylist<? extends Wanger> list2 = list; dodeljuje vrednost promenljive list promenljivoj list2, pa tada važi list2 == list. Pošto list2 ne dozvoljava dodavanje drugih elemenata, u tom trenutku je sigurna — možemo slobodno zvati get(), indexOf() i contains(). Razmislite: kada bismo u list2 mogli dodavati elemente, ove tri metode bile bi nesigurne, jer bi se njihove vrednosti mogle promeniti.
Pomoću džokera oblika <? super Wanger> možemo u Arraylist ubacivati elemente čija je nadklasa Wanger — pogledajmo primer.
Arraylist<? super Wanger> list3 = new Arraylist<>(4);
list3.add(new Wanger());
list3.add(new Wangxiaoer());
// Wanger w3 = list3.get(0);Treba imati na umu da iz list3 tipa Arraylist<? super Wanger> ne možemo čitati podatke.
Kratak pregled
Dobro, Sanmei, da još jednom ukratko sumiramo generike.
U Javi su generici mehanizam snažnog ograničavanja tipova koji proverava bezbednost tipova u vreme kompilacije i poboljšava ponovnu iskoristivost i čitljivost koda.
1) Parametrizacija tipova
Suština generika je parametrizacija tipova — prilikom definisanja klase, interfejsa ili metode možemo koristiti jedan ili više parametara tipa da označimo parametrizovan tip.
Na primer, generičku klasu možemo definisati ovako.
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}U ovom primeru <T> označava parametar tipa; svuda u klasi gde je potreban tip možemo koristiti T umesto konkretnog tipa. Pomoću generika možemo kreirati kutiju koja čuva objekte bilo kog tipa.
Box<Integer> intBox = new Box<>(123);
Box<String> strBox = new Box<>("Hello, world!");Generici su u praksi veoma rasprostranjeni — kontejnerske klase poput List, Set i Map u okviru kolekcija, kao i klase poput Future i Callable u okviru za konkurentno programiranje, sve koriste generike.
2) Brisanje tipova
U Java mehanizmu generika postoje dva važna pojma: brisanje tipova i džokeri.
Prilikom kompilacije generici se brišu, tako da se generički tip zamenjuje tipom Object. To je učinjeno radi povratne kompatibilnosti, kako se ne bi ugrozio postojeći Java kod.
Na primer, za sledeći kod:
List<Integer> intList = new ArrayList<>();
intList.add(123);
int value = intList.get(0);prilikom kompilacije Java kompajler zamenjuje generički tip List<Integer> sa List<Object>, a povratni tip metode get, Integer, sa Object, čime generisani bajtkod postaje ekvivalentan sledećem kodu:
List intList = new ArrayList();
intList.add(Integer.valueOf(123));
int value = (Integer) intList.get(0);Java generici deluju samo u vreme kompilacije; u vreme izvršavanja informacije o generičkim tipovima se ne čuvaju.
3) Džokeri
Džokeri služe da označe nepoznat tip — na primer List<?> označava List koji može čuvati objekte bilo kog tipa, ali nad njegovim elementima se ne mogu vršiti operacije dodavanja. Džokeri rešavaju situacije sa neodređenim tipom, pa se često koriste u parametrima i povratnim vrednostima metoda.
Korišćenjem džokera metode postaju opštije, uz zadržavanje bezbednosti tipova.
Na primer, definišimo generičku metodu:
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.print(obj + " ");
}
System.out.println();
}Ova metoda može primiti List bilo kog tipa, na primer List<Integer>, List<String> itd.
Gornje ograničenje (upper bounded wildcards)
Generici nude i gornje ograničenje <? extends T>, što znači da džoker prihvata samo tip T ili njegove podklase. Korišćenjem gornjeg ograničenja povećava se bezbednost tipova.
Na primer, definišimo metodu koja prihvata samo List tipa Number i njegovih podklasa:
public static void printNumberList(List<? extends Number> list) {
for (Number num : list) {
System.out.print(num + " ");
}
System.out.println();
}Ova metoda može primiti List<Integer>, List<Double> itd.
Donje ograničenje (lower bounded wildcards)
Donje ograničenje (Lower Bounded Wildcards) deklariše se ključnom rečju super, u obliku <? super T>, gde T označava parametar tipa. Ono znači da parametar tipa mora biti nadklasa (uključujući i samu klasu) neke zadate klase.
Kada u generičku kolekciju dodajemo elemente, upotreba gornjeg ograničenja može ograničiti tip elemenata, zbog čega neke tipove nije moguće dodati. Ako pak koristimo donje ograničenje, podtipovi zadatog tipa mogu se dodavati u kolekciju, čime se obezbeđuje potpunost elemenata.
Na primer, pretpostavimo da imamo klasu Animal i dve podklase Dog i Cat. Ako imamo kolekciju List<? super Dog>, njen parametar tipa mora biti Dog ili neki njen nadtip. U nju možemo dodavati elemente tipa Dog, kao i njegove podtipove, ali ne možemo dodavati elemente tipa Cat, jer Cat nije podtip od Dog.
Evo primera korišćenja donjeg ograničenja:
List<? super Dog> animals = new ArrayList<>();
// Mogu se dodavati elementi tipa Dog i njegovi podtipovi
animals.add(new Dog());
animals.add(new Bulldog());
// Ne može se dodavati element tipa Cat
animals.add(new Cat()); // Greška pri kompilacijiTreba imati na umu da, iako se donjim ograničenjem mogu dodavati neki podtipovi, pri čitanju elemenata možemo biti sigurni samo da su tipa Object — ne možemo garantovati da su zadati tip ili njegov nadtip. Zato je pri čitanju potrebna konverzija tipa, kao u primeru:
List<? super Dog> animals = new ArrayList<>();
animals.add(new Dog());
// Pri čitanju elementa potrebna je konverzija tipa
Object animal = animals.get(0);
Dog dog = (Dog) animal;Ukratko, Java mehanizam generika je veoma moćan mehanizam za ograničavanje tipova: proverava bezbednost tipova u vreme kompilacije i poboljšava ponovnu iskoristivost i čitljivost koda. Ipak, pri korišćenju generika treba obratiti pažnju na pitanja poput brisanja tipova i džokera, kako bi kod bio ispravan.
