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

6 Ohjelmointitekniikkaa: ArrayList, lausekkeita, keskeytyslauseita,komentotulkki, rekursio

(Muutettu viimeksi 31.10.2011, sivu perustettu 27.10.2010. Arto Wikla)

Tässä luvussa esitellään lyhyesti sekalainen joukko Java-ohjelmoinnissa tarjolla olevia tekniikoita ja välineitä.

Vaihtuvan mittainen taulukko: ArrayList<E>

Javassa on melkoinen joukko erilaisia kokoelmaluokkia, joiden ilmentymät tarjoavat kehittyneitä välineitä tietojoukkojen käsittelyyn.

Yksi hyvin käyttökelpoinen opetellaan jo nyt, toinen kurssin loppupuolella. Luokka ArrayList<E> toteuttaa näennäisesti "vaihtelevanmittaiset taulukot", joiden komponenttityyppi annetaan ArrayList-oliota luotaessa. Komponentit numeroidaan tavallisen taulukon tapaan indeksein alkaen nollasta. Komponenttien lisääminen ja poistaminen muuttaa lisäys- ja poistokohtaa seuraavien komponenttien indeksejä.

ArrayList<E> on yksi Javan ns. geneerisistä tyypeistä. Se tarkoittaa, että kääntäjälle kerrotaan ns. tyyppiparametrin avulla, millaisista alkioista kokoelma muodostuu. Tuon parametrin avulla kääntäjä osaa säästää ohjelmoijan monenlaiselta vaivannäöltä. [Tämä selitys on hieman yksinkertaistava ja vain osatotuus...]

Tyyppiparametri annetaan ArrayList-oliota luotaessa kulmasulkeissa. Javassa tyyppiparametri saa olla vain jokin luokka. Erityisesti siis alkeistyypit (int, double, boolean, ...) eivät kelpaa. Jos tällaisia arvoja halutaan tallettaa geneeriseen kokoelmaan, tyyppiparametriksi on annettava alkeistyyppiä vastaava ns. käärepaperiluokka ("wrapper"): Integer, Double, Boolean, ...

ArrayList-muuttujia määritellään ja ArrayList-oliota luodaan tähän tapaan:

ArrayList<Integer> kLukuja   = new ArrayList<Integer>();
ArrayList<Double>  dLukuja   = new ArrayList<Double>();
ArrayList<Boolean> totuuksia = new ArrayList<Boolean>();
ArrayList<String>  mJonoja   = new ArrayList<String>();
ArrayList<Varasto> varastoja = new ArrayList<Varasto>();
...

Kaikki Java-APIn valmiit luokat ja kaikki itse ohjelmoidut luokat kelpaavat ArrayList-olion komponenttityypiksi!

Joitakin ArrayList-luokan välineitä:

Esimerkki: Ohjelma lukee ensin joukon lukuja. Sitten lukuja voi poistaa. Lopuksi ohjelma tulostaa jäljelle jääneiden lukujen summan:

import java.util.*;   // täältä löytyy ArrayList-luokka

public class ArrayListEsim {

  private static Scanner lukija = new Scanner(System.in);

  public static void main(String[] args) {

    ArrayList<Integer> taulu = new ArrayList<Integer>();

    int luku;

    System.out.println("Syötä lukuja, nolla lopettaa.");    
    luku = lukija.nextInt();
    while (luku != 0) {
      taulu.add(luku);
      luku  = lukija.nextInt();
    }

    System.out.println(taulu);

    System.out.println("Syötä poistettavia lukuja, nolla lopettaa.");
    luku = lukija.nextInt();
    while (luku != 0) {
      boolean oli = taulu.remove(new Integer(luku));
      if (oli)         // remove(int index) poistaisi indeksin kohdalta!
        System.out.println(taulu);
      else 
        System.out.println("Ei löydy");
      luku = lukija.nextInt();
    }
 
    System.out.println(taulu);
    System.out.println("Alkioiden summa = " + summa(taulu));
  }

