Poikkeusten käsittely Javassa, osa 2¶
Esittelemme tässä osiossa lyhyesti Javan poikkeusluokkien hierarkiaa, kuinka määritetään oma poikkeusluokka, ja kuinka Javassa omatoimisesti aiheutetaan (“heitetään”) poikkeus.
Aiemmissa esimerkeissä on oltu tekemisissä esimerkiksi tyyppiä IOException
ja
tyyppiä NumberFormatException
olevien poikkeusten kanssa. Nämä ovat tarkemmin ottaen Javan
luokkakirjastossa määritettyjä luokkia, jotka on tarkoitettu poikkeusten käsittelyyn. Javan
luokkakirjaston kaikkien virheiden käsittelyluokkien yliluokka on Throwable
, ja
try
-catch
-rakenteella voi ottaa kiinni vain Throwable
-tyyppisiä olioita. Luokalla on
kaksi aliluokkaa Exception
ja Error
, jotka itsessäänkin toimivat yleisluontoisina
yliluokkina. Exception
on tarkoitettu kaikkien sellaisten luokkien yliluokaksi, joita
tavanomaisesti käytetään poikkeusten käsittelyssä. Error
liittyy sellaisiin vakaviin
virhetilanteisiin, joita ohjelmien ei tavallisesti tarvitse edes yrittää käsitellä (koska
niihin ei voisi reagoida järkevästi).
Kaikki Javan poikkeusluokat ovat luokan Exception
aliluokkia. Luokalla Exception
on
mm. seuraavat julkiset rakentimet ja jäsenfunktiot:
Rakentimet
Exception()
jaException(String msg)
. Jälkimmäinen tallettaa poikkeusolioon viestinmsg
, ja ensiksimainittu toimi kuinExceptio(null)
eli tallettaa viestiksinull
-viitteen.Viestiä voidaan käyttää välittämään jonkinlaista lisätietoa siitä, miksi poikkeus aiheutui.
Jäsenfunktio
getMessage()
, joka palauttaa poikkeusolioon talletetun viestin.Jäsenfunktio
printStackTrace()
, joka tulostaa standarditulosteeseen senhetkisen kutsupinon (millaisessa funktiokutsuketjussa ja millä koodirivillä koodin suoritus poikkeuksen aiheutumisen aikana oli).
Poikkeusten käsittely onnistuisi varsin pitkälti pelkästään luokalla Exception
; sen aliluokat,
kuten IOException
, eivät yleensä lisää sen yhteyteen mitään lisätoiminnallisuutta. Erilaisia
poikkeusluokkia ei käytetäkään niinkään eri poikkeusluokkien toteuttamien erilaisten toimintojen
vuoksi vaan lähinnä (1) koska niiden avulla voidaan määrittää erilliset poikkeuksen käsittelijät
eri tyyppisille poikkeuksilla ja (2) koska poikkeusluokkien nimet jo itsessään antavat
informaatiota siitä, millaisiin ongelmiin liittyvistä poikkeuksista esimerkiksi funktioiden
poikkeusmäärityksissä tai catch
-lohkoissa on kyse.
Oman poikkeusluokan voi toteuttaa varsin yksinkertaisti: peritään luokka Exception
ja
toteutetaan tarvittaessa rakennin, joka vain välittää parametrinsa yliluokalle.
Poikkeuksen aiheuttaminen eli heittäminen oma-aloitteisesti onnistuu Javassa throw
-lauseella
tapaan throw poikkeusOlio
. Mekanismi on sinänsä varsin samanlainen kuin C++:ssa. Merkittävin
ero on, että Javassa saa heittää vain Throwable
-tyyppisiä poikkeuksia kun taas C++:ssa
oikeastaan minkä tyyppisiä tahansa. Heitettävä poikkeusolio usein luodaan suoraan throw
-lauseen
yhteydessä new
-operaattorilla, esim. tapaan throw new IOException("Errod reading line 3")
.
Alla on esimerkki oman poikkeusluokan DivideByZeroException
määrityksestä ja käytöstä. Luokat
on käytännön syistä esitetty peräkkäin; ne tulisi määrittää eri tiedostoissa.
// Varsin yksinkerteinen oma poikkeusluokka DivideByZeroException.
public class DivideByZeroException extends Exception {
public DivideByZeroException(String msg) {
super(msg);
}
}
public class DivideByZeroTest {
public static double divide(int numerator, int denominator)
throws DivideByZeroException { // Ilmaistaan, millaisen poikkeuksen funktio voi heittää.
if(denominator == 0) { // Eihän yritetä jakaa nollalla?
// Viestissä voi kuvailla, millainen tilanne johtii virheeseen.
throw new DivideByZeroException(String.format("Trying to compute %d/%d!",
numerator, denominator));
}
return (double) numerator / denominator;
}
public static void main(String[] args) {
int num = 7;
for(int i = -2; i <= 2; i++) {
try {
System.out.format("%d/%d = %.3f%n", num, i, divide(num, i));
}
catch(DivideByZeroException e) {
System.out.println(e); // Tulostaa (yleensä) poikkeuksen viesteineen.
}
}
}
}
Esimerkkiohjelman ajo tulostaa:
7/-2 = -3,500
7/-1 = -7,000
DivideByZeroException: Trying to compute 7/0!
7/1 = 7,000
7/2 = 3,500
Koska Exception
on kaikkien poikkeustyyppien yliluokka, voi minkä tahansa poikkeuksen ottaa
kiinni otsaketta catch(Exception e)
käyttävällä lohkolla. Tällaisia ns. “catch all”-tyyppisiä
lohkoja tulisi kuitenkin käyttää harkiten, koska niitä käyttäessä ei hyödynnetä poikkeusluokan
ilmaisemaa tietoa ongelman luonteesta. Sinänsä catch all -lohkoja kuitenkin käytetään jopa
useinkin tilanteissa, joissa yksinkertaisesti halutaan ottaa kiinni kaikki mahdolliset poikkeukset.
On esim. hyvä muistaa, että Java pakottaa huomioimaan ainoastaan tarkistetut (“checked”)
poikkeukset, eivätkä esimerkiksi kääntäjä tai editori yleensä ohjaa ohjelmoijaa varautumaan
tarkistamattomiin (“unchecked”) poikkeuksiin. Koska koodista ei välttämättä ole helppo itse
päätellä, millaisia tarkistamattomia poikkeuksia voi aiheutua, voi niiden kiinni ottamisessa olla
näppärää käyttää catch all -lohkoa; tällöin ei ainakaan unohdeta mainita jotain mahdollisesti
aiheutuvaa poikkeustyyppiä. On hyvä muistaa, ettei tarkistamaton poikkeus tarkoita, että kyseessä
olisi jotenkin turha tai harvinainen poikkeus. Esimerkiksi NullPointerException
, joka aiheutuu
yritettäessä viitata luokan jäseneen null
-viitteen kautta, on tarkistamaton mutta ohjelmissa
turhan yleisestikin aiheutuva poikkeustyyppi.
Kun aiheutuu poikkeus, siirtyy ohjelman suoritus lähimpään poikkeuksen tyypin kanssa yhteensopivaan
catch
-lohkoon (ellei sellaista lainkaan löydy, päättyy ohjelman suoritus käsittelemättä jääneen
poikkeuksen vuoksi). Tässä on huomioitava, että jos try
-lohkon perässä on useita yhteensopivia
catch
-lohkoja, valitaan niistä ensimmäinen. Näin ollen jos esimerkiksi käyttää
catch all -lohkoa, tulee sen aina olla viimeinen catch
-lohko. Muuten kaikki sen jälkeiset
catch
-lohkot olisivat turhia, koska niitä ennen esiintyvä catch all -lohko ottaisi kaikki
poikkeukset kiinni ennen niitä. Alla on esimerkki try
-catch
-rakenteesta, jossa ei koskaan
päädyttäisi jälkimmäiseen catch
-lohkoon.
void doSomething(String s) {
try {
Integer.parseInt(s);
}
catch(Exception e) {
System.out.println("An exception was caught: " + e);
}
catch(NumberFormatException e) {
System.out.println("I would never get to print this exception: " + e);
}
}
Java-kääntäjä itse asiassa valvoo edelläkuvattua sääntöä. Jos yrität kääntää edellä annetun koodin, antaa Java-kääntäjä jokseenkin seuraavanlaisen virheilmoituksen:
SomeJavaFile.java:xy: error: exception NumberFormatException has already been caught
catch(NumberFormatException e) {
^