inhoudstafel en auteursrecht
* STER *


3. Controlestructuren

3.1 Sequentie

Java-opdrachten worden uitgevoerd in de volgorde waarin ze in het opdrachtenblok voorkomen, van boven naar onder en van links naar rechts. Het einde van een regel heeft geen bijzondere betekenis (nauwkeuriger: het wordt als een spatie beschouwd). Of we dus verscheiden opdrachten op één regel plaatsen, of één opdracht uitsmeren over verschillende regels, maakt voor de compiler niets uit.

Goede programmeurs gebruiken de regelindeling om het programma leesbaar te maken en te houden. Die kwaliteitszorg kan nog worden verbeterd door het gebruik van commentaar. Elke tekst tussen /* en */ of tussen // (dubbele schuine streep) en het einde van een regel wordt door de compiler genegeerd.

Voorbeeld

De volgende opdrachten hebben tot gevolg dat de inhoud van de veranderlijken x en y van plaats verwisseld wordt.

temp = x;
x = y;
y = temp;

Oefening 3.1

Waarom kan bovenstaand voorbeeld niet vereenvoudigd worden tot de volgende twee regels ?

y = x;
x = y;

Oplossing

Oefening 3.2

Breng uitgebreide commentaar aan bij de code van bovenstaand voorbeeld.

Oplossing

3.2 Selectie

In een voorwaardelijke opdracht wordt de uitvoering van een opdracht of een blok afhankelijk gemaakt van het al dan niet waar zijn van een of andere voorwaarde.

if (<voorwaarde>)
  <opdracht>

if (<voorwaarde>) {
  <reeks opdrachten>
}

De <voorwaarde> bestaat in het eenvoudigste geval uit een vergelijking van twee uitdrukkingen met behulp van een vergelijkingsoperator, dat is een van de volgende bewerkingen:

bewerking betekenis
== is gelijk aan
!= is verschillend van
< is strikt kleiner dan
> is strikt groter dan
<= is kleiner dan of gelijk aan
>= is groter dan of gelijk aan

Voorbeeld

De volgende opdracht verhoogt de waarde van getal met één eenheid als de oorspronkelijke waarde even is (als de rest bij deling door 2 nul bedraagt).

if (getal % 2 == 0)
  getal = getal + 1;

Optioneel kan de voorwaardelijke opdracht nog woorden uitgebreid met een else-deel voor het geval dat de voorwaarde onwaar is.

if (<voorwaarde>)
  <opdracht 1>
else
  <opdracht 2>

if (<voorwaarde>) {
  <reeks 1>
}
else {
  <reeks 2>
}

Eenvoudige voorwaarden kunnen worden samengesteld tot complexe voorwaarden met behulp van logische bewerkingen. Java kent ondermeer de volgende logische bewerkingen.

bewerking betekenis
&& en (is waar wanneer beide leden waar zijn)
|| of (is waar wanneer minstens een van beide leden waar is)
! niet (heeft slechts een rechterlid, waarvan de betekenis omgekeerd wordt - dus waar wordt onwaar en onwaar wordt waar)

Voorbeeld

De volgende opdracht gaat na of het getal a binnen het gesloten interval [1.5, 3.5] ligt. Zo ja, dan wordt a met 1 verhoogd. Zo neen, dan wordt a met 2 verminderd, en dan wordt een boodschap op het scherm gedrukt.

if ((a >= 1.5) && (a <= 3.5))
  a = a + 1;
else {
  a = a - 2;
  System.out.println("a = " + a);
}

Oefening 3.3

Formuleer een Java-uitdrukking die de volgende voorwaarde expliciteert: "a is verschillend van 3 en 4, of b is ten hoogste a."

Oplossing

Oefening 3.4

Wat is het effect van de volgende opdracht ?

if (a < 0)
  a = 0 - a;

Oplossing

3.3 Iteratie

Java kent drie verschillende structuren voor het herhalen van een opdracht of opdrachtenblok. De eenvoudigste is de while-lusopdracht met algemene vorm

while (<voorwaarde>) {
  <opdrachten>
}

