Helsingin yliopisto / Tietojenkäsittelytieteen laitos / 581258-1 Johdatus ohjelmointiin
Copyright © 1997 Arto Wikla. Tämän oppimateriaalin käyttö on sallittu vain yksityishenkilöille opiskelutarkoituksissa. Materiaalin käyttö muihin tarkoituksiin, kuten kaupallisilla tai muilla kursseilla, on kielletty.

2.4 Loogisia lausekkeita, valintaa ja toistoa

(Muutettu viimeksi 11.9.2003. Edellinen muutos 13.1.1998)

Tässä luvussa opetellaan ehtolausekkeiden kirjoittamista ja niiden käyttöä algoritmin toiminnan ohjaamiseen.

Tähän mennessä opitut ohjelmat ovat vähän tylsiä: ne suoritetaan aina samalla tavoin ensimmäisestä lauseesta viimeiseen. Valintalauseella ja ehdollisella toistolla algoritmin toiminta saadaan riippumaan syöttötiedoista. Toistolauseet ovat tavallaan varsinainen syy siihen, että tietokoneita kannattaa käyttää algoritmien suorittamiseen: vaikkei tietokoneista mihinkään 'viisaaseen' olisikaan, ne ovat hyvin nopeita mekaanisesti toistamaan alialgoritmeja. Ja ehkä viisas ohjelmoija voi saada koneenkin vaikuttamaan viisaalta...

Loogisia lausekkeita

Jo luvussa 2.2 tutustuttiin ohimennen loogisiin muuttujiin ja vakioihin:
  boolean oikein;
  oikein = false;
Loogisia lausekkeita (eli totuusarvoisia lausekkeita) rakennetaan vertailuista, loogisista operaatioista ja toisista loogisista lausekkeista.

Vertailuoperaatioita:

   >    suurempi kuin
   >=   suurempi tai yhtäsuuri kuin
   <    pienempi kuin
   <=   pienempi tai yhtäsuuri kuin
   ==   yhtäsuuri kuin
   !=   erisuuri kuin
Kaikki vertailut tuottavat totuusarvon true tai false.

On syytä pitää mielessa sijoitusoperaation "=" ja vertailuoperaation "==" ero:

   i = 1;  // "i saa arvokseen yksi"
   i == 1  // "onko i:n arvo yksi"

Suuremmuutta tai pienemmyyttä tutkivilla operaatioilla voi vertailla vain numeerisia lausekkeita, yhtäsuuruus ja erisuuruus ovat käytettävissä monien muidenkin arvojen vertailussa.

Esimerkkejä loogisista lausekkeista - laskettu totuusarvo sijoitetaan boolean-muuttujan arvoksi (tavallisempaa kuitenkin on loogisten lausekkeiden käyttäminen ehdollisissa lauseissa, valinnassa ja toistossa):

   int i = 7;
   double d = 3.14;
   boolean b, bb, bbb;

   b   = (i < 10);     // true
   bb  = (d > 10.0);   // false
   bbb = (8 == (i+1)); // true

   bb  = (b == bbb);   // true
   bb  = (b != bbb);   // false
   
Huom: Liukulukujen vertailussa on syytä käyttää vain erisuuruusvertailuja, koska ns. pyöristysvirheiden takia yhtäsuuruusvertailut voivat antaa odottamattomia tuloksia.

Huom: String-arvojen vertailussa "==" ja "!=" eivät tarkoita sisältöjen vertailua vaan merkkijononojen sijaintipaikkojen vertailua, koska String-arvot ovat olioita. String-vertailuun käytetään erikoisvälineitä. Asia opitaan myöhemmin.

Loogisia operaatioita:

   &&    ehdollinen "ja",  "and"
   ||    ehdollinen "tai", "or"
   ^     poissulkeva tai,  "xor" (myös !=)
   !     negaatio,  "ei"   "not"
Näiden operaatioiden ns. operandit eli laskettavat voivat olla vain totuusarvoisia lausekkeita, tavallisesti vertailuja.