  private static int summa(ArrayList<Integer> tau) {
    int summa = 0;
    for (int luku: tau) // for each!
        summa += luku;  // vaihtoehto:
    return summa;       //   for (int i=0; i < tau.size(); ++i)
                        //     summa += tau.get(i);
  }                     //
}

Esimerkki: Omat luokat – kuten jo peruskurssilla opittiin – ovat tyyppejä. Samoin myös ArrayList on tyyppi!

import java.util.*;

public class VarastoLista {
  public static void main(String[] args) {

    Varasto mehua  = new Varasto(100.0);
    Varasto olutta = new Varasto(100.0, 20.2);

    ArrayList<Varasto> varastoja = new ArrayList<Varasto>();

    varastoja.add(mehua);
    varastoja.add(olutta);

    varastoja.get(0).lisaaVarastoon(50.7);
    varastoja.get(0).otaVarastosta(3.14);

    System.out.println("Mehuvarasto: " + varastoja.get(0));
    System.out.println("Koko lista: " + varastoja);
  }

  // myös parametri voi (opittuun tapaan) toki olla tyyppiä Varasto

  private static void sitaSunTata(Varasto v) {
    // ...
  }

  // eikä mikään estä tietenkään ArrayList<Varasto>-tyyppistä parametria

  private static void sitaSunTata2(ArrayList<Varasto> vv) {
    // ...
  }

  // toki se voi olla myös paluuarvona -- tyyppi kun tyyppi ...

  private static ArrayList<Varasto> sitaSunTata3() {
    // ...
    return null; // jotain ei-void-metodista pitää aina palauttaa...
  }

  // ...
}

Ehdollinen lauseke

Javassa on näppärä väline valita kahdesta lausekkeesta jommankumman arvo:

(totuusarvo ? lauseke1 : lauseke2)

Totuusarvo lasketaan. Jos se on true, ehdollisen lausekkeen arvo on lauseke1, muuten lauseke2. (Sulkeet eivät ole välttämättömät, mutta suositeltavat.)

Joko lauseke1:n täytyy olla tyyppiä, joka ilman eksplisiittistä tyyppimuunnosta on sijoitettavissa lauseke2:n tyyppisen muuttujan arvoksi tai päinvastoin. Ehdollisen lausekkeen tyyppi on valittavien lausekkeiden tyypeistä laajempi. Tavallisesti toki molemmat valintavaihtoehdot ovat samaa tyyppiä.

Esimerkiksi ("a:lle ei-pienempi arvoista b ja c") ohjelmoidaan if-lauseella tuttuun tapaan:

if (b < c)
  a = c;
else
  a = b;

Ehdollista lauseketta käyttäen saman voi sanoa toisinkin:

a = (b < c  ?  c  :  b);

On makuasia, kumpi tapa on selkeämpi, mutta ei ole makuasia, että valintaperusteena on oltava selkeys!

Lausekelauseet

Eräitä Javan lausekkeita – arvon ilmauksia – voi käyttää lauseiden – komentojen – tapaan. Lausekkeen sivuvaikutukset tekevät jotakin (hyödyllistä?) ja lausekkeen varsinainen arvo jätetään kylmästi käyttämättä. Lauseke kirjoitetaan sellaisenaan ja päätetään puolipisteeseen.

Vain seuraavia lausekkeita voi käyttää lauseina:

Huom: Muita lausekkeita ei siis voi käyttää lauseina!

Keskeytyslauseet break, continue ja return

Keskeytyslauseella keskeytetään rakenteisen lauseen suoritus:

Keskeytyslauseet voivat joskus olla näppäriä esimerkiksi virhetilanteiden hoitamisessa. Ohjelman normaalin etenemisen ohjaamisessa niitä on syytä käyttää harkiten.

On kuitenkin yksi tyypillinen tilanne, jossa keskeytyslauseella ohjelman toimintalogiikka saadaan selvästi suoraviivaisemmaksi: "puolitoistakertainen toisto", johon tutustuttiin jo peruskurssilla.

