ComiaTip Generics ja Collections


Collections ja Generics

Kirjoittanut J-P Julkaistu 2.2.2009

Generics-tyyppiparametrit tulivat mukaan Java-ohjelmointiin kielen versiossa 5.0. Mitä hyötyä näistä tyyppiparametreista oikein on ja miten niitä tulisi käyttää?

Tyyppiparametrien avulla voidaan kirjoittaa helposti tyyppiturvallista koodia, välttää tyyppimuunnoksia ja selkeyttää rajapintoja. Voimme rakentaa luokkia joiden attribuuttien tyyppi voidaan päättää myöhemmin, vasta silloin kun luokkaa käytetään. Tyyppiparametrin tunnistaa luokannimen perästä löytyvistä <> -merkeistä, joiden välissä on jokin luokka, esimerkiksi collectioneista tuttu List -rajapinta on nykyään tarkemmin sanottuna List<E>, eli Luokan E -instansseja sisältävä lista. Java 5.0:n myötä onkin suositeltavaa käyttää tyyppiparametrisoitua listan muotoa perinteisen raakamuodon sijasta.

Tyypitetyt listat

Javan versiossa 5.0 collections luokat (eli tutut List, HasMap, Set jne...) muokattiin siten, että kokoelman luokat käyttävät tyypiparametreja. Collections -luokat ovatkin yleisin tyyppiparametrien käyttömuoto. Oikein käytettynä tyyppiparametrit poistavat tarpeen käyttää tyyppimuunnoksia, jotka voivat aiheuttaa ajonaikaisen ClassCastException -poikkeuksen.

Tyyppiparametrien käyttö on selkeintä esittää yksinkertaisten esimerkkien avulla. Ilman tyyppiparametreja koodi jossa käytetään Contact- olioita sisältävää listaa voisi näyttää tältä:

//Lista sisältää vain Contact olioita private List contacts = new ArrayList(5); public void printContactNames() { for (int i=0;i<contacts.size();i++) System.out.println(((Contact)contacts. get(i)).getFullName()); }

Ongelma on, että kääntäjä ei varoita meitä jos yritämme sijoittaa listaan esimerkiksi String-olioita. Kääntäjä kun ei ymmärrä kommenttiemme merkitystä. Seuraava sijoitus olisi siis kääntäjän mielestä täysin oikein:

contacts.add(”Matti Meikäläinen”);

Tämän seurauksena printContactNames-metodia kutsuttaessa tapahtuisi ajonaikainen ClassCastException, ja varsinaista virheen syytä voisi olla hyvin hankala löytää mikäli koodia on paljon.

Tyyppiparametrien avulla muotoilemme listan määrittelyn ja printContactNames-metodin uuteen muotoon:

//Lista sisältää vain Contact olioita private List<Contact> contacts = new ArrayList<Contact>(5); public void printContactNames() { for (int i=0;i<contacts.size();i++) System.out.println(contacts. get(i).getFullName()); }

Lisäämme siis listan tyyppimäärittelyn ja konstruktorin perään sen luokan nimen, jonka tyyppisiä olioita haluamme listaan sijoittaa. Tämän seurauksena voimme poistaa printContactNames -metodista tyyppimuunnoksen, koska List<Contact> luokan metodin get(int) palautusarvon tyyppi on Contact. Samalla myös add-metodin parametrin tyyppi muuttuu Objectista Contactiksi, joten jos yritämme sijoittaa listaan merkkijonoa kääntäjä huomaa virheemme eikä ohjelmamme käänny. Virhe huomataan jo käännösvaiheessa ja näin vältymme helposti ajonaikaiselta odottamattomalta ClassCastException -poikkeukselta ja työläältä virheen jäljittämiseltä.

Tyypitetyt listat metodien parametreina

Tyyppiparametrien avulla meidän on helppo välttää tyyppimuunnoksista johtuvia ongelmia. Ominaisuuden kääntöpuolena saatamme ohjelmia tehdessämme törmätä kuitenkin odottamattomiin ongelmiin. Ajatellaanpa esimerkiksi seuraavaa koodia, jossa käytössämme on Contact-luokka ja sen alaluokka CorporateContact (CorporateContact extends Contact):

public static List<Contact> union(List<Contact> contacts, List<Contact> contacts2){ List<Contact> union = new ArrayList<Contact>(); union.addAll(contacts); union.addAll(contacts2); return union; } public static void main(String[] argh){ List<Contact> contacts = new ArrayList<Contact>(5); List<CorporateContact> corporates = new ArrayList<CorporateContact>(5); List<Contact> union = union(contacts,corporates); }

Yllätykseksemme huomaamme, että kääntäjä antaa meille virheilmoituksen main-metodin viimeisestä rivistä. Mitäs tämä nyt on? CorporateContact on Contact-luokan alaluokka, joten tottahan toki meidän pitäisi pystyä antamaan union-metodille parametrina lista, joka sisältää Contact-luokan alaluokkia. Valitettavasti asia ei ole näin yksinkertainen List<Contact> ei ole luokan List<CorporateContact> yläluokka vaikka näillä listan elementeillä perimyssuhde onkin.

Apuun tulee tyyppiparametrien jokeri määre. Muutamme union-metodia seuraavasti:

public static List<Contact> union(List<? extends Contact> contacts, List<? extends Contact> contacts2){ List<Contact> union = new ArrayList<Contact>(); ...

Määritelmä ? extends Contact tarkoittaa mitä tahansa luokan Contact aliluokkaa (tai Contact luokkaa itseään. Eli List<? extends Contact> on lista Contact-luokkia tai sen aliluokkia.

Tyyppiparametrijokeri voidaan rajoittaa myös toisinpäin: ? super CorporateContact, eli mikä tahansa CorporateContact-luokan yläluokka. Tämä määritelmä on hyödyllinen esimerkiksi metodeissa, joille annetaan parametrina lista johon sijoitetaan uusia arvoja.

Voimme esimerkiksi muokata union listan muotoon, jossa sille annetaan valmis lista parametrina, johon muiden parametrina annettujen listojen sisältö lisätään:

public static void union(List<? extends Contact> contacts, List<? extends Contact> contacts2, List<? super Contact> union){ union.addAll(contacts); union.addAll(contacts2); } public static void main(String[] argh){ List<Contact> contacts = new ArrayList<Contact>(5); List<CorporateContact> corporates = new ArrayList<CorporateContact>(5); List<Object> union = new ArrayList<Object>(); union(contacts,corporates,union); }

Generics ja collections sekä paljon muuta Comian Java-ohjelmointi -kurssilla.