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

7 Luokkamäärittelyn mahdollisuuksia

(Muutettu viimeksi 17.10.2011, sivu perustettu 28.10.2010. Arto Wikla)

Luokka on Java-ohjelmoinnin peruskäsite: Jokainen olio on jonkin luokan ilmentymä, kaikki algoritmit ja muuttujat sijaitsevat jossakin luokassa. Itse asiassa lähes kaikki Javan lauseet ja määrittelyt ihan konkreettisestikin kirjoitetaan jonkin luokan sisään! Vain kaksi määrettä, package ja import, joilla ohjaillaan ohjelmatiedostojen keskinäisiä suhteita, kirjoitetaan Javassa luokkamäärittelyn ulkopuolelle!

Ilmentymämuuttujat ja luokkamuuttujat

Jo aiemmin on opittu, että muuttujia voidaan määritellä metodeissa ja luokissa; metodissa määritelty muuttuja syntyy, kun metodi käynnistyy ja häviää, kun metodin suoritus päättyy. Tällaisilla muuttujilla ei ole oletusalkuarvoa.

Tähän mennessä nähdyt esimerkit muuttujista metodien ulkopuolella ovat olleet oliokohtaisia ilmentymämuuttujia (instance variable), "piirustuksia muuttujista", "ei-staticceja" muuttujia. Näin määritelty muuttuja syntyy, kun luokasta luodaan ilmentymä eli olio. Jokainen luotava olio saa oman versionsa tällaisesta muuttujasta. Ilmentymämuuttujilla - kuten myös nyt opittavilla luokkamuuttujilla - on muuttujan tyypin mukainen oletusalkuarvo (0, 0.0, false, null).

Luokassa voidaan metodien ulkopuolella määritellä oliokohtaisten ilmentymämuuttujien lisäksi myös luokkakohtaisia luokkamuuttujia (class variable) eli "static-muuttujia". Tällainen muuttuja on olemassa, vaikka luokasta ei luotaisi ensimmäistäkään ilmentymää. Ja jos ilmentymiä luodaan, ilmentymät jakavat tällaisen muuttujan keskenään.

Jos metodien ulkopuolella määritellään muuttuja määreellä static, tarkoitetaan luokkamuuttujaa:

private static int muuttuja;

Jos vastaavaan paikkaan määritellään muuttuja ilman static-määrettä, kyseessä on ilmentymämuuttuja:

private int muuttuja;

Esimerkki luokkamuuttujan käytöstä: Jonotuskone

Toteutetaan jonotusnumeroita antaville olioille luokka. Laitteita voi olla siis useita, mutta ne jakavat saman "numerovaraston". Jokaisessa laitteessa on lisäksi laskuri, joka laskee laitteen käyttökertojen määrän.

Koska yhteinenJuoksevaNumero on luokkamuuttuja – ja liittyy siis luokkaan – siitä on olemassa vain yksi versio, jonka kaikki (mahdollisesti) luotavat oliot jakavat. Muuttuja kayttokertoja puolestaan on ilmentymämuuttuja, joten siitä jokainen (mahdollisesti) luotava olio saa oman versionsa. Ennen ensimmäisen ilmentymän luontia ensimmäistäkään muuttujaa kayttokertoja ei ole olemassa:

public class Jonotuskone {

  private static int yhteinenJuoksevaNumero = 0; // LUOKKAMUUTTUJA!
                                                 // arvo: aina "edellinen"

  private int kayttokertoja; // ilmentymämuuttuja

  public Jonotuskone() {
    kayttokertoja = 0;
  }

  public int annaNumero() {
    ++kayttokertoja;               // ilmentymämuuttujan kasvatus
    ++yhteinenJuoksevaNumero;      // luokkamuuttujan kasvatus
    return yhteinenJuoksevaNumero;
  }

  public int kayttoLkm() {
    return kayttokertoja;
  } 
}

Kokeillaan:

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

    Jonotuskone a = new Jonotuskone(),
                b = new Jonotuskone();

    System.out.println("a:sta saadaan "   + a.annaNumero() +
                     ", a:lla käyttäjiä " + a.kayttoLkm());

    System.out.println("b:stä saadaan "   + b.annaNumero() +
                     ", b:llä käyttäjiä " + b.kayttoLkm());

    System.out.println("Otetaan vaiteliaina b:stä 100 numeroa.");
    for (int i=0; i<100; ++i)
      b.annaNumero();        // lausekelause lauseena

    System.out.println("a:sta saadaan "   + a.annaNumero() +
                     ", a:lla käyttäjiä " + a.kayttoLkm());

    System.out.println("b:stä saadaan "   + b.annaNumero() +
                     ", b:llä käyttäjiä " + b.kayttoLkm());
  }
}

Ohjelma tulostaa:

a:sta saadaan 1, a:lla käyttäjiä 1
b:stä saadaan 2, b:llä käyttäjiä 1
Otetaan vaiteliaina b:stä 100 numeroa.
a:sta saadaan 103, a:lla käyttäjiä 2
b:stä saadaan 104, b:llä käyttäjiä 102

