Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Poikkeusten hallinta

osaamistavoitteet

  • Ymmärrät, mikä poikkeus on ja miten se eroaa ohjelman normaalista käskynkulusta.
  • Tiedät eron tarkastettujen (checked) ja tarkastamattomien (unchecked) poikkeusten välillä.
  • Osaat käsitellä poikkeuksia try-catch-finally -rakenteella.
  • Osaat heittää poikkeuksia (throw) ja ilmoittaa niistä metodin määrittelyssä (throws).
  • Ymmärrät poikkeusten hyödyt koodin luettavuuden ja virhehallinnan kannalta.
  • Osaat luoda omia poikkeusluokkia.

Poikkeus (engl. exception; lyhennetty muoto ilmauksesta exceptional event), on tilanne, joka syntyy ohjelman suorituksen aikana ja keskeyttää ohjelman normaalin käskynkulun. Poikkeus voi syntyä esimerkiksi seuraavissa tilanteissa:

  • Käyttäjä antaa virheellisen syötteen (esimerkiksi tekstin, kun odotetaan numeroa).
  • Yritetään lukea tiedostoa, jota ei ole olemassa.
  • Verkko- tai tietokantayhteys katkeaa kesken suorituksen.
  • Jaetaan nollalla laskutoimituksessa.
  • Viitataan listan tai taulukon indeksiin, jota ei ole olemassa.
  • Kutsutaan metodia olion viittauksella, joka on null.

Javassa poikkeusten hallintaan on sisäänrakennettu mekanismi, joka mahdollistaa virhetilanteiden hallitun käsittelyn ohjelmakoodissa. Tämän mekanismin avulla ohjelmoija voi määritellä, miten ohjelman tulee reagoida erilaisiin virhetilanteisiin ilman, että koko ohjelma kaatuu.

Poikkeusten heittäminen ja käsittely

Poikkeusten hallinta Javassa perustuu kolmeen pääkomponenttiin:

  1. Poikkeusten heittäminen (engl. throwing exceptions): Kun virhe tapahtuu, poikkeusolio luodaan joko ohjelmakoodissa tai JVM:n toimesta. Tämän jälkeen JVM keskeyttää normaalin suoritusvirran ja alkaa etsiä sopivaa käsittelijää kutsupinosta.
  2. Poikkeusten käsittely (engl. catching exceptions): Ohjelmoija voi määritellä koodilohkoja, jotka käsittelevät tiettyjä poikkeuksia try-catch-rakenteella.
  3. Lopullinen varmistus (engl. finally): finally-lohko, joka suoritetaan aina riippumatta siitä, tapahtuiko poikkeus vai ei, esimerkiksi resurssien vapauttamiseksi tai tilan palauttamiseksi.

Palaamme esimerkkeihin kohta, mutta pohditaan ensin järjestelmän toimintaa korkealla tasolla.

Kun metodin suorituksen aikana tapahtuu virhe, metodi luo virhettä kuvaavan poikkeusolion ja luovuttaa sen ajonaikaiselle järjestelmälle. Poikkeusolio sisältää muun muassa poikkeuksen tyypin sekä ohjelman tilan sillä hetkellä, kun virhe tapahtui. Tätä poikkeusolion luomista ja luovuttamista ajonaikaiselle järjestelmälle kutsutaan poikkeuksen heittämiseksi.

Kun poikkeus on heitetty, ajonaikainen järjestelmä alkaa etsiä poikkeukselle sopivaa käsittelijää (engl. exception handler) kutsupinosta siinä järjestyksessä, jossa metodeja on kutsuttu. Käsittelijä on sopiva, jos se pystyy käsittelemään heitetyn poikkeusolion tyypin mukaisen poikkeusolion. Jos sopiva käsittelijä löytyy, poikkeus välitetään sille. Tällöin sanotaan, että poikkeus "otetaan kiinni" (engl. catch).

Jos järjestelmä käy läpi koko kutsupinon löytämättä sopivaa käsittelijää, se säie (engl. thread), jossa virhe tapahtui, pysähtyy. Jos kyseessä on ohjelman pääsäie (engl. main thread), koko ohjelma kaatuu.

Alla oleva kaavio havainnollistaa karkeasti poikkeuksen heittämisen ja käsittelyn prosessia. Oletetaan, että main()-metodi kutsuu metodia a(), joka puolestaan kutsuu metodia b(), ja b() edelleen c()-metodia.

