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

2 Oliot ja kapselointi, luokka olion mallina, String-aksessoreita

(Muutettu viimeksi 17.9.2011 sivu perustettu 16.9.2010.)

Luokka ja olio

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 käytetään ongelmien ja "maailman" mallintamiseen sekä toisaalta myös ihan teknisesti ohjelmistoprojektien hallintaan ja ohjelmistojen arkkitehtuurin toteuttamiseen.

Luokkien monipuoliseen käyttöön tutustutaan jatkokurssin puolella. Nyt keskitytään yhteen tärkeään luokan käyttötapaan: luokan määrittelemiseen olion malliksi ja luokan ilmentymän eli olion (object) luontiin ja käyttöön.

Kapselointi

Luokka määrittelee jonkin toiminnallisuuden, "koneen piirustukset". Määrittelyn perusteella voidaan luoda luokan ilmentymiä, olioita, "koneita", joista jokainen toteuttaa 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 "koneen" 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ä!

Näin ohjelmoituja 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 ohjelmoida olion toteutuksen yksityiskohdat luokkamäärittelyn sisään – piiloon olion käyttäjältä – kutsutaan kapseloinniksi. Olion käyttäjän ei tarvitse tietää mitään olioden sisäisestä toiminnasta. Eikä hän itse asiassa edes saa siitä mitään tietää vaikka kuinka haluaisi!

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

Jo edellä on käytetty String-tyyppisiä olioita eli String-luokan ilmentymiä tähän tapaan! String-arvot ovat siinä mielessä abstrakteja ja samalla kapseloituja, että ne ovat käytettävissä vain ja ainoastaan niillä metodeilla, jotka String-luokka antaa String-olioiden käyttäjille. "String-koneen" sisäiseen rakenteeseen ei pääse mitenkään muuten puuttumaan.

String-luokassa (ks. Java-API) on suuri joukko operaatioita, joita voidaan soveltaa String-olioille, esimerkiksi:

String m = "kissa";
System.out.println(m.length());     // m.length()
if (m.equals("katti"))              // m.m.equals("katti")
   System.out.println("hau");
System.out.println(m.indexOf("s")); // m.indexOf("s")
// jne., jne.

Tällaisia String-luokassa määriteltyjä operaatioita, joita siis voi soveltaa kaikille String-olioille, kutsutaan nimellä aksessori eli aksessorimetodi. Niillä päästään käyttämään, "aksessoimaan" oliota sellaisin tavoin kuin aksessorien ohjelmoija on hyväksi katsonut.

Luokka on tyyppi

Itse ohjelmoidut ja valmiina saadut luokat ovat tyyppejä siinä kuin jo tutuksi tulleet int, double, boolean ja String. Itse asiassa tuo String onkin "valmiina saatu" luokka, nuo muut mainitut ovat Javan ns. alkeistyyppejä.

Kun jonkin luokan ilmentymä, olio, halutaan sijoittaa muuttujan arvoksi, muuttujan tyyppi on juuri tuo luokka! Niinpä String-olio sijoitetaan muuttujaan, jonka tyyppi on juuri tuo String:

String m = "kissa;

Kun jatkossa ohjelmoimme omia luokkia, niiden ilmentymät eli oliot sijoitetaan muuttujiin, joiden tyyppi on juuri tuo kyseinen luokka. Jos vaikka luokkamme on nimeltään Kissantassu, luokan ilmentymiä käsitellään seuraavaan tapaan:

Kissantassu k = new Kissantassu(); // LUODAAN olio ja asetetaan
                                   // se muuttujan arvoksi

k.kynnetUlos();    // ohjataan olion käyttäytymistä
k.raapaise();      // kutsumalla sen luokkaan ohjelmoituja
k.kynnetSisaan();  // aksessoreita
...

Uusi olio luodaan new-operaatiolla. Tästä pian lisää.

Sen lisäksi, että muuttujilla on tyyppi, myös parametreilla on tyyppi, arvon palauttavilla metodeilla on tyyppi, paluuarvon tyyppi,... Kaikissa näissä tilanteissa tyyppi voi olla mikä tahansa (!) Javan tyyppi, valmiina saatu tai itse ohjelmoitu luokka. Silloin muuttujan, parametrin tai metodin arvona on olio, kulloinkin kyseessä olevan luokan ilmentymä! Tälle idealle tulemme näkemään vielä paljon käyttöä!

Ja sitten itse tekemään: Kuulaskurin API-kuvaus

Ja sitten tekemään omaa luokkaa olion malliksi. Ensin pitää päättää ja määritellä tarkasti, mitä halutaan. Kun laaditaan välinettä ohjelmien laatimiseen, työkalua sovellusten toteuttamisen avuksi, tapana on luetella halutut toiminnot ja niiden merkitykset. Katso esimerkkinä vaikkapa, miten String-luokan määrittelyssä luetellaan joukko String-olioille sovellettavissa olevia operaatioita eli aksessoreita (ks. Java-API).

Olioilta vaadittujen toimintojen luetteloa on tapana kutsua API-kuvaukseksi ("Application Programming Interface").

Esimerkki: Jostain (käsittämättömästä?) syystä sovelluksen laatija tarvitsee välineen kuukausien numeroiden läpikäymiseen. Välineen ominaisuuksiksi halutaan seuraavaa:

Kuulaskuri-luokan API-kuvaus: (Ilmaistaan toivotut operaationimet Javan metodeina.)

Tällaiset määräykset/toiveet siis antaa sovelluksen tekijä, Kuulaskuri-luokan tilaaja. Ja nämä määräykset/toiveet Kuulaskuri-luokan toteuttaja sitten ohjelmoi luokkaansa, "koneen piirrustuksiin".

Tarkoitus on siis ohjelmoida väline Kuulaskuri käytettäväksi ohjelmoinnissa, muiden ohjelmien laadinnassa. Vielä ei tehdä graafisia ohjelmia, mutta yllä määriteltyä "laitetta" voi kuitenkin havainnollistaa seuraavalla sovelmalla:

Selain ei ymmärrä Javaa!

Ja kun on kerran "hyvät piirustukset" (=luokka) laadittu, niistä voidaan tietysti tehdä useampiakin ilmentymiä eli olioita:

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

