Ohjelmoinnin peruskurssien laaja oppimäärä Luento 16: Generics Javassa ja Scalassa Riku Saikkonen (merkityt ei-laajan kurssin kalvot: Otto Seppälä) 16. 2. 2011
Sisältö 1 Generics Javassa ja Scalassa 2 Generics-yksityiskohtia 3 Taustaa geneerisistä tyypeistä
Generics Java-kielen versiossa 5 kieleen lisättiin tuki geneerisille tyypeille Mahdollistavat luokan tai metodin sisältämien muuttujien ja paluuarvojen tyypin määrittämisen tapauskohtaisesti käyttämällä tyyppiparametreja Vähentävät tarvetta suorittaa tyyppipakotusta luokan ulkopuolella Esim Javan version 4 ArrayList säilöi vain Object-viittauksia jotka piti tyyppipakottaa rakenteesta otettaessa Vähentävät samalla ajonaikaisia tyyppipakotusvirheitä (ClassCastException) koska generics:it hoidetaan jo käännösvaiheessa 16:54
Generics Allaoleva rivi luo ArrayList:in joka säilöö String-olioita ArrayList<String> sanalista ArrayList<String>(); Tässä String on tyyppiparametrille annettava arvo Määritelmästä johtuen sanalistan add-metodi hyväksyy vain Stringluokan olioita parametrikseen aliluokan tai alirajapinnankin oliot kelpaavat. String:illä vaan ei ole aliluokkia eikä sitä voi periä koska luokka on final. ArrayList<String> ArrayList<String> sanalista sanalista ArrayList<String>(); ArrayList<String>(); sanalista.add( Hei sanalista.add( Hei vaan! ); vaan! ); String String sana sana sanalista.get(0); sanalista.get(0); 16:54
Geneerinen Luokka Seuraava pätee sekä luokille, että rajapinnoille,mutta yksinkertaisuuden vuoksi puhumme seuraavassa vain luokista Geneerisessä luokassa on aina yksi tai useampi tyyppiparametri Tyyppiparametrille annetaan jokin muuttujanimi, joka korvataan käännösaikaisesti oikeilla tyypeilla (esimerkissämme tämän tyyppimuuttujan nimi on OmaTyyppi) Luokkamäärittelyn yhteydessä tyyppiparametri esitellään < ja >- merkkien välissä heti luokan nimen jälkeen Muuuttujan kautta käytettävää tyyppiä voi tämän jälkeen käyttää luokassa esim. muuttujien ja paluuarvojen tyyppinä class class OmaLista<OmaTyyppi> OmaLista<OmaTyyppi> OmaTyyppi OmaTyyppi vaihdaalkio(omatyyppi vaihdaalkio(omatyyppi uusialkio) uusialkio)...... jne. jne. 16:54
Esimerkki Luokka jolla voi tallentaa millaisen tahansa olioparin. class class Pair Pair <FirstType, <FirstType, SecondType> SecondType> Pair(FirstType Pair(FirstType first, first, SecondType SecondType second) second) this.first this.first first; first; this.second this.second second; second; FirstType FirstType getfirst() getfirst() return return this.first; this.first; SecondType SecondType getsecond() getsecond() return return this.second; this.second; 16:54
Sama Scalalla edellisten kalvojen asiat toimivat samalla lailla Scalassakin syntaksiero: Javassa <>, Scalassa [ ] Aiempi ArrayList-esimerkki Scalalla import scala.collection.mutable.arraybuffer val sanalista ArrayBuffer[String] // tai: val sanalista: ArrayBuffer[String] ArrayBuffer sanalista + "Hei vaan!" sanalista ++ List("foo", "bar") val sana sanalista(0) // tai funktionaalisesti (konstruktorissa ei :tä eikä tarvita tyyppiä): val s1 Vector() // tai Vector[String]() tai List() val s2 s1 :+ "Hei vaan" val s3 s2 ++ List("foo", "bar") val sana s3(0) Aiempi Pair-esimerkki Scalalla class Pair[A, B](first: A, second: B) def getfirst(): A first // (Scala tekee vastaavat metodit def getsecond(): B second // automaattisestikin) Pair(1, "foo").getsecond() "foo"
Ongelma... import import java.util.arraylist; java.util.arraylist; class class PersonnelGroup PersonnelGroup <JobType> <JobType> private private ArrayList<JobType> ArrayList<JobType> people; people; PersonnelGroup() PersonnelGroup() this.people this.people ArrayList<JobType>(); ArrayList<JobType>(); addperson( addperson( JobType JobType Employee Employee ) ) people.add( people.add( Employee Employee ); ); printallnames() printallnames() for for (JobType (JobType person person : : people) people) System.out.println( System.out.println( person.getname() person.getname() ); ); 16:54
Ongelma... import import java.util.arraylist; java.util.arraylist; class class PersonnelGroup PersonnelGroup <JobType> <JobType> private private ArrayList<JobType> ArrayList<JobType> people; people; PersonnelGroup() PersonnelGroup() this.people this.people ArrayList<JobType>(); ArrayList<JobType>(); addperson( addperson( JobType JobType Employee Employee ) ) people.add( people.add( Employee Employee ); ); Ei toimi. Mistä tiedettäisiin että printallnames() printallnames() JobTypellä on metodi for for (JobType (JobType person person : : people) people) getname? System.out.println( System.out.println( person.getname() person.getname() ); ); 16:54
Geneerinen Luokka Tyyppiparametrille voi myös määrätä vaatimuksia <Tyyppi extends Tulostettava> Tyyppiparametriksi Tyyppi saa antaa vain : rajapinnan Tulostettava täyttäviä luokkia tai luokan Tulostettava aliluokkia <Tyyppi extends Tulostettava & Tallennettava> Tyyppiparametriksi Tyyppi saa antaa vain molemmat ehdot täyttäviä tyyppejä Tämä mahdollistaa rajoitteen määrittämien metodien kutsumisen Tyyppiparametrin tyyppisille olioille 16:54
import import java.util.arraylist; java.util.arraylist; class class PersonnelGroup PersonnelGroup <JobType <JobType extends extends BasicEmployee> BasicEmployee> private private ArrayList<JobType> ArrayList<JobType> people; people; PersonnelGroup() PersonnelGroup() this.people this.people ArrayList<JobType>(); ArrayList<JobType>(); printallnames() printallnames() for for (JobType (JobType person person : : people) people) System.out.println( System.out.println( person.getname() person.getname() ); ); addperson( addperson( JobType JobType Employee class BasicEmployee Employee ) ) class BasicEmployee private String name; people.add( people.add( Employee Employee ); private String name; ); BasicEmployee(String name) BasicEmployee(String name) this.name name; this.name name; String getname() String getname() return this.name; return this.name; 16:54
Geneerinen metodi Myös metodimäärittely voi olla geneerinen (riippumatta luokan geneerisyydestä) Tyyppiparametri esitellään puumerkissä ennen paluuarvon tyyppiä static <Tyyppi> ArrayList<Tyyppi> jokatoinen( Tyyppi[] taulu ) static <Tyyppi> ArrayList<Tyyppi> jokatoinen( Tyyppi[] taulu ) ArrayList<Tyyppi> uusitaulu ArrayList<Tyyppi>(); ArrayList<Tyyppi> uusitaulu ArrayList<Tyyppi>(); for (int i 0; i < taulu.length / 2; i++) for (int i 0; i < taulu.length / 2; i++) uusitaulu.add(taulu[i * 2]); uusitaulu.add(taulu[i * 2]); return uusitaulu; return uusitaulu; Metodia kutsuessa tyyppiparametria ei kuitenkaan erikseen anneta (kuten geneeristen luokkien kanssa) vaan se päätellään metodin parametrien tyypeistä 16:54 String[] taulukko Aa, Bb, Cc, Dd, Ee ; String[] taulukko Aa, Bb, Cc, Dd, Ee ; ArrayList<String> lista jokatoinen(taulukko); ArrayList<String> lista jokatoinen(taulukko);
Sama Scalalla Scalassakin tyyppiparametreja voi rajoittaa syntaksi on [A <: B] eikä <A extends B> myös geneeriset metodit toimivat Scalassa samaan tapaan kuin Javassa: tyyppiparametrit kerrotaan ennen parametrilistaa Aiempi jokatoinen-esimerkki Scalalla import scala.collection.mutable.arraybuffer def jokatoinen[a](taulu: Array[A]): ArrayBuffer[A] val uusi ArrayBuffer[A] for (i <- Range(0, taulu.length/2)) uusi + taulu(i*2) uusi jokatoinen(array(6,5,4,3,2,1)) ArrayBuffer(6,4,2) // tai Scala-maisemmin eli funktionaalisesti: def everyother[a](l: Seq[A]) l.indices.by(2).map l(_) everyother(array(6,5,4,3,2,1)) Vector(6,4,2) everyother(list(6,5,4,3,2,1)) Vector(6,4,2) everyother(arraybuffer(6,5,4,3,2,1)) Vector(6,4,2)
Sisältö 1 Generics Javassa ja Scalassa 2 Generics-yksityiskohtia 3 Taustaa geneerisistä tyypeistä
Mitä tyyppiparametrilla ei voi tehdä? Tyyppiparametrilla ei voi suoraan luoda olioita tai taulukoita Esim. edellisessä esimerkissä palautettiin lista olioita joiden tyyppi seurasi suoraan tyyppiparametrista. Vastaavaa taulukkoversiota ei olisi voinut tehdä seuraavasti : static <Tyyppi> Tyyppi[] jokatoinen( Tyyppi[] taulu ) static <Tyyppi> Tyyppi[] jokatoinen( Tyyppi[] taulu ) Tyyppi[] uusitaulu Tyyppi[ taulu.length / 2 ]; Tyyppi[] uusitaulu Tyyppi[ taulu.length / 2 ]; Jos tällainen tyyppiparametrin tyyppisen taulukon palauttava metodi olisi haluttu tehdä, joudutaan käyttämään Java:n reflection API:a ja luomaan tarvittavat oliot hieman hankalammin. (reflektiolla on lisäksi myös muita vaikutuksia) static <Tyyppi> Tyyppi[] jokatoinen2( Tyyppi[] taulu ) static <Tyyppi> Tyyppi[] jokatoinen2( Tyyppi[] taulu ) Tyyppi[] uusitaulu Tyyppi[] uusitaulu (Tyyppi[]) java.lang.reflect.array.instance( // Tämä tyyppipakotus (Tyyppi[]) java.lang.reflect.array.instance( // Tämä tyyppipakotus taulu.getclass().getcomponenttype(), // aiheuttaa warningin, taulu.getclass().getcomponenttype(), // aiheuttaa warningin, taulu.length / 2); // mutta koodissa ei ole taulu.length / 2); // mutta koodissa ei ole // virhettä // virhettä for (int i 0; i < taulu.length / 2; i++) for (int i 0; i < taulu.length / 2; i++) uusitaulu[ i ] taulu[ i * 2 ]; uusitaulu[ i ] taulu[ i * 2 ]; return uusitaulu; return uusitaulu; 16:54
Scalan tapa korjata edellinen rajoitus se, ettei taulukoita voi tehdä tyyppiparametrista, johtuu Javan taulukkotyypin rajoituksista muilla tyypeillä (kuten ArrayList ja Scalan ArrayBuffer edellä) ongelmaa ei ole Scalassa myös Java-taulukoita Array voi tehdä tyyppiparametrista mutta kääntäjää auttamaan tarvitaan ylimääräinen yksityiskohta tämä liittyy Java-virtuaalikoneen geneeristen tyyppien puutteisiin tyyppiparametrin syntaksi : ClassManifest lisää funktiolle ylimääräisen piilotetun (implicit) argumentin, joka välittää puuttuvan tyyppi-informaation uusia tyyppiparametrin mukaisia olioita ei Scalassakaan voi luoda Esimerkki mistä kääntäjä tietäisi konstruktorin argumenttien tyypit? def jokatoinen[a : ClassManifest](taulu: Array[A]): Array[A] val uusi Array[A](taulu.length/2) for (i <- Range(0, taulu.length/2)) uusi(i) taulu(i*2) uusi
Vaatimuksia tyyppiparametrin arvolle java.util.comparable-rajapinnan avulla voi tehdä paljon alkioiden järjestykseen liittyvää (järjestämistä, hakuja, jne) Rajapinta on yksinkertainen sen täyttävä luokka lupaa hyväksyä compare-metodin parametriksi annetun tyyppiparametrin tyyppisiä olioita. interface interface Comparable Comparable <T> <T> int int compareto(t compareto(t other); other); Voidaan siis toteuttaa luokka OmaLuku: class class Omaluku Omaluku implements implements Comparable Comparable <Omaluku> <Omaluku> int int compareto(omaluku compareto(omaluku other) other)...jotain...jotain Näin määriteltynä OmaLuku-luokan olioita aan siis verrata 16:54 toisiin luokan OmaLuku olioihin.
Vaatimuksia tyyppiparametrin arvolle OmaLuku olisi voinut kuitenkin täyttää jonkin muun rajapinnan, vaikka Comparable<String> jolloin sen olioita olisi pitänyt a verrata merkkijonoihin. (ei ehkä niin kovin hyödyllistä) esim. Järjestämisalgoritmeille on olennaista että olioita aan verrata toisiin saman luokan olioihin. Tällainenkin vaatimus aan kirjoittaa Vaatimukset tyypille tulevat vasta luokan olioita käyttävältä koodilta, vaikkapa järjestämismetodilta. (allaoleva metodimäärittely luokassa Collections) <T <T extends extends Comparable<T>> Comparable<T>> sort(list<t> sort(list<t> list) list)...eli sort-metodi suostuu ottamaan listoja joiden alkiot ovat verrattavissa itseensä. Edellisen kalvon OmaLuku-luokka täyttää tämän vaatimuksen joten järjestäminen onnistuu helposti ArrayList<Omaluku> ArrayList<Omaluku> lista lista............ java.util.collections.sort(lista); java.util.collections.sort(lista); // // Järjestää Järjestää listan listan alkiot alkiot // // compare-metodia compare-metodia käyttäen. käyttäen. 16:54
Sama Scalassa vaatimukset tyyppiparametreille toimivat Scalassa kuten Javassa syntaksi esim. def f[a <: Foo[A]](x: A) x Scalan vastineet Comparablelle ovat Ordered ja Ordering Ordered on lähempi vastine, mutta Ordering suositeltavampi: Orderingilla voi määritellä samalle luokalle useamman järjestyksen Esimerkki oman luokan järjestyksen määrittelemisestä case class MyPair[A <% Ordered[A], B <% Ordered[B]](first: A, second: B) extends Ordered[MyPair[A,B]] def compare(that: MyPair[A, B]): Int val f this.first.compare(that.first) if (f 0) this.second.compare(that.second) else f MyPair(1,"foo") < MyPair(1,"bar") false esimerkissä oleva <% eli view bound on kuten <:, mutta kelpuuttaa myös tyypit, jotka voi implisiittisesti muuntaa Ordered[A]-rajapinnan toteuttavaksi tyypiksi käytännössä primitiivityypit, esim. Int ja String, ovat tällaisia
WildCard List<?> List<?> lista; lista; lista lista ArrayList<String>(); ArrayList<String>(); lista lista ArrayList<Integer>(); ArrayList<Integer>(); lista lista ArrayList<IhanMikaVaanTyyppi>(); ArrayList<IhanMikaVaanTyyppi>(); Wildcard? Tässä? on ns. wildcard tyyppiparametri, jota ei nimetä Sen avulla voi tehdä mm. ylläolevan viittauksen listaan jonka alkiot voivat olla mitä tahansa tyyppiä. Ikävä kyllä List<?> listaan ei voi lisätä mikään tyyppisiä alkioita Listan alkioiden tyyppi ei ole tiedossa joten mitään metodia jonka parametrin tyyppi on tyyppiparametrin määrittämä ei voi kutsua. Alkioiden hakukin on vähän hankalampaa Esim get:in paluuarvon tyyppiä ei tiedetä, joten alkioita voi sijoittaa vain Object-tyyppisiin muuttujiin ilman tyyppipakotusta. 16:54
WildCard tallennalistaan( tallennalistaan( List List <? <? super super String> String> lista, lista, String String alkio) alkio) super : Wildcard tulee hyödyllisemmäksi kun sitä rajoitetaan Ylläolevan metodin parametrina on lista johon voi tallentaa merkkijonoja sekä String:in yliluokan (Object) olioita. Käytännössä siis mikä tahansa lista johon aan varmasti tallentaa merkkijonoja. (String:inhän saa tallentaa Object-tyyppiseen muuttujaan) super-määre toimii vain wildcardeille. Sen yhteydessä ei voi käyttää tyyppiparametria tyyliin Tyyppi super String extends : Aiemmin nähty extends toimii myös wildcardeille tulostalista( List <? extends Tulostettava> lista) Lista josta otettavat alkiot ovat Tulostettava-luokan/rajapinnan instansseja kaannalista( List<?> kaannatama) Rajoittamaton wildcard - Lista mitä vain tyyppiä Käytännössä (lähes) sama kuin List<? extends Object> 16:54
Entä Scala-vastineet? Javan?:n vastine Scalassa on _ esim. def f(a: List[_]) a Javan tyyppien super:n vastine on >: (vrt. <: eli extends) mutta Scalassa kannattaa wildcardin sijasta usein tehdä tyyppiparametrin varianssiannotaatio + tai - class Stack1[A]... class Stack2[+A]... class Stack3[-A]... Stack1[String]:llä ja Stack1[Object]:lla ei ole alityyppisuhdetta (alityypitys on A:n suhteen invarianttia) Stack2[String] <: Stack2[Object], sillä String <: Object (kovarianttia) Stack3[Object] <: Stack3[String] (kontravarianttia) lisätietoja: SbE 8.2 näitä tarvinnee käytännössä melko harvoin...
Sisältö 1 Generics Javassa ja Scalassa 2 Generics-yksityiskohtia 3 Taustaa geneerisistä tyypeistä
Java-ongelma: tyyppien poistaminen käännetyssä Javan tavukoodissa ei ole tietoa tyyppiparametreista tieto poistetaan käännösaikana (type erasure) samalla Java-kääntäjä tuottaa tavukoodiin ylimääräisiä tyyppipakotuksia tyyppiparametreja käytettäessä Java-virtuaalikone vaatii pakotuksen aiheuttaman ajonaikaisen tyyppitarkistuksen, vaikka kääntäjä tietää että se aina onnistuu joskus ongelma näkyy koodissakin: esim. instanceof ei pysty tunnistamaan tyyppiparametria pohjimmiltaan ongelma johtuu siitä, että geneeristen tyyppien tuki lisättiin vasta Java 5:een eli kieleen, jossa ei aiemmin ollut tukea niille samasta syystä Javan taulukoissa on edellä mainittuja rajoituksia (ne ovat peräisin aiemmista Javan versioista) Java 5 halusi olla yhteensopiva aiempien virtuaalikoneiden kanssa (mutta tämä ei kuitenkaan muista syistä lopulta toteutunut) Scalassa on enimmäkseen sama ongelma (sama virtuaalikone) lisätietoja: http://lamp.epfl.ch/~emir/bqbase/2006/10/16/erasure.html
Geneeristen tyyppien toteuttamisesta miten geneerisiä tyyppejä käyttävä koodi ajon aikana toimii? kaksi perusratkaisua: 1 geneerisestä luokasta ja metodista on vain yksi toteutus: koodi ottaa ajon aikana tarvittaessa selville, mitä konkreettisia tyyppejä se käsittelee (mm. Java, Scala) 2 käännetään etukäteen oma koodi jokaiselle ohjelmassa käytetylle tyyppiparametrin arvolle (mm. C++) jälkimmäisellä tavalla koodista saadaan tehokkaampaa, koska se tietää tarkat tyypit ja voi erikoistua (specialize) niihin erityisesti primitiivityyppejä (ei-oliot) käytettäessä mutta käännetty koodi pitenee, sillä siinä on useita kopioita samoista metodeista ensimmäistä aan optimoida: ajon aikainen kääntäjä (JIT) voi tehdä erikoistuneita versioita metodeista huomatessaan, että niitä käytetään paljon ohjelmoija voi pyytää kääntäjää kääntämään tietyt erikoistuneet versiot etukäteen (esim. Scala 2.8:n @specialized)
Parametrinen polymorsmi parametrinen polymorsmi on käsite, jonka mukaan funktio voi toimia eri tavalla riippuen tyyppiparametreistaan funktiolla on monta muotoa, ja kutsussa (usein implisiittisesti) annettu tyyppiparametri kertoo, mitä muotoa nyt käytetään käytännössä muodot eroavat vain niiltä kohdin, joissa tyyppiparametrien tyyppisiä arvoja käsitellään perinnän tuottama monimuotoisuus on vastaavasti alityyppipolymorsmia tai ad-hoc-polymorsmia (termien käyttö vaihtelee) aliluokan olio kelpaa yliluokan olion tilalle esim. metodi, joka ottaa argumentiksi yliluokan olion, voi muuntua käsittelemään myös aliluokan oliota generics on olio-ohjelmointikielten nimitys ja toteutus parametriselle polymorsmille
Parametrinen polymorsmi ilman olioita muissa kuin oliokielissä parametrinen polymorsmi on yleensä helpompaa ymmärtää ja eri kielissä enimmäkseen samanlaista (toisin kuin esim. moniperintä oliokielissä) vähemmän monimutkaisia yksityiskohtia se on hyvin yleisessä käytössä esim. Haskellissa ja ML:ssä näissä geneerisiä tyyppejä käytetään vielä enemmän kuin Javassa ja Scalassa tyyppipäättelijä tekee niistä ohjelmoijalle näkymättömämpiä monet genericsien monimutkaisuudet liittyvät parametrisen polymorsmin yhdistämiseen olioihin ja perintään eräs ohjelmointikielten teorian pitkäaikainen ongelma on ollut keksiä siisti tapa yhdistää parametrinen ja alityyppipolymorsmi tosin osa ongelmasta on ollut siinä, että perinnän toteutus ei ole yhtä vakiintunutta kuin parametrisen polymorsmin tutkimus jatkuu: Scalankaan ratkaisu ei liene viimeinen...