main()kutsuukutsuukutsuu.Heittää poikkeuksenc()Metodi, jossa virhetapahtuiEtsitäänsopivaakäsittelijääHeittää poikkeuksenb()Metodi, jossa ei oleeteenpäinpoikkeuskäsittelijääEtsitäänsopivaakäsittelijääOttaa poikkeuksena()Metodi, jossa onkiinnipoikkeuskäsittelijä

Esimerkissä tapahtuu seuraavaa:

  1. Metodi c() heittää poikkeuksen, esimerkiksi käsitellessään verkkoyhteyttä, mutta metodissa c() ei ole sopivaa käsittelijää.
  2. Ajonaikainen järjestelmä katsoo kutsupinossa seuraavana olevaa metodia, joka on b().
  3. Metodi b():llä ei myöskään ole sopivaa käsittelijää, joten tutkitaan edelleen seuraavaa metodia, joka on a().
  4. a()-metodilla on sopiva käsittelijä, joka ottaa poikkeuksen kiinni.
  5. Ohjelma jatkaa suoritustaan, kun a()-metodissa oleva käsittelijä on suoritettu.

Tarkastetut ja tarkastamattomat poikkeukset

Javassa poikkeukset jaetaan kahteen kategoriaan: tarkastettuihin (engl. checked) ja tarkastamattomiin (engl. unchecked). Nimitys tulee siitä, että tarkastettujen poikkeusten kohdalla käsittelyn olemassaolo tarkastetaan käännösaikana, kun taas tarkastamattomien poikkeusten käsittelyä ei tarkasteta.

Tarkastetut poikkeukset kuvaavat ympäristöstä tai syötteestä johtuvia ongelmia joihin ohjelma voi usein reagoida hallitusti. Tyypillisiä esimerkkejä ovat tiedostojen käsittely, verkkoyhteydet ja tietokantatoiminnot. Esimerkiksi tiedoston avaaminen voi epäonnistua, koska tiedostoa ei ole olemassa tai siihen ei ole lukuoikeuksia, vaikka ohjelmakoodi itsessään olisi täysin oikein. Tarkastettuja poikkeuksia ovat esimerkiksi

  • IOException, joka kuvaa syötteeseen tai tulosteeseen liittyviä ongelmia, kuten tiedoston lukemisen epäonnistumista, ja
  • SQLException, joka liittyy tietokantatoimintoihin.

Tarkastetut poikkeukset periytyvät Exception-luokasta.

Kun kääntäjä kohtaa koodin, joka voi heittää tarkastetun poikkeuksen, se ikään kuin ilmoittaa ohjelmoijalle: "Näen, että olet tekemässä jotain riskialtista (kuten lukemassa tiedostoa). En käännä ohjelmaasi, ennen kuin olet osoittanut, että olet ottanut huomioon mahdolliset ongelmat."

Tällöin on tehtävä jompikumpi seuraavista:

  1. käsiteltävä poikkeus try–catch-rakenteella (vrt. ylemmän kuvion a()-metodi), tai
  2. ilmoitettava metodin määrittelyssä throws-määreellä, että poikkeus voi siirtyä kutsujalle (vrt. ylemmän kuvion b()- ja c()-metodit).

Jos kumpaakaan ei tehdä, koodi ei käänny. Tätä vaatimusta kutsutaan catch or specify -vaatimukseksi.

Tarkastamaton poikkeus on poikkeus, jota ei tarkasteta käännösaikana, eikä sitä siten tarvitse käsitellä tai ilmoittaa etukäteen. Tällainen poikkeus voi kuitenkin laueta ohjelman ohjelman suorituksen aikana. Tyypillisiä tarkastamattomia poikkeuksia ovat

  • NullPointerException, joka tapahtuu, kun yritetään käyttää olion viitettä, joka on null,
  • IllegalArgumentException, joka tapahtuu, kun metodille annetaan sopimaton argumentti,
  • ArrayIndexOutOfBoundsException, joka tapahtuu, kun yritetään käyttää taulukon indeksiä, joka on taulukon raja-arvojen ulkopuolella, ja
  • ArithmeticException, joka tapahtuu, kun tapahtuu laskuvirhe, kuten jakaminen nollalla.

Tarkastamattomat poikkeukset periytyvät RuntimeException-luokasta.