Huom: Emme siis (ainakaan vielä) ala tehdä sovelmia (eli "appletteja") www-sivulla näytettäviksi. Nyt ohjelmoimme välineen, jota käyttäen voi laatia muita ohjelmia. Ja juuri tähän tyyliin myös Javan valmis kalusto on laadittu: Javan API sisältää tuhansia ja taas tuhansia valmiiksi ohjelmoituja luokkia, jotka helpottavat omien sovellusten toteuttamista!

Ja sitten itse tekemään: Kuulaskurin luokka

Kurssilla aiemmin tehdyt ohjelmat ovat olleet rakenteeltaan seuraavanlaisia:

public class Sovellus {
  private static void pikkuApulainen1(...) {
    ...
  }
  private static int pikkuApulainen2(...) {
    ...
  }
  ...
  public static void main(String[] args) {  // pääohjelma
    ...  kutsutaan noita "pikku apulaisia"
  }
}

Näin Javan luokkaa käytettiin ja käytetään perinteiseen "pääohjelma-aliohjelmat"-tyyliin.

Nyt ryhdymme kirjoittamaan luokkia myös "koneen piirustuksiksi" ja näin se menee:

public class Kuulaskuri {

   private int kuu;   // sallitut arvot 1,..12 
                      // Kenttä kuu on piilotettu, kapseloitu "tietorakenne".
 
   // ----- konstruktori: -------

   public Kuulaskuri() {  // Konstruktori on algoritmi, joka suoritetaan 
     kuu = 1;             // aina, kun luokasta luodaan ilmentymä.
   }                      // Se asettaa luotavan olion alkutilaan.

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

  public int moneskoKuu() {  // Tämä on ns. ottava aksessori eli "getteri"
    return kuu;              // Sillä pääsee (rajoitetusti?) tutkimaan olion tilaa.
  }

  public void seuraavaKuu() { // Tämä on ns. asettava aksessori eli "setteri".
    ++kuu;                    // Sillä pääsee (rajoitetusti?) muuttamaan olion tilaa.
    if (kuu == 13)
      kuu = 1;
  }
}

Luokkamäärittelyn kohta

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. Ja metodit ohjelmoidaan siten, että muuttuja ei voi saada muita arvoja kuin luvut 1, 2, 3, ..., 12.

Jokainen luotava olio saa ikioman version muuttujasta kuu ja siis yleisemmin sanottuna: Olio saa oman version kaikista luokassa määritellyista ilmentymämuuttujista.

Huom: Luokassa määriteltyjä ilmentymämuuttujia kutsutaan myös kentiksi ja attribuuteiksi. Joskus myös piirteiksi.

Luokassa on väline, jolla luokan ilmentymä eli olio luodaan, konstruktori (constructor), Konstruktori asettaa luotavan olion alkutilaan:

   public Kuulaskuri() {
     kuu = 1;
   }

Konstruktorin nimi on sama kuin luokan nimi. Konstruktorin rakenne on muuten kuin minkä tahansa metodin rakenne, mutta tyyppiä tai void-ilmausta ei ole. Kuulaskuri-luokan konstruktori asettaa jokaisen luotavan olion kuu-kentän arvoksi luvun 1.

On tyypillistä, että luokassa on useampiakin konstruktoreita erilaisin parametrein. Konstruktoreita voidaan ns. kuormittaa. Tästä myöhemmin.

Aksessori (accessor) on konstruktoria epämääräisempi käsite: Sillä tarkoitetaan metodia, jolla luokan yksityisiä (private) ilmentymämuuttujia tutkitaan tai muutetaan. Näitä usein kutsutaan "gettereiksi" ja "settereiksi".

Metodi ("getteri") palauttaa jonkin kentän arvon ("gettaa"):

  public int moneskoKuu() {
    return kuu;
  }

Aksessori moneskoKuu() siis palauttaa arvonaan private-kentän kuu arvon.

Metodi ("setteri") asettaa jonkin kentän arvon ("settaa"):

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

Aksessori seuraavaKuu() siis laskee muuttujalle kuu seuraavan kuukauden numeron.

Metodit on määritelty julkisiksi (public), koska juuri niillä luokan ilmentymiä käytetään, juuri ne ovat niitä "nappuloita ja mittareita", joilla konetta käytetään! Mitään muuta keinoa Kuulaskuri-olioden 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 Kuulaskuri-olioiden käyttäjän kannalta automaattista! Näin vältytään ohjelmointivirheeltä ainakin tässä yksityiskohdassa!

Ja sitten itse tekemään: Kuulaskurin käyttö – viimeinkin oliota kehiin

Luokan Kuulaskuri määrittely ei vielä aikaansaa konkreettista välinettä, jolla kuukausien numeroita voidaan läpikäydä. Se vain määrittelee, 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. Luotava olio voidaan sijoittaa muuttujaan, jonka tyyppi on juuri tuon olion luokka:

Kuulaskuri minun = new Kuulaskuri();
Kuulaskuri sinun = new Kuulaskuri();

Tässä syntyi kaksi Kuulaskurioliota. Molempia voidaan nyt käyttää luokan aksessoreilla:

System.out.println(minun.moneskoKuu() + ", " + sinun.moneskoKuu() );
minun.seuraavaKuu();
minun.seuraavaKuu();
System.out.println(minun.moneskoKuu() + ", " + sinun.moneskoKuu() );

for (int i=0; i<20; ++i)
  sinun.seuraavaKuu();
System.out.println(minun.moneskoKuu() + ", " + sinun.moneskoKuu() );
// ... jne ...

Miten luokka ja pääohjelma liittyvät toisiinsa

Pääohjelma voidaan kirjoittaa itse luokaan Kuulaskuri:

public class Kuulaskuri {

   private int kuu;   // sallitut arvot 1,..12
   //... Tässä siis tietekin koko Kuulaskurin määrittely.
  }

  public static void main(String[] args) {

    Kuulaskuri minun = new Kuulaskuri();
    Kuulaskuri sinun = new Kuulaskuri();

    System.out.println(minun.moneskoKuu() + ", " + sinun.moneskoKuu() );
    minun.seuraavaKuu();
    minun.seuraavaKuu();
    System.out.println(minun.moneskoKuu() + ", " + sinun.moneskoKuu() );

    for (int i=0; i< 20; ++i)
      sinun.seuraavaKuu();
    System.out.println(minun.moneskoKuu() + ", " + sinun.moneskoKuu() );
  }
}