Kertauksena esimerkki syötteiden tarkastamisesta while-, do-while- ja 1.5-toistolla.

While-ratkaisussa ensimmäinen syöttölukutarjokas joudutaan lukemaan ennen toiston aloittamista ja uusi tarjokas toistettavan alialgoritmin lopussa:

import java.util.Scanner;

public class Pariton {
  private static Scanner lukija = new Scanner(System.in);
  public static void main(String[] args) {

    System.out.println("Anna pariton luku.");
    int luku = lukija.nextInt();

    while (luku % 2 == 0) {
      System.out.println("Eihän luku " + luku + " ole pariton!");
      System.out.println("Yritä uudelleen");
      luku = lukija.nextInt();
    }

    System.out.println("Kyllä! " + luku + " todella on pariton.");
  }
}

Do-while-tekniikalla parillisuutta joudutaan tutkimaan sekä virheilmoituksen ehtona että toistoehtona:

import java.util.Scanner;

public class Pariton2 {
  private static Scanner lukija = new Scanner(System.in);
  public static void main(String[] args) {
    int luku;
    boolean parillinen;

    do {
      System.out.println("Anna pariton luku!");
      luku = lukija.nextInt();
      parillinen = (luku%2 == 0);
      if (parillinen) {
        System.out.println("Eihän "+luku+" ole pariton ...");
        System.out.println("Yritä uudelleen");
      }
    } while (parillinen);
    System.out.println("Hienoa, "+luku+" on pariton!");
  }
}

Keskeyttämällä "ikuinen toisto" toistettavan alialgoritmin keskellä ohjelman rakenne yksinkertaistuu:

import java.util.Scanner;

public class Pariton3 {
  private static Scanner lukija = new Scanner(System.in);
  public static void main(String[] args) {

    int luku;

    while (true) {  // keskeytetään toiston sisällä break-lauseella
      System.out.println("Anna pariton luku.");
      luku = lukija.nextInt();

      if (luku % 2 != 0) break;  // <<<<<<<<< toiston keskeytys <<<<<<<<<

      System.out.println("Eihän luku " + luku + " ole pariton!");
      System.out.println("Yritä uudelleen");
    }

    System.out.println("Kyllä! " + luku + " todella on pariton.");
  }
}

Tätä tekniikkaa käytettäessä on syytä kommentein selventää ohjelman lukijalle, mistä on kyse, ja erityisesti osoittaa keskeytyskohdat selkeästi.

Tätä tapaa ohjelmoida toistorakenne kutsutaan joskus "1.5-kertaiseksi toistoksi". Keskeytyskohtia voi toki olla useampiakin:

while (true) {
  if (alkuehto) break;
  lauseita...
  if (ehto_1) break;
  lisää lauseita ...
  if (ehto_2) break;
  ...
  if (ehto_n) break;
  lisää lauseita ...
  if (loppuehto) break;
}

[Ohjelmointikielessä ei itse asiassa tarvittaisi mitään muita toistolauseita – tällä voisi hoitaa kaikki tapaukset.]

[Valintalause switch]

Java-kieleen on (valitettavasti) valittu "monivalintalauseeksi" melko alkeellinen muoto switch-lauseesta. Sen käyttöä ei kurssilla vaadita, mutta koska se voi joskus tulla vastaan muiden ohjelmia korjatessa, tässä on aiheesta lyhyt selostus. Luennoilla ja harjoituksissa tämä kohta ohitetaan!

Switch-lause on muodoltaan seuraavanlainen:

switch (lauseke) {
  case vakio1: lause1;
               break;
  case vakio2: case vakio3:
               lause2;
               lause3;
               break;
  default:     lause4;
               break;
  }

Komentotulkki

