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

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 on olemassa vain metodin suorituksen ajan eikä sillä ole oletusalkuarvoa.

Aiemmissa esimerkeissä luokassa määritellyt muuttujat ovat olleet sellaisia jotka syntyvät, kun luokan ilmentymä eli olio syntyy. Tällaisilla muuttujilla on oletusalkuarvo.

Luokassa määritellyt muuttujat voivat olla ilmentymä- eli oliokohtaisia (ei-staattisia) tai kaikille ilmentymille yhteisiä eli luokkakohtaisia (staattisia).

Ohjelmoidaan ensin tuttuun tapaan:

   public class Esim1 {
     private int i;

     public Esim1(int i) {this.i=i;}

     public void aseta(int i) {this.i = i;}
     public int  ota() {return this.i;}
   }

   // ...

   Esim1 a = new Esim1(4), b = new Esim1(11), c = new Esim1(-19); 

   System.out.println(a.ota()+" "+b.ota()+" "+c.ota());

   a.aseta(5);
   System.out.println(a.ota()+" "+b.ota()+" "+c.ota());

   b.aseta(b.ota()+21);
   System.out.println(a.ota()+" "+b.ota()+" "+c.ota());

   c.aseta(c.ota()+31);
   System.out.println(a.ota()+" "+b.ota()+" "+c.ota());

   /* Tulostus:
                4 11 -19
                5 11 -19
                5 32 -19
                5 32 12 
   */
Jokaisella luokan Esim1 ilmentymällä (a, b, c) on oma versionsa kentästä i. 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).

Esimerkki:

   public class Esim2 {
     private int i;
     private static int j=0;

     public Esim2(int i) {this.i=i;}

     public void aseta(int i) {this.i = i;}
     public int  ota() {return this.i;}

     public void asetaJ(int i) {j = i;}
     public int otaJ() {return j;}

   }

   ...

   Esim2 a = new Esim2(4), b = new Esim2(5), c = new Esim2(-19);
   
   System.out.println(a.otaJ()+" "+b.otaJ()+" "+c.otaJ());

   a.asetaJ(5);
   System.out.println(a.otaJ()+" "+b.otaJ()+" "+c.otaJ());

   b.asetaJ(b.otaJ()+21);
   System.out.println(a.otaJ()+" "+b.otaJ()+" "+c.otaJ());

   c.asetaJ(c.otaJ()+31);
   System.out.println(a.otaJ()+" "+b.otaJ()+" "+c.otaJ());

   /* Tulostus:
                0 0 0
                5 5 5
                26 26 26
                57 57 57
   */

Tässä esimerkissä joka oliolla (a, b, c) on oma kenttä i, mutta ne jakavat kentän j.

Huom: "Luokkamuuttuja" ja "luokan muuttuja" tarkoittavat eri asioita: "luokan muuttuja" on mikä tahansa metodien ulkopuolella määritelty muuttuja, staattinen tai ei-staattinen, mikä tahansa luokan kenttä. "Luokkamuuttuja" on nimenomaan luokan staattinen 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 (staattisia ja ei-staattisia metodeja):

Edellisen esimerkin (Esim2) luokkamuuttujaa käyttävät metodit olisi luontevampi ohjelmoida staattisina:

     public static void asetaJ(int i) {j = i;}
     public static int otaJ() {return j;}

Jos staattisesta metodista yrittää käyttää ilmentymämuuttujaa, saa (legendaarisen!) virheilmoituksen:
   Can't make a static reference to nonstatic variable ...
Ja jos staattisesta metodista kutsuu ilmentymämetodia, saa ilmoituksen:
   Can't make static reference to method ilmentymämetodin nimi

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 voitaisiin ehkä kutsua luokka-aksessoriksi. (En ole itseäni lukuunottamatta tosin kuullut kenenkään käyttävän tätä nimeä, AW 14.11.2000)

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 abstraktien tietotyyppien toteutuksina. Luokkien kentät ovat toistaiseksi olleet aina yksityisiä eli private-määriteltyjä.

Luokan kenttä voidaan metodin tapaan määritellä myös julkiseksi (public). Tällöin kenttä on käytettävissä suoraan - so. ilman aksessoreita - luokan 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;
Huom: Luokkamuuttujaan voi viitata myös olion kautta:
    int luku = a.i + a.j;
Mutta tämä ei ole kaunista. Miksei?

Huom: Muuttujia 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.

Huom: Myös metodi (ja luokka!) voi olla ilman näkyvyydensäätelymäärettä. Silloin sekin on käytettävissa luokkansa pakkauksessa.

"Nelikenttä"

Luokan kenttien ja metodien määrittelyä toisaalta julkisiksi tai yksityisiksi, toisaalta luokkakohtaisiksi tai ilmentymäkohtaisiksi voi havainnollistaa seuraavana "nelikenttänä":

       |  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-
       |   Lue.kluku(), Math.random() | metodit