Tämä tyyli soveltuu yleensä vain hyvin yksinkertaisiin tilanteisiin, koska useimmiten sovelluksessa käytetään monia eri luokkia. Kuitenkin "olion piirustuksiksi" tarkoitettuihinkin luokkiin on usein järkevää sijoittaa pääohjelma, joka vaikkapa esittelee luokan käyttöä tai esimerkiksi kertoo, miten luokkaa käytetään.

Tavallisempi ("oikeassa elämässä" ainoa) tapa on ohjelmoida luokkaa käyttävä sovellus omaksi luokakseen:

public class Sovellus {

   //... Luokan sovellus oma kalusto, pääohjelman "pikku apulaisia" yms.

  public static void main(String[] args) {

    Kuulaskuri minun = new Kuulaskuri();
    Kuulaskuri sinun = new Kuulaskuri();

    System.out.println(minun.moneskoKuu() + ", " + sinun.moneskoKuu() );
    minun.seuraavaKuu();
    minun.seuraavaKuu();
    System.out.println(minun.moneskoKuu() + ", " + sinun.moneskoKuu() );

    for (int i=0; i< 20; ++i)
      sinun.seuraavaKuu();
    System.out.println(minun.moneskoKuu() + ", " + sinun.moneskoKuu() );
  }
}

Jos sovellus ja käytettävä luokka sijaitsevat samassa hakemistossa, ne "löytävät toisensa" automaattisesti. Myöhemmin opitaan, miten eri hakemistoissa olevat luokat voi opettaa löytämään toisensa.

Luokan luominen NetBeanssissä

Kun olet luonut pääohjelman ja sinulla on projekti auki, klikkaa vasemmalla olevasta projektivalikosta projektin nimen kohdalla hiiren oikeaa nappia, valitse new ja Java class ja anna luokalle nimi kohtaan Class name. Luokan nimen tulee alkaa isolla alkukirjaimella. Paina Finish niin luokka syntyy..

Toinen esimerkki: Laskuri ja Kahvikassa

Laskuri

Ohjelmoidaan ensin Kuulaskuriakin yksinkertaisempi laite, joka osaa vain kasvattaa lukumäärälaskuria annetusta alkuarvosta ylöspäin:

public class Laskuri {
  private int arvo;  // piilossa pidetty "tietorakenne"

  public Laskuri(int alkuarvo) {  // konstruktori
    arvo = alkuarvo;
  }
  public void kasvataArvoa() {    // setteri
    arvo = arvo + 1;
  }
  public int annaArvo() {         // getteri
    return arvo;
  }
}

Jokaisella Laskuri-luokan ilmentymällä eli Laskuri-oliolla on piilossa pidetty ilmentymämuuttuja arvo, jota voi ainostaan kasvattaa yhdellä ja jonka arvoa voi kysellä. Konstruktorille annetaan aina laskurin aloitusarvo parametrina.

Sovellus joka käyttää Laskuri-olioa

Laskuri-luokan määrittely ei vielä luo ensimmäistäkään Laskuri-olioa. Tehdään ohjelma Laskin, joka esittelee Laskuri-olion luonnnin ja käyttöä:

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

    Laskuri omaLaskuri = new Laskuri(0);   // luodaan uusi Laskuri-olio 

    System.out.println("omaLaskurin arvo on " + omaLaskuri.annaArvo());

    omaLaskuri.kasvataArvoa();             // kasvatetaan Laskuri-olion arvoa kahdesti
    omaLaskuri.kasvataArvoa();    

    System.out.println("omaLaskurin arvo on " + omaLaskuri.annaArvo());
  }
}

Ohjelman Laskin suoritus:

omaLaskurin arvo on 0
omaLaskurin arvo on 2

Matin Kahvikassa Laskuri-olioa käyttäen

Ohjelmoidaan toinenkin sovellus luokkaa Laskuri käyttäen. [Tällainen menettely – hyvin ohjelmoidun luokan uusiokäyttö eli uudelleenkäyttö – on oikeastikin hyvin tavanomaista!]

Ohjelmoidaan sovellus Kahvikassa, joka käyttää luokkaa Laskuri samaan tyyliin kuin edellinen esimerkki:

public class Kahvikassa {	
  public static void main(String[] args) {
    
    Laskuri mattiL = new Laskuri(0);
				
    System.out.println("Matti L. on juonut " + mattiL.annaArvo() + " kahvia.");

    System.out.println("Juodaan kahvia.");
		
    mattiL.kasvataArvoa();
    mattiL.kasvataArvoa();
	
    System.out.println("Matti L. on juonut " + mattiL.annaArvo() + " kahvia.");
  }
}

Ohjelman suoritus:

Matti L. on juonut 0 kahvia.
Juodaan kahvia.
Matti L. on juonut 2 kahvia.

Kahden Matin Kahvikassa

Laajennetaan kahvikassasovellusta siten, että myös Matti P. käyttää ohjelmaa ja tuo vanhat velkansa mukanaan. Kun muuttujan mattiP arvoksi luodaan Laskuri-olio, se asetetaan alkutilanteessa velkojen mukaiseen alkuarvoon (8 kuppia):

public class Kahvikassa {
	
  public static void main(String[] args) {
    
    Laskuri mattiL = new Laskuri(0);
    Laskuri mattiP = new Laskuri(8);
		
    System.out.println("Matti L. on juonut " + mattiL.annaArvo() + " kahvia.");
    System.out.println("Matti P. on juonut " + mattiP.annaArvo() + " kahvia.");

    System.out.prinltn("Juodaan kahvia.");

    mattiL.kasvataArvoa();
    mattiL.kasvataArvoa();
		
    mattiP.kasvataArvoa();
	
    System.out.println("Matti L. on juonut " + mattiL.annaArvo() + " kahvia.");
    System.out.println("Matti P. on juonut " + mattiP.annaArvo() + " kahvia.");
  }
}

Ohjelman tuloste olisi seuraavanlainen.

Matti L. on juonut 0 kahvia.
Matti P. on juonut 8 kahvia.
Juodaan kahvia.
Matti L. on juonut 2 kahvia.
Matti P. on juonut 9 kahvia.