Klassinen tapa toteuttaa vuorovaikutteisen ohjelman käyttöliittymä on komentotulkki. Ideana on että kone tarjoaa kehotemerkin, "promptin", ja käyttäjä kirjoittaa komennon, jonka kone toteuttaa. Toteutuksen jälkeen kone tarjoaa taas kehotemerkin. Tyypillinen esimerkki komentotulkkilogiikasta on käyttöjärjestelmän ns. "shell".

Komentotulkkilogiikan voi ohjelmoida seuraavaan tapaan:

import java.util.Scanner;

public class Komentotulkki {

  private static Scanner lukija = new Scanner(System.in);

  private static void teeA() {
    System.out.println("Toteutan operaation a");
    // ... tehdään työt ...
  }
  private static void teeB() {
    System.out.println("Toteutan operaation b");
    // ... tehdään työt ...
  }
  private static void teeC() {
    System.out.println("Toteutan operaation c");
    // ... tehdään työt ...
  }
  private static void teeD() {
    System.out.println("Toteutan operaation d");
    // ... tehdään työt ...
  }

  public static void main(String[] args) {
    char komento;

    while (true) {  // "ikuinen" toisto keskeytetään break-lauseella

       // kirjoitetaan kehotemerkki eli "prompti"
       System.out.print("kone> ");

       // eristetään komento             //  Huom: Jos komentoon
       String rivi = lukija.nextLine();  //  liittyisi parametreja,
       if ( rivi.length() > 0 )          //  nekin löytyisivät
         komento = rivi.charAt(0);       //  muuttujasta rivi!
       else
         komento = 'ö';  // mikä vain virheellinen merkki!

       // valitaan operaatio
       if (komento=='a')
         teeA();
       else if (komento=='b')
         teeB();
       else if (komento=='c')
         teeC();
       else if (komento=='d')
         teeD();
       else if (komento=='l')
         break; // ************** katkaistaan "ikuinen" toisto ****************
       else 
         System.out.println("Virheellinen komento!");

     } // end of while (true)
  }
}

Rekursiosta

Metodi voi olla myös ns. rekursiivinen. Se tarkoittaa, että metodi kutsuu itseään! Metodi ei tietenkään saa kutsua itseään ehdoitta vaan joskus kutsuketjun on päätyttävä. (Miten käy, jos vain kutsutaan ja kutsutaan?)

Laaditaan esimerkkinä rekursiivinen metodi Fibonaccin lukujen tulostamiseen:

public class Fibo {

  private static long fibo(int i) {
     if (i<3)
       return 1;
     else
       return fibo(i-1)+fibo(i-2);
  }

  public static void main(String[] args) {
    for (int i=1; i<10 ; ++i)
    System.out.println(i+ ". fibo on " + fibo(i));
  }
}

Odotetusti sovellus tulostaa:

1. fibo on 1
2. fibo on 1
3. fibo on 2
4. fibo on 3
5. fibo on 5
6. fibo on 8
7. fibo on 13
8. fibo on 21
9. fibo on 34

Tämä on erittäin tehoton tapa laskea Fibonaccin lukuja! Aikavaativuus on eksponentiaalinen. On vaikea laatia vieläkin tehottomampia algoritmeja! Kyse ei ole siitä, että Fibonaccin lukujen laskeminen rekursiivisesti sinänsä olisi tehotonta. Kyse on siitä, että nähdyssä tavassa samoja asioita lasketaan yhä uudelleen ja uudelleen. Paljon parempia rekursiivisiakin ratkaisuja löytyy.

Mutta on myös tilanteita, joissa rekursio on hyvä ratkaisu, esimerkkeinä vaikkapa binäärihaku ja ns. pikajärjestäminen.

Esimerkkinä binäärihaku rekursiivisena:

private static int binHaeRec(int[] taulu, int haettava,
                             int vasen, int oikea)      {

  int keskiKohta = (vasen+oikea)/2;
  if (vasen > oikea)
    return -1;
  if (haettava == taulu[keskiKohta]) 
    return keskiKohta;
  if (haettava < taulu[keskiKohta])
    return binHaeRec(taulu, haettava, vasen, keskiKohta-1);
  else
    return binHaeRec(taulu, haettava, keskiKohta+1, oikea);
  }