Tarkastamattomat poikkeukset kuvaavat yleensä ohjelmointivirheitä, ja näiden käsittelemistä try-catch-rakenteella ei vaadita lähdekoodissa, eikä se ole myöskään suositeltavaa. Esimerkiksi NullPointerException-poikkeus on usein selvä merkki ohjelmassa olevasta virheestä. Vaikka try-catch-rakenteella voikin kyllä ottaa NullPointerException-poikkeuksen kiinni, se ei yleensä ole järkevää, koska se vain peittää ohjelman virheen sen sijaan, että korjaisi sen.

Syntaksi

try-catch-rakenne näyttää seuraavalta:

try {
    // Koodilohko, jossa poikkeus voi tapahtua
} catch (Poikkeustyyppi poikkeus) {
    // Koodilohko, joka käsittelee poikkeuksen
}

throws-määre puolestaan määritellään metodin allekirjoituksessa seuraavasti:

void metodi() throws Poikkeustyyppi {
    // Metodin toteutus, joka voi heittää poikkeuksen
}

Poikkeuksia voi olla useita, ja ne voidaan käsitellä erikseen:

try {
    // Koodilohko, jossa poikkeus voi tapahtua
} catch (Poikkeustyyppi1 e1) {
    // Koodilohko, joka käsittelee Poikkeustyyppi1 -poikkeuksen
} catch (Poikkeustyyppi2 e2) {
    // Koodilohko, joka käsittelee Poikkeustyyppi2 -poikkeuksen
}

Jos metodi voi heittää useita tarkastettuja poikkeuksia, ne voidaan ilmoittaa throws-määreessä pilkulla erotettuna:

void metodi() throws Poikkeustyyppi1, Poikkeustyyppi2 {
    // Metodin toteutus, joka voi heittää useita poikkeuksia
}

finally

finally-lohkoa käytetään tilanteissa, joissa try-lohkon aikana avatut resurssit täytyy vapauttaa tai suoritusympäristön tila palauttaa varmasti riippumatta siitä, onnistuiko try-lohkon suoritus tai tapahtuiko poikkeus. Tyypillisiä esimerkkejä ovat tiedoston, verkkoyhteyden tai muun resurssin sulkeminen.

Kun käytössä on try-catch-finally, suoritus etenee näin:

  1. try suoritetaan.
  2. Jos poikkeus tapahtuu, sopiva catch suoritetaan.
  3. Lopuksi finally suoritetaan aina.

Alla on esimerkki tiedoston lukemisesta Scanner-luokan avulla. Paneudumme Scanner-luokkaan tarkemmin osassa 6.5, mutta lyhyesti: Scanner-olion avulla voidaan lukea tekstiä tiedostosta esimerkiksi merkki tai rivi kerrallaan. Käytettäessä try-catch-rakennetta Scanner-olio ei sulje itseään automaattisesti esimerkiksi poikkeuksen sattuessa, joten se pitää sulkea finally-lohkossa.

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

void lueTiedosto(String polku) {
    Scanner lukija = null;

    try {
        lukija = new Scanner(new File(polku));
        while (lukija.hasNextLine()) {
            IO.println(lukija.nextLine());
        }
    } catch (FileNotFoundException e) {
        IO.println("Tiedostoa ei löytynyt: " + polku);
    } finally {
        if (lukija != null) {
            lukija.close();
        }
        IO.println("Lukuoperaatio päättyi.");
    }
}

Yllä olevassa esimerkissä finally sulkee lukijan myös silloin, kun tiedoston lukeminen keskeytyy poikkeukseen. Jos oliota ei suljeta, "tiedostokahva", eli resurssi, joka on varattu tiedoston lukemiseen, jää auki. Käyttöjärjestelmillä on tiukat rajat sille, kuinka monta tiedostoa yhdellä prosessilla tai koko järjestelmällä voi olla auki samanaikaisesti. Jos ohjelmasi pyörii silmukassa tai palvelimella ja avaa tiedostoja sulkematta niitä, ns. file descriptor -taulukko täyttyy. Kun raja tulee vastaan, ohjelma kaatuu virheeseen, usein IOException: Too many open files, eikä se pysty enää avaamaan uusia tiedostoja. finally-lohko varmistaa, että tiedoston lukija suljetaan, vaikka lukeminen epäonnistuisi.

Nyky-Javassa resurssien hallintaan käytetään usein try-catch-rakenteen sijaan try-with-resources-rakennetta (JavaDoc), jolloin esimerkiksi Scanner osaa sulkea itsensä automaattisesti. Otetaan tästä esimerkki myöhemmissä osissa, kun olemme tutustuneet Closeable-rajapintaan, jonka avulla resurssit voidaan määritellä suljettaviksi.