Kahvikassa-ohjelma havainnollistaa, miillä tavoin Laskuri-oliota voitaisiin käyttää, jos laadittaisiin "oikea" sovellus kahvikirjanpitoon. Jokaiselle kahvinjuojalle luotaisiin oma Laskuri-olio, jota päivitettäisiin tarpeen mukaan.

Olioiden ja luokkien käyttäminen ohjelmoinnissa helpottaa vastuun jakamista ohjelman eri osien kesken ja mahdollistaa ideoiden ja lähdekoodin uudelleenkäytön uusissa ohjelmissa.

Ohjelmointikonventioita: "setterit" ja "getterit", viittaus itseen: this

Javan valmiissa kalustossa, Javan omassa "APIssa", ottavat ja asettavat aksessorit – "getterit" ja"setterit" – on usein nimetty etuliitteellä "get" ja "set" tyyliin getText(), getLabel(), setEnabled(true), setMaximumSize(...), ...

Moni suosii tätä tapaa myös omissa ohjelmissaan. Ja jopa silloin, kun tunnukset muuten ovat suomeksi. [Esim. minun (AW) silmilleni tuo on selkeää — get ja set ovat ikäänkuin "varattuja ilmauksia gettereiden ja settereiden tunnistamiseen. Tiedän toki myös ihmisiä, jotka hyppivät tätä käytäntöä nähdessään seinille ja saavat lisäksi näppylöitä...;-]

Kun luokkaa ohjelmoidaan olion malliksi, voi toisinaan tulla tarve puhua oliosta, jota juuri tällä kerralla käsitellään. Tähän tarkoitukseen Javassa on ilmaus this, joka vaikkapa aksessorin sisällä viittaa juuri siihen olioon, jota "juuri nyt" "aksessoidaan". Tekniikalla on erilasia käyttötarkoituksia. Yhteen niistä – ohjelmointityyliin liittyvään – tutustumme seuraavassa esimerkissä. Syvällisemmin asia selvinnee jatkokurssin puolella.

Minimuistion API: getSitaSunTata ja setSitaSunTata

Ohjelmoidaan "minimuistio", jonne voi tallettaa muistiin jonkin luvuista 1-100. Miksi tällaista tarvittaisiin, onkin sitten toinen kysymys... Ehkäpä vastaukseksi kelpaa: ohjelmoinnin opetteluun? ;-)

Minimuistio-olioille asetetaan vaatimukset:

Minimuistion toteutus: this

Usein Java-tyylissä, "Java-idiomissa", halutaan nimetä konstruktorin ja asettavan aksessorin muodollinen parametri samalla nimellä kuin asettava kenttä (eli attribuutti eli ilmentymämuuttuja). Tällöin metodin lohkossa tuo nimi tarkoittaa nimenomaan muodollista parametria. Mutta apu löytyy:

Luokkamäärittelyn sisällä voi puhua "kulloinkin kyseessä olevasta oliosta" ilmauksella this. Toistaiseksi ainoa ilo meille tästä on, että pääsemme puhumaan konstruktorin ja aksessorien algoritmeissa sekä muodollisesta parametrista, että kulloinkin kyseessä olevan olion kentästä, vaikka ne olisivat saman nimiset. Minimuistion toteutus valaissee:

public class Minimuistio {

  private int arvo;  // sallitaan vain arvot 1-100

  public Minimuistio(int arvo) {
    if (1 <= arvo && arvo <= 100)  // vain kelvollinen käytetään
      this.arvo = arvo;  // luotavan olion kenttään sijoitetaan parametrin arvo
    else              // ellei kelpaa, virhetilanne, mutta jotain on keksittävä
      this.arvo = 13; // ... on vähän tyhmää..., mutta asiaan palataan...
  }

  public int getArvo() {  // "getteri"
    return arvo;  // myös return this.arvo olisi korrekti, mutta tässä
  }               // sitä ei välttämättä tarvita, koska ei ole monta "arvoa"

  public void setArvo(int arvo) {  // "setteri"
    if (1 <= arvo && arvo <= 100)  // vain kelvollinen otetaan huomioon
      this.arvo = arvo;  // ilmentymämuuttujaan sijoitetaan parametrin arvo
  }
}

Myöhemmin this-ilmaukselle opitaan muutakin käyttöä.

Minimuistion käyttöä

Minimuistion käytössä ei pitäsi enää olla mitään uutta:

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

    Minimuistio rahaaTaskussa = new Minimuistio(20);
    Minimuistio onnenluku = new Minimuistio(14);

    System.out.println("Rahaa on  " + rahaaTaskussa.getArvo());
    System.out.println("Onnea tuo " + onnenluku.getArvo());

    rahaaTaskussa.setArvo(97);
    onnenluku.setArvo(84);

    System.out.println("Rahaa on  " + rahaaTaskussa.getArvo());
    System.out.println("Onnea tuo " + onnenluku.getArvo());

    rahaaTaskussa.setArvo(100000);
    onnenluku.setArvo(-666);

    System.out.println("Rahaa on  " + rahaaTaskussa.getArvo());
    System.out.println("Onnea tuo " + onnenluku.getArvo());
  }
}

Päätetään itse, millaisena olio tulostuu: toString()-metodi

Javassa kaikki oliot voidaan tulostaa sellaisinaan, ilman mitään lisämäärittelyitä print- ja println-operaatioilla, koska jokaisen olion luokka on perinyt yliluokaltaan tai viimeistään luokalta Object merkkiesityksen taidon, metodin public String toString(). [Äskeisen ja seuraavien virkkeiden muutamat oudot sanat selviävät myöhemmin.]

Kaikkien luokkien perimä taito olion merkkiesityksen muodostamiseen on hyvin tekninen. Jos tulostamme vaikkapa edellisen esimerkin lopussa:

System.out.println(rahaaTaskussa);
System.out.println(onnenluku);

saamme varsin kryptistä tietoa noista olioista:

Minimuistio@57fee6fc
Minimuistio@1feed786

Mutta voimme korvata perityn metodin public String toString() omalla versiollamme. Toisin sanoen annamme uuden merkityksen tuolle metodille. Näin voimme itse päättää, millaisena haluamme Minimuistio-olioiden tulostuvan.