Huom: Todellisuudessa jonotuskoneiden ohjelmointi olisi vähän mutkikkaanpaa: jos useita jonotuskoneita toimisi rinnakkain, pitäisi jotenkin pitää huoli siitä, että vain yksi kone kerrallaan pääsee käyttämään jaettua muuttujaa. Javassa on tähän välineitä, mutta niitä ei käsitellä tällä kurssilla.

Ilmentymämetodit ja luokkametodit

Samoin kuin muuttujat voivat "liittyä luokkaan tai olioon", myös metodit voivat olla luokkametodeita tai ilmentymämetodeita (class method, instance method).

Luokkametodeita on kurssilla jo käytetty pääohjelman "pikku apulaisina" tyyliin:

private static void autaPaaohjelmaa(...) { 
  ... 
}

Myös ilmentymämetodeita on ohjelmoitu monenlaisiksi aksessoreiksi, joilla olioiden piiloon kapseloituja rakenteita on käsitelty:

public void setSitaSunTata(...) {
  ... 
}

Jos metodi esitellään määreellä static, tarkoitetaan luokkametodia. Määreen static puuttuminen tarkoittaa, että kyseessä on ilmentymämetodi.

Ilmentymämetodia voi kutsua ainoastaan soveltamalla sitä johonkin ilmentymään. Tuttu tapa ilmaista olio, this-olio, on pistenotaatio:

olio.setSitaSunTata(...);

Ilmentymämetodi voi kutsua toista saman luokan ilmentymämetodia myös ilman pistenotaatiota, mutta silloin itse tätä kutsujaa joku on jo kutsunut pistenotaatiolla. Aina kun ilmentymämetodi on käynnissä, aina on olemassa "juuri nyt kyseessä oleva olio" eli this-olio!

Luokkametodi ei koskaan voi kutsua mitään ilmentymämetodia sellaisenaan. Yritys johtaa tuttuun kääntäjän virheilmoitukseen

non-static method cannot be referenced from a static context

Luokkametodi voi kutsua ilmentymämetodia ainoastaan, jos sillä on olio, jolle aksessoria tällä kertaa sovelletaan.

Pari sääntöä:

  1. Ilmentymämetodi pääsee käsiksi kaikkiin ilmentymämuuttujiin ja kaikkiin luokkamuuttujiin. Ilmentymämetodi voi kutsua kaikkia ilmentymämetodeita ja kaikkia luokkametodeita.

  2. Luokkametodi pääsee käsiksi ainoastaan luokkamuuttujiin. Se ei pääse käsiksi ensimmäiseenkään ilmentymämuuttujaan. Luokkametodi voi kutsua kaikkia luokkametodeita. Se ei voi kutsua ensimmäistäkään ilmentymämetodia.

Ilmentymä-luokka-suhdetta selventänevät seuraavat Java-totuudet:

Luokka voi olla olemassa ilman ensimmäistäkään oliota. Mutta ei ole olemassa oliota ilman luokkaa. Ja yhdestä ja samasta luokasta voi olla vaikka miten monta ilmentymää, oliota. Toisin sanoen, on olemassa oliottomia luokkia, mutta ei luokattomia olioita.

Esimerkki luokkametodin käytöstä: laajennettu Jonotuskone

Jatketaan edellistä Jonotuskone-esimerkkiä. Toisinaan, esim. aamuisin, jonotusnumerot halutaan aloittaa uudelleen ykkösestä. Täydennetään luokkaa luokkametodilla nollaaJonotus():

public class Jonotuskone {

  private static int yhteinenJuoksevaNumero = 0;  // LUOKKAMUUTTUJA!
                                                  // aina "edellinen"

  public static void nollaaJonotus() {    // LUOKKAMETODI!
    yhteinenJuoksevaNumero = 0;
  }

  private int kayttokertoja; // ilmentymämuuttuja

  public Jonotuskone() {
    kayttokertoja = 0;
  }

  public int annaNumero() {
    ++kayttokertoja   ;            // ilmentymämuuttujan kasvatus
    ++yhteinenJuoksevaNumero;      // luokkamuuttujan kasvatus
    return yhteinenJuoksevaNumero;
  }

  public int kayttoLkm() {
    return kayttokertoja;
  }
}

Tällaista luokkametodia voidaan kutsua luokka-aksessoriksi.

Käyttöesimerkki:

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

    Jonotuskone a = new Jonotuskone(),
                b = new Jonotuskone();

    System.out.println("a:sta saadaan "   + a.annaNumero() +
                     ", a:lla käyttäjiä " + a.kayttoLkm());

    System.out.println("b:stä saadaan "   + b.annaNumero() +
                     ", b:llä käyttäjiä " + b.kayttoLkm());

    System.out.println("Uusi aamu!");
    Jonotuskone.nollaaJonotus();     // ******* nollataan *******

    System.out.println("a:sta saadaan "   + a.annaNumero() +
                     ", a:lla käyttäjiä " + a.kayttoLkm());

    System.out.println("b:stä saadaan "   + b.annaNumero() +
                     ", b:llä käyttäjiä " + b.kayttoLkm());
  }
}

