Osa 11

Poikkeukset

Poikkeukset ovat tilanteita, joissa ohjelman suoritus päättyy virheeseen. Ohjelmassa on esimerkiksi kutsuttu null-viitteeseen liittyvää metodia, jolloin ohjelmassa tapahtuu poikkeus NullPointerException. Vastaavasti taulukon ulkopuolella olevan indeksin hakeminen johtaa poikkeukseen IndexOutOfBoundsException ym.

Osa Javassa esiintyvistä poikkeuksista on sellaisia, että niihin tulee aina varautua. Näitä ovat esimerkiksi tiedoston lukemisessa tapahtuvaan virheeseen tai verkkoyhteyden katkeamiseen liittyvät poikkeukset. Osa poikkeuksista taas on ajonaikaisia poikkeuksia — kuten vaikkapa NullPointerException —, joihin ei erikseen tarvitse varautua. Java ilmoittaa aina jos ohjelmassa on lause tai lauseke, jossa mahdollisesti tapahtuvaan poikkeukseen tulee varautua.

Poikkeusten käsittely

Poikkeukset käsitellään try { } catch (Exception e) { } -lohkorakenteella. Avainsanan try aloittaman lohkon sisällä on lähdekoodi, jonka suorituksessa tapahtuu mahdollisesti poikkeus. Avainsanan catch aloittaman lohkon sisällä taas määritellään poikkeustilanteessa tapahtuva käsittely, eli mitä tehdään kun try-lohkossa tapahtuu poikkeus. Avainsanaa catch seuraa myös käsiteltävän poikkeuksen tyyppi, esimerkiksi "kaikki poikkeukset" eli Exception (catch (Exception e)).

try {
    // poikkeuksen mahdollisesti heittävä ohjelmakoodi
} catch (Exception e) {
    // lohko johon päädytään poikkeustilanteessa
}

Avainsana catch eli ota kiinni tulee siitä, että poikkeukset heitetään (throw).

Kuten edellä todettiin, ajonaikaisiin poikkeuksiin kuten NullPointerException ei tarvitse erikseen varautua. Tällaiset poikkeukset voidaan jättää käsittelemättä, jolloin ohjelman suoritus päättyy virheeseen poikkeustilanteen tapahtuessa. Tarkastellaan erästä poikkeustilannetta nyt jo tutun merkkijonon kokonaisluvuksi muuntamisen kautta.

Olemme käyttäneet luokan Integer metodia parseInt merkkijonon kokonaisluvuksi muuntamiseen. Metodi heittää poikkeuksen NumberFormatException, jos sille parametrina annettu merkkijono ei ole muunnettavissa kokonaisluvuksi.

Scanner lukija = new Scanner(System.in);
System.out.print("Syötä numero: ");

int numero = Integer.parseInt(lukija.nextLine());
Esimerkkitulostus

Syötä numero: tatti Exception in thread "..." java.lang.NumberFormatException: For input string: "tatti"

Yllä ohjelma heittää poikkeuksen, kun käyttäjä syöttää virheellisen numeron. Ohjelman suoritus päättyy tällöin virhetilanteeseen.

Lisätään esimerkkiin poikkeuksen käsittely. Kutsu, joka saattaa heittää poikkeuksen asetetaan try-lohkon sisään, ja virhetilanteessa tapahtuva toiminta catch-lohkon sisään.

Scanner lukija = new Scanner(System.in);

System.out.print("Syötä numero: ");
int numero = -1;

try {
    numero = Integer.parseInt(lukija.nextLine());
} catch (Exception e) {
    System.out.println("Et syöttänyt kunnollista numeroa.");
}
Esimerkkitulostus

Syötä numero: 5

Esimerkkitulostus

Syötä numero: enpäs! Et syöttänyt kunnollista numeroa.

Avainsanan try määrittelemän lohkon sisältä siirrytään catch-lohkoon heti poikkeuksen tapahtuessa. Havainnollistetaan tätä lisäämällä tulostuslause try-lohkossa metodia Integer.parseInt kutsuvan rivin jälkeen.

