Sissejuhatus geneerilistesse tüüpidesse
Sissejuhatus
Koodi kirjutades tuleb ette olukordi, kus kood hakkab korduma. See juhtub tavaliselt siis, kui sama loogikat on vaja rakendada erinevate andmetüüpide puhul. Näiteks võib konteiner hoida väärtust, järjend salvestada elemente või meetod leida maksimaalse väärtuse. Kõigil neil juhtudel on loogika sisuliselt sama, sõltumata sellest, kas tegemist on arvuliste tüüpide, sõnede või programmeerija loodud andmetüüpidega.
Naiivne lähenemine oleks kirjutada iga andmetüübi jaoks eraldi meetodid või klassid, kuid see viib koodi dubleerimiseni ja läheb vastuollu DRY (*Do not Repeat Yourself*) põhimõtetega.
Java pakub sellise probleemi lahendamiseks geneerilisi tüüpe (generics). Geneerilised tüübid võimaldavad kirjutada koodi, mis on tüübistamise osas paindlik, kuid samas ohutu. Nende abil saab defineerida klasse, liideseid ja meetodeid nii, et konkreetne tüüp antakse ette parameetrina ning selle määrab koodi väljakutsuja.
Probleem ilma geneeriliste tüüpideta
Oletame, et on vaja luua lihtne konteiner-tüüpi andmestruktuur, mis hoiab endas täpselt ühte väärtust.
Kuna iga klass Javas pärineb Object klassist, siis esmane mõte oleks see probleem lahendada järgnevalt:
public class Box {
private Object value;
public Box(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}
Väärtust hoitakse Object tüüpi muutujas.
Sellest tulenevalt on võimalik sinna salvestada ükskõik millist viitetüüpi väärtust:
Box box = new Box("hello");
Kuid loodud konteinerist väärtust küsima minnes peab läbi viima tüübiteisendust:
String text = box.getValue(); // Compile error
Object text = box.getValue(); // OK, but no access to String specific methods
String text = (String) box.getValue(); // OK
Sellel lähenemisel on kaks märkimisväärset probleemi.
Esmalt, informatsioon väärtuse tegeliku tüübi kohta läheb kaotsi, kuna kompilaator näeb seda kui Object tüübi väärtusena.
Kuna Object on üldine ülemklass, ei ole võimalik kasutada konkreetsele tüübile omaseid meetodeid ilma eelneva teisenduseta.
Tüübiteisendus küll lahendab selle probleemi, kuid sellega võtab programmeerija vastutuse kompilaatorilt enda peale.
Sisuliselt antakse märku, et "olen kindel, et siin peab see tüüp selline olema, ära palun teosta kontrolli selle peal".
Kompilaator ei saa seda väidet kontrollida enne programmi käivitamist, mille tulemusena miski ei takista konteinerisse String tüüpi väärtust panna ja hiljem välja võttes seda teisendada näiteks Integer tüüpi objektiks:
Box box = new Box("hello");
Integer number = (Integer) box.getValue(); // ClassCastException at runtime
Kompilaatori jaoks on selline kood korrektne, kuid programmi käivitamisel lõppeb see erindiga.
Lahendus: geneerilised tüübid
Geneerilised tüübid lahendavad selle probleemi, võimaldades määrata tüübi parameetrina:
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
Klassi nime järel olev <T> on tüübiparameeter.
See toimib kui kohatäitjana, mis asendatakse klassi kasutamisel tegeliku tüübiga.
Nüüd tuleb uue Box-i loomisel selgelt määrata, millist tüüpi väärtust see endas hoiab:
Box<String> box = new Box<>("hello");
String text = box.getValue(); // no cast needed — compiler knows it's a String
Lisaks on tagatud tüübiohutus ehk kui proovida anda vale tüüpi väärtus, tuvastab kompilaator selle veana:
Box<String> box = new Box<>(42); // compile error — Integer is not a String
Viga avastatakse juba kompileerimise käigus, enne kui programm üldse käivitub.
Mida geneerilised tüübid võimaldavad
Geneerilised tüübid pakuvad kolme olulist eelist:
Tüübiohutus. Kompilaator tagab, et kasutatakse ainult õiget tüüpi väärtusi.
Vead, mis muidu ilmneksid programmi käitusajal ning lõppeksid krahhina, avastatakse juba koodi kompileerimise käigus.
Puudub vajadus liigse tüübiteisenduse järele. Tüübiinformatsioon säilib, mistõttu saab tagastatud väärtust kasutada otse, ilma täiendava teisenduseta.
Taaskasutatavus. Üks ja sama klassi või meetodi definitsioon töötab mis tahes tüübi korral.
Ilma geneeriliste tüüpideta oleks vaja eraldi StringBox, IntegerBox, PersonBox jne.
Näide varasemalt läbitud teemadest
Tegelikult oled geneeriliste tüüpidega juba varasemalt kokku puutunud.
Täpsemalt kollektsioone või Optional-i kasutades.
List<String>, Map<String, Integer>, Optional<Person> - kõik need kasutavad geneerilisi tüüpe.
Teemantsulgudes olev tüüp annab kompilaatorile märku, et millist tüüpi elemente antud konteiner endas hoiab.
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
String first = names.get(0); // no cast — the list knows its element type
Ilma geneeriliste tüüpideta peaks ArrayList salvestama Object-tüüpi väärtusi ning iga get()-i väljakutse nõuaks käsitsi tüübiteisendust.
Paremal pool new ArrayList<>() olevat teemantsulge (<>) nimetatakse teemandoperaatoriks.
Java suudab tüüpi tuletada väärtuse omistamisel vasaku poole põhjal, mistõttu ei ole vaja seda uuesti välja kirjutada.
List<String> list = new ArrayList<String>(); // Works, but unnecessary
List<String> list = new ArrayList<>(); // Diamond operator can derive the type from the left side of declaration
Raw types
Enne geneeriliste tüüpide lisamist (geneerilised tüübid tulid keelde Java 5-es) ei olnud kollektsioonidel tüübiparameetreid. Vanemas koodis võib kohata järgmist stiili:
ArrayList list = new ArrayList(); // raw type — no type argument
list.add(1);
list.add("one"); // compiles — no type checking
Object o = list.get(0); // returns Object, must cast manually
Raw type ehk toortüüp on geneeriline klass, mida kasutatakse ilma tüübiparameetrit määramata. Kompilaator lubab seda tagasiühilduvuse eesmärgil, kuid annab unchecked-warning tüüpi hoiatuse, kuna tüübikindlus pole tagatud.
Toortüüpe peaks iga hinna eest vältima. Alati määra geneerilisi tüüpe kasutades ka tüübiparameeter:
ArrayList<Integer> list = new ArrayList<>(); // correct
Toortüübid eksisteerivad ainult selleks, et tagada ühilduvus koodiga, mis kirjutati enne Java 5-t.
List<Object> tähendab selgesõnaliselt, et loend hoiab suvalisi objekte, samas kui List (toortüüp) on geneeriline tüüp, millelt on kogu tüübiinformatsioon eemaldatud ning tüübikontroll on sisuliselt välja lülitatud.