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 melko 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 on tietenkin määritelty luokkina 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:
import java.util.Scanner; public class TaulEx { private static Scanner lukija = new Scanner(System.in); 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 taulokon indeksiksi"); int i = lukija.nextInt(); 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!
Mutta syötteen numeerisuuden tarkistamiseen poikkeuksen sieppaaminen soveltuu mitä mainioimmin! Virheilmoituksista on ehkä jo tullut tutuksi poikkeus InputMismatchException: Kehitellään edellistä ohjelmaa:
import java.util.Scanner; import java.util.InputMismatchException; public class TaulEx2 { private static Scanner lukija = new Scanner(System.in); 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 taulokon indeksiksi"); int i = lukija.nextInt(); if (i<0 || i>9) { System.out.println("Kelvoton indeksi, pitää olla 0-9"); ok = false; } else { System.out.println(t[i]); ok = true; } } catch (InputMismatchException e) { System.out.println("Syöte ei kelpaa kokonaisluvuksi!"); lukija.nextLine(); // HUOM: 1) rivin loppu pois ok = false; // 2) lausekelause lauseena } } while (!ok); } }(Luvussa 2.4 opittiin toisenlainen tapa välttää ohjelman kaatuminen virheellisiin syötteisiin. Tässä opittu poikkeusten sieppaaminen on ehkä "oikeaoppisempi" tapa tällaisten virheiden käsittelyyn?)
Olemme tulostaneet standarditulostusvirtaan monentyyppisiä tietoja luokan System (pakkauksessa java.lang) staattisen kentän out avulla. Tuo kenttä on määritelty:
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.
Javan versio 1.5 täydensi Javan API-luokkia näppärällä luokalla Scanner, jonka konstruktorille kelpaa parametriksi mm. juuri tuo InputStream-olio.
Nyt tutustutaan kuitenkin tilanteeseen, jossa halutaan Javan tiedostoluokkien avulla - ilman taitavaa Scanner-oliota - lukea standardisyöttövirrasta System.in rivi kerrallaan. Miten se tehdään?
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. (Nykyään Double-luokasta löytyy myös julkinen luokkametodi parseDouble(String s), mutta esimerkin ja harjoituksen vuoksi "parsitaan doublet" toisin.)
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: Kuu on juustoa! Se oli: Kuu on juustoa! Anna kokonaisluku: 128 Se oli: 128 Anna desimaaliluku: 32.1 Se oli: 32.1Huom: Scannerin käytöstä poiketen nyt käytetäänkin desimaalipistettä!
Jos ohjelmalle syötetään virheellinen kokonaisluku 1.2, saadaan tulostus:
Anna rivi: Kuu on juustoa! Se oli: Kuu on juustoa! Anna kokonaisluku: 1.2 Exception in thread "main" java.lang.NumberFormatException: For input string: "1.2" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48) at java.lang.Integer.parseInt(Integer.java:456) at java.lang.Integer.parseInt(Integer.java:497) at SyottoKokeita.main(SyottoKokeita.java:23)Virheellinen desimaaliluku "kissa" aiheuttaa tulostuksen:
Anna rivi: Kuu on juustoa! Se oli: Kuu on juustoa! Anna kokonaisluku: 128 Se oli: 128 Anna desimaaliluku: kissa Exception in thread "main" java.lang.NumberFormatException: For input string: "kissa" at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1224) at java.lang.Double.valueOf(Double.java:447) at java.lang.Double.Kokeillaan sitten antaa tiedostonloppu (Linuxissa/Unixissa ctrl-d, Windowsissa ctrl-z) riviksi. Saadaan:(Double.java:539) at SyottoKokeita.main(SyottoKokeita.java:29)
Anna rivi: Se oli: null Anna kokonaisluku: ...Tästä opitaan, että tiedoston loputtua stdin.readLine()-metodi palauttaa arvon null, tilanne ei ole poikkeus!
Mutta null ei kelpaisi tietenkään Integer.parseInt(String)-metodille parametriksi; se aiheuttaisi NumberFormatException-poikkeuksen.
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: For input string: "12.3" Anna desimaaliluku:Annetaan nyt desimaaliluvuksi ensin "kissa" ja sitten desimaaliluku 12.3:
Anna desimaaliluku: kissa Virhe: java.lang.NumberFormatException: For input string: "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
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 = OmaOlio.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!