Scanner lukija = new Scanner(System.in);

System.out.print("Syötä numero: ");
int numero = -1;

try {
    numero = Integer.parseInt(lukija.nextLine());
    System.out.println("Hienosti syötetty!");
} catch (Exception e) {
    System.out.println("Et syöttänyt kunnollista numeroa.");
}
Esimerkkitulostus

Syötä numero: 5 Hienosti syötetty!

Esimerkkitulostus

Syötä numero: enpäs! Et syöttänyt kunnollista numeroa.

Ohjelmalle syötetty merkkijono enpäs! annetaan parametrina Integer.parseInt-metodille, joka heittää poikkeuksen, jos parametrina saadun merkkijonon muuntaminen luvuksi epäonnistuu. Huomaa, että catch-lohkossa oleva koodi suoritetaan vain poikkeustapauksissa.

Tehdään yllä olevasta luvun muuntajasta hieman hyödyllisempi. Tehdään siitä metodi, joka kysyy numeroa yhä uudestaan, kunnes käyttäjä syöttää oikean numeron. Metodin suoritus loppuu vasta silloin, kun käyttäjä syöttää kokonaisluvun.

public int lueLuku(Scanner lukija) {
    while (true) {
        System.out.print("Syötä numero: ");

        try {
            int numero = Integer.parseInt(lukija.nextLine());
            return numero;
        } catch (Exception e) {
            System.out.println("Et syöttänyt kunnollista numeroa.");
        }
    }
}
Esimerkkitulostus

Syötä numero: enpäs! Et syöttänyt kunnollista numeroa. Syötä numero: Matilla on ovessa tatti. Et syöttänyt kunnollista numeroa. Syötä numero: 43

Poikkeukset ja resurssit

Erilaisten käyttöjärjestelmäresurssien kuten tiedostojen lukemiseen on toteutettu erillinen versio poikkeustenhallinnasta. ns. try-with-resources -tyyppisessä poikkeustenhallinnassa avattava resurssi lisätään try-osaan määriteltävään ei-pakolliseen suluilla rajattavaan osaan.

Alla olevassa esimerkissä luetaan tiedoston "tiedosto.txt" kaikki rivit, jotka lisätään ArrayList-listaan. Tiedostoja lukiessa voidaan kohdata virhetilanne, joten tiedoston lukeminen vaatii erillisen "yrittämisen" (try) sekä mahdollisen virheen kiinnioton (catch).

ArrayList<String> rivit = new ArrayList<>();

// luodaan lukija tiedoston lukemista varten
try (Scanner lukija = new Scanner(new File("tiedosto.txt"))) {

    // luetaan kaikki tiedoston rivit
    while (lukija.hasNextLine()) {
        rivit.add(lukija.nextLine());
    }
} catch (Exception e) {
    System.out.println("Virhe: " + e.getMessage());
}

// tee jotain luetuilla riveillä

Yllä kuvattu try-with-resources -lähestymistapa on hyödyllinen resurssien käsittelyssä, sillä tässä tapauksessa ohjelma sulkee käytetyt resurssit automaattisesti. Tällöin esimerkiksi tiedostoihin liittyvät viitteet saavat luvan "kadota", koska niille ei ole enää käyttöä. Mikäli taas resursseja ei suljeta, ovat tiedostot käyttöjärjestelmän näkökulmasta käytössä kunnes ohjelma sammutetaan.

Käsittelyvastuun siirtäminen

Metodit ja konstruktorit voivat heittää poikkeuksia. Heitettäviä poikkeuksia on karkeasti ottaen kahdenlaisia. On poikkeuksia jotka on pakko käsitellä, ja on poikkeuksia joita ei ole pakko käsitellä. Poikkeukset käsitellään joko try-catch -lohkossa, tai heittämällä ne ulos metodista.

