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.2 Luokan rakenne

(Muutettu viimeksi 12.11.2009)

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!

Luokka on viittaustyypin (reference type) määrittely, luokan ilmentymä on luokan määrittelemää viittaustyyppiä oleva olio.

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, "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 eli "static-muuttujia". Tällainen muuttuja on olemassa, vaikka luokasta ei luotaisi ensimmäistäkään ilmentymää. Ja jos ilmentymiä luodaan, ne jakavat tällaisen muuttujan keskenään.

Esimerkki:

  public class Esim {

     private        int ilme;  // ilmentymämuuttuja
     private static int luok;  // luokkamuuttuja 

     public Esim(int a, int b) {
       ilme = a;
       luok = b;
     }
     // setterit:
     public void asetaIlm(int a) {ilme = a;}
     public void asetaLuo(int a) {luok = a;}

     public String toString() {
       return "ilme = " + ilme +", luok= " + luok;
     }

   // ...  Käyttöesimerkki pikku pääohjelmalla:

     public static void main(String[] args) {

       Esim x = new Esim(1, 2);
       System.out.println("X: " + x);
       // tulostus: X: ilme = 1, luok= 2

       Esim y = new Esim(3, 4);
       System.out.println("Y: " + y);
       System.out.println("X: " + x);
       // tulostus: Y: ilme = 3, luok= 4
       //           X: ilme = 1, luok= 4   !!  

       y.asetaIlm(8);
       y.asetaLuo(9);
       System.out.println("Y: " + y);
       System.out.println("X: " + x);
       // tulostus: Y: ilme = 8, luok= 9
       //           X: ilme = 1, luok= 9   !!
     }
   }

Molemmilla luokan Esim ilmentymillä (x, y) on oma versionsa kentästä ilme. Tämä kenttä on siis oliokohtainen eli ilmentymämuuttuja (instance variable) eli ei-staattinen muuttuja.

Kun luokan kenttä määritellään määreella static, kenttä liittyy luokkaan - ei erikseen kuhunkin olioon - ja kaikki luokan ilmentymät jakavat tuon saman luokkakohtaisen eli staattisen kentän, luokkamuuttujan (class variable). Esimerkissä oliot (x, y) siis jakavat kentän luok.

Huom: "Luokkamuuttuja" ja "luokan muuttuja" tarkoittavat eri asioita: "luokan muuttuja" voi tarkoittaa mitä tahansa luokassa (siis ei metodin sisällä!) määriteltyä muuttujaa. "Luokkamuuttujalla" tarkoitetaan nimenomaan ja vain luokan staattista kenttää!

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.

  public class Jonotuskone {

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

    private int käyttökertoja; // ilmentymämuuttuja

    public Jonotuskone() {
      käyttökertoja = 0;
    }

    public int annaNumero() {
      ++käyttökertoja;               // ilmentymämuuttujan kasvatus
      ++yhteinenJuoksevaNumero;      // luokkamuuttujan kasvatus
      return yhteinenJuoksevaNumero;
    }

    public int käyttöLkm() {
      return käyttökertoja;
    } 
  }

Testataan:
  public class TestaaJonotuskone {

    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.käyttöLkm());

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

      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.käyttöLkm());

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

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 asia on vähän mutkikkaampi: 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

Myös metodit voivat olla luokka- tai oliokohtaisia eli luokkametodeja tai ilmentymämetodeja (eli staattisia ja ei-staattisia metodeja):

Yllä nähdyn Esim-esimerkkiluokan luokkamuuttujan arvon asettava metodi olisi tyylikkäämpää ohjelmoida staattisena:

     public static void asetaLuo(int a) {
       luok = a;
     }
Jos staattisesta metodista yrittää käyttää ilmentymämuuttujaa, saa (legendaarisen!) virheilmoituksen:
non-static variable XXXXXX cannot be referenced from a static context
Ja jos staattisesta metodista kutsuu ilmentymämetodia, saa ilmoituksen:
non-static method XXXXXX cannot be referenced from a static context