(Zoals bij de voorwaardelijke opdracht, mogen ook hier de accoladen weggelaten worden indien het slechts om één te herhalen opdracht gaat.)

Telkens opnieuw wordt de <voorwaarde> getest, en als het resultaat waar is, voert Java de <opdrachten> nog eens uit. Zodra de voorwaarde ook maar één keer op onwaar uitkomt, is de lusopdracht afgelopen.

Voorbeeld

Het volgende programma berekent alle delers van het getal 1440. Het gaat daarbij als volgt tewerk: één voor één worden alle delers van 1 tot 1440 uitgeprobeerd, en telkens als de deling opgaat (als de rest nul is) wordt de deler afgedrukt.

class Delers {
  public static void main(String[] args) {
    int deeltal, i;
    deeltal = 1440;
    i = 1;
    while (i <= deeltal) {
      if (deeltal % i == 0)
        System.out.println(i);
      i = i + 1;
    }
  }
}

Belangrijk is dat bij een while-opdracht de test voorafgaat aan de opdrachten: het zou dus kunnen dat de opdrachten nooit worden uitgevoerd. Een ander extreem geval doet zich voor wanneer de test altijd waar is. We spreken dan om begrijpelijke redenen van een oneindige lus.

Veel lusopdrachten hebben met bovenstaand voorbeeld het volgende gemeen:

Dit soort lussen kan eigenlijk handiger worden uitgedrukt met een for-opdracht. De algemene vorm van de for-lus in Java luidt

for (<initialisatie> ; <voorwaarde> ; <increment>) {
  <opdrachtenblok>
}

waarbij <initialisatie> en <increment> gewone Java-opdrachten zijn. De interpretatie van de for kan worden uitgedrukt met behulp van een while:

<initialisatie> ;
while (<voorwaarde>) {
  <opdrachtenblok>
  <increment> ;
}

Voorbeeld

Het vorige voorbeeld kan korter worden herschreven als

class Delers {
  public static void main(String[] args) {
    int deeltal, i;
    deeltal = 1440;
    for (i = 1; i <= deeltal; i = i + 1)
      if (deeltal % i == 0)
        System.out.println(i);
  }
}

De derde en laatste soort lusopdracht in Java is de do-opdracht. Ze onderscheidt zich van de vorige twee doordat de test op het einde van de lus plaatsvindt. Het opdrachtenblok wordt dus altijd minstens één keer uitgevoerd.

De algemene vorm luidt

do {
  <opdrachtenblok>
}
while (<voorwaarde>) ;

(Merk op dat dit de enige van de drie lusopdrachten is waar de lusopdracht zelf op een kommapunt moet eindigen - in de andere twee gevallen maakt de sluitende accolade van het opdrachtenblok zulks overbodig.)

Voorbeeld

We geven nog eens een derde versie van het programma om delers van 1440 te zoeken. Dit keer testen we op het einde van de lus, weliswaar vlak nadat we de waarde van de teller i verhoogd hebben.

class Delers {
  public static void main(String[] args) {
    int deeltal, i;
    deeltal = 1440;
    i = 1;
    do {
      if (deeltal % i == 0)
        System.out.println(i);
      i = i + 1;
    }
    while (i <= deeltal);
  }
}

Oefening 3.5

Schrijf een Java-programma dat twee veranderlijken a en b van het type int initialiseert met twee positieve getallen, en dat vervolgens hun grootste gemene deler zoekt volgens het algoritme van Euclides:

  1. Ranschik de twee getallen zodat a het grootste is.
  2. Bereken de rest van de deling van a door b.
  3. Vervang a door b, en b door de zojuist berekende rest.
  4. Als b nu nul is, dan is a de grootste gemene deler. Anders beginnen we opnieuw vanaf stap 2.

Oplossing

3.4 Deelprogramma's

Gestructureerd programmeren vereist dat we een complex probleem kunnen splitsen in deelproblemen. Het is dan ook normaal dat onze programmeertaal toelaat, deze opsplitsing expliciet aan te geven in de vorm van het programma.

Een deelprogramma in Java wordt een methode genoemd. Methoden maken steeds deel uit van een klasse. Een methode wordt gekenmerkt door:

Methoden die geen betrekking hebben op een welbepaald object (zie volgend hoofdstuk) heten statisch. Zolang we nog niet met object-georiënteerd programmeren bezig zijn, dragen al onze methoden het sleutelwoord static.

De algemene vorm van een methode luidt voorlopig

public static <terugkeertype> <naam> (
  <parametertype> <parameternaam> ,
  <parametertype 2> <parameternaam 2> ,  ... ) {
  <opdrachtenblok>
}

Hierin is

Voorbeeld

De volgende code declareert een methode die twee gehele getallen als parameter neemt, en die een derde geheel getal teruggeeft.

  public static int machtsverheffing(int grondtal, int exponent) {
    int teller;
    int resultaat;
    resultaat = 1;
    for (teller = 0; teller < exponent; teller = teller + 1)
      resultaat = resultaat * grondtal;
    return resultaat;
  }

In al het bovenstaande zijn de begrippen "hoofdprogramma" en "deelprogramma" natuurlijk relatief: een methode kan op haar beurt andere methoden aanroepen. En zelfs het ultieme "hoofdprogramma" is zelf een methode, namelijk main.

Het aanroepen van een methode is gewoon een opdracht die deel uitmaakt van het hoofdprogramma. De vorm van een methode-aanroep binnen dezelfde klasse is

<naam> (
  <uitdrukking> ,
  <uitrukking 2 2> ,  ... );

Als een methode een terugkeertype heeft (dus niet void), dan is een methode-aanroep ook een uitdrukking. Het type van die uitdrukking is het terugkeertype van de methode zelf. Deze uitdrukking kan dan bijvoorbeeld optreden in het rechterlid van een toekenningsopdracht, of zelf een parameter vormen van een andere methode-aanroep, enzovoort.

Als een methode wordt aangeroepen vanuit een andere klasse dan die waarin ze gedefinieerd is, moet haar naam worden voorafgegaan door de naam van haar klasse en een punt.

Voorbeeld

De methode machtsverheffing uit het vorige voorbeeld kan als volgt worden gebruikt om het getal 25 te berekenen en het resultaat op te slaan in de veranderlijke aantal.

aantal = machtsverheffing(2, 5);

Als de methode machtsverheffing gedefinieerd is in een klasse met de naam Rekenwerk, en we willen haar oproepen vanuit een methode van de klasse Boekhouding, dan zie dat er als volgt uit.

aantal = Rekenwerk.machtsverheffing(2, 5);

Je kan ook complexere uitdrukkingen evalueren, zoals hier bijvoorbeeld (52 + 54)3:

aantal = machtsverheffing(machtsverheffing(5, 2) + machtsverheffing(5, 4), 3);

Uitgewerkt voorbeeld

We herschrijven het voorbeeldprogramma BTW.java van paragraaf 2.6 zodat de drie grote groepen opdrachten in drie afzonderlijke methoden worden gegroepeerd. We maken daarbij gebruik van terugkeerwaarden om de twee numerieke gegevens aan het hoofdprogramma door te spelen. Elke methode die gebruik maakt van het toetsenbord, zelfs onrechtstreeks (via een methode-aanroep, zoals main dat doet) dient throws IOException te vermelden.

Opgelet: een methode heeft geen toegang tot de veranderlijken die in andere methoden (dus in een ander blok) gedeclareerd worden. Parameters en terugkeerwaarden zijn voorlopig de enige mechanismen die ons ter beschikking staan voor communicatie tussen hoofd- en deelprogramma's.

import java.io.*;

class BTW {
  /** Stel een vraag aan de gebruiker en lees het getal
  *   met vlottende komma dat hij/zij vervolgens invoert.
  *   De enige parameter is:
  *     vraag: de tekst van de vraag aan de gebruiker
  *   De terugkeerwaarde is:
  *     het gelezen getal
  */
  public static double leesGetal(String vraag) throws IOException {
    BufferedReader toetsenbord;
    toetsenbord = new BufferedReader(new InputStreamReader(System.in));
    System.out.println(vraag);
    String invoertekst;
    invoertekst = toetsenbord.readLine();
    return Double.parseDouble(invoertekst);
  }