Alla olevassa esimerkissä luetaan parametrina annetun tiedoston rivit yksitellen. Tiedoston lukeminen saattaa heittää poikkeuksen — voi olla, ettei tiedostoa esimerkiksi löydy, tai voi olla ettei siihen ole lukuoikeuksia. Tällainen poikkeus tulee käsitellä. Poikkeuksen käsittely tapahtuu try-catch -lauseella. Seuraavassa esimerkissä emme juurikaan välitä poikkeustilanteesta, mutta tulostamme kuitenkin poikkeukseen liittyvän viestin.

public List<String> lue(String tiedosto) {
    List<String> rivit = new ArrayList<>();

    try {
        Files.lines(Paths.get("tiedosto.txt")).forEach(rivi -> rivit.add(rivi));
    } catch (Exception e) {
        System.out.println("Virhe: " + e.getMessage());
    }

    return rivit;
}

Ohjelmoija voi myös jättää poikkeuksen käsittelemättä ja siirtää vastuun poikkeuksen käsittelystä metodin kutsujalle. Vastuun siirto tapahtuu heittämällä poikkeus metodista eteenpäin lisäämällä tästä tieto metodin määrittelyyn. Tieto poikkeuksen heitosta — throws *PoikkeusTyyppi*, missä poikkeustyyppi esimerkiksi Exception — lisätään ennen metodirungon avaavaa aaltosulkua.

public List<String> lue(String tiedosto) throws Exception {
    ArrayList<String> rivit = new ArrayList<>();
    Files.lines(Paths.get(tiedosto)).forEach(rivi -> rivit.add(rivi));
    return rivit;
}

Nyt metodia lue kutsuvan metodin tulee joko käsitellä poikkeus try-catch -lohkossa tai siirtää poikkeuksen käsittelyn vastuuta eteenpäin. Joskus poikkeuksen käsittelyä vältetään viimeiseen asti, ja main-metodikin heittää poikkeuksen käsiteltäväksi eteenpäin:

public class Paaohjelma {
   public static void main(String[] args) throws Exception {
       // ...
   }
}

Tällöin mahdollinen poikkeus päätyy ohjelman suorittajalle eli Javan virtuaalikoneelle, joka keskeyttää ohjelman suorituksen poikkeukseen johtavan virheen tapahtuessa.

Poikkeusten heittäminen

Voimme heittää poikkeuksen throw-komennolla. Esimerkiksi NumberFormatException-luokasta luodun poikkeuksen heittäminen tapahtuisi komennolla throw new NumberFormatException(). Seuraava ohjelma päätyy aina poikkeustilaan.

public class Ohjelma {

    public static void main(String[] args) throws Exception {
        throw new NumberFormatException(); // Ohjelmassa heitetään poikkeus
    }
}

Eräs poikkeus, johon käyttäjän ei ole pakko varautua on IllegalArgumentException. Poikkeuksella IllegalArgumentException kerrotaan että metodille tai konstruktorille annettujen parametrien arvot ovat vääränlaiset. IllegalArgumentException-poikkeusta käytetään esimerkiksi silloin, kun halutaan varmistaa, että parametreilla on tietyt arvot.

Luodaan luokka Arvosana, joka saa konstruktorin parametrina kokonaislukutyyppisen arvosanan.

public class Arvosana {
    private int arvosana;

    public Arvosana(int arvosana) {
        this.arvosana = arvosana;
    }

    public int getArvosana() {
        return this.arvosana;
    }
}

Haluamme seuraavaksi varmistaa, että Arvosana-luokan konstruktorin parametrina saatu arvo täyttää tietyt kriteerit. Arvosanan tulee olla aina välillä 0-5. Jos arvosana on jotain muuta, haluamme heittää poikkeuksen. Lisätään Arvosana-luokan konstruktoriin ehtolause, joka tarkistaa onko arvosana arvovälin 0-5 ulkopuolella. Jos on, heitetään poikkeus IllegalArgumentException sanomalla throw new IllegalArgumentException("Arvosanan tulee olla välillä 0-5");.

