Helsingin yliopisto / Tietojenkäsittelytieteen laitos
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.

4.3 Luokan käyttö

(Muutettu viimeksi 13.11.2009)

Luvussa 4.1 tutustuttiin ideatasolla luokan ja olion käyttöön mallinnuksessa ja abstraktin tietotyypin toteuttamisessa. Tämä luku kertaa vanhoja ja esittelee uusia luokan käyttötapoja. Pääpaino on uusissa tavoissa.

Luokka ja olio mallinnuksessa

Luvussa 4.1 kirjoitin:
Olio-ohjelmoinnin perusajatus on ongelmakentän - sovellusalueen - kuvaaminen olioina, olioiden ominaisuuksina ja toimintoina sekä olioiden välisinä suhteina. Sovellusalueen käsitteet ja toimijat mallinnetaan olioina. Koska nuo käsitteet ja toimijat ovat usein pysyvämpiä kuin yksittäiset tiedonkäsittelytarpeet, olioiden rakenteen ja toiminnan voi olettaa säilyvän vaikka yksittäisen sovelluksen rakennetta muutetaan tai kun luodaan kokonaan uusia sovelluksia.

Ja:
Olioajattelu voidaan nähdä mallintamisena, jossa luokka vastaa jotakin yleiskäsitettä... Luokan ilmentymä on jokin erityinen olio, joka kuuluu tuohon luokkaan.

Jos tehtävänä olisi vaikkapa ratkoa luentokurssin jotakin tietojenkäsittelytehtävää, ongelmakentästä voitaisiin yrittää löytää olioita seuraavasti:

Joihinkin ongelmiin tämänkaltainen analyysi puree hyvin, joihinkin vähemmän luontevasti. Tällä kurssilla asiaa ei ole mahdollista syvemmin käsitellä. Hyvä kirja aiheesta on Kai Koskimies: Oliokirja, satku.fi, 2000.

Luokka abstraktin tietotyypin toteutuksena

Tästä on ollut useita esimerkkejä, aina luvun 2 Kuulaskurista ja Pikkuvarastosta lähtien.

Kirjoitin luvussa 4.1:
... tietorakenteen toteutus ja käyttö eristetään toisistaan. Tietorakenteen käyttäjä tuntee vain operaatioita, joilla (abstraktin) tietorakenteen tilaa muutetaan ja kysytään. Tietorakenteen toteuttaja puolestaan ei ota kantaa siihen, mihin tietorakennetta käytetään. Käyttäjää ja toteuttajaa yhdistää pelkästään sopimus siitä, mitä operaatiot (abstraktissa) tietorakenteessa saavat aikaan. Usein tietorakenteen toteutuksen piilottamista kutsutaan kapseloinniksi.

Abstraktin tietotyypin voi rakentaa olio-ohjelmoinnin välinein seuraavasti:

Luokka tietueena

Kun laaditaan luokka, jossa on vain julkisia kenttiä eikä lainkaan metodeita, saadaan rakenne, jota kutsutaan tietueeksi (esim. Pascalin record, C:n struct).

Tietueen idea on, että muuttujalla on ns. kenttiä: alimuuttujia, joiden tyypit voivat olla keskenään erilaisia (vrt. taulukkotyyppiin, joka kokoaa yhteen muuttujaan keskenään samantyyppisiä alimuuttujia).

Tietueen ja sen kenttien käyttöön liittyy usein erilaisia operaatioita, jotka on luontevaa rakentaa itse luokkaan. Tällöin päädytään "tavalliseen" luokkaan. Luokan käyttö tietueena saattaa olla merkki huonosta ohjelmansuunnittelusta!

Esimerkki: Laaditaan henkilötietue, jossa on kenttä henkilön nimelle, pituudelle ja iälle.

  public class Henkilo {
     public String nimi;
     public double pituus;
     public int ika;
  }
  ...

  Henkilo pomo = new Henkilo();
  Henkilo sihteeri = new Henkilo();

  pomo.nimi = "Maija";
  pomo.pituus = 163;
  pomo.ika = 24;

  sihteeri.nimi = "Pekka";
  sihteeri.pituus = 184;
  sihteeri.ika = 48;

Huom: Koska tietueet ovat olioita, sijoitus on viitteen kopiointi:
  Henkilo paasihteeri = sihteeri;
  ++paasihteeri.ika;

  // sihteerikin vanhentui!

Huom: Tämä ohjelmointityyli ei mitenkään varmista kenttien oikeaa käyttöä; mikään ei estä asettamasta pituudeksi lukua -3.14 eikä iäksi lukua -123456. Kapselointia käyttäen tätä ongelmaa ei tule, kunhan vain aksessorit vain ohjelmoidaan viisaasti!

Esimerkki: Lukupari (esim. tason piste, kompleksiluku, yms.) voidaan toteuttaa tietueena vaikkapa seuraavasti:

  public class Pari {
    public double x, y;

    public Pari(double x, double y) {
      this.x = x;
      this.y = y;
    }
  }

...

  Pari a = new Pari(3.14, -7);
  Pari b = new Pari(0.23, 10.1);
  Pari c = new Pari(0,0);

  c.x = a.x + b.y;
  c.y = a.y + b.x;

  // Mitä tekee a = b; ?
Koska kaikki double-arvot ovat sallittuja x- ja y-kenttiin, erillisten "settereiden" ja "gettereiden" ohjelmointi ei tässä tapauksessa mitenkään lisäisi kenttien tiedon pysymistä kelvollisena.

Koska Javassa luokilla on ns. oletuskonstruktori (ks. seur. luku), lukuparin voi toteuttaa vieläkin yksinkertaisemminkin:

  public class Pari {
    public double x, y;
  }