Operaation ehdollisuus tarkoittaa sitä, että operaation jälkimmäinen laskettava lasketaan vain, jos totuusarvo ei selviä jo ensimmäisestä! (Ehdottomat and ja or ovat "&" ja "|". Näitä käytettäessä molemmat operandit lasketaan aina.)

Esimerkkejä:

   int i = 7;
   double d = 3.14;
   boolean b, bb, bbb;

   b   = (i == 7) && (d > 10.0);  // false
   bb  = (i == 7) && (d < 10.0);  // true

   b   = (i == 7) || (d > 10.0);  // true
   bb  = (i != 7) || (d > 10.0);  // false

   bbb = !b || !!bb;              // false

   // huom:  !(i==7)  ja (i!=7) tarkoittavat samaa:
   //        "i ei ole seitsemän"

Valintoja ja koottu lause

Usein loogisia lausekkeita käytetään ohjaamaan jotakin rakenteista lausetta, esimerkiksi valintaa.

Ehdollinen lause eli if-lause on muotoa:

  if (ehto)
    lause

tai
  if (ehto)
    lause1
  else
    lause2

Ensimmäinen tarkoittaa, että jos ehto on true, lause suoritetaan. Jälkimmäinen, että jos ehto on true, lause1 suoritetaan, muuten suoritetaan lause2.

Esimerkki (käytetään lukemiseen luokan Lue välineitä):

public class KumpiSuurempi1 { /* ilmoittaa kumpi kahdesta syöttöluvusta 
                                 on suurempi                              */
  public static void main(String[] args) {
    double luku1, luku2;

    System.out.println("Anna kaksi lukua!");
    luku1 = Lue.dluku();
    luku2 = Lue.dluku();

    if (luku1 > luku2)
      System.out.println("Ensimmäinen luku on suurempi.");
    else
      System.out.println("Ensimmäinen ei ole suurempi.");
  }
}

Huom: Javassa yksinkertainen lause päättyy aina puolipisteeseen! Puolipiste on siis lauseen lopetin.

Jos valintavaihtoehto - valittava alialgoritmi - muodostuu useammasta lauseesta, nuo lauseet on koottava yhteen ns. lohkoksi (block) eli "kootuksi lauseeksi". Lohko on merkkien "{" ja "}" välissä oleva lauseiden jono. Noita merkkejä sanotaan joskus käskysulkeiksi. (Huomaa miten näitä samoja merkkejä käytetään luokan ja pääohjelman rajaamiseen! Lohkon loppusulkeen jälkeen puolipistettä ei käytetä.)

Esimerkki:

public class KumpiSuurempi2 { /* ilmoittaa kumpi kahdesta syöttöluvusta on 
                                 suurempi ja tulostaa luvuista suuremman */
  public static void main(String[] args) {
    double luku1, luku2;
    double suurempi;

    System.out.println("Anna kaksi lukua!");
    luku1 = Lue.dluku();
    luku2 = Lue.dluku();

    if (luku1 > luku2) {
       System.out.println("Ensimmäinen luku on suurempi.");
       suurempi = luku1;
    }
    else {
      System.out.println("Ensimmäinen ei ole suurempi.");
      suurempi = luku2;
    }
   System.out.println("Ei-pienemmän arvo on "+suurempi);
  }
}


Rakenteisen lauseen alilause voi olla itsekin rakenteinen lause, jonka alilause voi olla rakenteinen lause, ... Näistä nähdään lukuisia esimerkkejä myöhemmin.

Koska if-lauseita on kahdenlaisia - niihin joko liittyy tai ei liity else-osa - syntyy seuraavanlainen moniselitteisyysongelma:

Tarkoittaako

   if (a<b) if (c<d) e = f; else g=h; 
toimintaa:
   if (a<b)                    if (a<b) 
      if (c<d)                   if (c<d)
           e = f;      VAI          e = f;
   else                          else 
      g=h;                          g=h;

Toisin sanoen mihin if-lauseeseen esimerkin else-osa liittyy? (Kääntäjälle tekstin sisennyksellä ei ole viestiä! Ohjelmoijalle se on välttämätöntä!)