Esimerkki tarkastetusta poikkeuksesta

Oletetaan, että haluamme lukea tiedoston sisältöä. Tehdään se käyttäen modernista Javasta löytyvää Files.readString()-metodia. Huomaa, että tätä metodia käytettäessä ei tarvita finally-lohkoa, koska kyseinen metodi lukee tiedoston kerralla ja huolehtii tiedoston sulkemisesta automaattisesti. Jatkon kannalta on kuitenkin tärkeä muista, että näin ei ole kaikkien Files-luokan metodien, esim Files.lines() (JavaDoc) kanssa.

Files.readString()-metodi vaatii Path-olion argumentikseen, joten käytämme tässä myös Path.of()-metodia, mikä on kätevä tapa luoda Path-olio merkkijonon avulla.

import java.nio.file.Files;
import java.nio.file.Path;

void main() {
    String sisalto = lueTiedosto("data.txt");
    IO.println(sisalto);
}

String lueTiedosto(String polku) {
    return Files.readString(Path.of(polku));
}

Ohjelmamme kaatuu, koska Files.readString()-metodi voi heittää IOException-poikkeuksen. Tämä on tarkastettu poikkeus. Määritellään lueTiedosto()-metodille throws IOException -määre.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

void main() {
    String sisalto = lueTiedosto("data.txt");
    IO.println(sisalto);
}

String lueTiedosto(String polku) throws IOException {
    return Files.readString(Path.of(polku));
}

Ohjelmamme kaatuu edelleen, koska throws-määre heittää poikkeuksen edelleen kutsujalle (vrt. aiemman kuvion b()- ja c()-metodit). Koska main()-metodissa ei ole sopivaa käsittelijää, ohjelma kaatuu. Käsitellään poikkeus main()-metodissa try–catch-rakenteella.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

void main() {
    try {
        String sisalto = lueTiedosto("data.txt");
        IO.println(sisalto);
    } catch (IOException e) {
        IO.println("Tiedoston lukeminen epäonnistui: " + e.getMessage());
    }
}

String lueTiedosto(String polku) throws IOException {
    return Files.readString(Path.of(polku));
}

Nyt ohjelmamme toimii ja tulostaa virheilmoituksen, vaikka data.txt-tiedostoa ei olisikaan olemassa.

Esimerkki tarkastamattomasta poikkeuksesta

Oletetaan, että meillä on luokka Henkilo, jolla on getNimi()-metodi, joka palauttaa henkilön nimen. Luodaan nyt lista henkilöistä.

import java.util.List;

public class Henkilo {
    private String nimi;

    public Henkilo(String nimi) {
        this.nimi = nimi;
    }

    public String getNimi() {
        return nimi;
    }
}