public class Arvosana {
    private int arvosana;

    public Arvosana(int arvosana) {
        if (arvosana < 0 || arvosana > 5) {
            throw new IllegalArgumentException("Arvosanan tulee olla välillä 0-5");
        }

        this.arvosana = arvosana;
    }

    public int getArvosana() {
        return this.arvosana;
    }
}
Arvosana arvosana = new Arvosana(3);
System.out.println(arvosana.getArvosana());

Arvosana virheellinenArvo = new Arvosana(22);
// tapahtuu poikkeus, tästä ei jatketa eteenpäin
Esimerkkitulostus

3 Exception in thread "..." java.lang.IllegalArgumentException: Arvosanan tulee olla välillä 0-5

Jos poikkeus on esimerkiksi tyyppiä IllegalArgumentException, tai yleisemmin ajonaikainen poikkeus, ei sen heittämisestä tarvitse kirjoittaa erikseen metodin määrittelyyn.

Loading

Poikkeukset ja rajapinnat

Rajapintaluokissa voidaan määritellä metodeja, jotka saattavat heittää poikkeuksen. Esimerkiksi seuraavan rajapinnan Tiedostopalvelin toteuttavat luokat heittävät mahdollisesti poikkeuksen metodeissa lataa ja tallenna.

public interface Tiedostopalvelin {
    String lataa(String tiedosto) throws Exception;
    void tallenna(String tiedosto, String merkkijono) throws Exception;
}

Jos rajapinta määrittelee metodeille throws Exception-määreet, eli että metodit heittävät mahdollisesti poikkeuksen, tulee samat määreet olla myös rajapinnan toteuttavassa luokassa. Luokan ei kuitenkaan ole pakko heittää poikkeusta kuten alla olevasta esimerkistä näkee.

public class Tekstipalvelin implements Tiedostopalvelin {

    private Map<String, String> data;

    public Tekstipalvelin() {
        this.data = new HashMap<>();
    }

    @Override
    public String lataa(String tiedosto) throws Exception {
        return this.data.get(tiedosto);
    }

    @Override
    public void tallenna(String tiedosto, String merkkijono) throws Exception {
        this.data.put(tiedosto, merkkijono);
    }
}

Poikkeuksen tiedot

Poikkeusten käsittelytoiminnallisuuden sisältämä catch-lohko määrittelee catch-osion sisällä poikkeuksen johon varaudutaan catch (*Exception e*). Poikkeuksen tiedot tallennetaan e-muuttujaan.

try {
    // ohjelmakoodi, joka saattaa heittää poikkeuksen
} catch (Exception e) {
    // poikkeuksen tiedot ovat tallessa muuttujassa e
}

Luokka Exception tarjoaa hyödyllisiä metodeja. Esimerkiksi metodi printStackTrace() tulostaa stack tracen, joka kertoo miten poikkeukseen päädyttiin. Tutkitaan seuraavaa metodin printStackTrace() tulostamaa virhettä.

Esimerkkitulostus

Exception in thread "main" java.lang.NullPointerException at pakkaus.Luokka.tulosta(Luokka.java:43) at pakkaus.Luokka.main(Luokka.java:29)

Stack tracen lukeminen tapahtuu alhaalta ylöspäin. Alimpana on ensimmäinen kutsu, eli ohjelman suoritus on alkanut luokan Luokka metodista main(). Luokan Luokka main-metodin rivillä 29 on kutsuttu metodia tulosta(). Metodin tulosta rivillä 43 on tapahtunut poikkeus NullPointerException. Poikkeuksen tiedot ovatkin hyvin hyödyllisiä virhekohdan selvittämisessä.

Loading
Pääsit aliluvun loppuun! Jatka tästä seuraavaan osaan:

Muistathan tarkistaa pistetilanteesi materiaalin oikeassa alareunassa olevasta pallosta!