Esimerkki luokkametodin käytöstä: laajennettu Jonotuskone

Jatketaan äskeistä 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; // aina "edellinen"

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

    private int käyttökertoja; // ilmentymämuuttuja

    public Jonotuskone() {
      käyttökertoja = 0;
    }

    public int annaNumero() {
      ++käyttökertoja   ;            // ilmentymämuuttujan kasvatus
      ++yhteinenJuoksevaNumero;      // luokkamuuttujan kasvatus
      return yhteinenJuoksevaNumero;
    }

    public int käyttöLkm() {
      return käyttökertoja;
    }
  }
Tuollaista luokkametodia voidaan kutsua luokka-aksessoriksi.

Käyttöesimerkki:

  public class TestaaJonotuskone {

    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.käyttöLkm());

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

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

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

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

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.

Esimerkikiksi, kun määritellään:

  public class Kenttia {
    public int i;
    public static int j=56;

    public Kenttia() {this.i=1;}
    // ...
  }
voidaan ohjelmoida vaikkapa:
    Kenttia a = new Kenttia();
    int luku = a.i + Kenttia.j;
    a.i = Kenttia.j;
    // jne...
Huom: Luokkamuuttujaan voi viitata myös olion kautta
    int luku = a.i + a.j;
koska "jokaisella oliolla on luokka". Tämä ei kuitenkaan ole kaunista. Miksi ei?

Huom: Ilmentymä- ja luokkamuuttujia voidaan määritellä myös ilman näkyvyydensäätelymäärettä:

  public class Kenttia2 {
    int i;
    static int j=56;
  }
Tällöin kentät ovat käytettävissä siinä pakkauksessa, johon luokka itse kuuluu. Oletusarvoisesti luokka kuuluu ns. nimettömään pakkaukseen. Se tarkoittaa yleensä kaikkia luokkia, jotka ovat samassa hakemistossa. Myös metodit, konstruktorit ja luokatkin voi määritellä ilman näkyvyydensäätelymäärettä. Myös ne ovat tällöin käytettävissa vain pakkauksessa, jossa ne itse sijaitsevat. Pakkauksista lisää luvussa 4.6.

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.

Esimerkkimäärittelyitä selostuksineen:

Javan luokka on hyvin monipuolinen väline. Sitä voi käyttää niin mallinnukseen ja kapselointiin kuin perinteisempienkin ohjelmointiratkaisujen toteuttamiseen.

Luokassa voi olla seuraavia rakenneosia:

Useimmista on nähty jo esimerkkejä. Tarkastellaan luokkaa Luokka, joka hyvin tiiviisti esittelee luokan rakenneosia ja niiden välisiä suhteita (esimerkki on hyvä lukea huolellisesti!):
public class Luokka {

//-- muuttujia eli kenttiä -----------------------------

  private int i=1;       // joka ilmentymällä on oma i
  private static int j;  // luokkamuuttuja eli staattinen kenttä:
                         // j on yhteinen kaikille ilmentymille

  private int k;         // yksityinen ilmentymämuuttuja
  public static int m;   // jukinen luokkamuuttuja
  int n;                 // pakkausnäkyvyys
  protected int o;       // pakkaus- ja aliluokkanäkyvyys

  public static final int VAKIO=77; // jukinen luokkavakio

  private static int[] taulu;   // yksityinen luokkataulukko

  public int[][] matriisi = {{1,2},{3,4}}; // julkinen ilmentymämatriisi


//--  konstruktoreita -----------------------------------

  public Luokka() { 
    i+=2; j=3+VAKIO;
  }

  public Luokka(int i) {  // kuormittaminen! 
    this();               // konstruktorin kutsu!
    this.i=i+2*this.i;    // viittaus olion i:hin! 
    j=3*i;
  }

//-- metodeita -----------------------------------------

