Tässä luvussa tutustutaan syöttötietojen lukemisen yhteen tekniikkaan. Samalla nähdään pieni esimerkki kielen valmiiden välineiden käyttötapojen etsimisestä: Lukemisen toteuttaminen esitetään yksityiskohtaisena tarinana etsimisestä ja löytämisestä.
Poikkeusten käsittelyyn tutustutaan hyvin pintapuolisesti ja lähinnä vain lukuoperaatioiden toteuttamisessa.
Yksi tapa suhtautua tuollaisiin virheisiin on antaa käyttöjärjestelmän keskeyttää ohjelman suoritus (Pascal), toinen tapa on antaa ohjelman jatkaa virheen jälkeen kohti mahdollista katastrofia (C). Kolmas tapa on varustaa ohjelmoija välinein, joilla hän voi halutessaan itse ohjelmoida poikkeustilanteiden käsittelyn.
Javassa on monipuoliset välineet poikkeusten käsittelemiseen. Kieleen on määritely paljon valmiita poikkeuksia, esimerkiksi ArithmeticException, ArrayIndexOutOfBoundsException, ClassNotFoundException, NegativeArraySizeException, NullPointerException, NumberFormatException, FileNotFoundException, ...
Ohjelmoija voi myös laatia omia poikkeuksia erityisiin tarpeisiinsa. Poikkeukset ovat tietenkin luokkia nekin, mikäpä Javassa ei olisi ... Poikkeusten yliluokka on Exception.
Joihinkin poikkeuksiin ohjelmoijan on pakko varautua (checked exceptions), joidenkin poikkeusten tapahtumisen annetaan oletusarvoisesti kaataa ohjelman suoritus (unchecked exceptions).
Tiedostojen lukemiseen liittyvät poikkeukset kuuluvat edelliseen ryhmään. Ja esimerkiksi taulukon virheellinen indeksointi jälkimmäiseen.
Unchecked-poikkeukset ovat RuntimeException- tai Error-luokan tai jonkin niiden aliluokan ilmentymiä. Näihin kuuluu mm. juuri tuo hyvin tutuksi tullut ArrayIndexOutOfBoundsException.
Kun käytetään metodia, joka voi aiheuttaa "checked exception" -poikkeuksen, ohjelmoijan on joko ilmoitettava metodinsa otsikossa, että poikkeus voi tulla ("throws clause") tai itse käsiteltävä mahdollinen virhe try-catch-lauseella.
Poikkeustilanteen syntymistä kutsutaan poikkeuksen aiheuttamiseksi (throw), sen käsittelemistä poikkeuksen sieppaamiseksi (catch).
Poikkeus siepataan try-catch -lauseella:
try { ... Yritetään jotakin, joka voi aiheuttaa poikkeuksen Poikkeuksen_tyyppi. Jos poikkeusta ei aiheuteta, try-osa suoritetaan loppuun ja jätetään catch-osa väliin. } catch (Poikkeuksen_tyyppi poikkeuksen_nimi) { ... Käsitellään siepattu poikkeus: try-osa siis aiheutti poikkeuksen Poikkeuksen_tyyppi. }Try-catch -lauseessa voi olla useita catch-osia erilaisten poikkeusten sieppaamiseen. Lauseessa voi olla myös finally-osa, joka suoritetaan joka tapauksessa try-catch -lauseen lopuksi (jopa silloin, kun on lopetettu break- tai return-lauseeseen!).
Tarkastellaan esimerkkiä, jossa taulukon virheellinen indeksointi voi aiheuttaa poikkeuksen ArrayIndexOutOfBoundsException (TaulEx.java):
public class TaulEx { public static void main(String[] args) { int[] t = {9,8,7,6,5,4,3,2,1,0}; boolean ok; do { try { System.out.println("Anna luku 0-9 taulukon indeksiksi"); int i = Lue.kluku(); System.out.println(t[i]); // ^ tässä voi tulla virhe! ok = true; } catch (ArrayIndexOutOfBoundsException e) { System.out.println("Kelvoton indeksi, pitää olla 0-9"); ok = false; } } while (!ok); } }Tämä olisi toki järkevämpää ohjelmoida tavanomaiseen tapaan tarkistamalla indeksi if-lauseella!
public static final PrintStream out;Luokassa PrintStream on määritelty kuormitettuja ja helppokäyttöisiä tulostusmetodeita print ja println.
Luokassa System on määritelty myös standardisyöttövirta kenttänä in:
public static final InputStream in;InputStream-luokka on puolestaan melko köyhä lukemismetodeista: välineitä löytyy vain tavu kerrallaan lukemiseen.
Mitä tehdä, jos haluaisikin lukea rivin kerrallaan?
Java-henkinen ratkaisu on etsiä luokkaa, jossa on metodi rivin lukemiseen. Sellainen löytyykin: luokassa BufferedReader on metodi readLine().
Ongelma tulee siitä, että BufferedReader-luokalla ei ole konstruktoria, jolle voisi antaa kentän in parametriksi! Konstruktorin muodollisen parametrin tyyppi on abstrakti luokka Reader, eikä todelliseksi parametriksi voi antaa InputStream-oliota, koska InputStream on Object-luokan välitön aliluokka.
Ratkaisu löytyy luokkana InputStreamReader:
Ja näin BufferedReader-tyyppinen syöttövirtaolio voidaan asettaa muuttujan arvoksi lauseella:
BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));Ja nyt rivejä voi lukea String-muuttujaan:
String jono = stdin.readLine();
Seuraavissa esimerkeissä huomattavaa:
import java.io.*;
String mjono = "123"; // tms. int i = Integer.parseInt(mjono);Jos merkkijono ei kelpaa kokonaisluvuksi, aiheutuu poikkeus NumberFormatException.
String mjono = "123.45"; // tms. double d = new Double(mjono).doubleValue();Jos merkkijono ei kelpaa double-luvuksi, aiheutuu poikkeus NumberFormatException.
Tässä siis tavallaan siis annetaan ohjelmalle lupa kaatua!
Huom: Jos kyseessä on kutsuttava metodi, kutsuvan metodin täytyy puolestaan käsitellä poikkeus: joko omalla throws-ilmauksella tai try-cath -lauseella!
Ohjelma lukee merkkijonon, kokonaisluvun ja liukuluvun (SyottoKokeita.java):
import java.io.*; public class SyottoKokeita { public static void main(String[] args) throws Exception { BufferedReader stdin = new BufferedReader( new InputStreamReader(System.in) ); String jono; System.out.print("Anna rivi: "); System.out.flush(); jono = stdin.readLine(); System.out.println("Se oli: "+jono); System.out.print("Anna kokonaisluku: "); System.out.flush(); jono = stdin.readLine(); int luku = Integer.parseInt(jono); System.out.println("Se oli: "+luku); System.out.print("Anna desimaaliluku: "); System.out.flush(); jono = stdin.readLine(); double dluku = new Double(jono).doubleValue(); System.out.println("Se oli: "+dluku); } }Kun virheitä ei tule, ohjelman suoritus näyttää seuraavalta:
Anna rivi: Kissa kävelee ... Se oli: Kissa kävelee ... Anna kokonaisluku: 321 Se oli: 321 Anna desimaaliluku: 12.34 Se oli: 12.34Jos ohjelmalle syötetään virheellinen kokonaisluku 1.2, saadaan tulostus:
Anna rivi: Kissa kävelee ... Se oli: Kissa kävelee ... Anna kokonaisluku: 1.2 java.lang.NumberFormatException: 1.2 at java.lang.Integer.parseInt(Integer.java) at java.lang.Integer.parseInt(Integer.java) at SyottoKokeita.main(SyottoKokeita.java:22)Virheellinen desimaaliluku "kissa" aiheuttaa tulostuksen:
Anna rivi: Kissa kävelee ... Se oli: Kissa kävelee ... Anna kokonaisluku: 321 Se oli: 321 Anna desimaaliluku: kissa java.lang.NumberFormatException: kissa at java.lang.Double.Kokeillaan sitten antaa tiedostonloppu (ctrl-d) heti aluksi. Saadaan:(Double.java) at SyottoKokeita.main(SyottoKokeita.java:28)
Anna rivi: Se oli: null Anna kokonaisluku: java.lang.NumberFormatException: null at java.lang.Integer.parseInt(Integer.java) at java.lang.Integer.parseInt(Integer.java) at SyottoKokeita.main(SyottoKokeita.java:22)Tästä opitaan, että tiedoston loputtua stdin.readLine()-metodi palauttaa arvon null, tilanne ei ole poikkeus! Mutta null ei kelpaa Integer.parseInt(String)-metodille parametriksi.
Huom: Poikkeuksilla on omat toString()-metodinsa, joten niiden tulostaminen onnistuu vaivatta!
Huom: Varaudutaan myös tiedoston päättymiseen: kts. toistoehto. Miten käy ilman testiä jono!=null?
import java.io.*; public class PoikkeusKokeita { public static void main(String[] args) { BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); String jono=null; try { System.out.print("Anna kokonaisluku: "); System.out.flush(); jono = stdin.readLine(); int uluku = Integer.parseInt(jono); System.out.println("Se oli: "+uluku); } catch (Exception e) { System.out.println("Virhe: "+e); } boolean ok; do { try { System.out.print("Anna desimaaliluku: "); System.out.flush(); jono = stdin.readLine(); double udluku = new Double(jono).doubleValue(); System.out.println("Se oli: "+udluku); ok = true; } catch (Exception e) { System.out.println("Virhe: "+e); ok = false; } } while (!ok && jono!=null); } }Kun annetaan virheellinen kokonaisluku 12.3, ohjelma tulostaa virheilmoituksen ja jatkaa kyselemällä desimaalilukua:
Anna kokonaisluku: 12.3 Virhe: java.lang.NumberFormatException: 12.3 Anna desimaaliluku:Annetaan nyt desimaaliluvuksi ensin "kissa" ja sitten desimaaliluku 12.3:
Anna desimaaliluku: kissa Virhe: java.lang.NumberFormatException: kissa Anna desimaaliluku: 12.3 Se oli: 12.3Kokeillaan kokonaisluvun sijaan tiedoston loppumista (ctrl-d):
Anna kokonaisluku: Virhe: java.lang.NumberFormatException: null Anna desimaaliluku: Virhe: java.lang.NullPointerExceptionJa lopuksi vielä annetaan kelvollinen kokonaisluku ja tiedoston loppumerkki:
Anna kokonaisluku: 123 Se oli: 123 Anna desimaaliluku: Virhe: java.lang.NullPointerException
import java.io.*; public class UPoiKo { public static void main(String[] args) { BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); boolean loppu=false; do { try { System.out.print("Anna desimaaliluku: "); System.out.flush(); String jono = stdin.readLine(); double udluku = new Double(jono).doubleValue(); System.out.println("Se oli: "+udluku); loppu = true; } catch (NumberFormatException e) { System.out.println("Virhe: ei kelpaa numeroksi"); } catch (NullPointerException e) { System.out.println("Virhe: tiedosto loppui"); loppu = true; } catch (IOException e) { System.out.println("Virhe: jotain mätää tiedostossa"); loppu = true; } } while (!loppu); } }Jos ohjelmalle syötetään kissa, koira ja tiedostonloppu (ctrl-d), saadaan:
Anna desimaaliluku: kissa Virhe: ei kelpaa numeroksi Anna desimaaliluku: koira Virhe: ei kelpaa numeroksi Anna desimaaliluku: Virhe: tiedosto loppui
import java.io.*; public class PieniSuuri { public static void main(String[] args) { BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); System.out.println( "Pienet suuriksi ja suuret pieniksi (ctrl-d lopettaa)"); for (boolean loppu=false; !loppu; ) { try { System.out.print("Anna muutettava: "); System.out.flush(); String jono = stdin.readLine(); char[] taulu = jono.toCharArray(); for (int i=0; i<taulu.length; ++i) { if (Character.isLowerCase(taulu[i])) taulu[i] = Character.toUpperCase(taulu[i]); else if (Character.isUpperCase(taulu[i])) taulu[i] = Character.toLowerCase(taulu[i]); } System.out.println("Muutettuna: " + new String(taulu)); } catch (Exception e) { System.out.println("\n\nSelvä, työt on tehty\n"); loppu = true; } } } }Esimerkkisuoritus:
Pienet suuriksi ja suuret pieniksi (ctrl-d lopettaa) Anna muutettava: Agricolan ABC-kiria Muutettuna: aGRICOLAN abc-KIRIA Anna muutettava: Hyvää Päivää Rouva Presidentti! Muutettuna: hYVÄÄ pÄIVÄÄ rOUVA pRESIDENTTI! Anna muutettava: Selvä, työt on tehty
Huom: Luokka on hieman virheellinen Luokan kehittely-yrityksistä tullee harjoitustehtävä
import java.io.*; public class Lue { /*************************************************************************** Lukurutiinit Johdatus ohjelmointiin -kurssille syksyllä 1997 Arto Wikla TÄMÄ ON JAVAN VERSIOLLE 1.1.3 Operaatiot: Lue.rivi() antaa seuraavan syöttörivin (String) Lue.kluku() " " kokonaisluvun (int) Lue.dluku() " " desimaaliluvun (double) Lue.merkki() antaa seuraavan syöttörivin ensimmäisen merkin Operaatiot ovat sitkeitä, ne VAATIVAT kelvollisen syötteen! ***************************************************************************/ static BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); public static String rivi() { String arvo=null; boolean ok; do { try { arvo = stdin.readLine(); ok = true; } catch (Exception e) { System.out.println("Virhe rivin lukemisessa. Anna uusi!"); ok = false; } } while (!ok); return arvo; } /**************************************************************************/ public static int kluku() { int arvo=-1; boolean ok; do { try { arvo = Integer.parseInt(stdin.readLine()); ok = true; } catch (Exception e) { System.out.println("Kelvoton kokonaisluku. Anna uusi!"); ok = false; } } while (!ok); return arvo; } /**************************************************************************/ public static double dluku() { double arvo=-1; boolean ok; do { try { arvo = new Double(stdin.readLine()).doubleValue(); ok = true; } catch (Exception e) { System.out.println("Kelvoton desimaaliluku. Anna uusi!"); ok = false; } } while (!ok); return arvo; } /**************************************************************************/ public static char merkki() { String rivi = rivi(); try { return rivi.charAt(0); } catch (Exception e) { return ' '; } } }
Virhetilanteita, esimerkiksi virheellisiä parametrien arvoja on kurssin esimerkkiohjelmissa ja harjoitustehtävissä käsitelty monin eri tavoin: Metodi on voinut palauttaa tiedon virheestä totuusarvona tai jonkin muun tyypin erikoisarvona. Liian suuri tai pieni arvo on voitu tulkita suurimmaksi tai pienimmäksi kelvolliseksi arvoksi asiasta sen kummemmin virheellisen arvon antajalle tiedottamatta. Virheellisen indeksin on voitu sallia johtaa ohjelman kaatumiseen tavallisen indeksoinnin tapaan. Useimmiten olioparametrin arvoon null ei ole lainkaan varauduttu, vaan on luotettu ja edellytetty kutsujan pitävän huolta todellisen parametrin olemassaolosta. Jne., jne.
Mahdollisuuksia virheiden käsittelyyn on paljon, erilaisia virhetilanteita varmaan vieläkin enemmän...
Erityisen ongelmallisia Javalla ohjelmoitaessa ovat olioiden luonnissa tapahtuvat virheet, esimerkiksi konstruktorin parametrien virheelliset arvot tai jonkin tarvittavan resurssin (esim. tiedoston) hankinnassa tulevat virheet, koska konstruktorin kutsu (melkein) vääjäämättömästi johtaa olion luontiin. Näissä tilanteissa ongelmana on luotavan olion "laillisen" tilan varmistaminen. Asiaa on joissakin harjoitustehtävissä yritetty hoidella siten, että korvataan virheelliset arvot "oletusarvoilla". Tästäkin voi seurata murheita: olion luoja ei välttämättä edes tiedä, että jotakin meni pieleen, ja luulee saaneensa tilaamansa olion. Joskus luonteva ratkaisu voi olla erillisen onkoLuotuOlioKunnossa-metodin laatiminen. Olion luojan vastuulle jää luodun olion kunnon tarkistus.
Esimerkki oman onkoLuotuOlioKunnossa-metodin käytöstä:
public class OmaOlio { private boolean kunnossa; // muut kentät ... public OmaOlio(parametreja) { pyri asettamaan luotava olio lailliseen alkutilaan: ... if (kunnon olio syntyi) kunnossa = true; else kunnossa = false; } public boolean onkoLuotuOlioKunnossa() { return kunnossa; } ... } // ja sitten käyttöä: ... OmaOlio x = new OmaOlio(parametreja); if (!x.onkoLuotuOlioKunnossa()) käsittele virhetilanne; else // luonti onnistui ...Tätä menetettelyä on helppo laajentaa tilanteeseen, jossa olio voi särkyä olemassaolonsa aikana (esim. tiedosto särkyy).
Toinen tapa hoitaa konstruontivirheet on laatia erillinen luokkametodi olioiden luomiseen, ns. staattinen luontimetodi. Tuo metodi voi palauttaa arvonaan null, ellei kunnon oliota saatu luotua (tähän ei konstruktori pysty, koska this on vakio ja toisaalta vaikkapa return null ei ole mahdollinen konstruktorissa).
Esimerkki staattisesta luontimetodista:
public class OmaOlio { // kenttiä ... private OmaOlio(parametreja) { ... } public static OmaOlio luoOmaOlio(parametreja) { if (parametreissa vikaa tai jotain resurssia ei saada varattua) return null; else return new OmaOlio(parametreja); } ... } // ja sitten käyttöä: ... OmaOlio x = luoOmaOlio(parametreja); if (x == null) käsittele virhetilanne; else // luonti onnistui ...
Kolmas tapa hoitaa tällaiset virheet on poikkeusten käyttö. Tällöin on ainakin kaksi mahdollisuutta: käytetään checked- tai unchecked-poikkeuksia. Jälkimmäisiä ei tarvitse siepata ja niiden voidaan antaa kaataa ohjelman suoritus. Ensinmainitut on käsiteltävä joko try-catch-lauseella tai kutsuvan metodin throws-ilmauksella. (Unchecked-poikkeukset ovat RuntimeException- Errorluokan tai jonkin niiden aliluokan ilmentymiä)
Unchecked-esimerkki:
public class OmaOlio { // kenttiä ... public OmaOlio(parametreja) { if (parametreissa vikaa tai jotain resurssia ei saada varattua) throw new RuntimeException("Olion luonti ei onnistu. Kaikki loppuu!"); // parametrit kunnossa: ... } ... } // ja sitten käyttöä: ... OmaOlio x = new OmaOlio(parametreja); // jatkamaan päästään vain, jos poikkeusta ei tullut ...Huom: Jos vikaa on nimenomaan parametreissa, voi olla perusteltua käyttää RuntimeExceptionin sijasta IllegalArgumentExceptionia!
Checked-esimerkki:
public class OmaOlio { // kenttiä ... public OmaOlio(parametreja) throws Exception { if (parametreissa vikaa tai jotain resurssia ei saada varattua) throw new Exception("Olion luonti ei onnistu!"); // parametrit kunnossa: ... } ... } // ja sitten käyttöä: ... OmaOlio x; try { x = new OmaOlio(parametreja); } catch (Exception e) { tee mitä luonnin epäonnistuessa pitää tehdä } // tänne kun tullaan, olio on joko luotu tai virhe käsitelty ...Huom: Poikkeusten käyttäminen "tavallisten" tilanteiden hoitamiseen - ei siis "poikkeuksellisiin" tilanteisiin - voi olla epäviisasta, koska poikkeuksiin varautuminen (try-catch) saattaa hidastaa ohjelman suorittamista merkittävästi. Kokeile esimerkkiä: MittaaPoiAikaa.java!