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.

2.6 Luokista ja olioista

(Muutettu viimeksi 26.5.2006)

Java-kielessä luokka (class) on hyvin keskeinen käsite: Tavallinen ohjelma, sovellus, toteutetaan luokkana, www-sivuilla toimivat sovelmat ovat luokkia, ohjelmakirjastoja kerätään luokiksi, ... Luokkien välille voidaan muodostaa ns. luokkahierarkioita ns. periytymisen avulla, ... Luokkahierarkioita voidaan käyttää ongelmien ja "maailman" mallintamiseen sekä toisaalta myös ihan teknisesti ohjelmistoprojektien hallintaan ja ohjelmistojen "arkkitehtuuriin".

Tässä luvussa tutustutaan luokan määrittelemiseen ja luokan ilmentymän, ns olion (object), luontiin ja käyttöön.

Luokista ja olioista

Luokan yksi tärkeä käyttötapa on seuraavanlainen:

Luokka määrittelee jonkin toiminnallisuuden, "koneen piirustuksen". Määrittelyn perusteella voidaan sitten luoda luokan ilmentymiä, olioita, "koneita", jotka toteuttavat tuon toiminnallisuuden.

Ajatus on, että "koneen" toiminnan yksityiskohdat piilotetaan käyttäjältä, siis ohjelmalta (ja ohjelmoijalta!), joka käyttää luokan ilmentymiä. "Koneen" ohjelmoijalla ja käyttäjällä on yhteinen sopimus siitä, miten konetta käytetään: so. millaisia "mittareita", "nappuloita", "vipuja" yms. koneessa on ja mitä ne tarkoittavat. Mutta muuten luokan ohjelmoijan ja luokkaa käyttävän ohjelman laatijan ei tarvitse (eikä pidä) tietää toistensa tekemisistä. Sama henkilö voi toki toimia molemmissa tehtävissä!

Huom: Nuo "mittarit", "nappulat" ja "vivut" toteutetaan luokan julkisina (public) metodeina! (Tämä on osatotuus!)

Tällaisia luokkia voidaan kutsua abstrakteiksi tietotyypeiksi: luokan ilmentymän, olion, käyttäjä tietää, miten oliota käytetään, mutta hän ei tiedä miten olio on toteutettu. Luokan toteuttaja ei puolestaan välttämättä tiedä, mihin luokkaa käytetään, hän vain toteuttaa sovitunlaisen välineen.

Tällaista tapaa rakentaa ohjelmia kutsutaan myös kapseloinniksi: olion toiminnan yksityiskohdat piilotetaan olion luokkamäärittelyn sisään. Olioiden käyttäjien ei tarvitse tietää mitään olioiden sisäisestä toiminnasta. Eivätkä he itse asiassa myöskään saa siitä mitään tietää!

Tässä on kyseessä yksi menetelmä "unohtaa järjestelmällisesti" yksityiskohtia, jotta voitaisiin hallita isompia ongelmia kuin oikeastaan osataan. Menettelyllä on myös vaikutuksia ohjelman siirrettävyyteen, luotettavuuteen ja olemassaolevien ohjelmien uudelleenkäyttöön.

Näin käytettyinä luokat ovat tyyppejä siinä kuin jo tutuksi tulleet int, double, boolean ja String. On siis mahdollista määritellä muuttujia, joiden tyyppi on jokin itse määritelty luokka; olioita voi sijoittaa tuollaisten muuttujien arvoksi, olioita voi välittää parametreina, arvon palauttava metodi voi palauttaa olion, jonka tyyppi on jokin oma luokka...

Jo edellä on käytetty String-tyyppisiä olioita eli String-luokan ilmentymiä tähän tapaan! String-arvot ovat siinä mielessä abstrakteja, ettei niiden koneen sisäiseen esitykseen ja sijaintiin pääse mitenkään puuttumaan. Ne ovat siinä mielessä kapseloituja, että niitä voidaan käsitellä vain ja ainoastaan niillä metodeilla, jotka String-luokka antaa String-olioiden käyttäjille.

Luokan määrittely

Edellisessä luvussa käytimme luokkaa "pääohjelman ja aliohjelmien" toteuttamiseen:
public class Sovellus {

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