Ongelma on Javassa ratkaistu säännöllä: else-osa liittyy lähimpään edeltävään if-lauseeseen, johon ei ole vielä liittynyt else-osaa. Esimerkkitapauksessa siis jälkimmäinen tulkinta pätee. Ensimmäinen tulkinta saadaan kun suljetaan else-osaton if-lause omaksi lohkokseen:

  if (a<b) {
    if (c<d)
       e = f;
  }
  else
    g=h;


Melko usein algoritmeja laadittaessa ehdollisia lauseita joudutaan ketjuttamaan.

Esimerkki: Halutaan double-muuttujan a arvosta riippuen suorittaa eri lause seuraavissa tilanteissa:

Syntyy pitkä ketju if-lauseita, joiden else-osan alilause on if-lause:
   if ( a < 0 )
     lause1;
   else
     if ( a >=1  &&  a < 50 )
       lause2;
     else
       if ( a > 61 && a < 103 )
         lause3;
       else
         if ( a >= 203 && a <= 429 )
            lause4;
         else
           if ( a >= 929 && a < 1021 )
             lause5;
           else
             if ( a >= 1621 && a < 5000 )
               lause6;
             else
               lause7;

Tässä tilanteessa ohjelmoija kuitenkin mieltää arvovälit ennemminkin rinnakkaisiksi vaihtoehdoiksi, kuin valintojen alivalinnoiksi. Näin seuraava ulkoasu vastaa paremmin ohjelmoijan ajattelua:
   if ( a < 0 )
     lause1;
   else if ( a >=1  &&  a < 50 )
     lause2;
   else if ( a > 61 && a < 103 )
     lause3;
   else if ( a >= 203 && a <= 429 )
     lause4;
   else if ( a >= 929 && a < 1021 )
     lause5;
   else if ( a >= 1621 && a < 5000 )
     lause6;
   else
    lause7;

Huom: Ohjelman ulkoasu, 'lay-out', on syytä aina laatia sellaiseksi, että ohjelman toimintalogiikka on helppo hahmottaa. Alilauseet sisennetään, välilyöntejä ja tyhjiä rivejä on syytä käyttää erottamaan rakenteita toisistaan, ...

Mitä seuraava kääntäjän kannalta ihan kelvollinen ohjelmanpätkä tekee?

if (a<0) lause1; else if (a>=0&&a<51) lause2;else if(a 
>=51&&a<103) lause3;else if (a>=103&&a<429) lause4;else 
if (a>=429&&a<1021) lause5;else if (a>=1021&&a<5000)
lause6;else lause7;




Toistoja: for-lause

Yleensä ohjelmointikielissä on lause, jolla voi vaivattomasti käydä läpi joitakin arvoalueita. Usein algoritmeissa on esimerkiksi tarpeen tehdä jotakin peräkkäisillä arvoilla 0, 1, 2, ..., n.

Java-kielen for-toistolauseen muoto on

  for (alkuasetus; jatkamisehto; eteneminen)
    toistettava lause

Hyvin tavallinen ja luonteva käyttötapa for-lauseelle on arvoalueen läpikäynti:

     int i;
     for (i=0; i<6; ++i)
       System.out.println(i);
Lauseet tulostavat:
0
1
2
3
4
5

Huom: Tämän esimerkin toisto on tyylikkäämpää ohjelmoida toisin. (Mitä "tyylikkyys" tarkoittaa ohjelmoinnissa ja miksi sitä kannattaa tavoitella?):
     for (int i=0; i<6; ++i)
       System.out.println(i);
For-lauseen alkuasetuksessa voidaan siis määritellä muuttuja! Tällainen muuttuja on käytettävissä vain tuon for-lauseen sisällä.

For-lauseen toistama alialgoritmi voi toki sisältää for-toiston:

     for (int i=0; i<3; ++i)
       for (int j=0; j<2; ++j)
         System.out.println(i+"  "+j);
Tulostus:
0  0
0  1
1  0
1  1
2  0
2  1 

For-lauseessa voidaan käyttää int-tyyppisen "askelmuuttujan" sijaan myös vaikkapa double-muuttujaa. Ja eteneminen voi olla muutakin kuin ykkösellä kasvattamista tai vähentämistä:
     //Tulostetaan piin 20 pienemmät monikerrat:

     for (double piit = 3.14; piit < 20; piit+=3.14)
       System.out.println(piit);