...

  Pari a = new Pari();
  Pari b = new Pari();
  Pari c = new Pari(); 

  a.x = 3.14; a.y = -7;
  b.x = 0.23; b.y = 10.1;
  c.x = a.x + b.y;
  c.y = a.y + b.x;

Oman toString()-metodin tekeminen lukuparille voisi olla perusteltua. Jos jatketaan edellistä esimerkkiä ja tulostetaan:
  System.out.println(a + " " + b + " " + c);
saadaan esimerkiksi:
Pari@80ca74b Pari@80ca74c Pari@80ca74d
Ohjelmoidaan tyypille Pari oma tulostusmetodi:
  public class Pari {
    public double x, y;

    public String toString() {
      return "("+x+","+y+")";
    }
  }
Nyt samalla tulostusoperaatiolla saadaan kauniisti:
  (3.14,-7.0) (0.23,10.1) (13.24,-6.77)

Linkitetyt rakenteet

Luokan oma nimi on käytettävissa luokassa. Kun tehdään tietue, jonka jokin kenttä (tai useammat kentät) on luokan tyyppiä, voidaan ohjelmoida ns. linkitettyjä rakenteita.

Ns. linkitetty lista voidaan toteuttaa seuraavasti:

  public class Lista {
    int tieto;
    Lista linkki;
  }
  ...
  Lista p = new Lista();
  p.tieto = 7;
  p.linkki = new Lista();
  p.linkki.tieto = 3;
  p.linkki.linkki = new Lista();
  p.linkki.linkki.tieto = 19;

  for(Lista q=p; q!=null; q=q.linkki)
    System.out.println(q.tieto); 
Huom: Myös tässä tapauksessa olio-ohjelmointityyli ja kapselointi voisi olla parempi ratkaisu. Lista voitaisiin toteuttaa esimerkiksi seuraavaan tapaan (parempiakin on!):
public class Lista {

  public int tieto;
  private Lista linkki;

  public void lisaaAlkuun(int tieto) {
    ...
  }
  public void lisaaLoppuun(int tieto) {
    ...
  }
  public Lista annaSeuraava() {
    ...
  }
  // jne., jne, ...

}
  // käyttöä:
  ...
  Lista p = new Lista();
  p.lisaaAlkuun(7);
  p.lisaaLoppuun(7);
  p.lisaaLoppuun(19);
  ...

Luokka ohjelmakirjastona

Luokkaan voi kerätä yleiskäyttöisiä vakioita ja algoritmeja. Monet Javan valmiit välineet on määritelty kirjastoluokkina, esimerkiksi Math-luokka sisältää vakiot PI ja E sekä joukon laskennallisia metodeita (trigonometriset funktiot, yms.)

Kentät ja metodit määritellään luokkakohtaisina: static. Käyttäjälle tarjotut ovat public, apuvälineet private.

Tarvittaessa kenttiä voi alustaa staattisella alustuslohkolla.

Kirjastoluokka on syytä määritellä final-määreellä. Tällöin luokkaa ei voi periä (kts. kappale 4.4). Kun luokkaan lisäksi määritellään yksityinen (private) konstruktori, luokasta ei voi luoda myöskään ilmentymiä!

Esimerkki:

public final class EsimKirjasto {

  private EsimKirjasto() { } // ilmentymien esto!!

  // vakioita:

  public static final double VAKIO = 123.456;  // julkinen vakio
  private static int SALAVAKIO = 13;           // yksityinen vakio

  // kirjastometodeja:

  public static int randomInt(int max) {   // arvotaan kokonaisluku väliltä [1..max]
    return (int)(Math.random()*max) + 1;
  }

  public static char randomChar() {   // koodit 34-126  ('"'-'~')
    return (char)(randomInt(93)+33);  // käytetään omaa randomInt-metodia
  }

  public static void onnittele(String nimi, int montakoKertaa) {
    if (montakoKertaa == SALAVAKIO)
      System.out.println("Ähäkutti " + nimi + "!");
    else
      for (int i=0; i<montakoKertaa; ++i)
        System.out.println("Onnea " + nimi + "!");
  }
}

// kirjaston käyttöä:

for (int i=10; i<20; ++i)
  System.out.println(EsimKirjasto.randomChar()+" "+EsimKirjasto.randomInt(i));

EsimKirjasto.onnittele("Maija", 7);
EsimKirjasto.onnittele("Matti",13);

Luokka "vanhanaikaisena" ohjelmana

Javalla voi vallan mainiosti ohjelmoida perinteisen "proseduraalisen paradigman" mukaisesti Pascalin ja C:n tapaan. Se ei välttämättä ole kovin järkevää, joten seuraava esimerkki olkoon varoittava:
public class Ohjelma{

  // "globaalit vakiot ja muuttujat":

  static final int KOKO = 10;
  static int luku, indeksi;
  static double[] taulu = new double[KOKO];

  // "proseduurit" tai "void-funktiot":

  static void arvoluvut(int skaalaus) {
    for (int i=0; i<KOKO; ++i) 
      taulu[i] = Math.random()*skaalaus;
  }

  static void tervetuloa() {
    System.out.println("   Tervetuloa\n"
                      +"   ==========");
  }

  // "funktiot" tai "arvon palauttavat funktiot":

  static double siniToiseen(double argu) {
    return Math.sin(argu)*Math.sin(argu);
  }

  // "pääohjelma":

  public static void main(String[] args) {
    tervetuloa();
    arvoluvut(14);
    for (int i=0; i<KOKO; ++i)
       System.out.println(siniToiseen(taulu[i]));
  }
}


Takaisin luvun 4 sisällysluetteloon.