  private static int metodi2(...) {
    ...
  }
  ...

  public static void main(String[] args) {
    ...
  }
}
Tuollaisen luokan sisällä on siis vain pääohjelmametodi ja sen apuvälineiksi laadittujen private-metodien määrittelyjä.

Nyt ryhdymme laatimaan sellaisia luokkia, jotka voidaan ymmärtää "koneen piirustuksiksi", tietyn käyttäytymisen toteutuksiksi.

Luokassa voidaan metodien lisäksi määritellä muuttujia (eli kenttiä eli attribuutteja) (rakkaalla lapsella on monta nimeä). Nämä muuttujat esittävät "koneen" sisäistä tilaa (vrt. algoritmin tila luvussa 1.1).

Muuttujien arvoja käsitellään luokan metodien avulla. Metodien muuttujista poiketen luokan muuttujat ovat olemassa myös silloin, kun mitään luokan metodia ei olla suorittamassa. (Metodien muuttujista poiketen luokan muuttujilla on myös oletusalkuarvot, esim. 0, 0.0, false, null. Näistä myöhemmin.)

Esimerkki: Johonkin tarkoitukseen tarvitaan välinettä, jolla voi läpikäydä kuukausien numeroita 1, 2, 3, ... 12. Ehkäpä ohjelma simuloi ajan kulumista vuodesta toiseen kuukausi kerrallaan?

"Koneeseen" halutaan seuraavat "nappulat ja mittarit":

Kuulaskurit voisivat siis olla vaikkapa seuraavan näköisiä "laitteita":

Selain ei ymmärrä Javaa! Selain ei ymmärrä Javaa! Selain ei ymmärrä Javaa!

Nämä kuulaskurit on tosin tarkoitettu ihmisen käyttöön. Seuraavassa ohjelmoimme kuulaskurin ohjelmien käyttöön.

Luokan toteutus - "koneen sisäinen rakenne" - halutaan pitää piilossa luokan ilmentymien eli olioiden käyttäjiltä; toteutus halutaan kapseloida. Tahdotaan siis varmistaa, että "konetta" voi käyttää vain noilla luetelluilla välineillä, "koneen sisään ei pääse kurkistamaan eikä rattaita kääntelemään".

Kuulaskuri.java:

public class Kuulaskuri {

   private int kuu;   // sallitut arvot 1,..12

   // ----- konstruktori: -------

   public Kuulaskuri() {
     kuu = 1;
   }

  // ----- aksessorit: -----------

  public int moneskoKuu() {
    return kuu;
  }

  public void seuraavaKuu() {
    ++kuu;
    if (kuu == 13)
      kuu = 1;
  }
}

Määrittely
   private int kuu;
tarkoittaa, että luokassa on int-tyyppinen kenttä, ilmentymämuuttuja kuu, joka on käytettävissä vain luokan sisällä. Ainoastaan siis luokan omat metodit voivat käyttää muuttujaa kuu. Luokan metodit pitävät huolen, että muuttuja ei voi saada muita arvoja kuin luvut 1, 2, 3, ..., 12.

Luokassa on väline, konstruktori (constructor), jolla luokan ilmentymä eli olio luodaan. Konstruktori voi mm. asettaa luokan kentille alkuarvoja:

   public Kuulaskuri() {
     kuu = 1;
   }
Konstruktorin nimi on siis sama kuin luokan nimi. Konstruktorin määrittely on muuten kuin minkä tahansa metodin määrittely, mutta tyyppiä tai void-ilmausta ei voi käyttää. Tavallisen metodin määrittelystähän tuo osa ei voi puuttua!

Kuulaskuri-luokan konstruktori siis asettaa kentän kuu arvoksi luvun 1 aina kun luokan ilmentymä luodaan. (Ilmentymiä voi siis samaan aikaan olla useita.)

Luokalla voi olla useampiakin konstruktoreita erilaisin parametrein. Konstruktoreita voidaan siis kuormittaa.

Aksessori (accessor) on konstruktoria epämääräisempi käsite: Sillä tarkoitetaan metodia, jolla luokan private-kenttiä tutkitaan tai muutetaan. (Yleensä luokan kaikki metodit tavalla tai toisella käyttävät luokan kenttiä. Rajanveto aksessorin ja "muun työkalun" välillä on häilyvä.)

