Poikkeusten käsittely Javassa

Käymme tässä osiossa lyhyesti läpi Javan try-catch-rakenteen käytön poikkeusten käsittelyyn.

Aiemmin todettiin, että mm. monien syötteen lukuoperaatioiden yhteydessä on otettava kantaa mahdollisin poikkeuksiin. Tarkastellaan esimerkiksi seuraavaa yksinkertaista ohjelmaa:

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class NameLength {
  public static void main(String[] args) {
    var input = new BufferedReader(new InputStreamReader(System.in));
    System.out.print("Enter your name: ");
    String name = input.readLine();
    System.out.format("Your name \"%s\" has %d characters%n",
                                          name, name.length());
  }
}

Koodin kääntäminen keskeytyisi seuraavantapaiseen virheilmoitukseen:

NameLength.java:8: error: unreported exception IOException; must be caught or declared to be thrown
  String name = input.readLine();

Aiemmin tyydyimme lisäämään poikkeuksen aiheuttavan funktion otsakkeeseen poikkeusmäärityksen, joka luettelee funktiosta mahdollisesti ulos leviävät poikkeustyypit. Toinen vaihtoehto on huolehtia poikkeuksesta itse, niin sanotusti “ottaa poikkeus kiinni”. Tämä tapahtuu Javassa varsin samantapaisella ns. try-catch-rakenteella kuin esimerkiksi C++- ja Python-kielissä. Javan try-catch-rakenne on seuraavaa muotoa:

// Asetetaan poikkeuksen mahdollisesti aiheuttava koodi try-lohkoon.
try {
  // Koodilohko, jossa voi mahdollisesti aiheutua poikkeus joko suoraan
  // lohkossa itsessään tai jostain siinä kutsutusta funktiosta leviten.
}
// Try-lohkon perässä annetaan catch-lohkoja, jotka käsittelevät niitä edeltävän try-lohkon
// sisällä aiheutuvia poikkeuksia. Tässä esimerkkinä kaksi, jotka käsittelisivät
// kahta eri tyyppiä olevia poikkeuksia: PoikkeusTyyppiA ja PoikkeusTyyppiB.
catch(PoikkeusTyyppiA parametrimuuttuja) {
  // Koodilohko, jonka alkuun ohjelman suoritus suoraan hyppää, jos
  // edeltävässä try-lohkossa aiheutuu tyyppiä PoikkeusTyyppiA oleva poikkeus.
}
catch(PoikkeusTyyppiB parametrimuuttuja) {
  // Koodilohko, jonka alkuun ohjelman suoritus suoraan hyppää, jos
  // edeltävässä try-lohkossa aiheutuu tyyppiä PoikkeusTyyppiB oleva poikkeus.
}
// Catch-lohkojen perään voi halutessaan määrittää finally-lohkon, joka suoritetaan
// edeltävän try-lohkon jälkeen aina riippumatta siitä, aiheutuiko siinä jokin poikkeus.
// Ellei try-lohkossa aiheudu poikkeusta, suorittuu finally-lohko heti try-lohkon jälkeen.
// Jos try-lohkossa aiheutuu poikkeus, suorittuu mahdollisesti poikkeuksen käsittelevä
// catch-lohko ennen finally-lohkoa.
finally {
  // Koodilohko, joka suoritetaan aina riippumatta siitä, aiheutuiko poikkeuksia.
}

catch-lohko saa parametrina käsittelemäänsä poikkeusta vastaavan tyyppisen olion. Tällainen poikkeusolio voi esimerkiksi sisältää jotain tarkentavaa tietoa siitä, millainen poikkeus tarkemmin ottaen aiheutui.

Jos sama catch-lohko sopii sellaisenaan usean eri poikkeustyypin käsittelyyn, voi catch-lohkon parametrille luetella useita poikkeustyyppejä pystyviivamerkillä ‘|’ eroteltuina. Huomaa, että catch-lohkolla on tällöinkin vain yksi parametrimuuttuja. Esimerkiksi:

try {
  // Poikkeuksia mahdollisesti aiheuttava koodilohko.
}
catch(PoikkeusTyyppiA | PoikkeusTyyppiB parametrimuuttuja) {
  // Koodilohko, jonka alkuun ohjelman suoritus suoraan hyppää, jos edeltävässä
  // try-lohkossa aiheutuu tyyppiä PoikkeusTyyppiA tai PoikkeysTyyppiB oleva poikkeus.
}

