Vieläkin pikku muutoksia voi tulla...
(Muutettu viimeksi 5.10.2010, sivu perustettu 16.9.2010. Arto Wikla)
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.
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 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äinen rakenteeseen pääse mitenkään muuten puuttumaan.
Ehkä vielä parempi esimerkki "koneesta" on Scanner-olio:
Kun olemme lukeneet kokonaisluvun, olemme "vetäneet" Scanner-olion
"kahvasta" nextInt()
. Kun olemme tarvinneet
rivin, käyttämämme "kahva" on ollut nextLine()
, jne.
Molemmissa tapauksissa kone on pullauttanut käyttöömme syöttöluvun.
Tai särkynyt, jos syötteenä on ollut virheellistä tavaraa.
ika = lukija.nextInt(); nimi = lukija.nextLine();
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ä käyttäessämme Scanner-oliota syöttötietojen lukemiseen
sijoitamme Scanner olion vaikkapa muuttujan lukija
arvoksi:
Scanner lukija = new Scanner(System.in);
Tästä saattaa nokkela lukija ehkä jo aavistaa, millä ilmauksella uusia olioita synnytetään...;-)
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 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.
Vertaa tätä siihen, että myös esimerkiksi käyttämämme Scanner-olio tarjoaa
joukon välineitä juuri tähän tapaan: esimerkiksi nextInt()
-operaatio lukee
syöttövirrasta kokokonaisluvun ja palauttaa sen arvonaan, jne.
Olioilta vaadittujen toimintojen luetteloa on tapana kutsua API-kuvaukseksi ("Application Programming Interface").
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 jo tässä Javalla.)
Tällaiset määräykset/toiveet siis antaa sovelluksen tekijä, Kuulaskuri-luokan tilaaja. Ja nämä määräykset/toiveet täyttävän luokan Kuulaskurin 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:
Ja kun on kerran "hyvät piirustukset" (=luokka) laadittu, niistä voidaan tietysti tehdä useampiakin ilmentymiä eli olioita:
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!
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
...
}
}
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; } }
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. 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")
public int moneskoKuu() { return kuu; }
palauttaa arvonaan private-kentän kuu arvon. Metodi ("setteri")
public void seuraavaKuu() { ++kuu; if (kuu == 13) kuu = 1; }
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!
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. 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 ...
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 opetetaan, miten eri hakemistoissakin 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..
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.
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
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.
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.
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.
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:
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ä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()); } }
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...
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 algoritmi.
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
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.
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:
private double tilavuus; // paljonko varastoon mahtuu, > 0 private double saldo; // paljonko varastossa on nyt, >= 0
public Varasto(double tilavuus) { ... ... public Varasto(double tilavuus, double alkuSaldo) { ...
public double paljonkoMahtuu() { // huom: ominaisuus voidaan myös laskea return tilavuus - saldo; // ei tarvita erillistä kenttää vielaTilaa tms. }
public void lisaaVarastoon(double maara) { if (maara < 0) // virhetilanteessa voidaan tehdä return; // tällainen pikapoistuminenkin! ... public double otaVarastosta(double maara) { if (maara < 0) // virhetilanteessa voidaan tehdä return 0.0; // tällainen pikapoistuminenkin! ...
Kuten näkyy, void
-metodissa pelkkä return
riittää,
double
-metodissa return
tietenkin palauttaa
double
-arvon.
Metodissa otaVarastosta(double maara)
myös tilanne
"annetaan kaikki mitä voidaan, kun pyydettiin enemmän kuin on"
hoidellaan samalla tekniikalla:
if(maara > saldo) { // annetaan mitä voidaan double kaikkiMitaVoidaan = saldo; saldo = 0.0; // ja tyhjäksi menee return kaikkiMitaVoidaan; // poistutaan saman tien }
paljonkoMahtuu()
. Lisäämisaksessorissa
käytetään sitä hyväksi:
public void lisaaVarastoon(double maara) { ... if ( maara <= paljonkoMahtuu() ) // omia aksessoreita voi kutsua saldo = saldo + maara; // ihan suoraan sellaisinaan else ...
Huom: Myös kaksiparemetrisen konstruktorin alkusaldon tarkistus ja asetus voitaisiin hoitaa vastaavalla tekniikalla:
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 this.saldo = 0.0; lisaaVarastoon(alkuSaldo); // kutsutaan omaa aksessoria, joka osaa tämän homman }
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 */ } }
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.
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) ...
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
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:
import java.util.Scanner; public class OlioArvonaEsim { private static Scanner lukija = new Scanner(System.in); private static Varasto lueVarasto() { double tilavuus, saldo; do { System.out.println("Varaston tilavuus? (ei-negatiivinen)"); tilavuus = lukija.nextDouble(); } while (tilavuus < 0); do { System.out.println("Tuotteen määrä? (ei-negatiivinen)"); saldo = lukija.nextDouble(); } while (saldo < 0); return new Varasto(tilavuus, saldo); // HUOM: viite! } public static void main(String[] args) { Varasto varasto; System.out.println("Luodaanpa varasto!"); varasto = lueVarasto(); System.out.println("Varasto on: " + varasto); // ... } }
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:
tasapainossa()
ei vertaile vaakakuppien
painoja yhtäsuuruusvertailulla, koska double-arvot aina ovat likiarvoja.
Verrataan siis erotuksen itseisarvoa johonkin sopivan "pieneen lukuun".
Seurauksena vaaka voi olla "tasapainossa" vaikka esisuuruusvertailut
palauttaisivatkin tiedon vaakakuppien painojen eroamista. Toki tuon
vaa'an "resoluution" 0.001 voisi ottaa huomioon myös erisuuruusvertailuissa vaikkapa
kutsumalla niiden algoritmeissa ensin tasapainon tarkistusta.
Mieti miten. Toteuta!
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.
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
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.
String jono = "kissa"; int pituus = jono.length(); // pituus = 5;
Esimerkkiohjelma tulostaa tulostaa syöttörivin pituuden merkkeinä:
import java.util.Scanner; public class Rivinpituus { private static Scanner lukija = new Scanner(System.in); public static void main(String[] args) { System.out.println("Kirjoita rivi!"); String jono = lukija.nextLine(); int pituus = jono.length(); System.out.println("Sen pituus on "+pituus); } }
Esimerkkiohjelma suoritetaan uudelleen, jos käyttäjä vastaa jatkamiskysymykseen "kyllä":
import java.util.Scanner; public class Jatkoa1 { private static Scanner lukija = new Scanner(System.in); public static void main(String[] args) { String vastaus; do { System.out.println("\nNyt ohjelma tekee jotakin hyödyllistä...\n"); System.out.println("Haluatko jatkaa? (kyllä tai ei)"); vastaus = lukija.nextLine(); } while (vastaus.equals("kyllä")); } }
Esimerkkiohjelma loppuu, jos käyttäjä vastaa jatkamiskysymykseen "ei":
import java.util.Scanner; public class Jatkoa2 { private static Scanner lukija = new Scanner(System.in); public static void main(String[] args) { String vastaus; do { System.out.println("\nNyt ohjelma tekee jotakin hyödyllistä...\n"); System.out.println("Haluatko jatkaa? (kyllä tai ei)"); vastaus = lukija.nextLine(); } while (!vastaus.equals("ei")); } }
Esimerkki:
import java.util.Scanner; public class Jatkoa3 { private static Scanner lukija = new Scanner(System.in); public static void main(String[] args) { String vastaus; do { System.out.println("\nNyt ohjelma tekee jotakin hyödyllistä...\n"); System.out.println("Haluatko jatkaa? (kyllä tai ei)"); vastaus = lukija.nextLine(); } while (vastaus.equalsIgnoreCase("kyllä")); } }
Esimerkkiohjelma kahden syöttömerkkijonon vertailuun:
import java.util.Scanner; public class JonoVertailu { private static Scanner lukija = new Scanner(System.in); public static void main(String[] args) { String vastaus; do { System.out.print("Anna ensimmäinen merkkijono: "); String eka = lukija.nextLine(); System.out.print("Anna toinen merkkijono : "); String toka = lukija.nextLine(); int jarjKoodi = eka.compareTo(toka); if (jarjKoodi < 0) System.out.println("Ensimmäinen on ennen toista."); else if (jarjKoodi == 0) System.out.println("Jonot ovat samat."); else // jarjKoodi > 0 System.out.println("Toinen on ennen ensimmäistä."); System.out.println("\nHaluatko jatkaa? (enter jatkaa, kaikki muu lopettaa)"); vastaus = lukija.nextLine(); } while (vastaus.equals("")); } }
String jono = "kissa"; for (int i=0; i<jono.length(); ++i) System.out.println(jono.charAt(i));
tulostavat:
k i s s a
String jono = "kissa"; System.out.println(jono.indexOf('s')); // tulostus: 2 System.out.println(jono.indexOf('t')); // tulostus: -1
Huom: indexOf(String str)-versio on aika voimakas ja käyttökelpoinen! Esimerkki:
... System.out.println("Syötä rivi. Etsin siltä kissaa."); String syöttorivi = lukija.nextLine();; if (syöttorivi.indexOf("kissa") != -1) System.out.println("Jossakin kohdassa syöttörivillä on kissa!"); else System.out.println("Rivi on kissaton."); ...
Esimerkki:
String jono = "abcd123efg"; String osajono = jono.substring(4,7); System.out.println(osajono); // tulostus 123
Esimerkki
String jono = "Onko Matti kukkarossasi?"; System.out.println(jono.toLowerCase()); // tulostus: onko matti kukkarossasi? System.out.println(jono.toUpperCase()); // tulostus: ONKO MATTI KUKKAROSSASI?
Huom: Olion, johon operaatio kohdistetaan, ei välttämättä tarvitse olla muuttujan arvona. Myös merkkijonovakiolle voidaan soveltaa String-aksessoreita:
System.out.println("Onko Matti kukkarossasi?".toLowerCase());
Ohjelmointitekniikkaa: Tähän ominaisuuteen – merkkijonovakiollekin voidaan soveltaa String-aksessoreita – perustuen on näppärä ohjelmoida välineitä, jotka tutkivat, kuuluuko jokin merkki johonkin merkkijoukkoon. Esimerkiksi vokaalitesti voitaisiin ohjelmoida "pääohjelman pikku apulaisena" seuraavasti:
private static boolean onVokaali(char merkki) { return ("aeiouyåäöAEIOUYÅÄÖ".indexOf(merkki) != -1); }
Välinettä voi käyttää vaikkapa seuraavaan tapaan:
System.out.println("Syötä rivi. Tutkin vokaalien määrän."); String rivi = lukija.nextLine(); int vokaaleja = 0; for (int i=0; i<rivi.length(); ++i) if (onVokaali(rivi.charAt(i))) vokaaleja = vokaaleja + 1; System.out.println("Rivillä oli " + vokaaleja + " vokaalia.");
String jono = " Olipa kerran ... "; System.out.println("*" + jono + "*"); System.out.println("*" + jono.trim() + "*");
tulostaa:
* Olipa kerran ... * *Olipa kerran ...*
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.