  /** Bereken het BTW-bedrag en de verkoopprijs inclusief BTW
  *   en druk deze resultaten af op het scherm.
  *   De parameters zijn:
  *     netto: de verkoopprijs exclusief BTW
  *     btw: de BTW-aanslagvoet, in percent
  */
  public static void berekenResultaten(double netto, double btw) {
    double btwBedrag;
    btwBedrag = netto * btw / 100;
    double brutoBedrag;
    brutoBedrag = netto + btwBedrag;
    System.out.println("bedrag BTW: " + btwBedrag);
    System.out.println("bruto verkoopprijs: " + brutoBedrag);
  }

  /** Vraag de gebruiker een verkoopprijs exclusief BTW en
  *   een BTW-voet, en geef hem/haar het BTW-bedrag en
  *   de verkoopprijs inclusief BTW.
  */
  public static void main(String[] args) throws IOException {
    double nettoBedrag;
    nettoBedrag = leesGetal("netto verkoopprijs ?");

    double btwPercent;
    btwPercent = leesGetal("BTW-voet (in percent) ?");

    berekenResultaten(nettoBedrag, btwPercent);
  }
}

Oefening 3.6

Schrijf een methode faculteit met één parameter van het type int en met terugkeertype int die de faculteitsfunctie uit de combinatieleer berekent. Het getal "n faculteit", meestal genoteerd met een uitroepteken (n!), is het product van alle positieve gehele getallen tot en met n:

0! = 1
1! = 1 = 1
2! = 1 . 2 = 2
3! = 1 . 2 . 3 = 6
...

Vul het werk aan met een methode main die een getal n van het toetsenbord leest, en het resultaat n! afdrukt.

Oplossing

Een interessant aspect van Java is dat methoden ook zichzelf kunnen aanroepen. Dit fenomeen noemen we recursie. In het volgende voorbeeld wordt de eigenschap van machtsverheffing gebruikt dat

ab = a . ab-1

Als de exponent groter is dan 0, gebruiken we recursie om de machtsverheffing te reduceren tot een andere machtsverheffing met een exponent die één eenheid lager is.

import java.io.*;
class MachtsRecursie {
  public static int machtsverheffing(int grondtal, int exponent) {
    if (exponent == 0)
      return 1;
    else
      return grondtal * machtsverheffing(grondtal, exponent - 1);
  }

  public static void main(String[] args) throws IOException {
    BufferedReader toetsenbord;
    toetsenbord = new BufferedReader(new InputStreamReader(System.in));
    System.out.println("grondtal: ");
    int grondtal;
    grondtal = Integer.parseInt(toetsenbord.readLine());
    System.out.println("exponent: ");
    int exponent;
    exponent = Integer.parseInt(toetsenbord.readLine());
    int macht;
    macht = machtsverheffing(grondtal, exponent);
    System.out.println(grondtal + " tot de macht " + exponent
      + " is gelijk aan " + macht);
  }
}

Oefening 3.7

Herschrijf je antwoord op oefening 3.6 door gebruik te maken van de volgende recursieformule:

n! = n . (n - 1) als n > 0;
0! = 1.

Oplossing

Oefening 3.8

Schrijf een Java-programma dat de (oneindige) rij der Fibonaccigetallen afdrukt:

1
1
2
3
5
8
13
21
...

De rij begint met tweemaal 1, en elk volgend getal is de som van de twee voorgaande.

Natuurlijk kan geen enkele computer dit programma oneindig lang laten lopen. Op een bepaald moment worden de getallen zo groot dat het geheugen van alle bestaande computers tesamen niet volstaat om er één van voor te stellen ! Neem passende maatregelen om je programma elegant te laten stoppen wanneer de getallen te groot worden.

Een korte geschiedenis van de Fibonaccigetallen, en vele van hun merkwaardige eigenschappen, staan vermeld in paragraaf 1.2.8 van [Knu].

Oplossing


inhoudstafel en auteursrecht
* STER *

Valid HTML 4.0! Valid CSS!