Kappaleen alussa annettu ohjelma saataisiin kääntymään esimerkiksi lisäämällä siihen alla kuvatunlainen try-catch-rakenne, jonka catch-lohko käsittelee tyyppiä IOException olevia poikkeusia. Tässä vain tyydytään tulostamaan ruudulle tietoa, millainen poikkeus aiheutui:

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;

public class NameLength {
  public static void main(String[] args) {
    var input = new BufferedReader(new InputStreamReader(System.in));
    System.out.print("Enter your name: ");
    try { // Tämän lohkon sisällä voi aiheutua IOException-poikkeus.
      String name = input.readLine();
      System.out.format("Your name \"%s\" has %d characters%n",
                                          name, name.length());
    }
    catch(IOException e) {  // Käsittelijä IOException-poikkeukselle.
      System.out.println("An exception occurred: " + e);
    }
  }
}

Käsittelemme joitain try-catch-rakenteen käytössä huomioitavia teknisiä seikkoja myöhemmin lisää perinnän yhteydessä.

Eräs huomionarvoinen seikka on, että poikkeukset eivät liity pelkästään suoranaisiin ohjelman suorituksen aikana tapahtuviin virheisiin. Poikkeukset ovat oikeastaan hyppymekanismi, jonka avulla koodin suoritus voi hypätä koodin yhdestä paikasta (kohta, jossa aiheutuu tai “heitetään” poikkeus) toiseen paikkaan (poikkeuksen käsittelevä catch-lohko). Monet poikkeukset aiheutuvat vakavista virhe- tai ongelmatilanteista (esim. ohjelma suorittaa laittoman toimenpiteen, muisti loppuu, tms.), mutta monin paikoin poikkeuksia käytetään melko “rutiininomaisesti” välittämään tietoa siitä, onnistuiko jokin funktiokutsu suorittamaan tehtävänsä. Esimerkiksi luku-kääreluokkien (Integer, Double jne.) tarjoamat muunnosfunktiot merkkijonosta kyseisen tyypin arvoksi aiheuttavat muunnoksen epäonnistuessa tyyppiä NumberFormatException olevan poikkeuksen. Näin ollen esimerkiksi sinänsä tavanomainen tehtävä, jossa halutaan tutkia, esittääkö jokin merkkijono lukua, voidaan ratkaista poikkeusten käsittelyllä. Alla on esimerkkinä ohjelma, joka tulostaa kutakin komentoriviparametria kohden tiedon siitä, esittääkö kyseinen parametri desimaalilukua (joka voitaisiin tulkita double-arvoksi). Periaate on yksinkertainen: asetetaan muunnosta yrittävä funktion Double.parseDouble kutsu try-lohkoon, ja määritetään sen perään tyyppiä NumberFormatException olevia poikkeuksia käsittelevä catch-lohko.

public class NumberParameters {
  public static void main(String[] args) {
    for(String arg : args) {
      boolean isNumber = false;
      try { // Tämän lohkon sisällä voi aiheutua NumberFormatException-poikkeus.
        Double.parseDouble(arg);  // Yritetään muuntaa parametri double-arvoksi.
        isNumber = true;  // Tämä rivi ei suoritu, jos edellä aiheutuu poikkeus!
      }
      catch(NumberFormatException e) {
      } // Otetaan poikkeus kiinni, mutta ei tehdä mitään: isNumber on yhä false.
      System.out.format("The parameter \"%s\" %s a number.%n",
                                            arg, (isNumber ? "is" : "is NOT"));
    }
  }
}

Tämän esimerkin suorittaminen tapaan java NumberParameters one two 3.0 4 five 6 tulostaa:

The parameter "one" is NOT a number.
The parameter "two" is NOT a number.
The parameter "3.0" is a number.
The parameter "4" is a number.
The parameter "five" is NOT a number.
The parameter "6" is a number.

Luokkakirjastojen dokumentaatiossa yleensä kuvataan, millaisia poikkeuksia, ja millaisissa tilanteissa, sen tarjoamat toiminnot voivat aiheuttaa.

Javassa poikkeuksen mahdollisesti heittävää koodia ympäröi try{}-lohko, jonka perässä on

finally-lohko:

catch-lohko saa parametrina