Helsingin yliopisto / Tietojenkäsittelytieteen laitos / 581258-1 Johdatus ohjelmointiin
Copyright © 2005 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.

3.5 Metodeista

(Muutettu viimeksi 3.6.2005)

Metodi

Javassa nimetyt algoritmit ohjelmoidaan metodeina. Jotkin metodit palauttavat arvon funktion tapaan, toiset taas ovat proseduurin kaltaisia, ts. niitä voidaan käyttää vain lauseena. Myös arvon palauttavia metodeita voidaan kutsua lauseen tapaan (kts. lausekelauseet luvussa 3.4).

Jos luokassa on määritelty metodi:

  public static void main(String[] args) {
   ...
  }
luokka voidaan suorittaa sovelluksena. Jokaisessa luokassa saa olla tuollainen 'pääohjelmametodi'. Sitä voidaan käyttää vaikkapa luokan testausvälineenä ohjelmistoa laadittaessa vaikka luokka ei olisikaan sovellukseksi tarkoitettu.

Sovellus käynnistetään käynnistämällä luokka, jossa on main-metodi. Tuo metodi sitten määrittelee mitä tahtoo ja käynnistelee mitä tarvitaan.

Metodeita käytetään luokissa monissa tehtävissä: niiden avulla toteutetaan "abstraktin koneen" toiminnot, niillä toteutetaan aksessorit, joilla piilossa pidetyn tietorakenteen tilaa voidaan kysellä ja muuttaa, konstruktorit yms. ovat muodoltaan metodeita, aliohjelmat toteutetaan metodeina, ...

Metodin määreistä

Määreillä ("modifiers") säädellään mm. metodien näkyvyyttä :
  public protected private
  abstract static final synchronized native
Useimpiin näistä palataan tarkemmin myöhemmin. Toistaiseksi tyydytään seuraavaan:

Metodin tyypistä

Kun metodi on määritelty void-määreellä, se ei palauta mitään arvoa eikä metodia myöskään voi käyttää lausekkeena. (void ei ole tyyppi vaan ilmaus tyypin puuttumisesta!) Metodin tyyppi voi olla jokin alkeistyyppi tai viittaustyyppi. Jos metodilla on tyyppi, metodin täytyy palauttaa tyyppiin sopiva arvo return-lauseella (tai return-lauseilla).

Esimerkki:

public static char alkuKirjain(int luku) {

// palauttaa numeron alkukirjaimen

  switch (luku) {
    case 1: case 9:
      return 'Y';
    case 2: case 3: case 6: case 8: case 10:
      return 'K';
    case 4: case 0:
      return 'N';
    case 5:
      return 'V';
    case 7:
      return 'S';
    default:      // ei ollut 0...9
      return '*';
  }
}   

Metodin palauttama arvo voi siis olla myös viittaustyyppiä - esimerkiksi String-olio, jokin oman luokan ilmentymä tai vaikkapa taulukko-olio.

Esimerkkejä:

public static String kahdenna(String jono) {
  return jono + jono;
}
Jos käytetään äskeistä luokkaa Elukoita, voidaan ohjelmoida:
public class Elukoita {
  private String nimi;
  public Elukoita(String n) {
    nimi = n;
  }
  public void tulostakissa() {
    System.out.println("Kissa: " + nimi);
  }
}

public class Paaohjelma {

  private static Elukoita luoMissuOlio() {
    return new Elukoita("Missu");
  }

  public static void main(String[] args) {
    Elukoita ee = luoMissuOlio();
    ee.tulostakissa();
  }
}
Odotetusti ohjelma tulostaa:
Kissa: Missu

Esimerkissä metodi luoMissuOlio luo erään Elukoita-olion ja palauttaa sen arvonaan.

Metodin parametreista

Metodin muodolliset parametrit määritellään metodin nimen jälkeen sulkeissa. Vaikka parametreja ei olisi lainkaan, sulkeet on kirjoitettava! Määrittelyt ovat muotoa "tyyppi parametri". Parametrien on oltava yksikäsitteisiä eikä metodin paikallisilla muuttujilla saa olla samaa nimeä kuin millään muodollisella parametrilla.

