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() ja Exception(String msg). Jälkimmäinen tallettaa poikkeusolioon viestin msg, ja ensiksimainittu toimi kuin Exceptio(null) eli tallettaa viestiksi null-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) {
  ^

Kun ohjelmassa aiheutuu poikkeus, ohjelman suoritus

Erilaisia poikkeusluokkia käytetään

catch(Exception e)