Tulostus:
3.14
6.28
9.42
12.56
15.700000000000001
18.84

(Yllä nähdään esimerkki ns. pyöristysvirheestä: kun lasketaan 12.56+3.14 saadaan luku, joka tietokoneen esitystavalla onkin hieman suurempi kuin 15.70!)

For-lauseen osat (alkuasetus, jatkamisehto, eteneminen, toistettava lause) voivat olla monimutkaisempiakin. Toistaiseksi tyydymme käyttämään for-lausetta arvoalueiden läpikäyntiin.

Toistoja: while-lause

Ns. alkuehtoisen toiston idea on, että ennen jokaisen toistokerran aloittamista lasketaan toistoehto. Jos ehto sallii, toistetaan toistettava alialgoritmi kerran ja taas tutkitaan toistoehto, jne.

Tämä toistotapa soveltuu tilanteeseen, jossa toistokertoja tarvitaan nolla tai enemmän, alialgoritmia ei siis välttämättä toisteta kertaakaan.

While-toisto on muotoa

   while (jatkamisehto) 
     lause
Huom: toistettavan alialgoritmin on vaikutettava toiston ehtoon siten, että ehto joskus saa arvon false! (Miten muuten käy?)

Edellä nähty for-esimerkki:

     int i;
     for (i=0; i<6; ++i)
       System.out.println(i);
voidaan ohjelmoida while-lauseella:
     int i=0;
     while (i<6) {
       System.out.println(i);
       ++i;
     }

Laaditaan esimerkkiohjelma Tuplatw.java, joka tulostaa syöttölukuja kaksinkertaisina. Tuplattavien lukujen lukumäärää ei tiedetä etukäteen. Kun käyttäjä syöttää nollan, ohjelman suoritus päättyy (käytetään lukujen lukemiseen luokan Lue välineitä):
public class Tuplatw {
  public static void main(String[] args){

    System.out.println("Lukujen tuplausohjelma, nolla lopettaa");
    int luku = Lue.kluku();
    while (luku != 0) {
      System.out.println("Tuplana: "+luku*2);
      luku = Lue.kluku();
     }
    System.out.println("Siinä ne olivat.");
  }
} 
Ohjelman käyttö voi näyttää vaikkapa seuraavalta:
Lukujen tuplausohjelma, nolla lopettaa
3
Tuplana: 6
-8
Tuplana: -16
0
Siinä ne olivat.

[Sama ohjelma voidaan toteuttaa for-lauseella (Tuplatf1.java):
public class Tuplatf1 {
  public static void main(String[] args){

    System.out.println("Lukujen tuplausohjelma, nolla lopettaa");

     for  (int luku=Lue.kluku();  // alustus
           luku != 0;             // jatkamisehto
           luku=Lue.kluku()){     // eteneminen

        System.out.println("Tuplana: "+luku*2);
     }
    System.out.println("Siinä ne olivat.");
  }
} 
Tämän ratkaisun tyylikkyys on makuasia. (Vielä eksoottisempi ratkaisu on Tuplatf2.java.)
]

Toistoja: do-while -lause

Ns. loppuehtoisen toiston idea on, että ensin suoritetaan toistettava alialgoritmi kerran. Sitten lasketaan toistoehto. Jos ehto sallii, toistetaan toistettava alialgoritmi uudelleen ja taas tutkitaan toistoehto, jne.

Tämä toistotapa soveltuu tilanteeseen, jossa toistokertoja tarvitaan yksi tai enemmän, toistettava alialgoritmi siis suoritetaan ainakin kerran.

Do-while -toisto on muotoa

   do
     lause
   while (jatkamisehto)
Huom: toistettavan alialgoritmin on vaikutettava toiston ehtoon siten, että ehto joskus saa arvon false! (Miten muuten käy?)

Edellä nähdyt for- ja while-esimerkit

     int i;
     for (i=0; i<6; ++i)
       System.out.println(i);
ja
     int i=0;
     while (i<6) {
       System.out.println(i);
       ++i;
     }