-------|------------------------------|--------------------------
private| KENTÄT:                      | KENTÄT:
       | - piilossa pidetyt luokka-   | - olion piilossa pidetty
       | kohtaiset tietorakenteet     | tietorakenne, jota käsitel-
       | METODIT:                     | lää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 näistä on nähty jo esimerkkejä. Tarkastellaan luokkaa KaikkiMukaan, joka hyvin tiiviisti esittelee luokan rakenneosia ja niiden välisiä suhteita (esimerkki on syytä lukea huolellisesti!):
public class KaikkiMukaan {

//-- MUUTTUJIA ELI KENTTIÄ -----------------------------

  int i=1;       // joka ilmentymällä on oma i!

  static int j;  // luokkamuuttuja eli staattinen kenttä:
                 // j on yhteinen kaikille ilmentymille

  private int k; // piilossa pidetty kenttä

  static final int VAKIO=77; // vakio

  static int[] taulu;  

  int[][] matriisi = {{1,2},{3,4}};




//--  KONSTRUKTOREITA -----------------------------------

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

//-- METODEITA -----------------------------------------

  int laske(int x) {       // arvon palauttava, "funktio"
    return i+j+x+VAKIO;
  }  
  void tulosta(int x) {    // "arvoton", "proseduuri" 
    System.out.println(i+x);
  }
  static void tulostaJ() { // staattinen metodi
    ++j;                   // eli luokkametodi
    System.out.println(j);
    //  ++i; ei ole luvallinen!
    //      "Can't make a static reference to nonstatic variable"
  }  


//-- AKSESSOREITA ------------

  void asetaK(int x) {     // piilotetun asetus
    k = x;
  }
  int mitaKOn() {          // piilotetun kysely
    return k;
  }

//-- STAATTINEN ALUSTUSLOHKO -----------------

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

//-- LOPETUSMETODI ---------------------

  protected void finalize() throws Throwable {
    super.finalize();
    // ...
  }


//-- PÄÄOHJELMAMETODI ----------------

  public static void main(String[] a) {
    KaikkiMukaan x = new KaikkiMukaan();
    KaikkiMukaan y = new KaikkiMukaan(7);

    System.out.println(x.i+" "+y.i);

    System.out.println(x.taulu[5]+" "+      // sama taulu!
                       y.taulu[6]+" "+
                       KaikkiMukaan.taulu[7]);

    x.matriisi[0][1] = y.matriisi[1][0];   // eri matriisi!
    System.out.println(x.matriisi[0][1]+" "+ y.matriisi[0][1]);

    System.out.println("Böö! "+x.laske(5)+" "+y.laske(6));

    x.tulosta(7); y.tulosta(8);

    x.asetaK(89);                    // suojatun kentän
    System.out.println(x.mitaKOn()); // käyttöä operaatioin

    x.tulostaJ(); y.tulostaJ();      // luokkametodi!

    tulostaJ();                // oman luokan staattinen!
    // tulosta(8); //ei ole luvallinen!
    //      "Can't make static reference to method ..."

    KaikkiMukaan.tulostaJ();
    // KaikkiMukaan.tulosta(8);  //ei ole luvallinen!
    //      "Can't make static reference to method ..." 

  }
}

toString()-metodi

Kaikille olioille on olemassa ns. toString()-metodi, joka tavalla tai toisella tuottaa merkkijonoesityksen oliosta. (Tämä metodi on määritelty Object-luokassa. Se on korvattu erityisellä metodilla mm. alkeistyyppejä vastaavissa luokissa.)

Kun olio tulostetaan tai muuten muunnetaan String-olioksi, muutos tehdään juuri tällä toString()-metodilla. Perustyyppien muunnokset on määritelty luontevasti. Itse ohjelmoitujen luokkien oletusmuunnos String-olioksi ei ole yleensä sovelluksessa käyttökelpoinen (luokan nimi ja teknistä tietoa luokan talletuspaikasta).

Luokkaan voi itse ohjelmoida toString()-metodin, joka korvaa metodin oletusversion. Metodin on oltava public. Esimerkki:

public class NiNu {

  String nimi;
  int numero;

  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@80cb94a


Sisäluokista ("inner classes")

Javan versioon 1.1 lisättiin mahdollisuus määritellä luokkia luokkien sisään, ns. sisäluokkia (inner classes). Kenttien ja metodien lisäksi mm. luokan osina voi siis olla luokkia. Näillä luokilla voi itselläänkin olla rakenneosina luokkia.

Sisäluokkia käytetään selkeyttämään ohjelmaa erityisesti graafista käyttöliittymää ohjelmoitaessa. Niillä voidaan kurittomasti ohjelmoiden laatia myös erittäin vaikeaselkoisia ohjelmia!

Tällä kurssilla sisäluokkia ei ole mahdollista käsitellä tarkemmin. Niistä ei myöskään kokeessa kysellä. Halukkaiden tiedoksi sisäluokkatyyppejä:


Takaisin luvun 4 sisällysluetteloon.