Kaikki parametrit ovat arvoparametreja: muodollinen parametri on siis kuin metodin paikallinen muuttuja, jolle asetetaan alkuarvoksi kutsussa annettu todellinen parametri.

Viittaustyyppinen parametri on viitteen arvo.

Kun seuraava ohjelma suoritetaan (ParamKoe.java):

public class ParamKoe{

  public static void main(String[] args) {
    int i = 7;
    String a = " kissaa ";
    Pikkuvarasto v = new Pikkuvarasto(5.0, "Mehu"); 

    System.out.println(i + a + v);
    muutaKaikki(i, a, v);
    System.out.println(i + a + v);
  } 

  private static void muutaKaikki(int m, String s, Pikkuvarasto r) {
    m = 9;
    s = " hiirtä ";
    r.vieVarastoon(100);
    System.out.println(m + s + r);
  }
}
tulostuu:
7 kissaa (Mehu: 5.0)
9 hiirtä (Mehu: 105.0)
7 kissaa (Mehu: 105.0)
Ainoa pysyvä muutos tapahtui siis Pikkuvarasto-olion tilaan! Muodollinen parametri m sai alkuarvokseen luvun 7, muodollinen parametri s pantiin osoittamaan samaan "kissaan" kuin todellinen parametri a. Ja muodollinen parametri r pantiin osoittamaan samaan Pikkuvarasto-olioon kuin v osoittaa.

Kun m muutettiin luvuksi 9, ei se vaikuttanut mitenkään i:n arvoon. Kun s saa arvokseen viitteen "hiireen", ei a:lle tai "kissalle" käy kuinkaan. Mutta kun r:n viittaaman olion tilaa muutettiin, muuttui myös v:n viittaaman olion tila, koska kyseessä oli sama olio!

Metodin otsikko ja metodien kuormittaminen

Metodin otsikko (signature) muodostuu metodin nimestä ja sulkeisiin kirjoitetuista muodollisten parametrien määrittelyistä. Metodilla ei voi olla vaihtelevaa määrää parametreja, mutta samannimisiä, eri otsikolla varustettuja metodeita voi määritellä samassa 'nimiavaruudessa'. Tällaista kutsutaan kuormittamiseksi (overloading). Kääntäjä tunnistaa todellisista parametreista, mitä metodia missäkin kutsussa tarkoitetaan. Samannimisten metodien on erottava toisistaan nimenomaan parametreiltaan, erot määreissä ja tyypissä eivät riitä!

Esimerkki (KuormEsim.java):

public class KuormEsim {

   private static void rivi(String teksti) {
     System.out.println(teksti);
   }

   private static void rivi(String teksti, int sisennys) {
     for (int i=0; i<sisennys; ++i)
       System.out.print(' ');
     System.out.println(teksti);
   }

   private static void rivi(String teksti, char ymerkki) {
     System.out.print(ymerkki);
     System.out.print(teksti);
     System.out.println(ymerkki);
   }

   private static void rivi(int luku) {
     System.out.println(luku);
   }

   public static void main(String[] args){ // pääohjelma
     rivi("kissanpäivä");
     rivi("kissanpäivä", 7);
     rivi("kissanpäivä", '#');
     rivi(765);
   }
}
Sovellus tulostaa:
kissanpäivä
       kissanpäivä
#kissanpäivä#
765

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 (Fibo.java):

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!

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

Esimerkki: binäärihaku (BinHaeRec.java):


  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));

Esimerkki: pikajärjestäminen (Quicksort) (PikaJarj.java):

  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);

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

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

Keskimäärin pikajärjestäminen onkin O(n*log n).

Myös ns. keskinäinen rekursio (mutual recursion) on mahdollista. Esimerkki (AbraKadAbra.java):

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("");
  }
}
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



Takaisin luvun 3 sisällysluetteloon.