Halutkaamme vaikka yksinkertaisesti Minimuistio-olion tulostuvan seuraavaan tyyliin:

arvo on 97
arvo on 84

Eikun ohjelmoimaan:

public String toString() {
  return "arvo on " + arvo;
  }

Ja lisätään tämä metodi vielä oikealle paikalleen Minimuistioon:

public class Minimuistio {   // täydennetään toString()-metodilla

  private int arvo;  // sallitaan vain arvot 1-100

  public Minimuistio(int arvo) {
    if (1 <= arvo && arvo <= 100)  // vain kelvollinen käytetään
      this.arvo = arvo;  // luotavan olion kenttään sijoitetaan parametrin arvo
    else              // ellei kelpaa, virhetilanne, mutta jotain on keksittävä
      this.arvo = 13; // ... on vähän tyhmää..., mutta asiaan palataan...
  }

  public int getArvo() {  // "getteri"
    return arvo;  // myös return this.arvo olisi korrekti!
  }

  public void setArvo(int arvo) {  // "setteri"
    if (1 <= arvo && arvo <= 100)  // vain kelvollinen otetaan huomioon
      this.arvo = arvo;  // ilmentymämuuttujaan sijoitetaan parametrin arvo
  }

  public String toString() {
    return "arvo on " + arvo;
  }
}

Jo rupesi Lyyti kirjoittamaan...

Sama operaatio eri parametrein: kuormittaminen

Toisinaan jokin operaatio on sovelluksen laatijan kannalta "sama juttu", mutta operaation toteuttajan kannnalta aivan erilainen. Tuttu esimerkki on println-operaatio:

println(2 + 3);                // kokonaisluku
println(3.14 * 0.07);          // liukuluku
println("Tulos = " + (100-3)); // merkkijono
println(1<2 && 100 != 1000);   // totuusarvo true tai false

Sovelluksen laatija ajattelee, että tulostanpahan arvon, samantekevää minkä, kunhan vain arvo tökätään tulosvirtaan sovelluksen käyttäjän luettavaksi.

Mutta println-operaation toteuttajan kannnalta tilanne onkin ongelmallisempi: kokonaisluvun, liukuluvun, merkkijonon ja totuusarvon esitykset bittijonoina (tavujen jonona) tietokoneen muistissa poikkeavat perusteellisesti toisistaan. Tiedon esityksen muokkaaminen tulostettavaksi merkkikoodien jonoksi siis myös tapahtuu aivan eri tavoin; joka tilanteessa on aivan oma muista eroava algoritminsa.

Tällaisten ongelmien ratkaisemiseen Javassa ja monissa muissa kielissä on tekniikka nimeltään kuormittaminen (overloading). Metodien ja konstruktorien kuormittaminen tarkoittaa sitä, että kirjoitetaan useampia versioita metodista (tai konstruktorista). Versiot eroavat toisistaan muodollisten parametrien osalta. Kun kääntäjä näkee metodin kutsun, se tutkii todellisten parametrien tyypit ja valitsee niiden perusteella, mitä metodin (tai kosntruktorin) versiota kutsutaan.

Juuri tästä on kysymys myös tuon tutun println-operaation käytössä!

Kuormittamisesta nähdään jatkossa paljon esimerkkejä. Koska erityisesti konstruktorien kuormittaminen on hyvin tyypillistä, laajennetaan vielä kerran Minimuistio-luokkaa:

public class Minimuistio {  // kuormitetaan konstruktoria

  private int arvo;  // sallitaan vain arvot 1-100

  public Minimuistio() {  // parametriton konstruointi synnyttää olion,
    arvo = 1;             // jonka aloitusmuisto on yksi
  }

  public Minimuistio(int arvo) {
    if (1 <= arvo && arvo <= 100)  // vain kelvollinen käytetään
      this.arvo = arvo;  // luotavan olion kenttään sijoitetaan parametrin arvo
    else              // ellei kelpaa, virhetilanne, mutta jotain on keksittävä
      this.arvo = 13; // ... on vähän tyhmää..., mutta asiaan palataan...
  }

  public int getArvo() {  // "getteri"
    return arvo;  // myös return this.arvo olisi korrekti!
  }

  public void setArvo(int arvo) {  // "setteri"
    if (1 <= arvo && arvo <= 100)  // vain kelvollinen otetaan huomioon
      this.arvo = arvo;  // ilmentymämuuttujaan sijoitetaan parametrin arvo
  }

  public String toString() {
    return "arvo on " + arvo;
  }
}

Käyttöesimerkki:

Minimuistio alku = new Minimuistio();  // nyt onnistuu näinkin!
System.out.println(alku);              // tulostus:  arvo on 1

Varasto: API

Sitten tehdään vähän monipuolisempi luokkamäärittely olioiden malliksi. Varasto-olioiden halutaan toimivan seuraavaan tapaan (Varasto-luokan API):

Kuten näkyy, erityisesti virhetilanteiden käsittely ja niiden dokumentointi alkaa olla vähän hankalaa. Ja sellaista se on ihan oikeastikin... Virheiden käsittelyyn palataan kurssilla/kursseilla useampaan kertaan myöhemmin.

Varasto: toteutus

Nyt alamme lähestyä "oikeita" ohjelmia. API ei ollut enää ihan yksinkertainen ja luokan toteuksessakin on jo vähän vaivaa.

Ensin koko luokka, sitten tarkastellaan muutamia kohtia vielä erikseen:

public class Varasto {

  // --- piilotettu tietorakenteen toteutus: ---

  private double tilavuus;  // paljonko varastoon mahtuu,  > 0
  private double saldo;     // paljonko varastossa on nyt, >= 0

  // --- konstruktorit: ---

  public Varasto(double tilavuus) {  // tilavuus on annettava
   if (tilavuus > 0.0)
      this.tilavuus = tilavuus;
    else                    // virheellinen, nollataan
      this.tilavuus = 0.0;  // => käyttökelvoton varasto
 
    saldo = 0.0;     // oletus: varasto on tyhjä
  }

