Detaljna analiza Java generičkih tipova: Razumevanje principa i metoda primene generičkih tipova
„Erge, zašto su dizajnirani generički tipovi?“ Sanmej je otvoreno postavila pitanje.
„Sanmej, slušaj me polako.“ Rekao sam.
Java je dodala mehanizam generičkih tipova u verziji 1.5, navodno su stručnjaci potrošili oko 5 godina (zvuči vrlo teško). Sa generičkim tipovima, naročito pri korišćenju kolekcija, postaje standardnije.
Pogledaj sledeći jednostavan kod.
ArrayList<String> list = new ArrayList<String>();
list.add("Chenmo Wang Er");
String str = list.get(0);„Sanmej, možeš li zamisliti šta bi se radilo pre generičkih tipova?“
„Ne mogu da zamislim, Erge, ti reci.“
Možemo dizajnirati klasu Arraylist koristeći niz Object.
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, čuvamo i uzimamo podatke iz Arraylist.
Arraylist list = new Arraylist();
list.add("Chenmo Wang Er");
list.add(new Date());
String str = (String)list.get(0);„Sanmej, primetila li ova dva problema?“
- Arraylist može čuvati bilo koji tip podataka (i može čuvati stringove i mešati datume), zato što sve klase nasleđuju klasu Object.
- Pri uzimanju podataka iz Arraylist potrebna je prisilna konverzija tipa, zato što kompajler ne može potvrditi da uzimaš string ili datum.
„Mhm, tačno.“ Rekla je Sanmej.
U poređenju, možeš jasno osetiti prednost generičkih tipova: korišćenje tipa parametra rešilo je neizvesnost elemenata — u kolekciji sa parametrom tipa String nije dozvoljeno čuvati elemente drugih tipova, pri uzimanju podataka nije potrebna prisilna konverzija tipa.
Dizajnirajte generički tip
„Erge, kako da dizajniram generički tip?“
„Sanmej, ti početnik samo treba da zna da koristi generičke tipove, a ti ih želiš dizajnirati?! Ali, pošto želiš da znaš, brat ne može da odbije."
Prvo, redizajnirajmo klasu Arraylist prema standardu generičkih tipova.
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 sa jednom ili više tipova promenljivih. Klasa Arraylist uvodi tip promenljive E (Element, prvo slovo reči Element), okruženu uglastim zagradama <>, stavljenu iza imena klase.
Zatim možemo koristiti konkretne tipove (na primer stringove) za zamenu tipa promenljive za instancu generičke klase.
Arraylist<String> list = new Arraylist<String>();
list.add("Chenmo Wang San");
String str = list.get(0);Tip Date je takođe moguć.
Arraylist<Date> list = new Arraylist<Date>();
list.add(new Date());
Date date = list.get(0);Drugo, možemo definisati generičku metodu u negeneričkoj klasi (ili generičkoj klasi).
class Arraylist<E> {
public <T> T[] toArray(T[] a) {
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
}
}Međutim, iskreno, definicija generičke metode deluje malo nejasno. Evo slike (napomena: barem jedan od povratnog tipa metode i tipa parametra metoda mora biti generički tip).