void main() {
    List<Henkilo> henkilot = new ArrayList<>();
    henkilot.add(new Henkilo("Antti-Jussi"));
    henkilot.add(new Henkilo("Denis"));

Katsotaan seuraavaksi miten null-viittaukset voivat aiheuttaa NullPointerException-poikkeuksia. Oletetaan, että jostain kumman syystä listalle päätyisi null-arvo. Tällainen tilanne voisi syntyä esimerkiksi, jos henkilöiden tietoja luettaisiin ulkoisesta lähteestä, ja jokin tietue olisi puutteellinen, mutta se siitä huolimatta lisättäisiin listalle.

 public class Henkilo {
     private String nimi;
 
     public Henkilo(String nimi) {
         this.nimi = nimi;
     }
 
     public String getNimi() {
         return nimi;
     }
 }
void main() {
    List<Henkilo> henkilot = new ArrayList<>();
    henkilot.add(new Henkilo("Antti-Jussi"));
    henkilot.add(new Henkilo("Denis"));
    kasitteleListaa(henkilot);

    for (Henkilo h : henkilot) {
        IO.println(h.getNimi());
    }
}

void kasitteleListaa(List<Henkilo> henkilot) {
    // Jostain syystä listalle päätyy null-viittaus
    henkilot.add(null);
}

Tämähän ei ole main()-metodin näkökulmasta ollenkaan ilmeistä – se hoitaa vain omaa hommaansa. Ongelma onkin siinä, että kasitteleListaa()-metodi aiheuttaa sivuvaikutuksen (muuttaa listan sisältöä) nimenomaan sillä tavalla, joka rikkoo main()-metodin odotuksen siitä, että listalla on vain Henkilo-olioita. Ongelma ilmenee, kun main()-metodi yrittää kutsua getNimi()-metodia null-viittaukselle.

Toki tämä tilanne voitaisiin käsitellä try-catch-rakenteella, tai toisaalta tarkastamalla if-lauseella tulostettaessa, onko h-olio null-viittaus. Tämä on kuitenkin melkoisen tympeää, eikä ratkaise ongelman juurisyytä.

Useat poikkeusoliot

Sama try-lohko voi aiheuttaa useita eri poikkeuksia. Tällöin jokaiselle poikkeustyypille voidaan tehdä oma catch-lohko.

Alla Integer.parseInt() voi heittää NumberFormatException-poikkeuksen, jos syöte ei ole numero, ja jakolasku voi heittää ArithmeticException-poikkeuksen, jos jakaja on nolla.

void main() {
    laske("42", 2);   // onnistuu
    laske("abc", 2);  // NumberFormatException
    laske("42", 0);   // ArithmeticException
}

void laske(String syote, int jakaja) {
    try {
        int luku = Integer.parseInt(syote);
        int tulos = luku / jakaja;
        IO.println("Tulos: " + tulos);
    } catch (NumberFormatException e) {
        IO.println("Virheellinen luku: " + syote);
    } catch (ArithmeticException e) {
        IO.println("Nollalla ei voi jakaa.");
    }
}

Kun poikkeukset ovat eri tyyppisiä olioita, ne kannattaa käsitellä eri tavalla tilanteen mukaan.

Omien poikkeusolioiden tekeminen

Joskus valmiit poikkeusluokat eivät kuvaa ongelmaa riittävän tarkasti. Tällöin voit määritellä oman poikkeusluokan.

Yleensä valinta tehdään näin:

  1. Peri luokka Exception-luokasta, jos haluat tarkastetun poikkeuksen.
  2. Peri luokka RuntimeException-luokasta, jos haluat tarkastamattoman poikkeuksen.

Alla on esimerkki tarkastetusta omasta poikkeuksesta:

class EpakelpoSalasanaException extends Exception {
    public EpakelpoSalasanaException(String viesti) {
        super(viesti);
    }
}

void main() {
    try {
        rekisteroiKayttaja("abc");
        IO.println("Käyttäjä rekisteröity.");
    } catch (EpakelpoSalasanaException e) {
        IO.println("Rekisteröinti epäonnistui: " + e.getMessage());
    }
}

void rekisteroiKayttaja(String salasana) throws EpakelpoSalasanaException {
    if (salasana.length() < 8) {
        throw new EpakelpoSalasanaException("Salasanan pitää olla vähintään 8 merkkiä.");
    }
    if (!salasana.matches(".*[A-Z].*")) { // säännöllinen lauseke, joka tarkistaa, onko salasanassa iso kirjain
        throw new EpakelpoSalasanaException("Salasanassa pitää olla vähintään yksi iso kirjain.");
    }
    // ... muita tarkistuksia ...
}

Omien poikkeusten etu on se, että virheestä tulee semanttisesti tarkempi: poikkeuksen nimestä näkee heti, mitä sääntöä rikottiin. Toki salasanan tarkistamisessa olisi voinut käyttää myös if-lausetta ilman poikkeuksia, mutta poikkeuksella on se etu, että se pakottaa käsittelemään virhetilanteen, eikä sitä voi unohtaa.

Poikkeusten käsittelyn hyödyllisyys

Poikkeukset tuovat merkittäviä etua koodin luettavuuteen ja ylläpidettävyyteen. Ennen Javaa poikkeusten hallinta oli usein toteutettu erilaisten virhekoodien palauttamisella. Esimerkiksi C-kielessä funktiot saattoivat palauttaa virheen merkiksi erikoisarvoja, kuten -1 tai NULL. Tämä lähestymistapa oli altis virheille ja johti helposti siihen, että ohjelmoijat saattoivat unohtaa tarkistaa näitä virhekoodiarvoja. Virheentarkistus ja varsinainen toiminta sekoittuvat ns. "spagettikoodiksi". Alla esimerkki pseudokoodina.

avaaTiedosto;
JOS (onnistui) {
    lueKoko;
    JOS (kokoSelvisi) {
        varaaMuisti;
        JOS (muistiRiitti) {
            lueData;
             // ... jne ...
        } MUUTEN palautaVirhe -2;
    } MUUTEN palautaVirhe -3;
} MUUTEN palautaVirhe -5;

Poikkeusten avulla koodin "onnellinen polku" (ns. happy path) on selkeästi luettavissa, ja virheet on siivottu omiin lohkoihinsa.

try {
    avaaTiedosto();
    selvitaKoko();
    varaaMuisti();
    lueData();
} catch (TiedostoVirhe e) {
    käsitteleVirhe();
} catch (MuistiVirhe e) {
    käsitteleVirhe();
}

Joskus virhe tapahtuu syvällä metodikutsujen ketjussa (esim. metodi1 metodi2 metodi3 lueTiedosto). Ilman poikkeuksia jokaisen välissä olevan metodin (metodi2, metodi3) pitää tarkistaa paluuarvo ja välittää virhe eteenpäin, vaikka ne eivät itse osaisi tehdä virheelle mitään. Poikkeusten kanssa välissä olevat metodit voivat "väistää" (duck) poikkeuksen. Virhe "kuplii" automaattisesti ylöspäin kutsupinossa, kunnes se saavuttaa metodin, joka on kiinnostunut käsittelemään sen.

Koska poikkeukset ovat olioita, ne muodostavat hierarkioita. Tämä mahdollistaa joustavan virheenkäsittelyn. Voit napata juuri tietyn virheen (esim. FileNotFoundException), jos tiedät miten se korjataan. Voit myös napata yliluokan (esim. IOException), jolloin käsittelet yhdellä kertaa kaikki tiedonsiirtoon liittyvät ongelmat. Vaarallisena houkutuksena on napata yleinen yliluokka (esim. Exception), jolloin käsittelet kaikki mahdolliset poikkeukset, mutta et oikeastaan tiedä, miten niitä käsitellään. Tällaista catch (Exception e) -rakennetta tulee välttää, ellei todella aio käsitellä kaikkia mahdollisia virheitä -- myös odottamattomia bugeja. Käsittely liian yleisen olion tasolla voi kuitenkin jälleen kerran piilottaa ohjelmointivirheitä ja vaikeuttaa vianetsintää.

Eri kielissä on erilaisia lähestymistapoja poikkeusten hallintaan. Esimerkiksi Pythonissa kaikki poikkeukset ovat tarkastamattomia, ja ohjelmoijan ei tarvitse ilmoittaa etukäteen, että metodi voi heittää poikkeuksia. C++:ssa on sekä tarkastettuja että tarkastamattomia poikkeuksia, mutta niiden käyttö on vähemmän yleistä kuin Javassa. Rustissa poikkeuksia ei ole lainkaan, vaan virhetilanteet käsitellään Result-tyypin avulla, mikä pakottaa ohjelmoijan käsittelemään kaikki mahdolliset virheet eksplisiittisesti.

Osa tämä osan tekstistä pohjautuu Java-dokumentaatioon poikkeuksista.

Tehtävä 6.5 : Poikkeukset, osa 1. 1 p. Tee monivalintatehtävä TIMissä. Tee tehtävä TIMissä Tehtävä 6.6: Poikkeukset, osa 2. 1 p.

Tee main, joka lukee käyttäjältä silmukassa kokonaislukuja niin kauan, kuin käyttäjä antaa muun kuin tyhjän syötteen. Jos käyttäjä antaa muun kuin kokonaisluvun, tulosta virheilmoitus ja jatka lukemista.

Tallenna luvut taulukkoon. Tulosta lopuksi kaikki taulukkoon tallennetut luvut.

Tee tehtävä TIMissä
Tehtävä 6.7: Poikkeukset, osa 3. 1 p.

Tee ohjelma, joka tarkistaa, onko käyttäjän syöttämä ikä riittävä tiettyyn toimintaan, esimerkiksi ajokortin hankkimiseen.

Tee aliohjelma onkoIkaa, joka ottaa parametrina iän (int) ja palauttaa true, jos ikä on riittävä. Jos ikä on alle 18, heitä poikkeus IkaException, joka on oma tarkastettu poikkeusluokka. Anna sopiva poikkeusviesti, esimerkiksi "Ikä ei riitä.". Ohessa vinkiksi metodin esittelyrivi.

static boolean onkoIkaa(int ika) throws IkaException

Jos ikä on negatiivinen, heitä poikkeus IkaException viestillä "Ikä ei voi olla negatiivinen.".

Poista if-rakenne ja muokkaa main-metodia niin, että se kääntyy ja tulostaa oikeat asiat.

Tee tehtävä TIMissä