Arto Wikla 2011. Materiaalia saa vapaasti käyttää itseopiskeluun. Muu käyttö vaatii luvan.

5 Ohjelmointitekniikkaa: parametrivirheitä

(Muutettu viimeksi 23.9.2011, sivu perustettu 17.9.2011. Arto Wikla)

Virheisiin varautuminen on vaikeaa

Ohjelmointi on vaikeaa. Ei siksi, että algoritmien ja olioiden rakenteiden yksityiskohdat sinänsä olisivat vaikeita. Vaikeus tulee ainakin kahdesta muusta suunnasta:

Tässä luvussa tarkastellaan esimerkkejä virhetilanteiden hoitamisesta. Kuten todettu, virheiden käsittely on vaikeaa. Siksi asiaan palataan uudelleen myös jatkokurssilla.

Virheelliset parametrit metodeissa, konstruktoreissa ja aksessoreissa

Ohjelmoinnissa – erityisesti Java-maailmassa – ohjelman sisäiset virheet usein luokitellaan kahteen eri ryhmään:

Tähän palataan jatkokurssin puolella, kun opiskellaan ns. poikkeusten heittämistä ja niiden ns. sieppaamista.

Jo tässä vaiheessa on kuitenkin hyvä vähän miettiä asiaa.

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 taulukkoindeksoinnin tapaan. Useimmiten olioparametrin arvoon null ei ole lainkaan varauduttu, vaan on luotettu ja edellytetty kutsujan pitävän huolta todellisen parametrin olemassaolosta. Jne., jne.

Kuten sanottu, näihin asioihin palataan jatkokurssilla.

Pari tekniikkaa on kuitenkin hyvä oppia jo nyt.

Karsitaan parametrivirheet heti metodin alussa

Ohjelman logiikasta voi saada selkeämmän, jos heti alussa karsii virhetilanteet pois.

Esimerkki: Olkoon (vähän keinotekoisena) tehtävänä laatia pääohjelman avuksi metodi, joka hyväksyy parametrikseen vain positiivisen ja parillisen luvun, mutta ei kuitenkaan lukua 666. Metodi palauttaa kelvollisen parametrinsa arvon kolminkertaisena. Jos parametri on kelvoton, metodi palauttaa luvun -1:

private static int kolmenna(int param) {

  if (param < 0)  // ... kuvittele tänne monimutkaisemmat
    return -1;       //     virhetarkistukset ...
  if (param % 2 != 0)
    return -1;
  if (param == 666)
    return -1; 

  // nyt sitten voidaan jatkaa puhtaalta pöydältä:

  // ... kuvittele tänne monimutkaisempi algoritmi ...
  return 3 * param;
}

Kun virhetarkistukset ovat monimutkaisia – kuten jo yllä alkaa olla – tarkistukset voi ohjelmoida omaksi metodikseen. Äskeinen esimerkki voitaisiin muokata muotoon:

private boolean parametriKunnossa(int para) {
  return (para >=0) && (para%2===) && (para!=666); 
}

private static int kolmenna(int param) {

  if (!parametriKunnossa(param))
    return -1;

  // nyt sitten voidaan jatkaa puhtaalta pöydältä:

  // ... kuvittele tänne monimutkaisempi algoritmi ...
  return 3 * param;
}

Olioiden laiton tai arvaamaton tila ja olioiden särkyminen

Erityisen ongelmallisia Javalla ohjelmoitaessa ovat olioiden luonnissa ja aksessoinnissa tapahtuvat virheet, esimerkiksi konstruktorin parametrien virheelliset arvot, 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.

Edellä oli puhe siitä, miten taulukon virheellinen indeksointi johtaa ohjelman suorituksen päättymiseen. Ohjelmoijan siis pitää tuntea kielen dokumantaatiota tältä osin ja itse pitää huoli, ettei taulukkoa indeksoida väärin.

Vastaava logiikka on sovellettavissa myös omiin välineisiin. Esimerkisi edellisen luvun Varasto-luokan kaksiparametrinen konstruktori varoittamatta luo "nollavaraston" ja varoittamatta heittää mahtumattoman tuotteen menemään:

  public Varasto(double tilavuus, double alkuSaldo) { // kuormitetaan
   if (tilavuus > 0.0)
      this.tilavuus = tilavuus;
    else                    // virheellinen, nollataan
      this.tilavuus = 0.0;  // => käyttökelvoton varasto

    if (alkuSaldo < 0.0)
       this.saldo = 0.0;
    else if (alkuSaldo <= tilavuus)  // mahtuu
      this.saldo = alkuSaldo;
    else
      this.saldo = tilavuus;  // täyteen ja ylimäärä hukkaan!
  }

Samoin lisäävä aksessori heittää mahdollisen ylijäämän hukkaan:

  public void lisaaVarastoon(double maara) {
    if (maara < 0)   // virhetilanteessa voidaan tehdä 
       return;       // tällainen pikapoistuminenkin!

    if ( maara <= paljonkoMahtuu() )  // omia aksessoreita voi kutsua
      saldo = saldo + maara;          // ihan suoraan sellaisinaan
    else
      saldo = tilavuus;  // täyteen ja ylimäärä hukkaan!
  }

Tällaisissa tilanteissa voisi olla hyvin perusteltua toimia taulukon virheellisen indeksoinnin tapaan: vaatia ohjelmoijaa itse tarkistamaan parametrit ennen virheen vaaraa.

Vaikka poikkeuksiin tutustutaan varsinaisesti vasta jatkokurssilla, jo nyt saa "heittää poikkeuksen" IllegalArgumentExceptioni(). Eli karsitaan virheet heti alussa ja lopetetaan ohjelman suoritus siihen paikkaan ja annetaan edes kohtuullisen selkeä virheilmoitus.

Huomaa että tässä on kysymys siitä, että ohjelmoija "tietää" aina tarkastavansa itse, että parametrit ovat kunnossa. Ohjelmoija siis varautuu itse tekemäänsä virheeseen!

  public Varasto(double tilavuus, double alkuSaldo) {
    if (tilavuus <= 0.0)
      throw new IllegalArgumentException("Virheellinen tilavuus!");
    if (alkuSaldo < 0.0)
      throw new IllegalArgumentException("Negatiivinen alkusaldo!");
    if (alkuSaldo > tilavuus)
      throw new IllegalArgumentException("Liian suuri alkusaldo!");

    // kaikki kunnossa:
    this.tilavuus = tilavuus;
    this.saldo = alkuSaldo;
  }

  public void lisaaVarastoon(double maara) {
    if (maara < 0)
      throw new IllegalArgumentException("Negatiivinen lisäys!");
    if ( maara > paljonkoMahtuu() )
      throw new IllegalArgumentException("Liiallinen lisäys!");

    // kaikki kunnossa:
    saldo = saldo + maara;
  }

Saadaan kohtuullisen selviä virheilmoituksia joissa on teknisen selityksen lisäksi selvitystä virheen todellisesta syystä:

Exception in thread "main" java.lang.IllegalArgumentException: Virheellinen tilavuus!
	at Varasto.(Varasto.java:22)
	at VarastoEsittely.main(VarastoEsittely.java:51
...
Exception in thread "main" java.lang.IllegalArgumentException: Negatiivinen alkusaldo!
	at Varasto.(Varasto.java:24)
	at VarastoEsittely.main(VarastoEsittely.java:51)
...