  public Varasto(double tilavuus, double alkuSaldo) { // kuormitetaan
   if (tilavuus > 0.0)
      this.tilavuus = tilavuus;
    else                    // virheellinen, nollataan
      this.tilavuus = 0.0;  // => käyttökelvoton varasto

    if (alkuSaldo < 0.0)
       this.saldo = 0.0;
    else if (alkuSaldo <= tilavuus)  // mahtuu
      this.saldo = alkuSaldo;
    else
      this.saldo = tilavuus;  // täyteen ja ylimäärä hukkaan!
  }

  // --- ottavat aksessorit eli getterit: ---

  public double getSaldo() {
    return saldo;
  }
  public double getTilavuus() {
    return tilavuus;
  }
  public double paljonkoMahtuu() {  // huom: ominaisuus voidaan myös laskea
    return tilavuus - saldo;        //  ei tarvita erillistä kenttää vielaTilaa tms.
  }

  // --- asettavat aksessorit eli setterit: ---

  public void lisaaVarastoon(double maara) {
    if (maara < 0)   // virhetilanteessa voidaan tehdä 
       return;       // tällainen pikapoistuminenkin!

    if ( maara <= paljonkoMahtuu() )  // omia aksessoreita voi kutsua
      saldo = saldo + maara;          // ihan suoraan sellaisinaan
    else
      saldo = tilavuus;  // täyteen ja ylimäärä hukkaan!
  }

  public double otaVarastosta(double maara) {
    if (maara < 0)   // virhetilanteessa voidaan tehdä 
       return 0.0;   // tällainen pikapoistuminenkin!

    if(maara > saldo) {          // annetaan mitä voidaan
      double kaikkiMitaVoidaan = saldo;
      saldo = 0.0;               // ja tyhjäksi menee
      return kaikkiMitaVoidaan;  // poistutaan saman tien
    }
    // jos tänne päästään, kaikki pyydetty voidaan antaa
    saldo = saldo - maara;  // vähennetään annettava saldosta
    return maara;
  }

  // --- Merkkijonoesitys Varasto-oliolle: ----

  public String toString() {
    return("saldo = " + saldo + ", vielä tilaa " + paljonkoMahtuu() );
  }
}

Huomioita ja opittavaa:

Varasto: esittelysovellus

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

    Varasto mehua  = new Varasto(100.0);
    Varasto olutta = new Varasto(100.0, 20.2);

    System.out.println("Luonnin jälkeen:");
    System.out.println("Mehuvarasto: " + mehua);
    System.out.println("Olutvarasto: " + olutta);

/* Tulostus:
Luonnin jälkeen:
Mehuvarasto: saldo = 0.0, vielä tilaa 100.0
Olutvarasto: saldo = 20.2, vielä tilaa 79.8
*/

    System.out.println("Olutgetterit:");
    System.out.println("getSaldo()     = " + olutta.getSaldo());
    System.out.println("getTilavuus    = " + olutta.getTilavuus());
    System.out.println("paljonkoMahtuu = " + olutta.paljonkoMahtuu());

/* Tulostus:
Olutgetterit:
getSaldo()     = 20.2
getTilavuus    = 100.0
paljonkoMahtuu = 79.8
*/

    System.out.println("Mehusetterit:");
    System.out.println("Lisätään 50.7");
    mehua.lisaaVarastoon(50.7);
    System.out.println("Mehuvarasto: " + mehua);
    System.out.println("Otetaan 3.14");
    mehua.otaVarastosta(3.14);
    System.out.println("Mehuvarasto: " + mehua);

/* Tulostus:
Mehusetterit:
Lisätään 50.7
Mehuvarasto: saldo = 50.7, vielä tilaa 49.3
Otetaan 3.14
Mehuvarasto: saldo = 47.56, vielä tilaa 52.44
*/

    System.out.println("Virhetilanteita:");
    System.out.println("new Varasto(-100.0);");
    Varasto huono =  new Varasto(-100.0);
    System.out.println(huono);

    System.out.println("new Varasto(100.0, -50.7)");
    huono =  new Varasto(100.0, -50.7);
    System.out.println(huono);

/* Tulostus:
Virhetilanteita:
new Varasto(-100.0);
saldo = 0.0, vielä tilaa 0.0
new Varasto(100.0, -50.7)
saldo = 0.0, vielä tilaa 100.0
*/

    System.out.println("Olutvarasto: " + olutta);
    System.out.println("olutta.lisaaVarastoon(1000.0)");
    olutta.lisaaVarastoon(1000.0);
    System.out.println("Olutvarasto: " + olutta);

/* Tulostus:
Olutvarasto: saldo = 20.2, vielä tilaa 79.8
olutta.lisaaVarastoon(1000.0)
Olutvarasto: saldo = 100.0, vielä tilaa 0.0
*/

    System.out.println("Mehuvarasto: " + mehua);
    System.out.println("mehua.lisaaVarastoon(-666.0)");
    mehua.lisaaVarastoon(-666.0);
    System.out.println("Mehuvarasto: " + mehua);

/* Tulostus:
Mehuvarasto: saldo = 47.56, vielä tilaa 52.44
mehua.lisaaVarastoon(-666.0)
Mehuvarasto: saldo = 47.56, vielä tilaa 52.44
*/

    System.out.println("Olutvarasto: " + olutta);
    System.out.println("olutta.otaVarastosta(1000.0)");
    double saatiin = olutta.otaVarastosta(1000.0);
    System.out.println("saatiin " + saatiin);
    System.out.println("Olutvarasto: " + olutta);

/* Tulostus:
Olutvarasto: saldo = 100.0, vielä tilaa 0.0
olutta.otaVarastosta(1000.0)
saatiin 100.0
Olutvarasto: saldo = 0.0, vielä tilaa 100.0
*/

    System.out.println("Mehuvarasto: " + mehua);
    System.out.println("mehua.otaVarastosta(-32.9)");
    saatiin = mehua.otaVarastosta(-32.9);
    System.out.println("saatiin " + saatiin);
    System.out.println("Mehuvarasto: " + mehua);
/* Tulostus:
Mehuvarasto: saldo = 47.56, vielä tilaa 52.44
mehua.otaVarastosta(-32.9)
saatiin 0.0
Mehuvarasto: saldo = 47.56, vielä tilaa 52.44
*/
  }
}

Varastoesimerkki: Bensakirjanpito