voidaan ohjelmoida do-while -toistolla:
     int i=0;
     do {
       System.out.println(i);
       ++i;
     } while (i<6);

Yksi tyypillinen käyttö loppuehtoiselle toistolle on syöttötietojen tarkistaminen. Laaditaan sovellus Pariton.java, joka välttämättä haluaa parittoman luvun:
public class Pariton {
  public static void main(String[] args){

    int luku;
    boolean parillinen;

    do {
      System.out.println("Anna pariton luku!");
      luku = Lue.kluku();
      parillinen = (luku%2 == 0);
      if (parillinen) 
        System.out.println("Eihän "+luku+" ole pariton ...");
    } while (parillinen);
    System.out.println("Hienoa, "+luku+" on pariton!");
  }
}
Ohjelman käyttö näyttää vaikkapa seuraavalta:
Anna pariton luku!
468
Eihän 468 ole pariton ...
Anna pariton luku!
-32
Eihän -32 ole pariton ...
Anna pariton luku!
9
Hienoa, 9 on pariton!

Ohjelmaesimerkkejä: keskiarvo

Laaditaan kolme erilaista esimerkkisovellusta lukujen keskiarvon laskentaan. Ensimmäisessä käyttäjältä kysytään aluksi lukujen lukumäärä, toisessa lukujen loppuminen ilmaistaan ns. loppumerkillä, kolmas pyytää luvut yksitellen.

Esimmäinen ohjelmaversio (Karvo1.java) kysyy lukujen lukumäärän käyttäjältä.


public class Karvo1 { /* Sovellus keskiarvojen laskentaan, AW-97*/
  public static void main(String[] args) {

    int lukujenLkm = 0;
    boolean lkmOk;

    double luku,
           lukujenSumma = 0,
           lukujenKarvo;

    System.out.println("\n**** Keskiarvon laskenta ****\n");

   // Monenko luvun keskiarvo lasketaan 
   // (tarkistetaan, että määrä ei ole negatiivinen):

    do {
      System.out.print("Monenko luvun keskiarvo lasketaan? ");
      lukujenLkm = Lue.kluku();
      lkmOk = (lukujenLkm >= 0);   // nollakin kelpaa!
      if (!lkmOk)
        System.out.println("Virhe: negatiivinen ei kelpaa!");
    } while (!lkmOk); 


   // Lukujen summan laskenta:

    for (int monesko = 1; monesko <= lukujenLkm; ++monesko) {
      System.out.print("Anna "+monesko+". luku: ");
      luku = Lue.dluku();
      lukujenSumma += luku;
    }

  // Keskiarvon tulostus (estetään 0:lla jakaminen):

    if (lukujenLkm == 0)
      System.out.println("\nEi lukuja, ei keskiarvoa.\n");
    else {
      lukujenKarvo = lukujenSumma/lukujenLkm;
      System.out.println("\nKeskiarvo on "+lukujenKarvo+".\n");
    }
  }
}
Ohjelman suoritus voi näyttää seuraavanlaiselta:

**** Keskiarvon laskenta ****

Monenko luvun keskiarvo lasketaan? -23
Virhe: negatiivinen ei kelpaa!
Monenko luvun keskiarvo lasketaan? 3
Anna 1. luku: 40.1
Anna 2. luku: -3
Anna 3. luku: 9.1

Keskiarvo on 15.4.
 

Toinen ohjelmaversio (Karvo2.java) osaa laskea vain ei-negatiivisten lukujen keskiarvoja, mutta se on edellistä versiota joustavampi sikäli, että lukujen lukumäärää ei tarvitse tietää ennakolta. Ensimmäinen vastaantuleva negatiivinen luku on ohjelmalle ns. loppumerkki, jolla ohjelmalle ilmoitetaan, että syöttölukuja ei enää anneta lisää. Loppumerkki itse ei enää kuulu lukuihin, joiden keskiarvo lasketaan.
public class Karvo2 { /* Sovellus ei-negatiivisten lukujen 
                         keskiarvojen laskentaan, AW-97*/

