Ohjelmoinnin peruskurssien laaja oppimäärä Luento 12: Moniperintä ja rajapinnat, poikkeukset, kontinuaatioista Riku Saikkonen (merkityt ei-laajan kurssin kalvot: Otto Seppälä ja Juha Sorva) 2. 2. 2011
Sisältö 1 Moniperintä ja rajapinnat 2 Poikkeukset 3 Kontinuaatioista
Moniperinnästä edellä luokilla on ollut vain yksi (suora) yliluokka voisiko yliluokkia olla useampi? muutamassa oliokielessä voi (esim. Common Lisp ja C++) useimmissa vain rajoituksin moniperintä sallisi joskus parempia abstraktioita kuten tavallisellakin perinnällä, sillä voi vähentää koodin kopiointia samoin ongelman mallinnus olioiksi ja luokiksi olisi joskus luonnollisempaa moniperinnän avulla, sillä luonnollinen malli ei aina ole puu mutta käytännössä kokonainen moniperinnän tuki monimutkaistaisi monia oliojärjestelmiä liikaa
Moniperinnän ongelmia ohjelmointikielissä moniperinnän perusongelma: jos useammassa perintäverkon haarassa on korvattu sama metodi, mitä niistä käytetään? eri moniperintää tukevilla kielillä on hieman erilainen ratkaisu, mutta yleensä ne perustuvat siihen, missä järjestyksessä yliluokkien nimet kerrotaan aliluokassa mutta toiminta ei ole aina kovin intuitiivista tai helppoa ymmärtää... kentissä on samankaltainen ongelma: jos jokin yliluokka x peritään kahta polkua pitkin, pitäisikö perivään luokkaan tulla yksi vai kaksi kappaletta yliluokan x kenttiä? näkevätkö D-oliossa alla B:n ja C:n metodit eri vai saman x:n? class A { int x; } class B extends A {... } class C extends A {... } class D extends B and C {... } molempia tapoja on käytössä eri kielissä
Ohjelmointikielten tekemiä ratkaisuja usein ohjelmointikieli ratkaisee em. ongelmat rajoittamalla moniperintää esim. kielletään periminen niin, että useammassa haarassa olisi korvattu samoja metodeita tai ohjelmoijan pitää erikseen kertoa, minkä yliluokan metodi aliluokan oliosta näkyy Javan ratkaisu: vain rajapintojen moniperintä on mahdollista, varsinaisia suoria yliluokkia on aina yksi rajapinnassa ei ole kenttiä eikä metodien toteutuksia, joten em. ongelmia ei esiinny Scalassa on lisäksi luokan kaltainen rakenne nimeltä trait, jolla voi tehdä eräänlaista moniperintää (lisää myöhemmin)
Rajapinnat Rajapinta, interface, on Javan tarjoama mekanismi, jolla voidaan määritellä tyyppi määrittelemättä sille lainkaan toteutusta Abstraktia luokkaa muistuttava rakenne, joka ei kuitenkaan saa sisältää lainkaan metoditoteutuksia tai muuttujia (paitsi vakioita) Metodien esittelyihin ei kuitenkaan tarvita tässä abstract-sanaa, sillä rajapinnan metodit ovat aina abstrakteja ja julkisia Rajapinta näyttää muuten luokkamäärittelyltä, mutta sanan class sijaan tulee merkintä interface public interface Liikkuva { } public void siirry ( Paikka p ); 10:08
Perintä vs. Rajapinnat Milloin käyttää perintää ja milloin rajapintoja? Perintää käytetään kun luokat jakavat yhteistä koodia, yhteisiä muuttujia. Yhteinen toiminnallisuus voidaan usein sijoittaa abstraktiin yliluokkaan. Javassa luokka voi periä vain yhden luokan. Päätös käyttää perintää voi olla (* joskus este jatkokehityksessä ratkaisu on usein toiminnan delegointi Rajapintoja käytetään kun luokilla on osittain yhteinen ulkoinen rajapinta, mutta yleensä selkeästi eroavat toteutukset yhteisille ominaisuuksille. Rajapintojen nimet ovat usein ominaisuuksia kuten Comparable, Serializable, Hashable, HasColor, CanBeSaved jne. *) Delegaatiossa osa luokan toiminnoista on siirretty johonkin toiseen, erilliseen, luokkaan. Näin eri yliluokat omaavat aliluokat voivat käyttää yhteistä toiminnallisuutta yhä hyväkseen vaikka koodia ei duplikoida. 10:08
Rajapinnat Rajapinnalla määritetty tyyppi on yleensä jonkinlainen ominaisuus, jonka rajapinnan täyttävät luokat omaavat Jokainen ominaisuuden täyttävä luokka kertoo toteuttavansa (implements) rajapinnassa vaaditut metoditoteutukset Jos luokka sekä täyttää rajapinnan että perii luokan merkitään perintä ensin. public class Kontti implements Liikkuva { public void siirry ( Paikka p ){...toteutus public class Kirje implements Liikkuva { public void siirry ( Paikka p ){... Kirjeille sopiva toteutus 10:08
Rajapinnat Rajapinta Vain abstrakteja, julkisia metodeja (ei tarvitse erikseen kertoa) Vakioita = (public final static kenttiä) jotka pitää kaikki alustaa Koko rajapinnan näkyvyys on public tai default (ei mitään) Luokka voi täyttää samanaikaisesti monta eri rajapintaa Rajapinnat luetellaan pilkuilla eroteltuna listana implements-määreen jälkeen Moniperinnän kaltaista ongelmatilannetta ei synny...paitsi jos rajapinnoissa voi olla samannimiset metodit jotka on tarkoitettu aivan eri tehtäviin...tai jos muuten samanlaiset metodit omaavat erityyppiset paluuarvot 10:08
Rajapinnat Abstrakti luokka voi täyttää rajapinnan vaikka jotkin tai kaikki rajapinnan vaatimat metodit olisivat abstrakteja. Tämä siksi, että luokka joka lopulta toteuttaa abstrakteiksi jääneet metodit kuitenkin täyttää rajapinnan ei voi syntyä olioita, joilla ei olisi toteutuksia näille metodeille. Rajapinta voi periä yhden tai useamman rajapinnan koska toteutusristiriitaa ei juurikaan ole public interface RajapintaA extends RajapintaB, RajapintaC... Myöskään tässä moniperinnän kaltaista ongelmatilannetta ei synny...paitsi jos rajapinnoissa voi olla samannimiset metodit jotka on tarkoitettu aivan eri tehtäviin...tai jos muuten samanlaiset metodit omaavat erityyppiset paluuarvot 10:08
Rajapinta Comparable Comparable-rajapinnan määrittelemä ominaisuus on nimenmukaisesti verrattavuus Tämä rajapinta määrittelee yhden metodin compareto Olion tulee verrata itseään parametriolioon ja palauttaa kokonaisluvulla tieto siitä, onko parametriolio yhtäsuuri, suurempi vai pienempi Tämä riittää useimmille tietorakenteille ja algoritmeille monenlaiseen järjestyksen ylläpitoon. Algoritmit käsittelevät rajapinnan täyttäviä olioita Comparable-olioina. public interface Comparable <VerrattavaTyyppi> { } public int compareto ( VerrattavaTyyppi o ); 10:08
Rajapinta Comparable Suuri määrä Javan luokista täyttää Comparable-rajapinnan String Integer Date Vastaavasti mm. Collections Frameworkista löytyy paljon metodeja ja luokkia jotka osaavat toimia Comparable-olioiden kanssa esim Collections.sort osaa järjestää listoja joiden alkiot täyttävät Comparablerajapinnan Eclipse-esimerkki livenä... 10:08
Muita rajapintoja Serializable ns. Marker Interface joka ei määrittele lainkaan metodeja vaan jota käytetään pelkästään tietyn ominaisuuden omaavien luokkien tunnistamiseen Serializable antaa java:lle luvan sarjallistaa olio muistista kuvaukseksi jonka voi tallentaa/siirtää jonnekin ja myöhemmin/muualla taas ladata takaisin muistiin 10:08
Rajapinnat ja dynaaminen tyypitys rajapinnat ovat staattiseen tyypitykseen liittyvä käsite ne tarkistetaan käännösaikana metodikutsu suostutaan kääntämään vain jos kutsuttavan olion käännösaikainen tyyppi toteuttaa metodin sisältävän rajapinnan tai metodi on määritelty yliluokissa tämä on ns. nimipohjainen tyyppijärjestelmä (nominal type system): koodissa ilmoitetut luokkien nimet määräävät, mitä metodeja saa kutsua dynaamisesti tyypitetyissä kielissä yleensä metodikutsu sallitaan, jos kutsuttavalla oliolla on (ajon aikana) halutun niminen metodi näin mm. Pythonissa, Rubyssä ja Common Lispissä siis mikä tahansa olio, joka määrittelee oikean nimiset metodit kelpaa, vaikka se olisi perintähierarkiassa aivan muualla kuin ohjelmoija alun perin ajatteli tämä on nimeltään duck typing joustavampaa, mutta vähentää kääntäjän mahdollisuuksia löytää virheitä
Taustaa: mitä on staattinen tyypitys? edellä on monta kertaa puhuttu staattisen ja dynaamisen tyypityksen eroista perusero on toki: dynaamisessa tyypityksessä tyypit tarkistetaan ajon aikana staattisessa tyypityksessä tyypit tarkistetaan ennen suoritusta toinen tapa kuvata eroa: staattinen tyypitys on todistus eräästä ohjelman ominaisuudesta, jonka kääntäjä tarkistaa ja voi sen avulla välttää ajon aikaisia tarkistuksia kolmas tapa: staattisessa tyypityksessä muuttujilla on tyyppi dynaamisessa tyypityksessä arvoilla on tyyppi (yksittäinen muuttuja voi osoittaa mihin tahansa arvoon, mutta arvolla on tietty tyyppi)
Tyypityksen erikoisuuksia usein staattinen tyypitys on vain osittaista: joitakin tyyppejä joudutaan kuitenkin tarkistamaan ajon aikana varsinkin oliokielissä, sillä olion tyyppi eli luokka on dynaamisesta metodinvalinnasta johtuen osin ajon aikainen käsite monissa kielissä (mm. Java, C, C++) staattiset tyypit kerrotaan koodissa toinen vaihtoehto on tyyppipäättely, jossa kääntäjä päättelee staattiset tyypit tai osan niistä (esim. Haskell, ML, osin Scala) staattisen ja dynaamisen tyypityksen lisäksi on myös mahdollista jättää tyypit tarkistamatta (tyypitön, untyped, kieli) tai määrätä kaikille arvoille sama tyyppi heikko tyypitys (vs. vahva): tyyppejä ei tarkisteta kaikissa tilanteissa (esim. C-kielessä)
Sisältö 1 Moniperintä ja rajapinnat 2 Poikkeukset 3 Kontinuaatioista
Mihin poikkeuksia käytetään? Usein poikkeustilanteeseen ei voida reagoida mielekkäästi juuri siellä (siinä metodissa), missä poikkeustilanne syntyy. Esim. Mitä Integer.parseInt-metodin pitäisi tehdä, jos sen parametri on kelvoton? Metodin yleishyödyllisyys laskisi ratkaisevasti, jos se itse päättäisi. Metodin palautusarvoa voi joskus käyttää (ja käytetäänkin) erikoistilanteiden kuvaamiseen, mutta sillä on rajoituksensa. Esim. Metodi palauttaa taulukon pienimmän arvon. Mitä tehdään, jos taulukko onkin tyhjä? Poikkeusten käsittely perustuu Javassa siihen, että on mahdollista heittää poikkeus (eli nostaa poikkeus; engl. throw/raise an exception) eli välittää poikkeustilanneilmoitus eteenpäin sellaisen ohjelmakohdan huoleksi, joka määrittelee, miten poikkeustilanteesta selvitään. Ellei metodi voisi heittää poikkeusta, jouduttaisiin erikoistilanteisiin aina joko reagoimaan sen metodin sisällä, jossa ne syntyvät, tai välittämään tieto tilanteesta eteenpäin jollain tarkoitukseen vähemmän luonnollisella tavalla (yleensä metodin palautusarvona). 10:08
Poikkeusoliot, Exception-luokka Poikkeustilanteet kuvataan Javassa olioina (yllätys). Kaikki poikkeusoliot ovat luokan java.lang.exception ilmentymiä - joko suorasti tai epäsuorasti. Yleisluontoisia poikkeusolioita on mahdollista instantioida suoraan Exception-luokasta, mutta tavallisesti on mielekkäämpää luoda erikoistuneempi olio jostakin tämän emäluokan aliluokasta. Käytännössä poikkeusolioita luodaan lähes aina juuri silloin, kun halutaan ilmoittaa suoritettavan metodin kutsujalle, että metodin suoritus jostain syystä epäonnistui, eli halutaan heittää poikkeus. Tämä kutsuja(metodi) voi sitten päättää, miten suhtautuu asiaan. Poikkeukset muodostavat kommunikointikanavan kutsutulta metodilta kutsuvalle (vrt. metodin palautusarvo). 10:08
Poikkeusten heittäminen Poikkeuksen heittämiseen on Javassa erikseen määritelty throw-käsky. Heittolauseelle annetaan heitettäväksi yksi poikkeusolio. throw-lause keskeyttää metodin normaalin suorituksen. Suoritusta jatketaan ensimmäisestä löydetystä ohjelmakohdasta, johon on kirjattu menettely kyseisenlaisten poikkeusten käsittelyyn. if (hommaeitoiminut) throw new Exception( Juttu X meni pieleen. ); // Näille riveille ei mennä, jos poikkeus // heitettiin. Sen sijaan hypätään sellaiseen // ohjelmakohtaan, johon on merkitty erityisiä // poikkeuksenkäsittelykäskyjä. Tässä luodaan poikkeusolio ja heitetään se saman tien. (Nämä kaksi asiaa tehdäänkin hyvin usein yhdessä.) Ei onnistunut! Sä annoit tän tehtävän, joten tiedät kai sitten mitä tälle tehdään! Siihen miten tällainen poikkeuksenkäsittelijärakenne merkitään Javaohjelmaan, palataan tuossa tuokiossa. Tavallisesti tällaista poikkeuksenkäsittelijärakennetta ei löydy samasta metodista throw-lauseen kanssa. Tällöin poikkeus heitetään ulos metodista, 10:08 eli metodin suoritus keskeytyy kokonaan ja poikkeukselle lähdetään etsimään käsittelijää sitä kutsuneesta metodista.
Poikkeusmahdollisuudesta varoittaminen Suuri osa Javan poikkeuksista kuuluu ns. tarkastettavien poikkeusten kategoriaan (engl. checked exceptions). Monien mielestä ohjelmoijan itse määrittelemien poikkeustyyppien tulisi sisältyä tähän joukkoon. Vastakohtana vapaasti heitettävät poikkeukset, joita käsitellään myöhemmin. Jos jokin metodi saattaa heittää ulos tarkastettavan poikkeuksen, on metodin kerrottava kutsujilleen tästä määrittelynsä yhteydessä. Metodi varoittaa mahdollisesti heittävänsä poikkeuksen. Heittämisilmoitus merkitään throws-määrellä (vrt. throw-lause) metodin puumerkin yhteyteen Jos metodin suoritus voi mennä pieleen usealla eri tavalla, voi poikkeustyyppejä luetella useita. Okei, vaan jos jokin mättää, niin se on sun ongelma. public int esimerkkimetodi() throws Poikkeustyyppi1, Poikkeustyyppi2 /* jne. */ { //... } 10:08
Poikkeuksiin reagoiminen Jos jokin metodi kutsuu toista metodia, joka on ilmoittanut mahdollisesti heittävänsä (tarkastettavan) poikkeuksen, on kutsuvan metodin koodiin erikseen merkittävä, miten se suhtautuu tähän mahdollisuuteen. Vaihtoehdot ovat: sieppaaminen: reagoin suorittamalla nämä ja nämä koodirivit eteenpäin syöttäminen: reagoin välittämällä homman muiden ohjelman osien huoleksi. No, okei, tää on mun homma. En minäkään tiedä mitä sille tehdään! Hoida sinä siellä! Kääntäjä esittää vastalauseensa, ellei jompaa kumpaa suhtautumistapaa ole koodiin kirjattu. Näin ohjelmoija pakotetaan ottamaan kantaa siihen, miten (tarkastettavat) poikkeustilanteet käsitellään. Vrt. palautusarvojen käyttö erikoistilanteista tiedottamiseen. Huomattava osa käytössä olevien ohjelmien bugeista johtuu siitä, ettei 10:08 erilaisiin erikoistilanteisiin ole varauduttu huolella.
Poikkeusten sieppaaminen try { Metodi, joka kutsuu poikkeuksen mahdollisesti heittävää metodia, voi ilmoittaa itse hoitelevansa eli sieppaavansa (engl. catch) syntyvät poikkeukset (tai ainakin tietyntyyppiset poikkeukset). Tämä toteutetaan try-catch-lauseella: yritän tehdä nämä hommat, mutta jos niitä suorittaessa jokin menee pieleen, niin sieppaan poikkeusolion ja katson mitä sillä teen. Koodia, jonka ainakin yhdestä kohdasta kutsutaan poikkeuksen mahdollisesti heittävää metodia. } catch (jonkintyyppinen poikkeus) { Koodia, joka suorittamalla reagoidaan tällaiseen poikkeukseen. } catch (toisentyyppinen poikkeus) { Koodia, joka suorittamalla reagoidaan tähän toiseen poikkeustyyppiin. } (jne.) Jos jostain try-lohkossa kutsutusta metodista heitetään poikkeus, hypätään loppuosa lohkosta ohi ja siirrytään catchlohkoon. Jos try-lohkon suoritus onnistuu mutkitta, ei mitään catch-lohkoa suoriteta. catch-lohkoja voi olla usealle eri poikkeustyypeille. Usein yksikin riittää. Kun jonkin lohkoista loppu saavutetaan, jatkuu ohjelman suoritus tavalliseen tapaan try-catch-lauseen jälkeisiin lauseisiin. 10:08
try-catch Esimerkki Yritetään luoda uusi opiskelijaolio ja tulostaa sen opiskelijanumero. Opiskelijaluokan konstruktorista ilmoitetaan, että se voi heittää poikkeuksen. Varautumiseen on syytä. Sijoitetaan lauseet try-lohkoon. Jos konstruktori heittää poikkeuksen, ei tulostuslausetta suoriteta, vaan hypätään catch-osioon. Tämä lohko sieppaa luokan VirheellinenOpiskelijaData olioita (mahdollisten aliluokkien ilmentymät mukaan lukien). Kun lohko aktivoituu ja sieppaa poikkeuksen, sijoittuu heitetty olio automaagisesti paikalliseen muuttujaan (tässä nimeltä dataongelma), jonka käyttöalueena on catch-lohko. Vrt. metodien parametrit. Opiskelija opiskelija; try { opiskelija = new Opiskelija( Teemu Teekkari ); System.out.println( opiskelija.kerroopiskelijanumero() ); } catch ( VirheellinenOpiskelijadata dataongelma ) { } opiskelija = null; System.out.println( dataongelma.getmessage()); System.out.println( Virheellinen datarivi oli \ + dataongelma.kerrovirheellinendata() + \. ); Opiskelijadatassa ei ollut tasan kolmea sanaa. Virheellinen datarivi oli Teemu Teekkari. Kuvausviesti saadaan Exception- luokasta perityllä getmessagemetodilla. 10:08
printstacktrace-metodi Luokan Exception metodi printstacktrace tulostaa luettelon, josta käy ilmi, minkä metodien kutsujen seurauksena poikkeustilanne on syntynyt. Tulostuu poikkeusta luotaessa tallennettu kutsupino (engl. call stack). printstacktrace ja getmessage-metodia sekä muita kyseiselle poikkeustyypille määriteltyjä metodeita käyttäen voidaan edesauttaa virheiden etsintää ohjelmasta. Eräs poikkeuksien vahvuuksista on se, että niillä voi kätevästi välittää kuvauksia ja lisätietoja ongelmatilanteista. public void testaile() { try { this.opiskelija = new Opiskelija( Teemu Teekkari ); System.out.println(this.opiskelija.kerroOpiskelijanumero()); } catch (VirheellinenOpiskelijadata dataongelma) { this.opiskelija = null; dataongelma.printstacktrace(); } } tulostaa esim. VirheellinenOpiskelijadata: Opiskelijadatassa ei ollut tasan kolmea sanaa. at Opiskelija.<init> (Opiskelija.java:9) at Kokeiluohjelma.testaile (Kokeiluohjelma.java:29) at Kokeiluohjelma.main (Kokeiluohjelma.java:7) 10:08
Poikkeuksen heittäminen eteenpäin Metodi, joka kutsuu poikkeuksen mahdollisesti heittävää metodia, voi sieppaamisen sijaan suhtautua kutsumastaan metodista heitettyihin poikkeuksiin passiivisemminkin ja heittää ne (tai ainakin tietyntyyppiset niistä) eteenpäin sitä itseään kutsuneelle metodille. Tämä edellinen kutsuja voi sitten puolestaan jälleen joko siepata poikkeuksen tai heittää sen yhä aikaisemmalle tasolle kutsujen sarjassa. Eteenpäin heittäminen tehdään yksinkertaisesti merkitsemällä kyseinen poikkeustyyppi (tai -tyypit) tämän toisenkin metodin throws-osioon. Metodi testaile ilmoittaa vain heittävänsä VirheellinenOpiskelijadatapoikkeuksia, eikä itse määrittele niille käsittelytapaa. public void testaile() throws VirheellinenOpiskelijadata { this.opiskelija = new Opiskelija( Teemu Teekkari ); System.out.println(this.opiskelija.kerroOpiskelijanumero()); } Jos opiskelijakonstruktori heittää poikkeuksen, keskeytyy testaile-metodin suoritus konstruktorikutsuun samaan tapaan kuin jos siinä kohdassa olisi throw-lause. Konstruktorin luoma poikkeusolio heitetään ulos myös testaile-metodista. 10:08
Poikkeuksen heittäminen ulos ohjelmasta Poikkeuksen heittäminen ulos metodista on ongelmanratkaisun siirtämistä tuonnemmaksi. Jossakin on lopulta ilmoitettava, miten pulmaan reagoidaan tai ohjelman suoritus ei voi jatkua. Poikkeuksen voi periaatteessa heittää aina ohjelman käynnistysmetodiin main saakka. Myös käynnistysmetodin voi määrätä heittämään poikkeuksen, mutta tällöin vastaanottajaksi jää loppujen lopuksi enää main-metodia tulkkiohjelman välityksellä kutsunut ohjelman käyttäjä, jonka syliin ongelma kippautuu. Tämä johtaa normaalisti ohjelman (säikeen) kaatumiseen ja Javavirtuaalikoneen tulostamaan poikkeustilanneilmoitukseen. Erittäin huonoa tyyliä safiiri ~ % java Kokeiluohjelma VirheellinenOpiskelijadata: Opiskelijadatassa ei ollut tasan kolmea sanaa. at Opiskelija.<init> (Opiskelija.java:9) at Kokeiluohjelma.testaile (Kokeiluohjelma.java:29) at Kokeiluohjelma.main (Kokeiluohjelma.java:7) 10:08
Vapaasti heitettävät poikkeukset Tietyt Java-kielen peruskäyttöön kytkeytyvät poikkeustapaukset ovat niin yleisiä ja perustavaa laatua olevia, ettei niitä tarvitse eksplisiittisesti huomioida ohjelmakoodissa. Näitä vapaasti heitettäviä poikkeuksia (engl. unchecked exceptions) (vrt. tarkastettavat poikkeukset) ovat mm. NullPointerException: yritetään viitata null-viittauksen läpi. ArrayIndexOutOfBoundsException: viitataan olemattomaan taulukkoalkioon ClassCastException: kelvoton tyyppimuunnos ArithmeticException: esim. jaetaan nollalla NumberFormatException: esim. yritetään tulkita kissa lukuna Exception-luokalla on (java.lang-pakkauksessa) aliluokka RuntimeException. Sen ilmentymät ovat vapaasti heitettäviä. Javan vapaasti heitettäviä poikkeuksia kuvaavat luokat ovat siis luokan RuntimeException aliluokkia. Luokalle voi itsekin luoda aliluokkia, ja kehittää omia vapaasti heitettäviä 10:08 poikkeustyyppejä. Omista poikkeustyypeistä kannattaa kuitenkin yleensä tehdä tarkastettavia.
Vapaasti heitettävät poikkeukset Vapaasti heitettäviä poikkeuksia ei siis tarvitse kirjata throwsmerkinnällä, vaikka metodi niitä voisikin heittää. Monet näistä poikkeuksista ovatkin sellaisia, että ne voivat syntyä lähes missä vaan, ja olisi melko tuskaisaa kirjoittaa koodia, jossa jatkuvasti catch-lohkoin varauduttaisiin niihin Tämä ei tarkoita, etteivät vapaasti heitettävät poikkeukset tulisi samalla tavalla heitetyiksi metodilta toiselle kuin muutkin poikkeukset - kunnes ne siepataan tai lentävät ulos käynnistysmetodista. Ohjelman välitön kuolinsyy onkin usein juuri sieltä ulos sinkoutunut vapaasti heitettävä poikkeus. Ohjelmoijalle jää koko vastuu hoitaa nämä poikkeukset jotenkin - estämällä niiden syntyminen tai sieppaamalla ne. Myös vapaasti heitettäviä poikkeuksia voi (ja tilanteesta riippuen saattaa kannattaakin) siepata samaan tapaan kuin tarkastettaviakin poikkeuksia. Niiden heittymisen voi myös kirjata throws-määreellä metodien toiminnan täsmentämiseksi, vaikka se ei olekaan pakollista. 10:08
Virheet, Luokka Error Jos ajettaessa tapahtuu virhe, josta ohjelma ei pystyne toipumaan, synnyttää ja heittää ohjelmaa ajava Javavirtuaalikone virheolion. Esim. Yritetään luoda uusi olio, mutta tietokoneen muisti loppuu kesken. Esim. Joku ohjelman luokkatiedostoista on tuhoutunut tai siihen ei pääse käsiksi. Virheen syntyminen johtaa yleensä siihen, että ohjelma kaatuu ja näytölle tulostuu jonkinlainen virheilmoitus. Virheoliot ovat java.lang.errorin (tai sen aliluokan) ilmentymiä ja niitä heitetään samaan tapaan kuin vapaasti heitettäviä poikkeuksia. Virheolioita ei niiden luonteen vuoksi aina ole mielekästä siepata. Ne kertovat sellaisista hyvin vakavista, mahdollisesti laitteistoriippuvaisista ongelmista, joiden korjaaminen ohjelman ajon aikana ei usein ole realistista. 10:08
Heiteltävien asioiden hierarkia Throwable Oliot, joita voi heittää ja siepata. Exception Hankaluudet ja epätavalliset tilanteet. Error Lähes varmasti kuolettavat virheet.... Esim. FileNotFoundException sekä itse määriteltyjä poikkeuksia RuntimeException Vapaasti heitettävät poikkeukset... Esim. NullPointerException, ArrayIndexOutOfBoundsException sekä itse määriteltyjä poikkeuksia Esim. OutOfMemoryError... 10:08
try-catch-finally Catch-osioiden lisäksi try-lohkon perään voi sijoittaa myös finally-lohkon Finally-lohko suoritetaan try-lohkosta poistuttaessa kun mahdolliset catchlohkot on suoritettu. Finally suoritetaan vaikka try-lohkosta poistuttaisiin vapaasti heitettävällä poikkeuksella tai return-lauseella. ja se suoritetaan vasta returnin jo saatua palautettavan arvon. Tämän vuoksi finally on usein turvallinen paikka suorittaa joitakin siivousoperaatioita sillä mahdolliset ajonaikaisetkaan virheet eivät voi estää rivien suoritusta Kun finally on suoritettu koodin suoritus jatkuu kuten se olisi jatkunut ilman finally-lohkoa Esimerkki finally:n käytöstä tulee tietovirtojen yhteydessä maanantaina. 10:08
Poikkeusten ketjuttaminen Joskus heitettävä virhe johtuu toisesta poikkeuksesta ja tieto alkuperäisestä poikkeuksesta halutaan myös säilyttää, voidaan alkuperäinen poikkeus ketjuttaa uuden perään Käytännössä luodaan uusi poikkeusolio ja metodilla initcause kerrotaan tälle alkuperäisestä poikkeuksesta ennen kuin uusi poikkeus heitetään. 10:08
Poikkeukset kontrollirakenteena poikkeuksilla voisi periaatteessa toteuttaa muutaman muun kontrollirakenteen: break ja continue silmukasta (tee catch silmukan ehtoon) return funktiosta tai metodista (lisää funktion rungon ympärille trycatch) käytännössä koodin selkeyden ja tehokkuuden vuoksi ei kannata yleinen käytäntö: poikkeus kuvaa poikkeuksellista tilannetta tai virhettä, ei tavanomaista suorituksen etenemistä mutta joskus poikkeuksia käytetään myös muuten esimerkiksi monimutkaisesta rekursiosta voi hypätä pois heittämällä poikkeuksen
Poikkeusten toteuttaminen miten poikkeukset oikeasti toimivat? poikkeus hyppää aina ylöspäin kutsupinossa tai ulommas funktion sisällä ei toisen rakenteen sisään (paitsi catch-lohkoon) eikä paikkaan, jonka sisällä ei nyt olla toteutus voisi olla: pidetään kirjaa kutsupinosta ja koodin sisäkkäisistä rakenteista poikkeuksen heittäminen purkaa osan näistä (samaan tapaan kuin myöhemmin tehtäisiin, jos poikkeusta ei olisi tullut) ja asettaa ohjelman tilaan tiedon poikkeuksesta trycatch-rakenne tarkistaa koodinsa suorittamisen jälkeen, onko joku asettanut tiedon poikkeuksesta (jos ei, siirtyy eteenpäin catch-lohkojen yli)
Sisältö 1 Moniperintä ja rajapinnat 2 Poikkeukset 3 Kontinuaatioista
Mikä on kontinuaatio? kontinuaatio (continuation) eli jatko sisältää laskennan tilan, tarkemmin sanoen: kutsupinon ja ympäristön (muuttujien arvot) paikan koodissa eli lausekkeen, jota parhaillaan evaluoidaan myös esim. tiedon jo evaluoiduista alilausekkeista kontinuaatio kertoo, mitä tämän kohdan jälkeen tehdään esimerkki: eräs kontinuaatio Scheme-lausekkeesta (let ((a 1)) (- (+ a a) (* a 2))) on kohdassa, jossa alilausekkeet - ja (+ a a) on jo evaluoitu ja lauseketta (* a 2) ollaan juuri evaluoimassa kontinuaatiota voi ajatella poikkeusten yleistyksenä: heitetty poikkeus unohtaa throw-kohdan kontinuaation ja hyppää erääseen trycatch-lauseen kontinuaatioon (catchin alkuun) kontinuaatioilla voi hyppiä muullakin tavalla ja monta kertaa samaan kohtaan käsite on varsin monimutkainen, joten ei tarvitse huolestua, jos ei ymmärrä kaikkea...
Eksplisiittiset kontinuaatiot Schemessä Schemessä laskennan nykyisen kontinuaation voi ottaa ohjelmassa talteen ja siihen voi myöhemmin hypätä takaisin jopa useita kertoja (palataan joka kerta samaan kohtaan) sama lauseke voi siis palata monta kertaa esim. eri paluuarvoilla kontinuaation talletusmuoto on proseduuri, jota kutsumalla kontinuaatioon hypätään kontinuaation kutsuminen ei enää palaa sinne, mistä kutsu tehtiin (samoin kuin throw) kutsussa annetaan argumentti, josta tulee kontinuaation alun perin luoneen lausekkeen arvo (sitä oltiin evaluoimassa kun kontinuaatio luotiin) samanlainen ominaisuus löytyy muutamasta muustakin kielestä (mm. Ruby), mutta kovin yleinen se ei ole
Schemen call-with-current-continuation Scheme-primitiivi call-with-current-continuation (lyh. call/cc) kutsuu argumenttinaan saamaansa proseduuria antaen kontinuaation sille argumentiksi käyttö yleensä: (call/cc (lambda (k)...)) tämän call/cc-lausekkeen arvo on normaalisti...-lausekkeen arvo mutta jos k:ta kutsuu, koko call/cc-lausekkeen arvoksi tulee k:lle annettu argumentti, ja evaluointi etenee niin kuin call/cc-lauseke olisi juuri palannut k:ta voi kutsua joko lausekkeen sisältä (vrt. break tai poikkeukset) tai myöhemmin
Scheme-esimerkki Kontinuaatioesimerkki (define (copy-list-if-positive l) (call/cc (lambda (k) (define (loop l) (cond ((null? l) '()) ((<= (car l) 0) (k '())) (else (cons (car l) (loop (cdr l)))))) (loop l)))) (copy-list-if-positive '(1 2 3 4 5)) (1 2 3 4 5) (copy-list-if-positive '(1 2-3 4 5)) ()
Mihin kontinuaatioita käytetään? eksplisiittisillä kontinuaatioilla voi (ainakin periaatteessa) toteuttaa monia kontrollirakenteita, esimerkiksi: poikkeukset sekä break silmukasta ja return funktiosta generaattorit ja epädeterministinen laskenta (useita vaihtoehtoisia paluuarvoja, joita kokeillaan yksi kerrallaan) voi tallettaa laskennan tilan ja jatkaa siitä myöhemmin tätä käytetään joissain WWW-sovelluskehyksissä kun sovellus jää odottamaan käyttäjältä vastausta esim. lomakkeeseen, sen tila talletetaan kontinuaatioon jos/kun vastaus tulee, kontinuaatiota kutsutaan käyttäjältä saadulla syötteellä sovelluksen ohjelmoijalle tämä näyttää tavalliselta odottavalta (blocking, synchronous) I/O:lta sovellus voi palata tilassaan taaksepäin (esim. selaimen Back-napin tukemiseksi) vanhan kontinuaation kutsulla
Continuation-passing style ohjelmointityyli, jolla kontinuaatiot saa käyttöön, vaikka ohjelmointikieli ei tukisi eksplisiittisiä kontinuaatioita tarvitsee kuitenkin ensimmäisen luokan funktiot (käytännössä myös häntärekursio-optimoinnin) myös käännöstekniikka: jotkut funktionaalisten kielten kääntäjät muuttavat koodin CPS-muotoon eräs tapa toteuttaa eksplisiittiset kontinuaatiot ja poikkeukset CPS:ssä kaikilla proseduureilla on ylimääräinen kontinuaatioargumentti (yleensä k) proseduurit palaavat aina kutsumalla k:ta paluuarvollaan (eivät siis koskaan palauta arvoa normaalisti) tämä kutsu on aina häntärekursiivinen k vastaa call/cc:n tuottamaa kontinuaatiota sovelluksesta riippuu, kuinka pitkälle CPS-muoto viedään SICP-kirjan luvun 4.3 tulkki on kirjoitettu (noin) CPS-tyyliin
CPS-esimerkki Tavallinen kertoma (define (fact n) (if (= n 0) 1 (* n (fact (- n 1))))) Eräs CPS-kertoma (define (=k a b k) (k (= a b))) (define (-k a b k) (k (- a b))) (define (*k a b k) (k (* a b))) ; ''primitiivi'', ei CPS-muodossa ; ''primitiivi'', ei CPS-muodossa ; ''primitiivi'', ei CPS-muodossa (define (fact n k) (=k n 0 (lambda (pred) (if pred (k 1) (-k n 1 (lambda (arg) (fact arg (lambda (res) (*k n res k))))))))) (fact 10 display) 10