Luodaan ohjelma bensa-aseman kirjanpitoa varten. Kirjanpito-ohjelma hallinnoi bensa-asemaa ja päävarastoa. Hallinnoitavat varastot ovat pikkuZYZ, sekä päävarasto. PikkuXYZ on pienehkö bensa-asema, joten sinne mahtuu myös vähemmän bensaa kuin päävarastolle. Toteutetaan kummatkin varastot Varasto-olioina.

public class Bensakirjanpito {

  public static void main(String[] args) {
    Varasto pikkuZYZ = new Varasto(2000.0, 100.0);
    Varasto paaVarasto = new Varasto(40000.0, 32000.0);
    double saatuMaara;

    saatuMaara = pikkuZYZ.otaVarastosta(50);
    System.out.println("Pikku ZYZ:ltä tankattiin " + saatuMaara + " litraa bensaa.");
    System.out.println("Pikku ZYZ:llä jäljellä " + pikkuZYZ.getSaldo() + " litraa.");

    saatuMaara = pikkuZYZ.otaVarastosta(50);
    System.out.println("Pikku ZYZ:ltä tankattiin " + saatuMaara + " litraa bensaa.");
    System.out.println("Pikku ZYZ:llä jäljellä " + pikkuZYZ.getSaldo() + " litraa.");

    System.out.println("Siirretään päävarastosta lisää bensaa Pikku ZYZ:lle");

    double siirrettavaMaara = paaVarasto.otaVarastosta(2000);
    pikkuZYZ.lisaaVarastoon(siirrettavaMaara);

    System.out.println("Pikku ZYZ:lle sirrettiin " + siirrettavaMaara + " litraa");
    System.out.println("Pikku ZYZ:llä jäljellä " + pikkuZYZ.getSaldo() + " litraa.");
  }
}

Ohjelma tulostaa seuraavanlaisen kirjanpidon:

Pikku ZYZ:ltä tankattiin 50.0 litraa bensaa.
Pikku ZYZ:llä jäljellä 50.0 litraa.
Pikku ZYZ:ltä tankattiin 50.0 litraa bensaa.
Pikku ZYZ:llä jäljellä 0.0 litraa.
Siirretään päävarastosta lisää bensaa Pikku ZYZ:lle
Pikku ZYZ:lle sirrettiin 2000.0 litraa
Pikku ZYZ:llä jäljellä 2000.0 litraa.

Alkeistyyppi ja viittaustyyppi

Javassa – kuten pitäisi jo olla tuttua – kaikilla arvoilla on tyyppi. Tyypit jakautuvat toteutustekniikaltaan kahteen ryhmään:

Useimmiten ohjelmoijan ei tarvitse ajatella tätä eroa, mutta joskus sen ymmärtäminen auttaa. Eroa voi luonnehtia siten, että

Esimerkki:

int i = 1;
int j = i;  // j saa oman kopion luvun 7 bittiesityksestä

Kuulaskuri a = new Kuulaskuri();
Kuulaskuri b = a; // b saa  oman kopion viitteestä yhteiseen Kuulaskuri-olioon

j = j+1;
System.out.println(i + " " + j); // tulostus: 1 2

b.seuraavaKuu();
System.out.println(a.moneskoKuu() + " " + b.moneskoKuu()); // tulostus: 2 2

Huom: Viittaustyypeillä on erikoisarvo null, joka tarkoittaa tyhjää viitettä, viitettä "ei mihinkään". Sitä voi myös tutkia ehtolausekkeella: if (a==null) ...

Olion voi välittää parametrina

Itse laaditut luokat ovat siis tyyppejä siinä kuin kieleen valmiiksi sisällytetyt 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;
   Varasto v, w = new Varasto(300.0);
   ...
   public int meto1(Kuulaskuri a) {
   ...
   }
   public Varasto meto2(int i) {
   ...
   }
   public Kuulaskuri meto3(Varasto 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ä

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:

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());
  }
}

Ohjelma tulostaa:

3
1
3

Olio voi olla metodin palauttamana arvona

Mikä tahansa tyyppi kelpaa metodin palautusarvon tyypiksi. Siis erityisesti myös kaikkia itse ohjelmoituja luokkia voi käyttää metodin tyyppina.

Ohjelmoidaan esimerkkinä työkalumetodi, joka pyytää käyttäjältä varaston tiedot ja palauttaa kutsujalle Varasto-olion:

public class OlioArvonaEsim {

  private static Varasto lueVarasto() {
    double tilavuus, saldo;

    do
      tilavuus = Pop.kysyDouble("Varaston tilavuus? (ei-negatiivinen)");
    while (tilavuus < 0);

    do
      saldo = Pop.kysyDouble("Tuotteen määrä? (ei-negatiivinen)");
    while (saldo < 0);

    return new Varasto(tilavuus, saldo);  // HUOM: viite!
  }

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

    Pop.ilmoita("Luodaanpa varasto!");
    varasto = lueVarasto();

    Pop.ilmoita("Varasto on: " + varasto);

    // ...
  }
}

Olio voi sisältää olioita: Vaaka kahdesta Varastosta

Luokkamäärittelyn kentät (=piilossa pidetty tietorakenne) voivat olla mitä tahansa tyyppiä. Rajoituksia ei ole. Kenttä voi olla tyypiltään Javan API:n luokka, itse ohjelmoitu luokka, ... Yllä nähdyissä esimerkeissä kentät ovat olleet alkeistyyppisiä, mutta on hyvin tavallista, että kenttien tyyppi on jotain viittaustyyppiä. Ja kenttien arvoina on siis olioita! Ja jälleen on syytä muistaa, että tasmällisesti ilmaistuna kenttien arvoina on tällöin viitteitä olioihin.

Laaditaan pieni esimerkki, jossa Vaaka-olion vaakakupit toteutetaan Varasto-olioina:

public class Vaaka {

  // piilossa pidetty tietorakenne:
  private Varasto vasenKuppi, oikeaKuppi;
  private double kuppikoko;

  // konstruktori:
  public Vaaka(double kuppikoko) {
    vasenKuppi = new Varasto(kuppikoko);
    oikeaKuppi = new Varasto(kuppikoko);
    this.kuppikoko = kuppikoko;
  }