  public static void main(String[] args) {

    int lukujenLkm = 0;

    double luku,
           lukujenSumma = 0,
           lukujenKarvo;

    System.out.println("\n**** Keskiarvon laskenta ****\n");

   // Käyttöohjeen tulostus:

    System.out.println("Syötä luvut, joiden keskiarvon haluat laskea!\n"+
                        "Negatiivinen luku ilmoittaa lukujen loppuvan.\n");


   // Lukujen summan laskenta:

    luku = Lue.dluku();      // 1. luku (voi olla jo loppumerkki!)
    while (luku >= 0) {
      lukujenSumma += luku;
      ++lukujenLkm;
      luku = Lue.dluku();    // seuraava luku tai loppumerkki
    }

  // Keskiarvon tulostus (estetään 0:lla jakaminen):

    if (lukujenLkm == 0)
      System.out.println("\nEi lukuja, ei keskiarvoa.\n");
    else {
      lukujenKarvo = lukujenSumma/lukujenLkm;
      System.out.println("\nKeskiarvo on "+lukujenKarvo+".\n");
    }
  }
}
Ohjelman käyttö näyttää vaikkapa seuraavanlaiselta:

**** Keskiarvon laskenta ****

Syötä luvut, joiden keskiarvon haluat laskea!
Negatiivinen luku ilmoittaa lukujen loppuvan.

21.2
0.02
33
2.5
-1

Keskiarvo on 14.18.
 

Kolmas ohjelmaversio kysyy ennen jokaista syöttölukua, halutaanko lukuja vielä syöttää. Kysymykseen vastaaminen on tässä esimerkissä vielä kömpelöä, koska merkkijonojen vertailua ei vielä ole opittu.

public class Karvo3 { /* Sovellus keskiarvojen laskentaan,
                         kysellään "onko vielä lukuja"     */

  public static void main(String[] args) {

    int lukujenLkm = 0,
        jatkuu;

    double luku,
           lukujenSumma = 0,
           lukujenKarvo;

    System.out.println("\n**** Keskiarvon laskenta ****\n");

   // Lukujen summan laskenta:

    do {
      System.out.println("Onko vielä lukuja? (1:on, muu luku: ei)");
      jatkuu = Lue.kluku();
      if (jatkuu == 1) {
        System.out.println("Anna " + (lukujenLkm + 1) + ". luku");
        luku = Lue.dluku();
        lukujenSumma += luku;
        ++lukujenLkm;
      }
    } while (jatkuu == 1);

  // Keskiarvon tulostus (estetään 0:lla jakaminen):

    if (lukujenLkm == 0)
      System.out.println("\nEi lukuja, ei keskiarvoa.\n");
    else {
      lukujenKarvo = lukujenSumma/lukujenLkm;
      System.out.println("\nKeskiarvo on "+lukujenKarvo+".\n");
    }
  }
}

Ohjelman suoritus voi näyttää vaikkapa seuraavalta:
**** Keskiarvon laskenta ****

Onko vielä lukuja? (1:on, muu luku: ei)
1
Anna 1. luku
76
Onko vielä lukuja? (1:on, muu luku: ei)
1
Anna 2. luku
-4
Onko vielä lukuja? (1:on, muu luku: ei)
0

Keskiarvo on 36.0.

Luvussa 2.7 opitaan, miten vastaus voitaisiin toteuttaa String-arvona:
    ...
    String jatkuu;
    ...
    do {
      System.out.println("Onko vielä lukuja? (kyllä/ei)");
      jatkuu = Lue.rivi();

      if (jatkuu.equals("kyllä")) {
        System.out.println("Anna " + (lukujenLkm + 1) + ". luku");
        luku = Lue.dluku();
        lukujenSumma += luku;
        ++lukujenLkm;
      }
    } while (jatkuu.equals("kyllä"));
    ...

Suoritusesimerkki:
Onko vielä lukuja? (kyllä/ei)
kyllä
Anna 1. luku
314
Onko vielä lukuja? (kyllä/ei)
kyllä
Anna 2. luku
51
Onko vielä lukuja? (kyllä/ei)
kyllä
Anna 3. luku
-23
Onko vielä lukuja? (kyllä/ei)
ei

Keskiarvo on 114.0.


Takaisin luvun 2 sisällysluetteloon.