Metodi

  public int moneskoKuu() {
    return kuu;
  }
palauttaa arvonaan private-kentän kuu arvon, metodi
  public void seuraavaKuu() {
    ++kuu;
    if (kuu == 13)
      kuu = 1;
  }
laskee muuttujalle kuu seuraavan kuukauden numeron.

Metodit on määritelty julkisiksi, koska juuri niillä luokan ilmentymiä käytetään, juuri ne ovat niitä "nappuloita ja mittareita", joilla konetta käytetään! Mitään muuta keinoa luokan käyttäjällä ei ole käsitellä kenttää kuu!

Tässä esimerkissä käyttäjältä kätketty "tietorakenne" on äärimmäisen yksinkertainen, pelkkä yksittäinen muuttuja. Mutta samaan tapaan voidaan kätkeä myös monimutkaisempien tietorakenteiden toteutukset luokan käyttäjältä. Käyttäjä tuntee luokasta vain public-metodeiden nimet ja merkitykset. Hän ei mitenkään muuten pääse käsiksi tietorakenteen konkreettiseen toteutukseen. Siksi näin ohjelmoitua luokkaa voidaan kutsua "abstraktiksi tietotyypiksi", tämä juuri on sitä kapselointia.

Vaikka esimerkkiluokka saattaa näyttää käyttökelvottoman yksinkertaiselta, se tarjoaa erään palvelun, jota ei saataisi, jos käytettäisiin tavallisia int-tyyppisiä muuttujia kuukausien läpikäyntiin: tuon luokan ilmentymä ei voi koskaan esittää mitään muuta lukuarvoa kuin kelvollista kuukauden numeroa ja eteneminen 12:sta 1:een on luokan käyttäjän kannalta automaattista! Näin vältytään ohjelmointivirheeltä ainakin tässä yksityiskohdassa!

Olion luonti

Luokan Kuulaskuri määrittely ei vielä aikaansaa konkreettista välinettä, jolla kuukausien numeroita voidaan läpikäydä. Se määrittee, millaisia nämä läpikäyjät ovat, miten ne on toteutettu ja miten niitä käytetään.

Luokan ilmentymä (instance of a class) eli olio (object) luodaan operaatiolla new:

   new Kuulaskuri()
Ilmaus tarkoittaa, että luodaan uusi olio, jonka tyyppi on Kuulaskuri. Olion konstruktori asettaa uuden olion alkutilaan. Ilmaus on lauseke eli sillä itsellään on arvo. Tämä arvo on viite tuohon juuri luotuun olioon!

Luokka Kuulaskuri on tyyppinimi siinä kuin vaikkapa jo tuttu String-luokka. On siis mahdollista määritellä muuttujia, joiden tyyppi on Kuulaskuri:

   Kuulaskuri tämäKuu;
Näin määritellyllä muuttujalla ei ole kuitenkaan alkuarvonaan viitettä mihinkään Kuulaskuri-olioon. Mutta muuttujan arvoksi voidaan sijoittaa viite mihin tahansa tällaiseen olioon. Muuntyyppisiä arvoja muuttuja tämäKuu ei voi saada! (Jos tämäKuu on metodin muuttuja, sillä ei ole mitään alkuarvoa, jos se on luokassa määritelty muuttuja, sen oletusalkuarvo on null, joka tarkoittaa ns. tyhjää viitettä. Näihin asioihin tutustutaan aikanaan luvussa 4.)

Määrittely

   Kuulaskuri tuoKuu = new Kuulaskuri();
luo uuden Kuulaskuri-olion ja sijoittaa muuttujan tuoKuu arvoksi viitteen tähän olioon.

Huom: Vaikka olioita käsitellään aina viitteiden avulla, useimmiten viitteitä ei tarvitse ajatella. Usein on tapana sanoa "muuttujan arvona on olio" vaikka tarkoitetaan, että muuttujan arvona on viite olioon. Näin menetellään tälläkin kurssilla:

Nyt tätä tuoKuu-muuttujan arvona olevaa oliota voidaan käyttää Kuulaskuri-luokan tarjoamin julkisin metodein:

   int i = tuoKuu.moneskoKuu(); // i saa arvon 1

   tuoKuu.seuraavaKuu();        // kuukausi kasvaa yhdellä

   System.out.println(tuoKuu.moneskoKuu());
                                // tulostetaan arvo 2

   tämäKuu = tuoKuu;   // tämäKuu VIITTAA SAMAAN OLIOON
                       // kuin muuttuja tuoKuu! Tässä siis
                       // kopioidaan viite, ei oliota!

Olion metodeita kutsutaan pisteilmauksella:
    olio.metodi(parametrit)
Oliolle ovat käytettävissä olion luokassa määritellyt metodit, tarkemmin sanottuna ns. ilmentymämetodit. Metodeita kutsutaan edellisestä luvusta tuttuun tapaan: parametrien pitää olla oikeanlaisia, metodilla voi olla tyyppi, metodi voi olla tyypitön (void), ... (Käytettävissä ovat ainakin kaikki luokan public-metodit, mikään private-metodi ei ole käytettävissä luokkamäärittelyn ulkopuolella, ...Tämä asia täsmentyy myöhemmin!)

Esimerkkiohjelma (Kuulaskuritesti.java):

public class KuulaskuriTesti {

  public static void main (String[] args){

     Kuulaskuri tämäKuu;     
     Kuulaskuri tuoKuu = new Kuulaskuri();

     // käydään läpi 27 kuukautta:

     System.out.println("Kaksikymmentäseitsemän kuukautta:");
     for (int i=0; i<27; ++i){
       System.out.print(tuoKuu.moneskoKuu()+" ");
       tuoKuu.seuraavaKuu();
     }
     System.out.println();

     // kopioidaan viite, ei oliota:

     tämäKuu = tuoKuu;

     System.out.println("tuoKuun:n arvo on nyt "+tuoKuu.moneskoKuu());
     System.out.println("tämäKuun:n arvo on nyt "+tämäKuu.moneskoKuu());

     // edetään seuraavaan kuukauteen, myös tuoKuu etenee,
     // koska muuttujat viittaavat SAMAAN olioon:

     tämäKuu.seuraavaKuu();

     System.out.println("tuoKuun:n arvo on nyt "+tuoKuu.moneskoKuu());
     System.out.println("tämäKuun:n arvo on nyt "+tämäKuu.moneskoKuu());

     // luodaan kaksi uutta Kuulaskuri-oliota, vanhaan ei jää
     // enää viitteitä, se jää roskienkerääjän hoideltavaksi:

     tämäKuu = new Kuulaskuri();
     tuoKuu  = new Kuulaskuri();

     // edistetään tämäKuu:n osoittamaa oliota kahdesti:

     tämäKuu.seuraavaKuu();
     tämäKuu.seuraavaKuu();

     // tulostetaan 15 peräkkäistä kuukauden numeroa MOLEMMISTA
     // olioista:

     System.out.println("Viisitoista kuukautta rinnakkain:");
     for (int i=0; i<15; ++i){
       System.out.println(tämäKuu.moneskoKuu()+"\t"+ 
                          tuoKuu.moneskoKuu());
       tämäKuu.seuraavaKuu();
       tuoKuu.seuraavaKuu();
     }
  }
}

Esimerkki: miten kopioida yhden kuulaskurin kuukausi toiseen kuulaskuriin?

Kerran luennolla kysyttiin, miten voisi asettaa Kuulaskuri-olion samaan kuukauteen kuin jokin toinen Kuulaskuri-olio.

Toki voisi ohjelmoida algoritminpätkän, joka tutkii, mitä kuuta toinen laskuri osoittaa, ja pyöritellä sitten ensimmäistä seuraavaKuu()-metodilla, kunnes kuut ovat samat.

Mutta jos tuo operaatio olisi tavallinen, Kuulaskurin määrittelyä voisi täydentää seuraavasti:

public class Kuulaskuri {
  private int kuu;   // sallitut arvot 1,..12
  public Kuulaskuri() { ...
  public int moneskoKuu() {...
  public void seuraavaKuu() {...

  public void asetaSamaksi(Kuulaskuri tämänKanssa) {
    kuu = tämänKanssa.moneskoKuu();
  }
}
Nyt voisi ohjelmoida vaikkapa seuraavasti:
   tämäKuu = new Kuulaskuri();
   tuoKuu  = new Kuulaskuri();

   for (int i=0; i<6; ++i)
     tämäKuu.seuraavaKuu();

   // tämäKuu.moneskoKuu()  on nyt 7
   // tuoKuu.moneskoKuu()   on nyt 1

   tuoKuu.asetaSamaksi(tämäKuu);

   // molemmat ovat nyt 7!


Luokan rakenteesta

Luokka "koneen piirustuksena" on muodoltaan seuraavanlainen:
public class Kone {

  // koneen sisäinen rakenne eli ulkopuolisilta
  // piilotettu tietorakenne:

  private int i;
  private boolean b;
  private double d;
  private String s;
  ...

  // luokan KONSTRUKTORIT, metodit, joilla kukin luokan 
  // ilmentymä eli olio asetetaan alkutilaan; "koneen 
  // rakentaminen":

  public Kone() {
    ...
  }

  public Kone(int a) {  // konstruktoreita voi kuormittaa!
    ...
  }

  public Kone(String b, double c) {
    ...
  }
  ...

  // luokan AKSESSORIT, julkiset metodit, joilla piilossa
  // pidetyn tietorakenteen tilaa voi tutkia ja muuttaa,
  // "koneen nappulat ja mittarit":

  public int annaI() {          // ottava aksessori, "getteri"
    return i;
  }

  public void asetaI(int arvo) { // asettava aksessori, "setteri"
    i = arvo;
  }
  ...
 
  public void teeJotain() {
    i = ...;
    s = ...;
    ...
  }

  public void teeJotain(String jono) {  // kuormitetaan!
    i = ...;
    s = ...;
    ...
  }

  public double hmmTulo() {
    ...
    return ...;
  }
  ...

  // luokan muut metodit Kone-olioiden käsittelyyn:

  public void tulostaTila() {
    System.out.println(...);
    ...
  }

  public int laskeSitaSunTata(...) {
    ...
  }
  ...
  
  // apuvälineitä muiden metodien toteuttamiseen, VAIN
  // LUOKAN OMAAN KÄYTTÖÖN:

  private void metodi1(...) {          // ilmentymämetodi
    ...
  }

  private static void metodi2(...) {   // luokkametodi
    ...
  }

  private int metodi3(...) {
    ...
  }
  ...

}

Viittaus "tähän olioon": this

Koska luokka on luotavan olion malli, jokaisella luokan ilmentymällä on omat kopionsa luokassa määritellyistä muuttujista. Nämä muuttujat ovat ns. ilmentymämuuttujia (instance variable). Myöhemmin opitaan myös toisenlaisia muuttujia, luokkakohtaisia ns. luokkamuuttujia (class variable)!

Luokan ohjelmoija voi viitata "juuri tähän ilmentymään" ilmauksella this. Luokka Kuulaskuri voidaan ohjelmoida seuraavasti:

public class Kuulaskuri {

   private int kuu;   // sallitut arvot 1,..12

   // ----- konstruktori: -------

   public Kuulaskuri() {
     this.kuu = 1;
   }

  // ----- aksessorit: -----------

  public int moneskoKuu() {
    return this.kuu;
  }

  public void seuraavaKuu() {
    ++this.kuu;
    if (this.kuu == 13)
      this.kuu = 1;
  }
}
Kirjoitustapa painottaa ja selkiyttää sitä, että kenttä kuu on nimenomaan jokaisen olion oma kenttä, "juuri tämän ilmentymän" kenttä. Tätä tapaa on syytä suosia!

Esimerkkitehtävä kapseloinnista:

Seuraava on vanha harjoitustehtävä hieman muokattuna. Kannattaa tehdä!

Esimerkkiluokka: Pikkuvarasto

Ohjelmassa käsitellään yksinkertaisia varastoja. Joka varastosta tarvitaan vain tiedot "tuotteen nimi" ja "tuotteen määrä". Varastoja käsitellään seuraavin välinein (tämä on se "sopimus" luokan toteuttajan ja käyttäjän välillä):

Varastoja konstruoidaan seuraavin tavoin:

Varastojen sisältöjä käsitellään aksessorein: Muita työkaluja: Laaditaan luokka (Pikkuvarasto.java):
public class Pikkuvarasto {

  // toteutuksen tietorakenteet:

  private double määrä;  // tuotteen määrä  >= 0
  private String tuote;  // tuotteen nimi, huom: muuttumaton,
                         //                koska ohjelmoija niin haluaa ;-)
  // ----- konstruktorit: -----------

  public Pikkuvarasto() {
    this.määrä = 0.0;
    this.tuote = "(nimetön)";
  }

  public Pikkuvarasto(double määrä, String tuote) {
    this.tuote = tuote;  // !! 
    if (määrä > 0)
      this.määrä = määrä;
    else
      this.määrä = 0.0;
  }

  // -----  aksessorit: --------

  public double paljonkoOn() {
    return this.määrä;
  }

  public String mikäNimi() {
    return this.tuote;
  }

  public void vieVarastoon(double paljonko) { // vain >0 kelpaa
    if (paljonko > 0) 
      this.määrä += paljonko;
  }

  public double otaVarastosta(double paljonko) { // vain >0 kelpaa
    if (paljonko <= 0) 
      return 0;
    if (paljonko <= this.määrä) {  // vähennetään varastosta
      this.määrä -= paljonko;      // koko parametri
      return paljonko;
    } else {                  // annetaan mitä voidaan!
      paljonko = this.määrä;
      this.määrä = 0;
      return paljonko;
    }
  }


  // ------ muita työkaluja Pikkuvarasto-olioiden käsittelyyn: -------


  public String toString() {  // varastotilanne merkkijonona
    return "("+this.tuote+": "+this.määrä+")";
  }

  public Pikkuvarasto summa(Pikkuvarasto toinen) {
    return new Pikkuvarasto(this.määrä + toinen.paljonkoOn(), 
                            this.tuote + toinen.mikäNimi());
  }

  // ...  

}

Käyttöesimerkki (Pikkuvarastotesti.java):
public class PikkuvarastoTesti {
  public static void main(String[] args) {

    Pikkuvarasto mehua =  new Pikkuvarasto(10, "Mehu");
    Pikkuvarasto olutta = new Pikkuvarasto(123.4, "Olut");
    Pikkuvarasto bensaa = new Pikkuvarasto(90.1, "Bensa"); 

    // Koska luokkaan on määritelty toString-metodi, 
    // muuttujien arvoja voidaan tulostaa sellaisenaan:

    System.out.println(mehua);
    System.out.println(olutta);
    System.out.println(bensaa);   

/* TULOSTUS:

(Mehu: 10.0)
(Olut: 123.4)
(Bensa: 90.1)
-------------------------------------------------------------- 
*/
    // parametriton konstruktori:

    Pikkuvarasto a = new Pikkuvarasto();
    System.out.println(a);

/* TULOSTUS:

((nimetön): 0.0)
-------------------------------------------------------------- 
*/
    // viitteen kopiointi:

    Pikkuvarasto b = mehua;
    System.out.println("" + b + mehua);

/* TULOSTUS:

(Mehu: 10.0)(Mehu: 10.0)
-------------------------------------------------------------- 
*/
    // kasvatetaan b:tä, myös muuttuja mehua kasvaa:

    b.vieVarastoon(25);
    System.out.println("" + b + mehua);

/* TULOSTUS:

(Mehu: 35.0)(Mehu: 35.0)
-------------------------------------------------------------- 
*/
    // testataan metodeita paljonkoOn() ja mikäNimi()

    System.out.println("Muuttuja bensaa:" + bensaa + ", määrä=" +
                        bensaa.paljonkoOn()+ ", nimi=" +
                        bensaa.mikäNimi());

/* TULOSTUS:

Muuttuja bensaa:(Bensa: 90.1), määrä=90.1, nimi=Bensa
-------------------------------------------------------------- 
*/  
    // varastoon vienti:

    System.out.println(bensaa);
    bensaa.vieVarastoon(27.4);
    System.out.println(bensaa);
    bensaa.vieVarastoon(-34.1);
    System.out.println(bensaa);

/* TULOSTUS:

(Bensa: 90.1)
(Bensa: 117.5)
(Bensa: 117.5)
-------------------------------------------------------------- 
*/
    // varastosta ottaminen:
 
    System.out.println(olutta);
    double saatiin = olutta.otaVarastosta(10.2);
    System.out.println("Saatiin " + saatiin + ", tilanne: "+olutta);
    saatiin = olutta.otaVarastosta(-78.8); 
    System.out.println("Saatiin " + saatiin + ", tilanne: "+olutta);
    saatiin = olutta.otaVarastosta(175.5);
    System.out.println("Saatiin " + saatiin + ", tilanne: "+olutta);

/* TULOSTUS:

(Olut: 123.4)
Saatiin 10.2, tilanne: (Olut: 113.2)
Saatiin 0.0, tilanne: (Olut: 113.2)
Saatiin 113.2, tilanne: (Olut: 0.0)
-------------------------------------------------------------- 
*/
    // summan laskenta:

    olutta.vieVarastoon(432.1);
    Pikkuvarasto c = mehua.summa(olutta);
    System.out.println("" + mehua + olutta + c);

    Pikkuvarasto d = c.summa(bensaa);
    System.out.println("" + c + bensaa + d);
 
    d = d.summa(bensaa.summa(mehua));
    System.out.println(d);

/* TULOSTUS:

(Mehu: 35.0)(Olut: 432.1)(MehuOlut: 467.1)
(MehuOlut: 467.1)(Bensa: 117.5)(MehuOlutBensa: 584.6)
(MehuOlutBensaBensaMehu: 737.1)
-------------------------------------------------------------- 
*/

  }
}

Olio parametrina ja metodin palauttamana arvona

Itse laaditut luokat ovat siis tyyppejä siinä kuin kieleen valmiiksi rakennetut tyypitkin. Toisin sanoen omat luokat laajentavat kalustoa, jolla muuttujia voidaan määritellä, parametreja välittää, ...

Olemme käyttäneet valmiita tyyppejä esimerkiksi seuraavaan tapaan:

   String s, t="kissa", u;
   ...
   public int meto1(String a) {
   ...
   }
   public String meto2(int i) {
   ...
   }
   public String meto3(String j) {
   ...
   }

Omia luokkia eli omia tyyppejä voidaan käyttää ihan vastaavasti:
   Kuulaskuri s, t = new Kuulaskuri(), u;
   Pikkuvarasto v, w = new Pikkuvarasto(32.1, "Öljy");
   ...
   public int meto1(Kuulaskuri a) {
   ...
   }
   public Pikkuvarasto meto2(int i) {
   ...
   }
   public Kuulaskuri meto3(Pikkuvarasto j) {
   ...
   }

Kaikki omat tyypit siis ovat luokkia, ja näin kaikki omien tyyppien arvot ovat luokan ilmentymiä eli olioita.

Kun olio (eli viite olioon!) välitetään parametrina, muodollinen parametri saa alkuarvokseen viitteen olioon. Vaikka muodollinen parametri on olemassa vain metodin suorituksen ajan ja siihen tehdyt muutokset eivät vaikuta metodin ulkopuolelle, viitattuun olioon tehdyt muutokset jäävät voimaan metodin suorituksen päätyttyäkin. Metodi voi siis muuttaa parametrina saamansa olion arvoa, jos olion luokka tarjoaa välineitä arvon muuttamiseen.

Laaditaan pieni pääohjelmaluokka, johon ohjelmoidaan metodi Kuulaskuri-olion kasvattamiseen halutulla kuukausimäärällä (ParEsim.java):

public class ParEsim {
  
  private static void superKasvata(Kuulaskuri lask, int paljonko) {
    for (int i=1; i <= paljonko; ++i)
      lask.seuraavaKuu();
  }

  public static void main(String[] args) {
    Kuulaskuri eka = new Kuulaskuri();
    Kuulaskuri toka = new Kuulaskuri();

    System.out.println(eka.moneskoKuu()+" "+toka.moneskoKuu());

    eka.seuraavaKuu();
    superKasvata(toka, 7);

    System.out.println(eka.moneskoKuu()+" "+toka.moneskoKuu());
  }
}
Ohjelma tulostaa:
1 1
2 8
Näin siis parametrina välitetyn Kuulaskuri-olion arvoa muutettiin metodissa superKasvata ja tuo muutos jäi voimaan metodin suorituksen päätyttyäkin.

Jos sen sijaan muutetaan itse parametrin arvoa (eli viitettä), muutos ei vaikuta todelliseen parametriin (ParEsim2.java):

public class ParEsim2 {
  
  private static void yritaVaihtaa(Kuulaskuri muodPar) {
    muodPar = new Kuulaskuri();
    System.out.println(muodPar.moneskoKuu());
  }

  public static void main(String[] args) {
    Kuulaskuri todPar = new Kuulaskuri();
    todPar.seuraavaKuu();
    todPar.seuraavaKuu();
    System.out.println(todPar.moneskoKuu());
    
    yritaVaihtaa(todPar); // TODELLINEN PAR. EI MUUTU!

    System.out.println(todPar.moneskoKuu());
  }
}
Tulostus:
3
1
3


Metodi voi palauttaa arvonaan luokan ilmentymän eli vaikkapa jonkin oman tyypin arvon.

Ohjelmoidaan metodi, joka pyytää käyttäjältä varaston tiedot ja palauttaa kutsujalle Pikkuvarasto-olion (OlioArvonaEsim.java):

import java.util.Scanner;

public class OlioArvonaEsim {
  private static Scanner lukija = new Scanner(System.in);

  private static Pikkuvarasto lueVarasto() {
    String nimi;
    double sisalto;

    System.out.println("Tuotteen nimi?");
    nimi = lukija.nextLine();

    do {
      System.out.println("Tuotteen määrä? (ei-negatiivinen)");
      sisalto = lukija.nextDouble();
    } while (sisalto < 0);

    return new Pikkuvarasto(sisalto, nimi);  // HUOM: viite!
  }

  public static void main(String[] args) {
    Pikkuvarasto varasto;

    System.out.println("Luodaanpa varasto!");
    varasto = lueVarasto();

    System.out.println("Varasto on: " + varasto);

    // ... 
  }
}

Huom: Esimerkeissä käytettiin pääohjelman kutsumia "aliohjelmia", private static -metodeita, mutta samaan tapaan vaikkapa abstraktin tietotyypin toteuttavan luokan metodeille voi välittää olioita parametreina ja tuollaisetkin metodit voivat palauttaa arvonaan jonkin luokan ilmentymän eli olion!

Täydennetään luokkaa Pikkuvarasto metodilla teeKopio(), joka nimensä mukaan tekee kopion Pikkuvarasto2-oliosta (Pikkuvarasto2.java).

public class Pikkuvarasto2 {

  private double määrä;  // tuotteen määrä  >= 0
  private String tuote;  // tuotteen nimi

  public Pikkuvarasto2() { ...
  public Pikkuvarasto2(double määrä, String tuote) { ...
  public double paljonkoOn() { ...
  public String mikäNimi() { ...
  public void vieVarastoon(double paljonko) { // vain >0 kelpaa
  public double otaVarastosta(double paljonko) { ...
  public String toString() { ...
  public Pikkuvarasto2 summa(Pikkuvarasto2 toinen) { ...

  public Pikkuvarasto2 teeKopio() {
    double määrä = this.määrä;
    String tuote = this.tuote;
    Pikkuvarasto2 uusi = new Pikkuvarasto2(määrä, tuote);
    return uusi;

    // tai lyhyemmin: 
    //   return new Pikkuvarasto2(this.määrä, this.tuote),

  }
  // ...  
}
Kokeillaan uutta välinettä:
public class Pikkuvarasto2Testi {
  public static void main(String[] args) {

    Pikkuvarasto2 mehua =  new Pikkuvarasto2(10, "Mehu");
    Pikkuvarasto2 a, b;

    a = mehua;
    b = mehua.teeKopio();

    System.out.println("mehua: "+mehua+", a: "+a+", b: "+b );

    mehua.vieVarastoon(2);
    a.vieVarastoon(3);
    b.vieVarastoon(4);

    System.out.println("mehua: "+mehua+", a: "+a+", b: "+b );
  }
}
Ohjelma tulostaa:
Mehua: (Mehu: 10.0), a: (Mehu: 10.0), b: (Mehu: 10.0)
Mehua: (Mehu: 15.0), a: (Mehu: 15.0), b: (Mehu: 14.0)



Takaisin luvun 2 sisällysluetteloon.