  // setterit:
  public void addVasempaan(double maara) {
    vasenKuppi.lisaaVarastoon(maara);
  }
  public void addOikeaan(double maara) {
    oikeaKuppi.lisaaVarastoon(maara);
  }
  public void tyhjennaVaaka() {
    vasenKuppi. otaVarastosta(kuppikoko);
    oikeaKuppi. otaVarastosta(kuppikoko);
  }

  // getterit:
  public boolean vasenPainavampi() {
    return vasenKuppi.getSaldo() > oikeaKuppi.getSaldo();
  }
  public boolean oikeaPainavampi() {
    return vasenKuppi.getSaldo() < oikeaKuppi.getSaldo();
  }
  public boolean tasapainossa() {
    return Math.abs(vasenKuppi.getSaldo() - oikeaKuppi.getSaldo()) < 0.001;
  }

  // merkkiesitys:
  public String toString() {
    return "|"+vasenKuppi.getSaldo()+"|___o___|"+oikeaKuppi.getSaldo()+"|";
  }
}

Esittelysovellus:

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

    Vaaka v = new Vaaka(100.0);
    System.out.println(v);
    v.addVasempaan(3.14);
    v.addOikeaan(21.7);
    System.out.println(v);
    tulostaVertailunTulos(v);

    v.addVasempaan(36.2);
    System.out.println(v);
    tulostaVertailunTulos(v);

    System.out.println("Tyhjennetään vaaka.");
    v.tyhjennaVaaka();
    System.out.println(v);
    tulostaVertailunTulos(v);

    v.addVasempaan(3.14000);
    v.addOikeaan  (3.14001);
    System.out.println(v);
    tulostaVertailunTulos(v);
  }

  private static void tulostaVertailunTulos(Vaaka x) {
    if (x.vasenPainavampi())
      System.out.println("Vasen on painavampi.");
    if (x.oikeaPainavampi())
      System.out.println("Oikea on painavampi.");
    if (x.tasapainossa())
      System.out.println("Vaaka on tasapainossa.");
  }
}

Esittely tulostaa:

|0.0|_____o_____|0.0|
|3.14|_____o_____|21.7|
Oikea on painavampi.
|39.34|_____o_____|21.7|
Vasen on painavampi.
Tyhjennetään vaaka.
|0.0|_____o_____|0.0|
Vaaka on tasapainossa.
|3.14|_____o_____|3.14001|
Oikea on painavampi.
Vaaka on tasapainossa.

Tässä esimerkissä on itse asiassa aika paljon opittavaa! Kannattaa perehtyä ideaan ja yksityiskohtiin. Muutamia huomioita:

String-luokka ja char-alkeistyyppi: esimerkki olioideasta ja myös hyötykäyttöön

Tässä aliluvussa tutustutaan String-luokkaan ja sen tarjoamiin metodeihin sekä merkkityyppiin char. Ne ovat sinänsäkin hyödyllisiä, mutta ne myös havainnollistavat sitä, miten Java-kielen valmiit välineet on toteutettu luokkina ja miten ohjelmoija voi ajatella (ja joutuu ajattelemaan!) olioita luokkien ilmentyminä ja operaatioita luokkien metodeina.

Alkeistyyppi char

Merkkijonot muodostuvat merkeistä. Merkit tietokoneessa esitetään bittijonoiksi koodattuina — kuten itse asiassa kaikki tietokoneessa esitetyt tiedot. Tyyppi char on Javan merkkityyppi ja se kuuluu alkeistyyppeihin. Merkit eivät siis ole olioita kuten vaikkapa merkkijonot.

Merkkiarvot ovat etumerkittömiä kahden tavun mittaisia kokonaislukuja(!). Javan sisäinen merkkikoodi on ns. Unicode-koodi, joka sisältää maailman useimpien kirjoitusjärjestelmien kirjain- ja numeromerkit: mm. kreikka, kopti, venäjä, heprea, arabia, devanagari, bengali, gurmukhi, gujarati, tamili, telegu, tai, lao, hiragana, katakana, ...

Merkkivakiot ohjelmatekstissä esitetään yksinkertaisissa lainausmerkeissä : 'A', 'k', ' ', '@', ... [Ohjelmoija voi antaa merkkivakion myös ns. heksadesimaaliesityksenä, joka on muotoa \uXXXX, missä X on heksadesimaaliluku 0-f.]

Merkkiarvon sijoittaminen int-muuttujalle on luvallista, päinvastainen on kiellettyä:

   int i = 7; 
   char c = '#';
   ...
   i = c;  // SALLITTU, i saa arvokseen merkkikoodin!
   c = i;  // KIELLETTY! (tietoa voisi kadota! (char on intiä suppeampi))

Ohjelmanpätkä

   char c = '#';
   int  i = c;  // i:llä ja c:llä on siis arvonaan sama kokonaisluku
   System.out.println("merkkinä: " + c); // Kääntäjä päättelee muuttujan tyypistä, miten
   System.out.println("lukuna  : " + i); // muuttujan arvo tulkitaan tulostuksessa

tulostaa:

  merkkinä: #
  lukuna  : 35

Jos haluaa int-muuttujan tulkittavan merkkikoodiksi, se onnistuu eksplisiittisellä tyyppimuunnoksella:

  for (int i=40; i<110; ++i)
    System.out.print( (char)i );

Tulostuu:

()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklm

Luokka String

Seuraavassa kuvaillaan joitakin String-olioiden käsittelyn välineitä. Täydellinen luettelo String-luokan konstruktoreista ja aksessoreista löytyy String-luokan API-kuvauksesta. Välineitä on paljon. Niitä ei tietenkään tarvitse opetella ulkoa! Kun niitä käytetään, ne "katsotaan API:sta". Seuraavat ovat kuitenkin usein aloittelijallekin käyttökelpoisia.

Huom: Kerran luotua String-oliota ei voi muuttaa, koska luokassa String ei ole ainuttakaan operaatiota String-arvon muuttamiseen. String oliot ovat ns. "immutaabeleita". Juuri tällä tavoin - Javan kapselointia käyttäen - luokan String ohjelmoija on toteuttanut päätöksensä, etteivät String-oliot ole muutettavissa!

Kaikki edellä luetellut String-arvoiset metodit luovat aina uuden olion! Merkkijonojen muokkaamiseen käytetään muita välineitä. Niistä myöhemmin.