Sada pozovimo 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);
}Generički ograničavači
Zatim, pričajmo o generičkim ograničavačima extends.
Pre objašnjenja ovog ograničavača, pretpostavimo da imamo tri klase, njihove definicije su sledeće.
class Wanglaoer {
public String toString() {
return "Wang Laoer";
}
}
class Wanger extends Wanglaoer{
public String toString() {
return "Wang Er";
}
}
class Wangxiaoer extends Wanger{
public String toString() {
return "Wang Xiaoer";
}
}Koristimo ograničavač extends da bismo ponovo dizajnirali klasu Arraylist.
class Arraylist<E extends Wanger> {
}Kada dodamo element Wanglaoer u Arraylist, kompajler će prijaviti grešku: Arraylist dozvoljava samo dodavanje objekata Wanger i njegovih potklassa Wangxiaoer, ne dozvoljava dodavanje njene nadklase Wanglaoer.
Arraylist<Wanger> list = new Arraylist<>(3);
list.add(new Wanger());
list.add(new Wanglaoer());
// Metoda add(Wanger) u tipu Arraylist<Wanger> nije primenjiva za argumente
// (Wanglaoer)
list.add(new Wangxiaoer());To znači, ograničavač extends može suziti opseg generičkog tipa.
Brisanje tipova
„Oh, razumem. Erge, čuo sam da virtuelna mašina nema generičke tipove?“
„Sanmej, dobro si se pripremila. Brat može sigurno odgovoriti, virtuelna mašina nema generičke tipove."
„Kako utvrditi da li virtuelna mašina ima generičke tipove?“ pita Sanmej.
„Samo dekompajliramo bajt kod generičke klase i vidimo!" Koristeći alat za dekompajliranje (koristio sam jad kada sam pisao ovaj članak, možeš koristiti i druge alate) da dekompajliramo class datoteku, rekao sam, „Sanmej, vidi.“
// Dekompajlirano sa Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Dekompajler opcije: packimports(3)
// Naziv izvorne datoteke: 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;
}Tip promenljive <E> je nestao, zamenjena je Object!
„S obzirom na to, ako generička klasa koristi ograničavač extends, šta će biti rezultat?“ Sanmej je postavila vrlo pametno pitanje.
Pogledaj 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 dekompajliranja:
// Dekompajlirano sa Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Dekompajler opcije: packimports(3)
// Naziv izvorne datoteke: Arraylist2.java
package com.cmower.java_demo.fanxing;
// Referencirane klase paketa 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, tip promenljive <E extends Wanger> je nestao, E je zamenjena sa Wanger“, rekao sam, „Kroz ova dva primera možemo videti da Java virtuelna mašina briše generički tip promenljive i zamenjuje ga ograničavajućim tipom (ako nema ograničenja, koristi Object)“
„Erge, da li brisanje tipova može prouzrokovati probleme?“ Sanmej je postavila još jedno vrlo visokoprofilno pitanje.
„Sanmej, zaista, brisanje tipova zaista može imati nekoliko problema.“ 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 plitkom nivou svesti, prirodno ćemo misliti da su Arraylist<String> list i Arraylist<Date> list dva različita tipa, zato što su String i Date različite klase.
Ali zbog brisanja tipova, gornji kod neće proći kompajliranje — kompajler će prijaviti grešku (ovo je upravo „problem“ koji izaziva brisanje tipova):
>Brisanje metode method(Arraylist<String>) je isto kao druga metoda u tipu
Cmower
>
>Brisanje metode method(Arraylist<Date>) je isto kao druga metoda u tipu
CmowerPribližno značenje je, tipovi parametara ove dve metode su isti nakon brisanja.
To znači, method(Arraylist<String> list) i method(Arraylist<Date> list) su metode istog tipa parametra, ne mogu postojati istovremeno. Tip promenljive String i Date će automatski nestati nakon brisanja, stvarni parametar metode method je Arraylist list.
Postoji izreka: „Sto puta čuti nije jednako jednom videti“, ali čak i ni videći nije nužno istinito — problem brisanja generičkih tipova može dobro ilustrovati ovaj stav.
Generički džokeri
„Oh, razumem. Erge, čuo sam da generički tipovi imaju i džokere?"
„Sanmej, odjednom mislim da si vrlo pogodna da budeš ljupka programerka! Tvoja priprema je zaista odlična, čak znaš za džokere!“
Džoker se koristi engleskim upitnikom (?) za predstavljanje. Pri kreiranju generičkog objekta možemo koristiti ključnu reč extends da ograničimo potklasu, možemo koristiti ključnu reč super da ograničimo nadklasu.
Pogledaj sledeći kod.
// Definišemo generičku klasu Arraylist<E>, E predstavlja tip elementa
class Arraylist<E> {
// Privatne članske promenljive, čuvaju niz elemenata i broj elemenata
private Object[] elementData;
private int size = 0;
// Konstruktor, prosleđuje početni kapacitet initialCapacity, kreira niz Object određenog kapaciteta
public Arraylist(int initialCapacity) {
this.elementData = new Object[initialCapacity];
}
// Dodaje element na kraj niza, vraća uspešnost dodavanja
public boolean add(E e) {
elementData[size++] = e;
return true;
}
// Vraća element sa navedenim indeksom
public E get(int index) {
return (E) elementData[index];
}
// Pronalazi indeks prvog pojavljivanja navedenog elementa, ako se ne pronađe 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;
}
// Određuje da li se navedeni element pojavljuje u nizu
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
// Pretvara elemente niza u string za izlaz
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 sa navedenim indeksom, vraća element pre menjanja
public E set(int index, E element) {
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
}Dodata metoda
indexOf(Object o), određuje poziciju elementa uArraylist. Napomena: parametar jeObject, a ne generičkiE.Dodata metoda
contains(Object o), određuje da li se element nalazi uArraylist. Napomena: parametar jeObject, a ne generičkiE.Dodata metoda
toString(), olakšava štampanjeArraylist.Dodata metoda
set(int index, E element), olakšava menjanje elemenataArraylist.
Zbog brisanja generičkih tipova, izjavu Arraylist<Wanger> list = new Arraylist<Wangxiaoer>(); nije moguće kompajlirati, iako je Wangxiaoer potklasa od Wanger. Ali ako zaista treba ovakva „nadgradnja“ veza, šta raditi? U tom trenutku treba da džokeri igraju ulogu.
Koristeći oblik džokera <? extends Wanger>, može se realizovati generička nadgradnja, pogledaj 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 list2 je Arraylist<? extends Wanger>, prevedeno, list2 je Arraylist čiji je tip Wanger i njegove potklase.
Pažnja, „ključno“ stiže! list2 ne dozvoljava dodavanje objekata Wanger ili Wangxiaoer kroz metodu add(E e), jedini izuzetak je null.
„Onda je čudno, ako ne dozvoljava čuvanje elemenata, šta je svrha Arraylist<? extends Wanger> list2?“ curious Sanmej pita.
Iako ne može dodati elemente u list2 kroz metodu add(E e), može 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()));Izjava Arraylist<? extends Wanger> list2 = list; dodeljuje vrednost list list2, u ovom trenutku list2 == list. Budući da list2 ne dozvoljava dodavanje drugih elemenata, u ovom trenutku je bezbedno — možemo spokojno izvršavati get(), indexOf() i contains() na list2. Razmisli, ako se može dodati element u list2, ove 3 metode postaju manje bezbedne, njihove vrednosti bi se mogle promeniti.
Koristeći oblik džokera <? super Wanger>, može se dodati element čiji je nadklasa Wanger u Arraylist, pogledaj primer.
Arraylist<? super Wanger> list3 = new Arraylist<>(4);
list3.add(new Wanger());
list3.add(new Wangxiaoer());
// Wanger w3 = list3.get(0);Treba primetiti da se ne mogu uzimati podaci iz list3 tipa Arraylist<? super Wanger>.
Zaključak
Dobro, Sanmej, napravićemo jednostavan zaključak o generičkim tipovima.
U Javi, generički tipovi su mehanizam jakog tipa, mogu proveriti sigurnost tipova u vreme kompajliranja, i poboljšati ponovnu upotrebljivost i čitljivost koda.
1) Parametrizacija tipova
Suština generičkih tipova je parametrizacija tipova, to jest, pri definisanju klase, interfejsa ili metode, može se koristiti jedan ili više tipova parametara za predstavljanje parametrizovanog tipa.
Na primer, može se definisati generička klasa 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> predstavlja tip parametra, može se koristiti T bilo gde u klasi gde je potreban tip. Korišćenjem generičkih tipova, možemo kreirati kutiju koja može čuvati bilo koji tip objekta.
Box<Integer> intBox = new Box<>(123);
Box<String> strBox = new Box<>("Hello, world!");Generički tipovi se vrlo široko primenjuju u stvarnom razvoju, na primer kontejnerske klase List, Set, Map itd. u framework-u kolekcija, kao i alatke klase Future, Callable itd. u okviru konkurentnosti koriste generičke tipove.
2) Brisanje tipova
U mehanizmu Java generičkih tipova postoje dva važna koncepta: brisanje tipova i džokeri.
Generički tipovi će obrisati generički tip pri kompajliranju, zameniti generički tip tipom Object. To je radi kompatibilnosti unazad, izbegavanje uticaja na postojeći Java kod.
Na primer, za sledeći kod:
List<Integer> intList = new ArrayList<>();
intList.add(123);
int value = intList.get(0);Pri kompajliranju, Java kompajler će zameniti generički tip List<Integer> sa List<Object>, povratni tip vrednosti get metode Integer zameniti sa Object, generisani bajt kod je ekvivalentan sledećem kodu:
List intList = new ArrayList();
intList.add(Integer.valueOf(123));
int value = (Integer) intList.get(0);Java generički tipovi deluju samo pri kompajliranju, pri izvršavanju ne čuvaju informacije o generičkom tipu.
3) Džokeri
Džoker se koristi za predstavljanje određenog nepoznatog tipa, na primer List<?> predstavlja List koji može čuvati bilo koji tip objekta, ali ne može dodavati elemente u njega. Džokeri se mogu koristiti za rešavanje situacije gde je tip neizvestan, na primer u parametrima ili povratnim vrednostima metode.
Korišćenje džokera može učiniti metodu više opštom, uz osiguranje sigurnosti tipova.
Na primer, definisimo generičku metodu:
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.print(obj + " ");
}
System.out.println();
}Ova metoda može prihvatati bilo koju List, na primer List<Integer>, List<String> itd.
Gornji ograničeni džoker
Generički tipovi takođe nude gornji ograničeni džoker <? extends T>, što znači da džoker može prihvatiti samo T ili potklasu od T. Korišćenje gornjeg ograničenog džokera može poboljšati sigurnost tipova programa.
Na primer, definisimo metodu koja prihvata samo List brojeva i njegovih potklassa:
public static void printNumberList(List<? extends Number> list) {
for (Number num : list) {
System.out.print(num + " ");
}
System.out.println();
}Ova metoda može prihvatiti List<Integer>, List<Double> itd.
Donji ograničeni džoker
Donji ograničeni džoker (Lower Bounded Wildcards) se deklariše ključnom rečju super, sintaktički oblik je <? super T>, gde T predstavlja parametar tipa. To znači da parametar tipa mora biti nadklasa određene klase (uključujući samu klasu).
Kada treba dodati elemente u generičku kolekciju, ako se koristi gornji ograničeni džoker, tip elemenata u kolekciji može biti ograničen, tako da se ne mogu dodati određeni tipovi elemenata. Ali, ako se koristi donji ograničeni džoker, može se dodati pottip navedenog tipa u kolekciju, osiguravajući integritet elemenata.
Na primer, pretpostavimo da postoji klasa Animal i dve potklase Dog i Cat. Sada imamo List<? super Dog> kolekciju, njen parametar tipa mora biti Dog ili nadklasa tipa Dog. Možemo dodati elemente tipa Dog u ovu kolekciju, i možemo dodati njene potklase. Ali, ne možemo dodati elemente tipa Cat, zato što Cat nije potklasa od Dog.
Ispod je primer korišćenja donjeg ograničenog džokera:
List<? super Dog> animals = new ArrayList<>();
// Može dodati elemente tipa Dog i njihove pottipove
animals.add(new Dog());
animals.add(new Bulldog());
// Ne može dodati elemente tipa Cat
animals.add(new Cat()); // Kompajliranje grešiTreba primetiti da, iako se pomoću donjeg ograničenog džokera mogu dodati određeni pottipovi elemenata, pri čitanju elemenata moguće je osigurati samo da su tipa Object, ne moguće je osigurati da su navedenog tipa ili njegovog nadtipa. Stoga, pri čitanju elemenata potrebna je konverzija tipa, kao što je prikazano:
List<? super Dog> animals = new ArrayList<>();
animals.add(new Dog());
// Pri čitanju elemenata potrebna je konverzija tipa
Object animal = animals.get(0);
Dog dog = (Dog) animal;Uopšte, mehanizam Java generičkih tipova je vrlo moćan mehanizam ograničenja tipova, može proveriti sigurnost tipova pri kompajliranju, i poboljšati ponovnu upotrebljivost i čitljivost koda. Ali, pri korišćenju generičkih tipova treba obratiti pažnju na probleme kao što su brisanje tipova i džokeri, kako bi se osigurala tačnost koda.
