Ohjat ottaako oliot?
Luokista luodut ilmentymät
kantaemosta perityt
rajapinnalla rajatut.
Itsestäänkö ilmaantuvat,
sanomatta siunaantuvat?
Viestejä hyö viskoviksi
kaiken koodin korvaajiksi.
Nyt on virhe pienen pieni
ei valta noin suuren suuri.
Taas työ itse tehtäväksi
oliot olkoonkin avuksi.
Luokat luotava lujiksi
vakaan vastuun kantajiksi
ylläpidon ystäviksi
tehtävien taitajiksi.
Oman homman hoitajaksi
tuodun tiedon taattajaksi
sisältö sen suojaamaksi
paljon piiloon pistäväksi.
Perintääkin pohdittava
katsottava koostamista
muodostajaa muotoiltava
rajapintoja raakattava.
Metodeja mietittävä
attribuutteja aateltava
viestejäkin viskeltävä
olioita ohjaillessa.
Mitä tässä luvussa käsitellään?
· yksinkertaiset luokat
· olioiden perusteet
· olioterminologia
· koostaminen
· perintä
· polymorfismi
Syntaksi:
luokan esittely: class Nimi extends Isa implements Rajapinta { // 0-1 x Isa
// 0-n x Rajapinta , erot.
private yksityinen_attribuutti // vain itse näkee, 0-n x
private yksityinen_metodi // 0-n x
protected suojattu_attribuutti // perillinen näkee 0-n x
protected suojattu_metodi // 0-n x
public julkinen_attribuutti_paha // kaikki näkee 0-n x WWW
public julkinen_metodi // 0-n x
package paketin_attribuutti // paketissa näkee 0-n x
package paketin_metodi // 0-n x, package on oletus
}
attr kuten muuttuja
attrib.esitt. tyyppi attr;
metodin esitt. kuten aliohjelman esittely
viit.olion metod: olio.metodin_nimi(param,param) // 0-n x param
isäluokan
metodiin viit. super.metodin_nimi(param,param)
muodostaja Nimi(param_lista) // voi olla monta eri param_listoilla
Luvun esimerkkikoodit:
Tähän lukuun on kasattu suuri osa olioihin liittyvää asiaa yhden esimerkin valossa. Esimerkin yksinkertaisuuden takia se ei anna joka tilanteessa täyttä hyötyä esitetyistä ominaisuuksista. Lisäksi asiaa voi olla yhdelle lukukerralla liikaa ja esimerkiksi perintä, rajapinnat ja polymorfismi kannattaa ehkä jättää myöhemmälle lukukerralle.
Emme tässä ryhdy pohtimaan kovin syvällisiä siitä, miten olio-ohjelmointiin on päädytty. Todettakoon kuitenkin että olio-ohjelmointi on hyvin luonnollinen jatke rakenteelliselle ohjelmoinnille heti, kun huomataan siirtää käsiteltävä data ja dataa käsittelevä koodi samaan ohjelman osaan. Tämä toimenpide voidaan tehdä tietenkin myös perinteisillä ohjelmointikielilläkin. Puhtaat oliokielet eivät vaan jätä edes muuta mahdollisuutta. Lähestymme asiaa evoluutiomaisesti - niin kuin kehitys on olioihin johtanut. Loput ylilaulut olioista kannattaa lukea jostakin hyvästä kirjasta.
Aloitetaanpa tutkimalla Aikalisa esimerkkiämme. Pääohjelmassa esiteltiin muuttuja tunteja varten ja muuttuja minuutteja varten. Aluksi tämä saattaa tuntua hyvin luonnolliselta ja niin se onkin, niin kauan kuin ohjelman koko pysyy pienenä. Entäpä ohjelma jossa tarvitaan paljon kellonaikoja?
olioalk\Aikalis4.java - useita aika "muuttujia"
... alku kuten Aikalisa.java
public static void main(String[] args) {
int h1=12,m1=15;
int h2=13,m2=16;
int h3=14,m3=25;
tulosta(h1,m1);
tulosta(h2,m2);
tulosta(h3,m3);
}
Hyvinhän tuo vielä toimii? Tosin Javassa ei voitu tehdä aliohjelmaa, joka muuttaisi "kellonaikaa". Kiertotienä voisi tallentaa ajan taulukkoon, sillä taulukko välitetään Javassa viitteenä ja silloin taulukon arvoja voisi muuttaa aliohjelmassa. Mutta tämäkin kiertotie lakkaisi toimimasta jos alkioiden pitäisi olla keskenään eri tyyppiä. Nykyversiossa on lisäksi ongelmana se, että jos joku tulee ja sanoo, että sekunnitkin mukaan! Tulee paljon työtä jos on paljon aikoja.
Tehtävä 9.1 Tulostus
Mitä ohjelma Aikalis4.java tulostaa?
Olio-ohjelmoinnin tärkeimpiä ideoita on kasata tiedot (muuttujat) ja niitä käsitelevät koodit yhteiseksi "paketiksi", olioksi, joka osaa tehdä tiedoille tarvittavat käsittelyt. Lisäksi suojataan tiedot niin, ettei niitä pääse kukaan muu muuttamaan kuin itse olio.
Itse asiassa vanhalla C-kielelläkin pystyi kirjoittamaan "olioita", kirjoittamalla tietuetyypin esittelyn ja sitä käyttävät aliohjelmat yhdeksi aliohjelmakirjastoksi. Näin data ja sitä käsittelevät aliohjelmat on kapseloitu yhdeksi paketiksi.
Nyt astuu kuvan mukaan olio-ohjelmoijat ja he nimittävät sitten näin syntyneitä aliohjelmia metodeiksi (method), tai C++-kirjallisuudessa jäsenfunktioiksi (member function). Olion alkioita, kenttiä nimitetään sitten attribuuteiksi.
Itse "kokoelma" saakin nimen luokka (class) ja luokkaa vastaava muuttuja - luokan ilmentymä - on sitten se kuuluisa olio (object)
Muutetaanpa Aikalisa kunnon luokaksi ja olioksi:
olioalk\Aika.java - kunnon olioksi
/**
* Ensimmäinen kunnon olioesimerkki
* @author Vesa Lappalainen
* @version 1.0, 01.02.2003
*/
public class Aika {
private int h=0, m=0;
/**
* Alustaa ajan
* @param h tunnit
* @param m minuutit
*/
public Aika(int h,int m) { // Muodostaja
this.h = h;
this.m = m;
}
/**
* Tulostaa ajan muodossa 15:05
*/
public void tulosta() {
System.out.println("" + h + ":" + (m<10?"0":"")+m);
}
/**
* Lisää aikaan valitun minuuttimäärän
* @param lisa_min lisättävä minuuttimäärä
*/
public void lisaa(int lisa_min) {
int yht_min = h * 60 + m + lisa_min;
h = yht_min / 60;
m = yht_min % 60;
}
public static void main(String[] args) {
Aika a1 = new Aika(12,15);
Aika a2 = new Aika(13,16);
Aika a3 = new Aika(14,25);
a1.lisaa(55); a1.tulosta();
a2.lisaa(27); a2.tulosta();
a3.lisaa(39); a3.tulosta();
}
}
Siinäpä se! Ovatko muutokset edelliseen nähden suuria? Siis iso osa koko olio-ohjelmoinnista (ja tietotekniikasta muutenkin) on markkinahenkilöiden huuhaata ja yleistä hysteriaa "kaiken ratkaisevan" teknologian ympärillä. No, tosin olio-ohjelmoinnissa on puolia, joita emme vielä ole nähneetkään, joiden ansiosta olio-ohjelmointia voidaan pitää uutena ohjelmointia ja ylläpitoa helpottavana teknologiana. Näitä ovat mm. perintä ja polymorfismi (monimuotoisuus), joihin emme valitettavasti tällä kurssilla ehdi perehtyä kovinkaan syvällisesti.
No takaisin esimerkkiimme. Uutta on lähinnä se, että metodien (no sanotaan tästä lähtien funktioita metodeiksi) parametrilistat ovat lyhentyneet. Itse olion tietoja ei tarvitse enää viedä parametrina, koska metodit ovat luokan sisäisiä ja tällöin luokkaa edustava olio kyllä tuntee itse itsensä.
...
public void lisaa(int lisa_min) {
int yht_min = h * 60 + m + lisa_min;
h = yht_min / 60;
m = yht_min % 60;
}
...
Metodia kutsutaan ilmoittamalla olion nimi ja metodi, jota kutsutaan
a1.lisaa(55); a1.tulosta();
Tällekin on keksitty oma nimi: välitetään oliolle viesti "tulosta" (message passing). Tässä kuitenkin jatkossa voi vielä lipsahtaa ja vahingossa sanomme kuitenkin, että kutsutaan metodia tulosta, vaikka ehkä pitäisi puhua viestin välittämisestä.
Kerrataanpa vielä termit edellisen esimerkin avulla:
Vinkki Älä hämäänny termeistä |
oliotermi perinteinen termi
-----------------------------------------------------------------------------
Aika - aika-luokka tietuetyyppi
h,m - aika-luokan attribuutteja tietueen alkio
lisaa,tulosta - aika-luokan metodeja funktio,aliohj.
a1,a2,a3 - olioita, jotka ovat aika-luokan ilmentymiä muuttuja
a1.lisaa(55) - viesti olioille a1: lisää 55 minuuttia aliohjelman kutsu
Luokka on tavallaan "piparkakkumuotti" kaikille samankaltaisille "olioille". Luokalla ei sinänsä tee mitään (ellei siinä ole static-aliohjelmia), ellei siitä luo luokkaa edustavaa oliota.
Aika a1 = new Aika(12,15);
Javan "olio-muuttujathan" eivät olleet mitään muuta kuin pelkkiä viitteitä keossa sijaitseviin varsinaisiin olioihin. new -operaattori luo kekoon uuden olion ja palauttaa viitteen tähän olioon.
Pelkkä olion luominen ilman viitteen sijoittamista mihinkään on useimmiten hyödytöntä
new Aika(12,15); // Tähän olioon ei päästä käsiksi L
Kerran luodun olion viite voidaan luonnollisesti sijoittaa toiseen viitteeseen:
a2 = a1; // molemmat viitteet viittaavat samaan olioon.
Kun olioon ei ole enää yhtään viitettä, muuttuu olio Javassa roskaksi ja muistinsiivous (roskienkeruu, garbage collection, gc) vapauttaa ajallaan olion viemän muistitilan.
Aika a1 = new Aika(12,15);
...
a1 = null; // a1 ei viittaa enää olioon => olio muuttuu roskaksi
tai
{ // lohkon alku, jonka sisällä viite esitelty
Aika a1 = new Aika(12,15);
...
} // Viite a1 lakkaa olemasta => olio muuttuu roskaksi
Luokan attribuuteille ja metodeille on suojaustasot, jotka oletuksena ovat pakettikohtaisia, eli metodeja voi kutsua kuka tahansa samaan pakettiin kirjoitetun luokan metodi. Erityisesti kuka tahansa samassa paketissa oleva metodi voi muuttaa attribuuttien arvoja ilman että olio tätä itse huomaa.
|
Kuka voi käyttää metodia/attribuuttia |
|||
Suojaus |
kaikki |
aliluokan metodit |
paketin metodit |
luokan metodit |
Private |
|
|
|
x |
Package |
|
|
x |
x |
Protected |
|
x |
x |
x |
Public |
x |
x |
x |
x |
Kuva 9.1 Suojaustasot
Kirjoitamme ensin testiluokan:
/**
* Testiohjelma Aika-luokalle
* @author Vesa Lappalainen
* @version 1.0, 01.02.2003
*
*/
public class Aikatesti {
public static void main(String[] args) {
Aika a1 = new Aika(12,15);
Aika a2 = new Aika(13,16);
Aika a3 = new Aika(14,25);
a1.lisaa(55); a1.tulosta();
a2.lisaa(27); a2.tulosta();
a3.lisaa(39); a3.tulosta();
}
}
Jos esimerkkimme metodi lisaa esiteltäisiin:
private void lisaa(int lisa_min) {
niin testiohjelma lakkaisi toimimasta, koska esimerkiksi pääohjelman kutsu
a1.lisaa(55)
tulisi laittomaksi luokan jäsenen lisaa ollessa yksityinen (private).
Erityisen tärkeää on kuitenkin että ei voida kirjoittaa testiohjelmassa
a1.h = 28; // private-attribuuttiin ei saa viitata
Käytännössä attribuutit kannattaa lähes poikkeuksetta kirjoittaa yksityisiksi. Kaikista pahinta mitä olio-ohjelmoija voi tehdä on kirjoittaa julkisia attribuutteja.
Nyt vasta alkaakin olio-ohjelmoinnin hienoudet! Aloittelijasta saattaa tuntua että mitä turhaa tehdään asioista monimutkaisempaa kun se onkaan! Väärinkäytetyt ja virheelliset arvot muuttujilla on ollut ohjelmoinnin kiusa alusta alkaen. Nyt meillä on mahdollisuus päästä niistä eroon kapseloinnin (jotkut sanovat kotelointi, encapsulation) ansiosta. Eli kaikki arvojen muutokset (eli olio tapauksessa olion tilojen muutokset) voidaan suorittaa kontrolloidusta, vain olion itse siihen suostuessa. Mutta miten sitten alustuksen tapauksessa?
Javan olioilla on yksi erityinen metodi: muodostaja (konstruktori, rakentaja, constructor), jota kutsutaan muuttujan syntyessä. Muodostajan tehtävä on alustaa olion tila ja luoda mahdollisesti tarvittavat dynaamiset muistialueet. Näin voidaan järjestää se, että olion tila on aina tunnettu olion syntyessä.
Joissakin oliokielissä konstruktori ilmoitetaan omalla avainsanallaan. Javassa muodostaja on metodi, jolla on sama nimi kuin luokalla. Muodostajia voi olla useitakin. Muodostaja on aina tyypitön, siis ei edes void-tyyppiä.
olioalk\Aika.java - muodastaja alustamaan tiedot
public Aika(int h,int m) { // Muodostaja
this.h = h;
this.m = m;
}
Esimerkissämme muodostaja on esitelty 2-parametriseksi ja sitä ”kutsutaan” olion luonnin yhteydessä:
Aika a1 = new Aika(12,15);
Nyt ei kuitenkaan voida esitellä oliota ilman alkuarvoa
Aika aika = new Aika();
Kääntäjä antaisi esimerkiksi virheilmoituksen:
"Aikatesti.java": Error #: 300 : constructor Aika() not found in class Aika at line 16, column 21
Parametritonta muodostajaa sanotaan oletusmuodostajaksi (default constructor). Sellainen on luokalla aina ilman muuta, jos luokalle ei ole esitelty yhtään muodostajaa. Jos luokalle esitellään muodostaja, ei oletusmuodostaja enää tulekaan automaattisesti.
Meidän pitäisi päättää nyt paljonko kellomme on, jos sitä ei erikseen ilmoiteta. Olkoon kello vaikka 0:0, eli keskiyö. Esittelemme oletusmuodostajan
olioalk\Aika2.java - lisätään oletusmuodostaja
...
public class Aika2 {
private int h=0, m=0;
public Aika2() { // Oletusmuodostaja
h = 0; m = 0;
}
public Aika2(int h,int m) { // Muodostaja
this.h = h;
this.m = m;
}
...
public static void main(String[] args) {
Aika2 a1 = new Aika2(12,15);
...
a3.lisaa(39); a3.tulosta();
Aika2 aika = new Aika2();
aika.tulosta();
}
}
Tässä tapauksessa oletusmuodostajaksi olisi kelvannut myös tyhjä lohko. Miksi?
public Aika2() { }
Emme edelleenkään ole ottaneet kantaa siihen, mitä tapahtuu, jos joku yrittää alustaa oliomme mielettömillä arvoilla, esimerkiksi:
Aika a1 = new Aika(42,175);
Toisaalta miten joku voisi muuttaa ajan arvoa muutenkin kuin lisaa-metodilla? Teemmekin aluksi metodin aseta, jota kutsuttaisiin
a1.aseta(12,15); a2.aseta(16,23);
Nyt pitää kuitenkin päättää mitä tarkoittaa laiton asetus! Jos sovimme että minuuteissa yli 59 arvot ovat aina 59 ja alle 0:n arvot ovat aina 0, voisi aseta-metodi olla kirjoitettu seuraavasti:
public void aseta(int ih,int im) {
h = ih; m = im;
if ( m > 59 ) m = 59;
if ( m < 0 ) m = 0;
}
Jos taas haluaisimme, että ylimääräinen osuus siirtyisi tunteihin, voitaisiinkin tämä tehdä "kierosti" lisaa-metodin avulla
olioalk\Aika4.java - sisäinen tilan valvonta asetuksessa
...
public class Aika4 {
private int h=0, m=0;
/**
* Asettaa uuden ajan ja pitää huolen että aika on aina oikeaa muotoa.
* @param h asetettavat tunnit
* @param m asetettavat minuutit
*/
public void aseta(int h,int m) {
this.h = h; this.m = m; lisaa(0);
}
public Aika4() { // Oletusmuodostaja
aseta(0,0);
}
/**
* Alustaa ajan niin että minuutit ovat aina 0
* @param h tunnit
*/
public Aika4(int h) {
aseta(h,0);
}
public Aika4(int h,int m) { // Muodostaja
aseta(h,m);
}
public void tulosta() {...
public void lisaa(int lisa_min) { ...
public static void main(String[] args) {
Aika4 a1 = new Aika4();
Aika4 a2 = new Aika4(13);
Aika4 a3 = new Aika4(14,25);
a1.tulosta(); a2.tulosta(); a3.tulosta();
a1.aseta(12,15); a2.aseta(16,-15);
a1.tulosta(); a2.tulosta();
}
}
Huomattakoon, että samalla kun tehdään aseta-metodi, kannattaa muodostajassakin kutsua sitä. Näin olion tilaa muutetaan vain muutamassa metodissa, jotka voidaan ohjelmoida niin huolella, ettei mitään yllätyksiä pääse koskaan tapahtumaan. Tämä rupeaa jo muistuttamaan olio-ohjelmointia!
Tehtävä 9.2 Negatiivinen minuuttiasetus
Mitä ohjelma Aika4.java tulostaisi? Miksi ohjelma toimisi halutulla tavalla?
Tehtävä 9.3 Tuntien tarkistus
Ohjelmoi myös tunneille tarkistus, missä pidetään huoli siitä, että tunnit ovat aina välillä 0-23.
Tehtävä 9.4 Päivämääräolio
Esittele luokka, jolla kuvataan päivämäärä. Kirjoita ainakin sopiva muodostaja ja metodi tulosta, joka tulostaa päivämäärän.
Edellisessä esimerkissä oli kolme samannimistä metodia Aika4. Kussakin oli eri määrä parametrejä. Tätä sanotaan metodin kuormittamiseksi, eli mahdollisuudeksi määritellä lisää merkityksiä (eng. overloading) metodin nimelle. Varsinainen kutsuttava metodi tunnistetaan nimen ja parametrilistassa olevien lausekkeiden avulla. Metodin nimi koostuukin tavallaan nimen ja parametrilistan yhdisteestä. Siten jos olisi vaikka kutsut
a1.tulosta(); // Tulostaisi 14:15
a1.tulosta("Kello on "); // Tulostaisi Kello on 14:15
niin kumpikin tulosta-kutsu kutsuu eri metodia. Metodien kuormitus onkin varsin mukava lisä ohjelmointiin, se ei kuitenkaan ole varsinaisia olio-ohjelmoinnin piirteitä.
Huomattakoon että kuormitetuissa metodeissa ero on oltava parametreissa, pelkkä ero metodin paluuarvossa ei riitä erottelemaan mitä metodia tarkoitetaan.
Tehtävä 9.5 Mitäs me tehtiin kun ei ollut kuormitusta?
Miten asiat on hoidettava C-kielessä, kun siellä funktioiden nimien kuormitus ei ole mahdollista, vaan kunkin funktion nimen tulee olla yksikäsitteinen.
Tehtävä 9.6 Lisäys yhdellä
Tee vielä uusi lisaa metodi, jota voidaan kutsua a1.lisaa(); jolloin metodi lisää aikaa yhdellä minuutilla.
Tehtävä 9.7 Vain tuntien asettaminen
Kirjoita vielä yksi lisaa-metodi, jolla voidaan asettaa pelkät tunnit.
Jos verrataan aliohjelmaa
public static void lisaa(Aika5 aika,int lisa_min) {
int yht_min = aika.h * 60 + aika.m + lisa_min;
aika.h = yht_min / 60;
aika.m = yht_min % 60;
}
...
lisaa(a1,55);
ja metodia
public void lisaa(int lisa_min) {
int yht_min = h * 60 + m + lisa_min;
h = yht_min / 60;
m = yht_min % 60;
}
...
a1.lisaa(55);
niin helposti näyttää, että ensin mainitussa funktiossa on enemmän parametrejä. Tosiasiassa kummassakin niitä on täsmälleen sama määrä. Nimittäin jokaisen metodin ensimmäisenä näkymättömänä parametrinä tulee aina itse luokan osoite, this. Voitaisiinkin kuvitella, että metodi onkin toteutettu:
"public void lisaa(Aika5 this, int lisa_min) {" // Näin EI SAA KIRJOITTAA!!!
{
int yht_min = this.h * 60 + this.m + lisa_min;
this.h = yht_min / 60;
this.m = yht_min % 60;
}
...
"a1.lisaa(a1,55)";
Oikeasti this -viitettä ei saa esitellä, vaan se on ilman muuta mukana parametreissa sekä esittelyssä että kutsussa. Mutta voimme todellakin kirjoittaa:
public void lisaa(int lisa_min) {
{
int yht_min = this.h * 60 + this.m + lisa_min;
this.h = yht_min / 60;
this.m = yht_min % 60;
}
Jonkun mielestä voi jopa olla selvempi käyttää this-viitettä luokan attribuutteihin viitattaessa, näinhän korostuu, että käsitellään nimenomaan tämän luokan attribuuttia h, eikä mitään muuta muuttujaa h. Joskus this-osoitinta tarvitaan välttämättä palautettaessa oliotyyppisellä metodilla olion koko tila (esim. viite olioon). Lisäksi joissakin kielissä this-osoittimen vastinetta (usein self) on aina käytettävä.
Usein this-osoitinta käytetään, jos ei haluta antaa metodin parametrilistan muuttujille eri nimiä kuin vastaavilla attribuuteilla:
public void aseta(int h,int m) {
this.h = h; this.m = m; lisaa(0);
}
Vaihtoehtonahan olisi esimerkiksi
public void aseta(int ih,int im) { // i = initialize
h = ih; m = im; lisaa(0);
}
Pidimme jo aikaisemmin toiveena sitä, että voisimme laajentaa luokkaamme käsittelemään myös sekunteja. Miksi emme tehneet tätä heti? No tietysti olisi heti pitänyt älytä laittaa mukaan myös sekunnit, mutta tosielämässäkin käy usein näin, eli hyvästäkin suunnittelusta huolimatta toteutuksen loppuvaiheessa tulee vastaan tilanteita, jossa alkuperäiset luokat todetaan riittämättömiksi.
Tämän laajennuksen tekemiseen on olio-ohjelmoinnissa kolme mahdollisuutta: Joko muuttaa alkuperäistä luokkaa, periä alkuperäisestä luokasta laajempi versio tai tehdä uusi luokka, jossa on alkuperäinen luokka yhtenä attribuuttina.
Tutustumme seuraavassa kaikkiin kolmeen eri mahdollisuuteen.
Läheskään aina ei voi täysin välttää sitäkään, etteikö alkuperäistä luokkaa joutuisi muuttamaan. Jos näin joudutaan tekemään, pitäisi tämä pystyä tekemään siten, että jo kirjoitettu luokkaa käyttävä koodi säilyisi täysin muuttumattomana (tai ainakin voitaisiin päivittää minimaalisilla muutoksilla) ja vasta uudessa koodissa käytettäisiin hyväksi luokan uusia ominaisuuksia.
Jos luokka on saatu joltakin kolmannelta osapuolelta, ei luokan päivittäminen edes ole mahdollista, vaan silloin täytyy turvautua muihin (parempiin) tapoihin.
Tehtävä 9.8 Luokan muuttaminen
Muuta ohjelmaa Aika4.java siten, että ajassa on mukana myös sekunnit. Kuitenkin niin, että alkuperäinen testiohjelma säilyy sellaisenaan toimivana. Voit lisätä testiohjelmaan uusia rivejä sekuntien testaamiseksi.
Tehtävä 9.9 Sekuntien tulostus aina tai oletuksena
Muuta edellistä ohjelmaa siten, että sekunnit tulostetaan aina.
Muuta edellistä ohjelmaa siten, että sekunnit tulostetaan oletuksena jos ne on != 0.
Seuraava mahdollisuus olisi uuden luokan koostaminen (aggregation) vanhasta aikaluokasta ja sekunneista. Tämä mahdollisuus meillä on aina käytössä vaikkei alkuperäistä lähdekoodia olisikaan käytössä. Tätä vaihtoehtoa pitää aina vakavasti harkita.
Valitettavasti emme voi aivan täysin onnistua. Nimittäin alkuperäisen luokan tulosta oli niin tyhmästi toteutettu, että se tulosti aina rivinvaihdon. Tämmöistä hölmöyttä ei pitäisi mennä koskaan tekemään ja siksipä alkuperäinen luokka pitää joka tapauksessa palauttaa valmistajalle remonttiin. No luokan valmistaja muutti tulosta-metodia siten, että se oletuksena tulostaa rivinvaihdon (merk. lf = line feed), mutta pyydettäessä jättää sen tekemättä. Näin vanha koodi voidaan käyttää muuttamattomana.
Palaamme tulostusongelmaan myöhemmin ja keksimme silloin paremman ratkaisun, jota olisi pitänyt käyttää jo alunperin.
/**
* Tulostaa ajan muodossa 15:05
* @param lf tulostetaanko rivinvaihto vai ei
*/
public void tulosta(boolean lf) {
System.out.print("" + h + ":" + (m<10?"0":"")+m);
if ( lf ) System.out.println();
}
/**
* Tulostaa ajan muodossa 15:05 sekä aina rivinvaihdon
*/
public void tulosta() {
tulosta(true);
}
Nyt voimme kirjoittaa uuden luokan, joka koostetaan luokasta Aika5 ja sekunneista:
olioalk\AikaSek7.java - laajentaminen koostamalla
/**
* Luokan laajentaminen koostamalla
* @author Vesa Lappalainen
* @version 1.0, 01.02.2003
*/
public class AikaSek7 {
private Aika5 hm = new Aika5();
private int s;
/**
* Asettaa uuden ajan ja pitää huolen että aika on aina oikeaa muotoa.
* @param h asetettavat tunnit
* @param m asetettavat minuutit
* @param s asetettavat sekunnit
*/
public void aseta(int h,int m, int s) {
hm.aseta(h,m); this.s = s; lisaa(0,0);
}
public void aseta(int h,int m) { aseta(h,m,0); }
public void aseta(int h) { aseta(h,0); }
public AikaSek7() { }
/**
* Alustaa ajan
* @param h tunnit
* @param m minuutit
* @param s sekunnit
*/
public AikaSek7(int h,int m, int s) { // Muodostaja
aseta(h,m,s);
}
public AikaSek7(int h,int m) { aseta(h,m,0); }
public AikaSek7(int h) { aseta(h,0,0); }
/**
* Tulostaa ajan muodossa 15:05
* @param lf tulostetaanko rivinvaihto vai ei
*/
public void tulosta(boolean lf) {
hm.tulosta(false);
System.out.print(":" + (s<10?"0":"")+s);
if ( lf ) System.out.println();
}
/**
* Tulostaa ajan muodossa 15:05 sekä aina rivinvaihdon
*/
public void tulosta() { tulosta(true); }
/**
* Lisää aikaan valitun minuuttimäärän
* @param lisa_min lisättävä minuuttimäärä
* @param lisa_sek lisättävä sekunttimäärä
*/
public void lisaa(int lisa_min,int lisa_sek) {
s += lisa_sek;
hm.lisaa(lisa_min+s/60);
s %= 60;
}
public void lisaa(int lisa_min) { lisaa(lisa_min,0); }
public static void main(String[] args) {
Aika5 a1 = new Aika5();
Aika5 a2 = new Aika5(13);
Aika5 a3 = new Aika5(14,175);
a1.tulosta(); a2.tulosta(); a3.tulosta();
a1.aseta(12,15); a2.aseta(16,-15);
a1.tulosta(); a2.tulosta();
AikaSek7 a4 = new AikaSek7(14,55,45); a4.tulosta();
a4.lisaa(3,30); a4.tulosta();
}
}
Luokassa on niin vähän ominaisuuksia, että uudessa luokassamme olemme joutunee itse asiassa tekemään kaiken uudelleen ja on kyseenalaista olemmeko hyötyneet vanhasta luokasta lainkaan. Tämä on onneksi lyhyen esimerkkimme vika, todellisilla luokilla säästö kokonaan uudestaan kirjoitettuun verrattuna olisi moninkertainen.
Viimeisenä vaihtoehtona tarkastelemme perintää (inheritance). Valinta koostamisen ja perinnän välillä on vaikea. Aina edes olioasiantuntijat eivät osaa sanoa yleispätevästi kumpiko on parempi. Nyrkkisääntönä voisi pitää seuraavaa is-a -sääntöä:
Jos voi sanoa että lapsiluokka on (is-a) isäluokka, niin peritään.
Jos sanotaan että lapsiluokassa on (has-a) isäluokka, niin koostetaan
Kokeillaanpa: "luokka jossa on aika sekunteina" on "aika-luokka". Kuulostaa hyvältä. Siis perimään:
olioalk\AikaSek8.java - laajentaminen perimällä
/**
* Luokan laajentaminen perimällä
* @author Vesa Lappalainen
* @version 1.0, 01.02.2003
*/
public class AikaSek8 extends Aika5 { // perii kaikki luokan Aika5 ominaisuudet
private int s = 0;
/**
* Asettaa uuden ajan ja pitää huolen että aika on aina oikeaa muotoa.
* @param h asetettavat tunnit
* @param m asetettavat minuutit
* @param s asetettavat sekunnit
*/
public void aseta(int h,int m, int s) {
this.s = s; super.aseta(h,m); lisaa(0,0);
}
public AikaSek8() { }
/**
* Alustaa ajan
* @param h tunnit
* @param m minuutit
* @param s sekunnit
*/
public AikaSek8(int h,int m, int s) { // Muodostaja
aseta(h,m,s);
}
public AikaSek8(int h,int m) { aseta(h,m,0); }
public AikaSek8(int h) { aseta(h,0,0); }
/**
* Tulostaa ajan muodossa 15:05
* @param lf tulostetaanko rivinvaihto vai ei
*/
public void tulosta(boolean lf) {
super.tulosta(false);
System.out.print(":" + (s<10?"0":"")+s);
if ( lf ) System.out.println();
}
/**
* Lisää aikaan valitun minuuttimäärän
* @param lisa_min lisättävä minuuttimäärä
* @param lisa_sek lisättävä sekunttimäärä
*/
public void lisaa(int lisa_min,int lisa_sek) {
s += lisa_sek;
super.lisaa(lisa_min+s/60);
s %= 60;
}
public static void main(String[] args) {
Aika5 a1 = new Aika5();
Aika5 a2 = new Aika5(13);
Aika5 a3 = new Aika5(14,175);
a1.tulosta(); a2.tulosta(); a3.tulosta();
a1.aseta(12,15); a2.aseta(16,-15);
a1.tulosta(); a2.tulosta();
AikaSek8 a4 = new AikaSek8(14,55,45); a4.tulosta();
a4.lisaa(3,30); a4.tulosta();
AikaSek8 a5 = new AikaSek8(); a5.tulosta();
AikaSek8 a6 = new AikaSek8(12); a6.tulosta();
AikaSek8 a7 = new AikaSek8(12,15); a7.tulosta();
}
}
Tässä tapauksessa kirjoittamisen vaiva oli melkein sama kuin koostamisessakin. Niitä aseta, lisaa ja tulosta metodeja, jotka löytyivät jo kantaluokasta Aika5 ei tarvinnut kirjoittaa. Kuitenkin esimerkiksi kaikki eri versiot muodostajasta pitää kirjoittaa, sillä muodostaja ei periydy Javassa.
Joissakin tapauksissa perimällä pääsee todella vähällä. Otamme tästä myöhemmin esimerkkejä, kunhan pääsemme eroon syntaksin esittelystä.
Lapsiluokka, aliluokka (child class, subclass) on se joka perii (Javassa extands) ja isäluokka, yliluokka (parent class, super) se joka peritään. Käytetään myös nimitystä välitön ali/yliluokka, kun on kyseessä perintä suoraan luokalta toiselle, kuten meidän esimerkissämme.
Javassa välitön yliluokka ilmoitetaan aliluokan esittelyssä:
public class AikaSek8 extends Aika5 {
Jos täytyy viitata yliluokan metodeihin, joille on kirjoitettu aliluokassa oma määrittely, käytetään yliluokan viitettä super
super.lisaa(lisa_min+s/60);
Yliluokan viitettä ei tarvita, mikäli samannimistä metodia ei ole aliluokassa.
Perintää kuvataan piirroksessa:
Kuva 9.2 Aika perinnällä
Edellisestä esimerkistä ei oikeastaan paljastunut vielä mitään, mikä olisi puoltanut perintää. Korkeintaan snobbailu uudella syntaksilla. Mutta tosiasiassa pääsemme tästä kiinni olio-ohjelmoinnin tärkeimpään ominaisuuteen, ominaisuuteen jota on vaikea saavuttaa perinteisellä ohjelmoinnilla: polymorfismi (polymorphism) eli monimuotoisuus.
Lisätäänpä vielä testiohjelman loppuun:
Aika5 a1 = new Aika5(); a1.aseta(12,15);
AikaSek8 a4 = new AikaSek8(14,55,45); a4.tulosta();
...
Aika5 aika = a1; aika.tulosta(); // Esimerkki polymorfismista
aika = a4; aika.tulosta();
Tulostus:
12:15
14:59:15
Mistä tässä oli kyse? Viite aika on monimuotoinen, eli sama osoitin voi osoittaa useaan eri tyyppiseen luokkaan. Tämä on mahdollista, jos luokat ovat samasta perimähierarkiasta kuten tässä tapauksessa ja viite on tyypiltään näiden yhteisen kantaluokan olion viite.
Miksi edellä jälkimmäisessä aika.tulosta() kutsussa kutsuttiin luokan AikaSek8 tulosta metodia eikä Aika5 -luokan metodia tulosta. Tai tarkkaan ottaen kutsujärjestys on seuraava:
main aika=a4;
-> aika.tulosta();
-> Aika5.tulosta()
-> AikaSek8.tulosta(true) // super.tulosta(false)
-> Aika5.tulosta(boolean lf)
Eli luokassa AikaSek8 ei ole parametritonta tulosta metodia, siksi kutsutaan luokan Aika5 tulosta-metodia. Tämä taas kutsuu boolean-tyyppisellä parametrilla varustettua tulosta-metodia. Sellainen löytyy siitä oliosta, jota aika tällä hetkellä edustaa, eli luokan AikaSek8 metodi tulosta(boolean lf). Vastaavasti tämä metodi kutsuu pakotetusti yliluokan metodia super.tulosta(false). Jos sana super oli jäänyt pois, olisi kutsuta seurannut päättymätön rekursio.
Edellä mainittu on toteutettu siten, että alkuperäisessä luokassa kerrotaan että vasta ohjelman suoritusaikana selvitetään mistä luokasta todella on kyse, kun metodia kutsutaan. Tällaista ominaisuutta sanotaan myöhäiseksi sidonnaksi (late binding) (tälle monisteellekin tulee kyllä myöhäinen sidonta). Vastakohtana tälle on esimerkiksi C++:n oletustapa kutsua metodeja, eli aikainen sidonta (early binding). Sidonnan sisäisen mekanismin opettelun jätämme jollekin toiselle kurssille (ks. vaikkapa Olio-ohjelmointi ja C++/VL).
Javassa kutsutapana on onneksi aina myöhäinen sidonta, koska muuten perinnässä ei ole oikein mieltä.
Kun aliluokaan kirjoitetaan vastaava metodi kuin yliluokassa, käytetään tästä termiä uudelleenmäärittäminen (korvaaminen, syrjäyttäminen, overriding).
Tehtävä 9.10 Miksi vielä yksi lisaa-kutsu?
Osaatko selittää miksi aseta-metodissa pitää olla kutsut this.s = s; super.aseta(h,m); lisaa(0,0);? Jos osaat, olet jo melkein valmis Java-ohjelmoija!
Tehtävä 9.11 Ei turhaa lisaa-kutsua
Mitä tarvitsee muuttaa jotta viimeinen lisaa-kutsu saadaan pois?
Joskus kirjoittamalla vähän enemmän voi saada aikaan nopeampaa ja turvallisempaa koodia. Niin tässäkin tapauksessa. Nimittäin säätäminen siinä, ettemme vaivautuneet kirjoittamaan muodostajia hirveän huolella, johtaa siihen että esimerkiksi alustus
AikaSek8 a4 = new AikaSek8(14,55,45);
kutsuu kaikkiaan seuraavia metodeja:
AikaSek8(14,55,45);
Aika5(); // koska oli Aika5 pitää alustaa aluksi
Aika5.aseta(0,0);
Aika5.lisaa(0); // Tässä Aika5, koska ei vielä "kasvanut" AikaSek8
AikaSek8.aseta(14,55,45);
Aika5.aseta(14,55);
Aika5.lisaa(0); //
Aika.lisaa(0,0);
AikaSek8.lisaa(0,0);
Aika5.lisaa(0,0);
Olisitko arvannut! Enää ei tarvitse ihmetellä miksi olio-ohjelmat ovat isoja ja hitaita. Totta kai voitaisiin sanoa, että hyvän kääntäjän olisi pitänyt huomata tuosta optimoida päällekkäisyydet pois. Mutta tämä on vielä tänä päivänä kova vaatimus kääntäjälle. Mutta ehkäpä voisimme ohjelmoijina vähän auttaa:
public AikaSekB(int h,int m, int s) { // Muodostaja
super(h,m); this.s = s; lisaa(0,0);
}
public AikaSekB(int h,int m) { super(h,m); s = 0; }
public AikaSekB(int h) { super(h); s = 0; }
public AikaSekB() { super(); s = 0; }
public AikaB() { this.h = 0; this.m = 0; }
public AikaB(int h) { aseta(h,0); }
public AikaB(int h,int m) { aseta(h,m); }
Samalla on riisuttu pois alustukset
private int h=0, m=0;
ja muutettu muotoon
private int h,m;
Nyt on saatu ainakin seuraavat edut: oletustapauksessa kumpikin luokka alustuu pelkillä 0:ien sijoituksilla, ilman yhtään ylimääräistä kutsua.
ELI! Perityn yliluokan muodostajaa kutsutaan automaattisesti, jollei sitä itse tehdä. Nyt parametrillisessa muodostajassa kutsutaan yliluokan muodostajaa, ja näin voidaan välttää ainakin oletusmuodostajan kutsu:
public AikaSekB(int h,int m, int s) { super(h,m); this.s = s; lisaa(0,0); }
Nyt alustuksesta
AikaSekB a4 = new AikaSekB(14,55,45);
seuraa seuraavat metodikutsut
AikaSekB(14,55,45);
AikaB(14,55); // koska oli Aika5 pitää alustaa aluksi
AikaB.aseta(14,55);
AikaB.lisaa(0); // Tässä AikaB, koska ei vielä "kasvanut" AikaSek8
AikaSekB.lisaa(0,0);
AikaB.lisaa(0,0);
Turhaahan tuossa on vieläkin, mutta tippuihan kuitenkin noin puolet pois! Joka tapauksessa periminen tuottaa jonkin verran koodia, aina kun yliluokan metodeja käytetään hyväksi. Ja jollei käytettäisi, kirjoitettaisiin samaa koodi uudelleen, eli palattaisiin 70-luvulle. Nykyisin kasvanut koneteho kompensoi usein tehottomamman oliokoodin ja olio-ohjelmoinnin ansiosta pystytään kirjoittamaan luotettavampia (?) ja monimutkaisempia ohjelmia. Eli etusija pitää antaa koodin selkeydelle ja ylläpidettävyydellä. Edellä esitetyt "kikkailut" huonontavat ylläpidettävyyttä jos jotakin oleellista muutetaan.
Tehtävä 9.12 Yliluokan alustajan kutsu
Pöytätestaa sekä AikaSek8.java että AikaSekB.java alustuksilla
AikaSek8 a4 = new AikaSek8(1,95,70);
AikaSekB a4 = new AikaSekB(1,95,70);
Osa aikaisemmista ongelmista olisi voitu kiertää, mikäli olisimme päässeet käsiksi luokan yksityisiin tietoihin. Esimerkiksi alkuperäisen luokan tulosta olisi voitu jättää koskemattomaksi vaikka se olikin väärin tehty. Tätä olisi voitu helpottaa sillä, että kantaluokassa Aika olisi julistettu h ja m protected suojauksella. Tosin vain perityssä versiossa tästä olisi ollut apua.
Muutenkin saattaa tulla tilanteita, joissa luokan ulkopuolinen haluaa päästä käsiksi sisäisiin tietoihin. Ainakin lukemana niitä. Eihän ole ollenkaan tavatonta ajankaan kanssa, että joku haluaisi tietää tunnit, muttei tulostaa? Mikä ratkaisuksi? Julistetaanko kaikki attribuutit julkisiksi (public)? No ei sentään! Kirjoitetaan saantimetodi kullekin attribuutille, jonka perustellusti voidaan katsoa tarpeelliseksi jollekin ulkopuoliselle voitavan julkaista:
"Lopullinen" versio aikaluokastamme voisikin siis olla seuraava:
olioalk\AikaC.java - saantimetodit
public class AikaC {
private int h,m;
...
public int getH() { return h; }
public int getM() { return m; }
...
}
Huomattakoon nyt, että perinnässä ei tarvitse määritellä uudestaan saantifunktioita getH() ja getM(), ainoastaan uudet, eli esimerkissämme getS().
Nyt voitaisiin esimerkiksi kutsua:
System.out.println("Tunnit = " + a1.getH());
Mikä tässä sitten on erona attribuuttien julkaisemiseen verrattuna? Se että attribuutit ovat nyt tietyssä mielessä vain luettavissa (read-only), eli niitä voi lukea saantimetodien avuilla, mutta niitä voi asettaa vain aseta-metodin avulla, joka taas pystyy suorittamaan oikeellisuustarkistukset ja näin olion tila ei koskaan pääse muuttumaan olion itsensä siitä tietämättä.
Tehtävä 9.13 Saantimetodi sekunneille
Täydennä AikaSekB.java:hen em. saantimetodit ja lisäksi getS() aliluokkaan AikaSekB.
Tehtävä 9.14 Saantimetodien käyttäminen
Muuta vielä edellisessä tehtävässä jokainen mahdollinen viittaus luokan sisälläkin saantimetodeja käyttäväksi suoran attribuuttiviittauksen sijasta.
Kapseloinnin ansiosta luokan käyttämiseksi on tullut selvä rajapinta (interface): metodit, joilla olion tilaa muutetaan. Tämän rajapinnan ansiosta luokka muuttuu "mustaksi laatikoksi", jonka sisällöstä ulkomaailma ei tiedä mitään, mutta jonka kanssa voi kommunikoida metodien avulla.
Kuva 9.3 Musta laatikko
Tämä luokan sisustan piilottaminen antaa meille mahdollisuuden toteuttaa luokka oleellisesti eri tavalla. Voimme esimerkiksi toteuttaa ajan minuutteina vuorokauden alusta laskien:
/**
* Vaihettu sisäinen esitystapa
* @author Vesa Lappalainen
* @version 1.0, 01.02.2003
*/
public class AikaD {
private int yht_min;
public void aseta(int h,int m) { yht_min = 60*h + m; }
public AikaD() { aseta(0,0); }
public AikaD(int h) { aseta(h,0); }
public AikaD(int h,int m) { aseta(h,m); }
public void tulosta(boolean lf) {
int m = getM();
System.out.print("" + getH() + ":" + (m<10?"0":"")+m);
if ( lf ) System.out.println();
}
public void tulosta() { tulosta(true); }
public void lisaa(int lisa_min) { yht_min += lisa_min; }
public static void lisaa(AikaD aika,int lisa_min) { aika.lisaa(lisa_min); }
public int getH() { return yht_min / 60; }
public int getM() { return yht_min % 60; }
public static void main(String[] args) {
AikaD a1 = new AikaD();
AikaD a2 = new AikaD(13);
AikaD a3 = new AikaD(14,25);
a1.tulosta(); a2.tulosta(); a3.tulosta();
a1.aseta(12,15); a2.aseta(16,-15);
a1.tulosta(); a2.tulosta();
lisaa(a1,55); a1.tulosta();
System.out.println("Tunnit = " + a1.getH());
}
}
Tehtävä 9.15 minuutteina()
Lisää AikaC:hen ja AikaD:hen, eli molempiin sisäisiin toteutustapoihin saantimetodi getMinuutteina, joka palauttaa kellonajan vuorokauden alusta minuutteina laskettuna.
Yksi perinnän tärkeimmistä ominaisuuksista on mahdollisuus monimuotoisuuteen, polymorfismiin. Esimerkiksi
AikaB ajat[] = new AikaB[5];
ajat[0] = a1; ajat[1] = a2; ajat[2] = a3; ajat[3] = a4;
ajat[4] = new AikaSekB(23,59,59);
for (int i=0; i < ajat.length; i++ ) {
ajat[i].tulosta(false); System.out.print(" +" + i + " => ");
ajat[i].lisaa(i); ajat[i].tulosta();
}
Taulukko ajat koostuu viitteistä AikaB-luokan olioihin. Myös AikaSekB toteuttaa saman rajapinnan, koska se on perittyä samasta luokasta. Siksi taulukkoon voi laittaa mitä tahansa AikaB-luokan jälkeläisluokankin olioita.
Esimerkissä AikaSek7 luokka koostettiin sekunneista ja luokan Aika5 oliosta. Nyt valitettavasti vain polymorfismi ei toimi, eli AikaSek7 ja Aika5 eivät ole perimissuhteessa toisiinsa. Niillä on kyllä Javassa yhteinen kantaluokka Object, koska Javassa kaikki luokat periytyvät Object-luokasta. Mutta yhteistä aikaan liittyvää rajapintaa niillä ei ole. Kömpelö polymorfismi saataisiin aikaan seuraavasti:
Object ajat[] = new Object[5];
ajat[0] = a1; ajat[1] = a2; ajat[2] = a3; ajat[3] = a4;
ajat[4] = new AikaSekE(23,59,59);
for (int i=0; i < ajat.length; i++ ) {
if ( ajat[i] instanceof Aika5 ) {
Aika5 aika = (Aika5)ajat[i]; // pakotettu tyypin muunnos
aika.tulosta(false); System.out.print(" +" + i + " => ");
aika.lisaa(i); aika.tulosta();
}
if ( ajat[i] instanceof AikaSek7 ) {
AikaSek7 aika = (AikaSek7)ajat[i]; // pakotettu tyypin muunnos
aika.tulosta(false); System.out.print(" +" + i + " => ");
aika.lisaa(i); aika.tulosta();
}
}
Tavassa jossa joudutaan testaamaan olion tyyppiä, tulee uusien tyyppien lisääminen järjestelmään erittäin työlääksi.
Javassa avuksi tulee rajapintakäsite. Teemme ensin "mallin" siitä, minkälainen on vähintään kaikkien Aika-luokkien rajapinta:
public interface AikaRajapinta {
void aseta(int h,int m);
void tulosta(boolean lf);
public void tulosta();
void lisaa(int lisa_min);
int getH();
int getM();
}
Seuraavaksi kaikki luokat, jotka halutaan kuuluvan "samaan kategoriaan", ilmoitetaan toteuttavan tämän rajapinnan:
public class AikaE implements AikaRajapinta {
private int h, m;
public void aseta(int h,int m) { this.h = h; this.m = m; lisaa(0); }
public AikaE() { this.h = 0; this.m = 0; }
public AikaE(int h) { aseta(h,0); }
public AikaE(int h,int m) { aseta(h,m); }
public void tulosta(boolean lf) {
System.out.print("" + h + ":" + (m<10?"0":"")+m);
if ( lf ) System.out.println();
}
public void tulosta() { tulosta(true); }
public void lisaa(int lisa_min) {
int yht_min = h * 60 + m + lisa_min;
h = yht_min / 60;
m = yht_min % 60;
}
public int getH() { return h; }
public int getM() { return m; }
}
Sitten esim. koosteluokka ilmoitetaan toteuttamaan myös sama rajapinta:
public class AikaSekE implements AikaRajapinta {
private AikaE hm = new AikaE();
private int s;
public void aseta(int h,int m,int s) { hm.aseta(h,m); this.s = s; lisaa(0,0); }
public void aseta(int h,int m) { aseta(h,m,0); }
public void aseta(int h) { aseta(h,0); }
public AikaSekE() { aseta(0,0,0}; }
public AikaSekE(int h,int m, int s) { aseta(h,m,s); }
public AikaSekE(int h,int m) { aseta(h,m,0); }
public AikaSekE(int h) { aseta(h,0,0); }
public void tulosta(boolean lf) {
hm.tulosta(false);
System.out.print(":" + (s<10?"0":"")+s);
if ( lf ) System.out.println();
}
public void tulosta() { tulosta(true); }
public void lisaa(int lisa_min,int lisa_sek) {
s += lisa_sek;
hm.lisaa(lisa_min+s/60);
s %= 60;
}
public void lisaa(int lisa_min) { lisaa(lisa_min,0); }
public int getH() { return hm.getH(); }
public int getM() { return hm.getM(); }
public static void main(String[] args) {
AikaE a1 = new AikaE();
AikaE a2 = new AikaE(13);
AikaE a3 = new AikaE(14,175);
a1.tulosta(); a2.tulosta(); a3.tulosta();
a1.aseta(12,15); a2.aseta(16,-15);
a1.tulosta(); a2.tulosta();
AikaSekE a4 = new AikaSekE(14,55,45); a4.tulosta();
a4.lisaa(3,30); a4.tulosta();
// Rajapintaan perustuva esimerkki polymorfisesta taulukosta
AikaRajapinta ajat[] = new AikaRajapinta[5];
ajat[0] = a1; ajat[1] = a2; ajat[2] = a3; ajat[3] = a4;
ajat[4] = new AikaSekE(23,59,59);
for (int i=0; i < ajat.length; i++ ) {
ajat[i].tulosta(false); System.out.print(" +" + i + " => ");
ajat[i].lisaa(i); ajat[i].tulosta();
}
}
}
Näin voimme jälleen tehdä taulukon, johon voimme laittaa kaikkia AikaRajapinta-määrittelyn toteuttavien luokkien olioita.
Jos Javassa ei peritä luokkaa mistään, niin se periytyy aina Object-luokasta. Näin siksi, että kaikki oliot saadaan samaan hierarkiaan ja voidaan esimerkiksi tallentaa samaan tietorakenteeseen. Käytännössä tämä ei ole kovin kätevää, sillä silloin tietorakenteessa olevilla olioilla on käytössä vain Object-luokan metodit. Jotta olioilla voitaisiin tehdäkin jotakin, pitää niiden tyyppi muuntaa vastaamaan niiden varsinaista luokkaa.
Object-luokassa on kuitenkin muutama tärkeä metodi, joiden olemassa olosta ohjelmoijan on hyvä olla tietoinen:
Object clone(); // tekee oliosta itsestään kopion
boolean equals(Object obj); // vertaa oliota toiseen olioon
int hashCode(); // palauttaa olioon liityvän "lajitteluavaimen"
String toString(); // palauttaa olion merkkijonona
/**
* Luokka toteuttamaan sovitun julkisen rajapinnan ja Object-
* luokan metodeja.
* @author Vesa Lappalainen
* @version 1.0, 01.02.2003
*/
public class AikaF implements AikaRajapinta {
private int h, m;
/**
* Asettaa uuden ajan ja pitää huolen että aika on aina oikeaa muotoa.
* @param h asetettavat tunnit
* @param m asetettavat minuutit
*/
public void aseta(int h,int m) {
this.h = h; this.m = m; lisaa(0);
}
public AikaF() { this.h = 0; this.m = 0; }
/**
* Asettaa uuden ajan ja pitää huolen että aika on aina oikeaa muotoa.
* @param h asetettavat tunnit
*/
public AikaF(int h) {
aseta(h,0);
}
/**
* Alustaa ajan
* @param h tunnit
* @param m minuutit
*/
public AikaF(int h,int m) { // Muodostaja
aseta(h,m);
}
/**
* Tulostaa ajan muodossa 15:05
* @param lf tulostetaanko rivinvaihto vai ei
*/
public void tulosta(boolean lf) {
System.out.print(toString());
if ( lf ) System.out.println();
}
/**
* Tulostaa ajan muodossa 15:05 sekä aina rivinvaihdon
*/
public void tulosta() {
tulosta(true);
}
/**
* Lisää aikaan valitun minuuttimäärän
* @param lisa_min lisättävä minuuttimäärä
*/
public void lisaa(int lisa_min) {
int yht_min = h * 60 + m + lisa_min;
h = yht_min / 60;
m = yht_min % 60;
}
public static void lisaa(AikaF aika,int lisa_min) {
int yht_min = aika.h * 60 + aika.m + lisa_min;
aika.h = yht_min / 60;
aika.m = yht_min % 60;
}
public int getH() { return h; }
public int getM() { return m; }
public String toString() {
return "" + h + ":" + (m<10?"0":"")+m;
}
public boolean equals(Object o) {
if ( !(o instanceof AikaRajapinta) ) return false;
AikaRajapinta a = (AikaRajapinta)o;
return getH() == a.getH() && getM() == a.getM();
}
public Object clone() {
return new AikaF(getH(),getM());
}
public int hashCode() {
return 3600*getH() + 60*getM();
}
public static void main(String[] args) {
AikaF a1 = new AikaF();
AikaF a2 = new AikaF(13);
AikaF a3 = new AikaF(14,25);
a1.tulosta(); a2.tulosta(); a3.tulosta();
a1.aseta(12,15); a2.aseta(16,-15);
a1.tulosta(); a2.tulosta();
lisaa(a1,55); a1.tulosta();
System.out.println("Tunnit = " + a1.getH());
System.out.println(a1.toString());
}
}
Esimerkiksi tekemällä metodi
public String toString() {
return "" + h + ":" + (m<10?"0":"")+m;
}
vältämme kaikki tulostukseen liittyvät ongelmat. Jos halutaan verrata kahta Aika-oliota keskenään, kannattaa kirjoittaa equals-metodi.
public boolean equals(Object o) {
if ( !(o instanceof AikaRajapinta) ) return false;
AikaRajapinta a = (AikaRajapinta)o;
return getH() == a.getH() && getM() == a.getM();
}
equals-metodia kirjoitettaessa on oltava huolellinen, sillä parametrina saattaa tulla oikean tyyppinen olio tai sitten väärän tyyppinen olio. equals-metodin pitää toteuttaa seuraavat ominaisuudet:
Olkoon seuraavassa a1,a2 ja a3 kolme luokan oliota.
reflektiivisyys: a1.equals(a1) pitää olla aina tosi
symmetrisyys: a1.equals(a2) == a2.equals(a1)
transitiivisuus: jos a1.equals(a2) && a2.equals(a3) niin a1.equals(a3)
luonnollisesesti toistuvien equals kutsujen pitää palauttaa samoille olioille
sama arvo, mikäli olioiden samuuteen vaikuttava tila ei muutu
Jos luokkaan toteutetaan equals-metodi, on siihen toteutettava myös hajautusarvo. Javan tietorakenteet tarvitsevat hajautusarvoa. Hajautusarvon täytyy palauttaa sama luku olioille, jotka ovat equals-vertailussa saman arvoisia. Mutta kaksi eriarvoistakin oliota saa palauttaa saman hajautusarvon. Meidän tapauksessamme hajautusarvoksi voidaan valita vaikkapa sekunnit vuorokauden alusta:
public int hashCode() {
return 3600*getH() + 60*getM();
}
Lisäksi monessa tilanteessa tarvitaan oliosta samanlainen kopio. Tätä varten toteutetaan clone-metodi:
public Object clone() {
return new AikaF(getH(),getM());
}
Tehtävä 9.16 equals toString avulla
Toteuta equals-metodi toString-metodin avulla. Arvioi ratkaisun tehokkuutta.
Tehtävä 9.17 equals AikaSek-luokkaan
equals-metodiin tulee ongelmia toteutettaessa AikaSek-luokkaa. Mieti mitä.
Tehtävä 9.18 AikaSek perimällä.
Esimerkissä AikaSekF on toteutettu sekunnit sisältävä aikaluokka koostamalla. Kokeile miten nyt onnistuu perintä AikaF-luokasta ja mitä metodeja pitää korvata.
Tehtävä 9.19 Vertailu
Tutki dokumenteista rajapintaa Comparable ja muuta luokat AikaF ja AikaSekF toteuttamaan tuo rajapinta.
Alunperin kirjoittamamme luokka Aika kokikin varsin kovia tarkemmassa tarkastelussa. Näistä muutoksista osa oli vielä aivan perusasioita; läheskään kaikkea emme vieläkään ole ottaneet huomioon (vertailu, syöttö päätteeltä, muuntaminen merkkijonoksi ja takaisin, lisäyksessä tapahtuvan ylivuodon luovuttaminen päivämäärälle, jne.). Miten sitten monimutkaisempien luokkien kanssa? Niin kauan pärjää, kun luokat on omaan käyttöön. Heti kun yritetään tehdä yleiskäyttöisiä luokkia (joka on yksi olio-ohjelmoinnin tavoite), tuleekin ongelmia vastaan.
Paremmalla suunnittelulla luokasta olisi heti voinut tulla yleiskäyttöisempi. Usein jopa joudutaan tekemään kahden luokan yläpuolelle abstrakti, tai muuten vaan yhteinen yliluokka, josta hieman toisistaan poikkeavat luokat peritään. Kuljimme tämän pitkän tien sen vuoksi, että lukija oppisi ymmärtämään miksi valmiit luokat eivät ole parin rivin koodinpätkiä.
Tulevaisuudessa ohjelmoijat jakaantunevatkin selvästi kahteen ryhmään: toiset käyttävät valmiita luokkia (mikä on helppoa, jos luokat ovat kunnossa, vrt. Delphi tai Visual Basic, ehkä myös osittain Java ja Jonnen pyynnöstä mainitaan tietysti Python). Ammattitaitoisempi ryhmä sitten suunnittelee ja tekee näitä yleiskäyttöisiä luokkia. Sitä mukaa kun luokkia saadaan valmiiksi eri "elämän aloille", siirtyy ammattilaiset yhä spesifimmille aloille.
Esimerkiksi M$:in Windowsin ohjelmointiin tarkoitetut luokkakirjastot ovat paisuneet niin suuriksi, että niiden käyttämistä tuskin kukaan enää hallitsee, ja ennen kuin entisen kirjaston on ehtinyt edes auttavasti oppia, tuleekin uusi versio. Tästä tulee oravanpyörä, jossa voi olla kova homma pysyä mukana, jollei ohjelmateollisuus keksi jotakin uutta ja mullistavaa avuksi.
Joka tapauksessa haave siitä, että näkee näyn ja keksii hyvän luokan, jota muut sitten yhtään muuttamatta voivat käyttää hyväkseen, kannattaa heittää Ylistönrinteen sillan alle. Mieluummin kannattaa alistua siihen, että opettelee käyttämään hyviä luokkia ja imee niitä käyttäessään ideoita siitä, miten parantaa omia luokkiaan seuraavalla kerralla.
Olio-ohjelmoinnin eräs tavoite on tuottaa ohjelmoijien käyttöön yleiskäyttöisiä komponentteja, jotta jokainen ei keksisi samaa pyörää uudelleen. Erityisesti graafisen ohjelmoinnin puolella ja sekä myös tietokantaohjelmoinnin puolella näitä komponentteja onkin varsin mukavasti. Borlandin Delphillä syntyy melkein Kerho-ohjelmaamme vastaava Windows-ohjelma lähes koodaamatta, pelkästään pudottelemalla komponentteja lomakkeelle.
Jos kerrankin pääsisin vastakkain nykykielten kehittäjien kanssa, niin tekisi kovasti mieli kysyä ovatko he koskaan tehneet oikeaa ohjelmaa. Nimittäin lähes kielestä riippumatta kunnolliset merkkijonot loistavat poissaolollaan. Ja ohjelmoijat ovat käyttäneet äärettömästi työtunteja tehdessään itselleen aluksi edes auttavaa merkkijonokirjastoa. Ainoastaan "lelukielissä" - Basicissä ja Turbo Pascalissa on ollut hyvät ja turvalliset merkkijonot.
C-kielen char jono[10] on todellinen aikapommi, jonka aukkoisuuteen perustuu vielä tänäkin päivänä useat hakkereiden kikat murtautua vieraisiin tietojärjestelmiin. Katsotaanpa ensin mitä C-merkkijonoille voi/ei voi tehdä:
char s1[10],s2[5],*p; L
p = "Kana" // Toimii!
p[0] = 'S'; // Toimii! Mutta jatkossa käy huonosti...
s1 = "Kissa"; // ei toimi!
strcpy(s2,"Koira"); // Huonosti käy! Miksi? Älä käytä koskaan...
if ( s1 < s2 ) ... // Sallittu, mutta tekee eri asian kuin lukija arvaakaan...
gets(s1); // Itsemurha, tämä on eräs kaikkein hirveimmistä funktioista
// lukee päätteeltä rajattomasti merkkejä ...
fgets(s1,sizeof(s1),stdin); // Oikein! Tosin rivinvaihto jää jonoon jos syöte
// on lyhyempi kuin 9 merkkiä
printf(s1); // Ohohoh! Tämä jopa toimii!!!
cout << s1; // Ja jopa tämäkin!!!
cin >> s1; // Taas itsemurha ....
Jos käytetään C-kieltä, pitää käyttää varsin paljon aikaa siihen miten C:n merkkijonoja voidaan kohtuullisen turvallisesti käyttää.
Onneksi C++:assa on kohtuullinen merkkijonoluokka. Nyt jo! Yli 10 vuotta kielen kehittämisen jälkeen...
Katso esimerkiksi: http://www.iki.fi/gaia/tekstit/cxxstring/
Javassa on vastaavasti kaksi merkkijonoluokkaa: String ja StringBuffer. Ensin mainittu koskee merkkijonoja, joita ei koskaan (immutable) tarvitse muuttaa, vaan riittää aina luoda uusi merkkijono. Jälkimmäistä käytetään, mikäli jonoon tulee paljon muutoksia (mutable).
Tehtävä 9.20 Ensimmäinen melkein järkevä olio
Täydennä seuraavat luokat ja testaa ohjelma.
/**
* Henkilöluokka
* Täydennä luokka.
* @author Vesa Lappalainen
* @version 1.0, 05.02.2003
*/
public class Henkilo {
private String nimi;
private int ika;
private double pituus_m;
Henkilo(String nimi, int ika, double pituus_m) {
}
public String toString() {
return "";
}
public void kasvata(double cm) {
}
public static void main(String[] args) {
}
}
/**
* Opiskelija, joka on peritty henkilöstä
* @author Vesa Lappalainen
* @version 1.0, 05.02.2003
*/
public class Opiskelija extends Henkilo {
double keskiarvo;
Opiskelija(String nimi, int ika, double pituus_m, double keskiarvo) {
super(nimi,ika,pituus_m);
}
public String toString() {
return "";
}
public static void main(String[] args) {
Henkilo Kalle = new Henkilo("Kalle",35,1.75);
System.out.println(Kalle);
Kalle.kasvata(2.3);
System.out.println(Kalle);
Opiskelija Ville = new Opiskelija("Ville",21,1.80,9.9);
System.out.println(Ville);
}
}