Ohjelma tulostaa:

a:sta saadaan 1, a:lla käyttäjiä 1
b:stä saadaan 2, b:llä käyttäjiä 1
Uusi aamu!
a:sta saadaan 1, a:lla käyttäjiä 2
b:stä saadaan 2, b:llä käyttäjiä 2

Luokan lataaminen ja olion luonti

Luokkien staattisten ja dynaamisten ominaisuuksien ymmärtämiseksi on hyvä tuntea hieman ohjelman ajoaikaista mallia:

Julkiset kentät

Olemme tähän saakka käyttäneet luokkia hyvin kurinalaisesti toisaalta vain "pääohjelmaluokkina" ja toisaalta olioiden malleina, abstraktien tietotyyppien kapseloituina toteutuksina. Luokissa metodien ulkopuolella määritellyt kentät eli muuttujat ovat toistaiseksi olleet aina yksityisiä eli private-määriteltyjä.

Luokassa esiteltyjä ilmentymämuuttujia ja luokkamuuttujia voidaan – metodien tapaan – määritellä myös julkisiksi (public). Tuollaiset kentät ovat käytettävissä suoraan - so. ilman aksessoreita - luokkamäärittelyn ulkopuolelta.

Yksi perinteinen ohjelmointitekniikka ennen olio-ohjelmoinnin yleistymistä oli ns. tietueiden käyttö. Ideana on, että muuttuja on rakenteinen: muuttuja sisältää kenttiä, johin viitataan kenttänimillä. Esimerkkejä tällaisesta tyylistä ovat C-kielten struct ja Pascalin record.

Javalla tätä tyyliä voi harrastaa laatimalla luokkia, joissa on vain julkisia kenttiä eikä lainkaan metodeita.

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.

Tietueen ja sen kenttien käyttöön liittyy usein myös 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!

Jos kentän arvoille ei ole mitään rajoituksia, aksessoreiden ohjelmointi saattaa olla tarpeetonta:

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 (tästä lisää myöhemmmin), lukuparin voi toteuttaa vieläkin yksinkertaisemmin:

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 on silti erittäin hyvin 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)

Staattisten muuttujien käyttö

Luokkamuuttujien (eli staattisten muuttujien) käyttäminen olio-ohjelmoinnissa ei ole aivan ongelmatonta. "Oliopuristien" mielestä se on suorastaan sopimatonta. Kannattaa olla tarkkana...

Muutamissa tilanteissa Java-ohjelmoinnissa luokkamuuttujat ovat silti käyttökelpoisia:

"Nelikenttä"

Luokan kenttien ja metodien määrittelyä toisaalta julkisiksi tai yksityisiksi, toisaalta luokkakohtaisiksi tai ilmentymäkohtaisiksi voi havainnollistaa seuraavana "nelikenttänä", oikeastaan "nelikuutiona", jonka "dimensiot" ovat static--ei-static, public--private, kenttä--metodi:

       |  static                      | ei-static
-------|------------------------------|-------------------------
public | KENTÄT:                      | KENTÄT:
       | - julkiset kirjastovakiot,   | - "tietueen" kentät
       |   esim Math.PI               | METODIT:
       | METODIT:                     | - aksessorit ja muut
       | - pääohjelmametodi           |   piilossa pidetyn tieto-
       | - kirjastometodit, esim.     |   rakenteen käsittely-
       |   Math.random()              |   metodit
       | -"luokka-aksessorit"         |
-------|------------------------------|--------------------------
private| KENTÄT:                      | KENTÄT:
       | - piilossa pidetyt luokka-   | - olion piilossa pidetty
       |   kohtaiset tietorakenteet   |   tietorakenne, jota käsi-
       | METODIT:                     |   tellään aksessorein
       | - pääohjelman "pikku apu-    | METODIT:
       |   laiset"                    | - aksessoreiden "pikku
       | - kirjastometodien "pikku    |   apulaiset"
       |   apulaiset"                 |
-------|------------------------------|---------------------------

Muitakin käyttötapoja toki on. Ja näkyvyysmääreitä on muitakin kuin nuo public ja private.

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.

Kirjastoluokka on usein tapana määritellä final-määreellä. Tällöin luokkaa ei voi periä (tästä myöhemmin). 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);

[Linkitetyt rakenteet]

Luokan oma nimi on käytettävissa luokassa: Luokassa määritellyn kentän tyyppi siis voi olla oma luokka!

Kun ohjelmoidaan tietue, jossa yksi tai useampi kenttä on luokan tyyppiä, voidaan toteuttaa 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 olisi parempi ja ennen kaikkea turvallisempi 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);
  ...