  public int laske(int x) {  // arvon palauttava ilmentymämetodi,
    return i+j+x+VAKIO;      // "funktio"
  }

  private void tulosta(int x) {  // "arvoton" metodi, "proseduuri"
    System.out.println(i+x);
  }

  static void tulostaJ() { // staattinen metodi eli luokkametodi
    ++j;                   // pakkausnäkyvyydellä
    System.out.println(j);
    //  ++i; ei olisi luvallinen:
    //  "non-static variable i cannot be referenced from a static context"
  }

//-- aksessoreita ------------

  public void asetaK(int x) {     // piilotetun asetus,
    k = x;                        // asettava aksessori
  }
  public int mitaKOn() {          // piilotetun kysely,
    return k;                     // ottava aksessori
  }

//-- staattinen alustuslohko -----------------

  static {
    taulu = new int[8];
    for (int i=0; i<taulu.length; ++i)
      taulu[i] = i*i;
  }    
//-- pääohjelmametodi ----------------

  public static void main(String[] args) {

    Luokka olioA = new Luokka();
    Luokka olioB = new Luokka(7);

    System.out.println(olioA.i+" "+olioB.i);

    System.out.println(olioA.taulu[5]+" "+
                       olioB.taulu[6]+" "+
                       Luokka.taulu[7]);

    olioA.matriisi[0][1] = olioB.matriisi[1][0];
    System.out.println(olioA.matriisi[0][1]+" "+ olioB.matriisi[0][1]);

    System.out.println("Böö! "+olioA.laske(5)+" "+olioB.laske(6));

    olioA.tulosta(7); olioB.tulosta(8);

    olioA.asetaK(89);
    System.out.println(olioA.mitaKOn());

    olioA.tulostaJ(); olioB.tulostaJ();

    tulostaJ();                // oman luokan staattinen!
    // tulosta(8); ei olisi luvallinen!
    // "non-static method tulosta(int) cannot be referenced from a
    //  static context"

    Luokka.tulostaJ();
    // Luokka.tulosta(8);  ei myöskään olisi luvallinen!
  }
}

toString()-metodi

Metodi toString() määrittelee olioiden esityksen merkkijonona. Koska metodi on määritelty Object-luokassa, se on sovellettavissa kaikille olioarvoille. Object-luokan toString()-metodin antama merkkiesitys on hyvin tekninen; se antaa vain tiedon olion luokasta ja suoritusaikaisesta sijainnista. Monet luokat määrittelevät oman versionsa metodista ja siis olioiden merkkiesityksen.

Kun olio tulostetaan tai muuten muunnetaan String-olioksi, muunnos tehdään juuri tällä toString()-metodilla.

Omiin luokkiinkin on järkevää ohjelmoida toString()-metodi, joka korvaa metodin oletusversion. Esimerkki:

public class NiNu {

  private String nimi;
  private int numero;

  public NiNu(String ni, int nu) {
    nimi = ni;
    numero = nu;
  }

  public String toString() {
   return nimi + " no. " + numero;
  }
} 
Kun ohjelmoidaan:
    NiNu a = new NiNu("Kissa", 9);
    System.out.println(a); 
tulostuu:
Kissa no. 9
Ilman omaa toString()-metodia lauseet tulostavat esim.:
NiNu@1f6a7b9

Sisäluokista ("inner classes")

Javassa on mahdollista määritellä luokkia myös luokkien sisällä, ns. sisäluokkia (inner classes). Kenttien ja metodien lisäksi mm. luokan osina voi siis olla luokkia. Näillä luokilla voi itselläänkin olla rakenneosinaan luokkia.

Sisäluokkia käytetään erityisesti graafisia käyttöliittymiä ohjelmoitaessa.

Tällä kurssilla sisäluokkia ei käsitellä tarkemmin. Niistä ei myöskään kokeessa kysellä.

Halukkaiden tiedoksi sisäluokkatyyppejä:


Takaisin luvun 4 sisällysluetteloon.