Kutsu on esimerkiksi:

int[] a = {10, 20, 30, 40, 50};

System.out.println(binHaeRec(a, 30, 0, a.length-1));

Huom: Yleensä hakualgoritmille tavataan antaa parametrina vain taulukko ja haettava arvo, ei hakualueen ala- ja yläindeksiä kuten yllä. Ja niin on myös hyvä tehdä! Tämän kurssin kannnalta asialla ei ole suurempaa merkitystä, mutta jatkossa rekursiivinen binäärihaku on syytä ohjelmoida seuraavaan tyyliin:

public static int binHaeRec(int[] taulu, int haettava) {
  binHaeRecAlueelta(taulu, haettava, 0, taulu.length-1)
}

private static int binHaeRecAlueelta(int[] taulu, int haettava,
                             int vasen, int oikea)      {

  int keskiKohta = (vasen+oikea)/2;
  // ... kuten yllä
  // ...
  if (haettava < taulu[keskiKohta])
    return binHaeRecAlueelta(taulu, haettava, vasen, keskiKohta-1);
  else
    return binHaeRecAlueelta(taulu, haettava, keskiKohta+1, oikea);
  }

Ja silloin kutsu kirjoitetaan tyyliin:

int[] a = {10, 20, 30, 40, 50};

System.out.println(binHaeRec(a, 30));

Esimerkki: pikajärjestäminen (Quicksort):

private static void pikaJarjesta(int[] taulu, int alku, int loppu) {
  int jakoAlkio, vasen, oikea;

  vasen = alku;
  oikea = loppu;

  jakoAlkio = taulu[(alku+loppu)/2];
  do {

     while (taulu[vasen]<jakoAlkio)
       ++vasen;
     while (jakoAlkio<taulu[oikea])
       --oikea;
     if (vasen<=oikea) {
       int apu = taulu[vasen];
       taulu[vasen] = taulu[oikea];
       taulu[oikea] = apu;
       ++vasen;
       --oikea;
     }
  } while (vasen < oikea);

  if (alku < oikea)
    pikaJarjesta(taulu, alku, oikea);
  if (vasen < loppu)
    pikaJarjesta(taulu, vasen, loppu);
}

Kutsu on esimerkiksi:

int[] a = {40, 20, 50, 10, 30};

pikaJarjesta(a, 0, a.length-1);

Huom: Yleensä järjestämisalgoritmille tavataan antaa parametrina vain järjestettävä taulukko, ei järjestettävän osan ala- ja yläindeksiä kuten yllä. Ja niin on myös hyvä tehdä! Tämän kurssin kannnalta asialla ei ole suurempaa merkitystä, mutta jatkossa pikajärjestäminen on syytä ohjelmoida seuraavaan tyyliin:


public static void pikaJarjesta(int[] taulu) {
  pikaJarjestaAlue(taulu, 0, taulu.length-1);
}

private static void pikaJarjestaAlue(int[] taulu, int alku, int loppu) {
  int jakoAlkio, vasen, oikea;

  vasen = alku;
  oikea = loppu;

  jakoAlkio = taulu[(alku+loppu)/2];
  do {
     // ... kuten yllä
     }
  } while (vasen < oikea);

  if (alku < oikea)
    pikaJarjestaAlue(taulu, alku, oikea);
  if (vasen < loppu)
    pikaJarjestaAlue(taulu, vasen, loppu);
}

Ja kutsu silloin kirjoitetaan tyyliin:

int[] a = {40, 20, 50, 10, 30};

pikaJarjesta(a);

Vaikka pikajärjestäminen onkin pahimmassa tapauksessa aikavaativuudeltaan O(n2) se on aivan ylivoimainen verrattuna vaikkapa lisäysjärjestämiseen!

Voit itse tutkia tilannetta ohjelmalla VertaileLisQuick.java (nanosekuntiversio: VertaileLisQuickB.java). Ohjelmoinnin perusteet -kurssilla vertailtiin vastaavalla tavalla alkeellisempia järjestämisalgoritmeja: VertaileJarjAlgoritmeja.java (nanosekuntiversio: VertaileJarjAlgoritmejaB.java).

Keskimäärin pikajärjestäminen onkin O(n*log n). Ja se "pahin tapaus" on erittäin harvinainen.

Myös ns. keskinäinen rekursio (mutual recursion) on mahdollista. Kyse on siitä, että metodi kutsuu toista metodia, joka puolestaan kutsuu ensin mainittua. Monimutkaisemmatkin rekursiosuhteet ovat mahdollisia &ndash ja sellaisia myös käytetään esimerkiksi ohjelmointikielten jäsentäjien ohjelmoinnissa.

Esimerkki kahden metodin keskinäisestä rekursiosta:

public class AbraKadAbra {

  private static void abra (String jono) {
    if (jono.length() < 44) {
      jono += "ABRA";
      System.out.println("Abra ennen kadin kutsua  : "+jono);
      kad (jono);
      System.out.println("Abra jälkeen kadin kutsun: "+jono);        
    }
  }

  private static void kad (String jono) {
    if (jono.length() < 44) {
      jono += "KAD";
      System.out.println("Kad ennen abran kutsua   : "+jono);
      abra (jono);
      System.out.println("Kad jälkeen abran kutsun : "+jono);
    }
  }

  public static void main(String[] args) {
    abra("");
  }
}

Tämä taianomainen sovellus tulostaa:

Abra ennen kadin kutsua  : ABRA
Kad ennen abran kutsua   : ABRAKAD
Abra ennen kadin kutsua  : ABRAKADABRA
Kad ennen abran kutsua   : ABRAKADABRAKAD
Abra ennen kadin kutsua  : ABRAKADABRAKADABRA
Kad ennen abran kutsua   : ABRAKADABRAKADABRAKAD
Abra ennen kadin kutsua  : ABRAKADABRAKADABRAKADABRA
Kad ennen abran kutsua   : ABRAKADABRAKADABRAKADABRAKAD
Abra ennen kadin kutsua  : ABRAKADABRAKADABRAKADABRAKADABRA
Kad ennen abran kutsua   : ABRAKADABRAKADABRAKADABRAKADABRAKAD
Abra ennen kadin kutsua  : ABRAKADABRAKADABRAKADABRAKADABRAKADABRA
Kad ennen abran kutsua   : ABRAKADABRAKADABRAKADABRAKADABRAKADABRAKAD
Abra ennen kadin kutsua  : ABRAKADABRAKADABRAKADABRAKADABRAKADABRAKADABRA
Abra jälkeen kadin kutsun: ABRAKADABRAKADABRAKADABRAKADABRAKADABRAKADABRA
Kad jälkeen abran kutsun : ABRAKADABRAKADABRAKADABRAKADABRAKADABRAKAD
Abra jälkeen kadin kutsun: ABRAKADABRAKADABRAKADABRAKADABRAKADABRA
Kad jälkeen abran kutsun : ABRAKADABRAKADABRAKADABRAKADABRAKAD
Abra jälkeen kadin kutsun: ABRAKADABRAKADABRAKADABRAKADABRA
Kad jälkeen abran kutsun : ABRAKADABRAKADABRAKADABRAKAD
Abra jälkeen kadin kutsun: ABRAKADABRAKADABRAKADABRA
Kad jälkeen abran kutsun : ABRAKADABRAKADABRAKAD
Abra jälkeen kadin kutsun: ABRAKADABRAKADABRA
Kad jälkeen abran kutsun : ABRAKADABRAKAD
Abra jälkeen kadin kutsun: ABRAKADABRA
Kad jälkeen abran kutsun : ABRAKAD
Abra jälkeen kadin kutsun: ABRA