Olio-ohjelmointi Periytyminen ja monimuotoisuus. 1. Periytyminen

Samankaltaiset tiedostot
812347A Olio-ohjelmointi, 2015 syksy 2. vsk. IV Periytyminen ja monimuotoisuus

815338A Ohjelmointikielten periaatteet Harjoitus 5 Vastaukset

T Olio-ohjelmointi Osa 5: Periytyminen ja polymorfismi Jukka Jauhiainen OAMK Tekniikan yksikkö 2010

Sisällys. JAVA-OHJELMOINTI Osa 6: Periytyminen ja näkyvyys. Luokkahierarkia. Periytyminen (inheritance)

9. Periytyminen Javassa 9.1

815338A Ohjelmointikielten periaatteet

12. Monimuotoisuus 12.1

9. Periytyminen Javassa 9.1

Olio-ohjelmoinnissa luokat voidaan järjestää siten, että ne pystyvät jakamaan yhteisiä tietoja ja aliohjelmia.

Sisällys. 9. Periytyminen Javassa. Periytymismekanismi Java-kielessä. Periytymismekanismi Java-kielessä

Operaattoreiden ylikuormitus. Operaattoreiden kuormitus. Operaattoreiden kuormitus. Operaattoreista. Kuormituksesta

Sisällys. 9. Periytyminen Javassa. Periytymismekanismi Java-kielessä. Periytymismekanismi Java-kielessä

815338A Ohjelmointikielten periaatteet Harjoitus 3 vastaukset

1. Olio-ohjelmointi 1.1

12. Monimuotoisuus 12.1

Luokassa määriteltävät jäsenet ovat pääasiassa tietojäseniä tai aliohjelmajäseniä. Luokan määrittelyyn liittyvät varatut sanat:

Olio-ohjelmointi Javalla

12 Mallit (Templates)

Rajapinnasta ei voida muodostaa olioita. Voidaan käyttää tunnuksen tyyppinä. Rajapinta on kuitenkin abstraktia luokkaa selvästi abstraktimpi tyyppi.

Geneeriset luokat. C++ - perusteet Java-osaajille luento 6/7: Template, tyyppi-informaatio, nimiavaruudet. Geneerisen luokan käyttö.

Virtuaalifunktiot ja polymorfismi

C++ rautaisannos. Kolme tapaa sanoa, että tulostukseen käytetään standardikirjaston iostreamosassa määriteltyä, nimiavaruuden std oliota cout:

Taulukot. Jukka Harju, Jukka Juslin

JAVA-PERUSTEET. JAVA-OHJELMOINTI 3op A JAVAN PERUSTEET LYHYT KERTAUS JAVAN OMINAISUUKSISTA JAVAN OMINAISUUKSIA. Java vs. C++?

T Olio-ohjelmointi Osa 3: Luokka, muodostin ja hajotin, this-osoitin Jukka Jauhiainen OAMK Tekniikan yksikkö 2010

Opintojakso TT00AA11 Ohjelmoinnin jatko (Java): 3 op Rajapinnat ja sisäluokat

ITKP102 Ohjelmointi 1 (6 op)

Sisällys. 1. Omat operaatiot. Yleistä operaatioista. Yleistä operaatioista

Sisällys. 11. Rajapinnat. Johdanto. Johdanto

1. Omat operaatiot 1.1

Olio-ohjelmointi Syntaksikokoelma

Metodit. Metodien määrittely. Metodin parametrit ja paluuarvo. Metodien suorittaminen eli kutsuminen. Metodien kuormittaminen

Opintojakso TT00AA11 Ohjelmoinnin jatko (Java): 3 op Taulukot & Periytyminen

Olio-ohjelmointi Olioperusteinen ohjelmointi C++-kielellä. 1. Johdanto

15. Ohjelmoinnin tekniikkaa 15.1

on ohjelmoijan itse tekemä tietotyyppi, joka kuvaa käsitettä

Harjoitus Olkoon olemassa luokat Lintu ja Pelikaani seuraavasti:

Osoitin ja viittaus C++:ssa

Tehtävä 1. TL5302 Olio-ohjelmointi Koe Malliratkaisuja. Tässä sekä a)- että b)-kohdan toimiva ratkaisu:

Ohjelmoinnin jatkokurssi, kurssikoe

C++11 Syntaksi. Jari-Pekka Voutilainen Jari-Pekka Voutilainen: C++11 Syntaksi

A) on käytännöllinen ohjelmointitekniikka. = laajennetaan aikaisemmin tehtyjä luokkia (uudelleenkäytettävyys)

812341A Olio-ohjelmointi, IX Olioiden välisistä yhteyksistä

Sisällys. Yleistä attribuuteista. Näkyvyys luokan sisällä. Tiedonkätkentä. Aksessorit. 4.2

812341A Olio-ohjelmointi Peruskäsitteet jatkoa

Abstraktit tietotyypit ja olio-ohjelmointi

15. Ohjelmoinnin tekniikkaa 15.1

Java-kielen perusteet

Sisällys. Yleistä attribuuteista. Näkyvyys luokan sisällä ja ulkopuolelta. Attribuuttien arvojen käsittely aksessoreilla. 4.2

Sisällys. Metodien kuormittaminen. Luokkametodit ja -attribuutit. Rakentajat. Metodien ja muun luokan sisällön järjestäminen. 6.2

Sisällys. JAVA-OHJELMOINTI Osa 7: Abstrakti luokka ja rajapinta. Abstraktin luokan idea. Abstrakti luokka ja metodi. Esimerkki

812347A Olio-ohjelmointi, 2015 syksy 2. vsk. VII Suunnittelumallit Adapter ja Composite

Sisällys. 6. Metodit. Oliot viestivät metodeja kutsuen. Oliot viestivät metodeja kutsuen

Olion elinikä. Olion luominen. Olion tuhoutuminen. Olion tuhoutuminen. Kissa rontti = null; rontti = new Kissa();

812336A C++ -kielen perusteet,

16. Javan omat luokat 16.1

812347A Olio-ohjelmointi, 2015 syksy 2. vsk. X Poikkeusten käsittelystä

Java kahdessa tunnissa. Jyry Suvilehto

Luokat ja oliot. Ville Sundberg

TIE Ohjelmistojen suunnittelu. Luento 8..9: moniperintä

Periytyminen. Luokat ja olio-ohjelmointi

Ohjelmistojen mallintaminen Luokkakaaviot Harri Laine 1

812347A Olio-ohjelmointi, 2015 syksy 2. vsk. II Johdanto olio-ohjelmointiin

Javan perusteita. Janne Käki

4. Luokan testaus ja käyttö olion kautta 4.1

Ohjelmistojen mallintaminen luokkamallin lisäpiirteitä

Opintojakso TT00AA11 Ohjelmoinnin jatko (Java): 3 op Pakkaukset ja määreet

7. Oliot ja viitteet 7.1

Olio-ohjelmointi Poikkeusten käsittelystä. 1. Johdanto

C# olio-ohjelmointi perusopas

Kompositio. Mikä komposition on? Kompositio vs. yhteyssuhde Kompositio Javalla Konstruktorit set-ja get-metodit tostring-metodi Pääohjelma

Tietueet. Tietueiden määrittely

19. Olio-ohjelmointia Javalla 19.1

Ohjelmointitaito (ict1td002, 12 op) Kevät Java-ohjelmoinnin alkeita. Tietokoneohjelma. Raine Kauppinen

Sisällys. 19. Olio-ohjelmointia Javalla. Yleistä. Olioiden esittely ja alustus

Operaattorin ylikuormitus ja käyttäjän muunnokset

Sisällys. Mitä on periytyminen? Yksittäis- ja moniperiytyminen. Oliot ja perityt luokat. Periytymisen käyttö. 8.2

2. Olio-ohjelmoinista lyhyesti 2.1

Sisällys. 15. Lohkot. Lohkot. Lohkot

Aalto Yliopisto T Informaatioverkostot: Studio 1. Oliot ja luokat Javaohjelmoinnissa

ITKP102 Ohjelmointi 1 (6 op)

Lohkot. if (ehto1) { if (ehto2) { lause 1;... lause n; } } else { lause 1;... lause m; } 16.3

Olio-ohjelmointi Johdanto olio-ohjelmointiin

Oliot viestivät metodeja kutsuen

Lohkot. if (ehto1) { if (ehto2) { lause 1;... lause n; } } else { lause 1;... lause m; } 15.3

\+jokin merkki tarkoittaa erikoismerkkiä; \n = uusi rivi.

Ohjelman virheet ja poikkeusten käsittely

Mikä yhteyssuhde on?

Java-kielen perusteet

Java-kielen perusteet

4. Olio-ohjelmoinista lyhyesti 4.1

Periytyminen (inheritance)

13 Operaattoreiden ylimäärittelyjä

Olio-ohjelmointi Geneerisyys. 1. Johdanto

Mitä on periytyminen?

Ohjelmointitaito (ict1td002, 12 op) Kevät Java-ohjelmoinnin alkeita. Tietokoneohjelma. Raine Kauppinen

Informaatioteknologian laitos Olio-ohjelmoinnin perusteet / Salo

Oliosuunnitteluesimerkki: Yrityksen palkanlaskentajärjestelmä

Rajapinta (interface)

Ohjelmointikieli TIE Principles of Programming Languages Syksy 2017 Ryhmä 19

Transkriptio:

Olio-ohjelmointi Periytyminen ja monimuotoisuus Edellisessä osassa tarkasteltiin olio-ohjelmoinnin ensimmäistä perusominaisuutta: sitä, että luokat muodostavat kapseloinnin ja tiedon kätkennän toteuttavia tietotyyppejä. Kun tähän lisätään vielä periytyminen ja monimuotoisuus, päästään täysipainoiseen olio-ohjelmointiin. Periytymistä ja monimuotoisuutta C++-kielessä käsitellään esimerkiksi kirjan [Eck] luvuissa 14 ja 15 sekä kirjan [Hie] luvuissa 23-27. 1. Periytyminen Periytyminen tarkoittaa luokan ominaisuuksien sisällyttämistä uuteen luokkaan. Perittävää luokkaa nimitetään kantaluokaksi tai yliluokaksi, perivää luokkaa nimitetään aliluokaksi. Ensin tässä käsitellään periytymistä yleisesti kirjan [Bud] (luku 8) pohjalta ja sitten esitetään, miten C++-kielessä toteutetaan periytyminen. Oliosuunnittelussa voidaan käyttää ns. is-a -testiä yleissääntönä kuvaamaan, onko kahden luokan välillä periytyvyyssuhde. Mikäli luokan A oliot ovat erikoistapauksia luokan B olioista ( an A is a B ), luokka A voi periä luokan B. Esimerkiksi kala on eläin ja ahven on kala, joten kalaa kuvaava luokka voisi periä eläintä kuvaavan luokan ja ahventa kuvaava luokka voisi periä kalaa kuvaavan luokan. Sen sijaan kalaa ja evää kuvaavien luokkien välillä ei ole vastaavaa suhdetta, joten kala-luokan ei ole syytä periä evä-luokkaa, vaikka joskus näkeekin tämän tyyppistä ohjelmointia; se edustaa kuitenkin epäonnistunutta suunnittelua. Luontevampaa onkin määritellä tällaisessa tapauksessa luokkien välillä koostumussuhde: kala-luokka voi sisältää eväolion osaolionaan. Periytyminen on vahva mekanismi, jolla saavutetaan monia hyötyjä. Ehkä tärkein periytymisen etu on uudelleenkäyttö: Kun jo valmis ja testattu luokka peritään, saadaan usein merkittävä osa ohjelmaa valmiina ja vältetään koodin uudelleen kirjoittaminen. Koodin luotettavuuskin paranee. Periytyminen mahdollistaa uudelleen käytettävien ohjelmistokomponenttien laatimisen. Koska aliluokka perii (tavallisessa periytymisen muodossa) kantaluokan julkisen rajapinnan, periytymisen avulla voidaan taata, että rajapinnat samankaltaisiin olioihin ovat samanlaiset. Periytyminen sallii myös olio-ohjelmoinnissa ilmenevän erittäin tärkeän tyypin monimuotoisuudesta, jota käsitellään kappaleessa 2. Olioiden ja periytymisen sisällyttäminen ohjelmaan tuo luonnollisesti myös mukanaan varjopuolia. Oliotekniikoilla rakennettu ohjelma on yleensä hieman tehottomampi kuin puhtaasti proseduraalinen ohjelma. Tosin ero on useimmiten jokseenkin merkityksetön, jos ohjelma on asianmukaisesti suunniteltu ja toteutettu. Olio-ohjelma on myös yleensä kooltaan hieman suurempi; tälläkään ei ole tavallisesti olennaista merkitystä. Monimutkaisen luokkarakenteen liittäminen ohjelmaan voi kuitenkin lisätä merkittävästi ohjelman loogista kompleksisuutta ja siten tehdä siitä vaikeasti hallittavan. Tästä syystä kannattaa välttää erityisesti periytymisen liikakäyttöä. 1.1. Julkinen periytyminen C++:ssa Periytymistä ei ole toteutettu kaikissa oliokielissä samalla tavoin. Esimerkiksi Java-kielessä periytymisen malli on varsin yksinkertainen: Kaikkien luokkien yhteinen kantaluokka on Javan Object-luokka ja jokainen muu luokka perii täsmälleen yhden luokan. Periytymisen mekanismi on aina sama. C++-kielessä sen sijaan luokalla ei välttämättä ole lainkaan kantaluokkaa, ja 1

periytyminen voi tapahtua kolmella mekanismilla (julkinen, suojattu ja yksityinen). Lisäksi luokka voi periä useamman kuin yhden luokan. Ylivoimaisesti yleisimmin käytetty periytymisen muoto on yhden luokan julkinen periminen. Tätä tapaa käsitellään seuraavaksi ja muut tapaukset jätetään lyhyelle maininnalle. Julkista periytymistä tulee käyttää, kun perivää tyyppiä voidaan käyttää aina samassa yhteydessä kuin kantaluokkaa. Tällöin sanotaan että aliluokka on kantaluokan alityyppi. Tässä tapauksessa aliluokka perii kantaluokaltaan sekä liittymän että toteutustavan ja periytyminen näkyy luokkahierarkiasta ulospäin. Tämä on Java-kielessä ainoa mahdollinen periytymisen muoto. Ohjelmakoodissa julkinen periytyminen merkitään seuraavasti: class Ali: public Kanta // Ali-luokan attribuutit ja metodit ; Edellä Ali-luokka perii siis Kanta-luokan. Huomaa, että aliluokka perii kaikki kantaluokan attribuutit riippumatta niiden näkyvyysmääreistä; aliluokan olio sisältää osanaan ilmentymän kantaluokasta. Seuraavat kaaviot hahmottavat tilannetta. ColoredPoint-luokka perii Point-luokan. ColoredPoint-olio sisältää ilmentymän Point-luokasta. Edellä olevassa kaaviossa ColoredPoint perii Point-luokan. Näin ollen ColoredPoint-luokan oliot sisältävät myös Point-luokan jäsenmuuttujat x_coord ja y_coord. Julkisessa periytymisessä (eikä missään muussakaan periytymisen muodossa) kantaluokan yksityiset (private-tyyppiset) jäsenet eivät kuitenkaan näy aliluokassa, joten niitä ei voi suoraan aliluokassa käyttääkään. Näin ollen muuttujia x_coord ja y_coord ei voi suoraan käyttää ColoredPoint-luokassa. Mikäli aliluokan on käytettävä kantaluokan yksityisiä attribuutteja, kantaluokkaan on tehtävä sopivat julkiset tai suojatut metodit, jotka palauttavat tai asettavat attribuuttien arvot. Kantaluokan suojatut (protected-tyyppiset) ja julkiset (public-tyyppiset) jäsenet näkyvät aliluokissa, joten ne ovat suoraan käsiteltävissä, kun luokka peritään. Jäsenten näkyvyys ei myöskään muutu julkisessa periytymisessä, joten esimerkiksi suojatut jäsenet näkyvät läpi koko luokkahierarkian. 2

Edellä esiintynyt luokka ColoredPoint perii myös Point-luokan metodit print ja move. Koska move on julkinen metodi, sitä voidaan kutsua myös ColoredPoint-olion kautta. Point-luokan print-metodi toteutetaan uudelleen ColoredPoint-luokassa, joten ColoredPoint-olion kautta kutsuttaessa suoritettaisiin uudelleen toteutettu print. Metodi changecolor on olemassa ainoastaan luokassa ColoredPoint, joten tätä metodia ei luonnollisestikaan voisi kutsua Pointluokan olion kautta. Kun aliluokan olio luodaan, ensimmäiseksi kutsutaan jotakin kantaluokan muodostinta, ts. olio rakennetaan kantaluokasta lähtien. Kantaluokan muodostimen kutsu voi olla ohjelmoijan tekemä, jolloin sen täytyy sijoittua aliluokan muodostimen alustuslistaan. Ellei ohjelmoija tee kantaluokan muodostimen kutsua itse, kutsutaan automaattisesti kantaluokan oletusmuodostinta. Hajottimien kutsujärjestys on päinvastainen kuin muodostimien: olio tuhotaan aliluokasta kantaluokkaa kohti, ts. ensin kutsutaan aliluokan hajotinta ja sitten kantaluokan hajotinta. Esimerkiksi seuraava ohjelma #include <iostream> using namespace std; class Kanta Kanta() cout << "Kanta-luokan oletusmuodostin" << endl; Kanta(int i) cout << "Kanta-luokan kokonaislukumuodostin" << endl; ~Kanta() cout << "Kanta-luokan hajotin" << endl; ; class Ali : public Kanta Ali() cout << "Ali-luokan oletusmuodostin" << endl; Ali(int i) : Kanta(i) cout << "Ali-luokan kokonaislukumuodostin" << endl; ~Ali() cout << "Ali-luokan hajotin" << endl; ; int main() tulostaa Ali a; return 0; Ali a(10); 3

Kanta-luokan oletusmuodostin Ali-luokan oletusmuodostin Ali-luokan hajotin Kanta-luokan hajotin Kanta-luokan kokonaislukumuodostin Ali-luokan kokonaislukumuodostin Ali-luokan hajotin Kanta-luokan hajotin koska ensin luodaan Ali-luokan olio käyttämällä sen oletusmuodostinta, joka kutsuu ensiksi automaattisesti Kanta-luokan oletusmuodostinta. Tämän jälkeen luotu olio tuhotaan: ensin kutsutaan Ali-luokan hajotinta ja sitten Kanta-luokan hajotinta. Seuraavaksi luodaan Ali-luokan olio käyttämällä muodostinta, joka ottaa parametrikseen kokonaisluvun. Tämä muodostin kutsuu alustuslistassaan Kanta-luokan kokonaislukumuodostinta, minkä jälkeen suoritetaan Ali-luokan muodostin. Lopuksi luotu olio tuhotaan jo tuttuun tapaan. HUOM! Edellisessä esimerkkiohjelmassa luokat on kirjoitettu pääohjelmatiedostoon esitysteknisistä syistä. Normaalisti luokka määritellään omassa otsikkotiedostossaan. 1.2. Muut periytymisen muodot C++:ssa Edellisessä kappaleessa käsiteltiin yksinkertaista julkista periytymistä. C++:ssa on myös mahdollista käyttää suojattua ja yksityistä periytymistä sekä moniperiytymistä. Näitä käsitellään lyhyesti tässä. Päivi Hietasen kirjan [Hie] luvuissa 27 ja 28 perehdytään mainittuihin periytymisen muotoihin. Toteutustavan periytyminen Jos kantaluokka peritään tavallista, julkista periytymistä käyttäen aliluokkaan, kantaluokan julkinen rajapinta on näkyvissä aliluokan olioiden käyttäjille. Tämä onkin useimmiten tarkoituksenmukaista. Joskus kuitenkin halutaan aliluokassa peittää kantaluokan julkinen rajapinta ja periä ainoastaan kantaluokan toteutus. Sama tietorakenne voidaan esimerkiksi toteuttaa monen eri tallennusrakenteen päälle. Tietorakenteen käyttäjien ei kuitenkaan yleensä ole aiheellista päästä käyttämään tallennusrakenteen palveluja. Tällaisissa tapauksissa voidaan käyttää yksityistä tai suojattua periytymistä. Näitä periytymisen muotoja nimitetään toteutustavan periytymiseksi (implementation inheritance). Yksityisessä periytymisessä kantaluokan kaikkien jäsenten näkyvyys muuttuu privatetyyppiseksi. Näin ollen ne eivät näy edes aliluokasta perittävissä luokissa. Suojatussa periytymisessä kantaluokan protected- ja public-jäsenet ovat näkyvyydeltään protected-tyyppisiä, joten ne eivät näy luokkahierarkiasta ulospäin, mutta ovat näkyvissä luokkahierarkian sisällä. Yksityinen periytyminen toteutetaan C++:ssa muuttamalla luokkaa perittäessä avainsana public avainsanaksi private; suojattu periytyminen taas käyttämällä avainsanaa protected. Ellei periytymisessä anneta lainkaan näkyvyysmäärettä, oletetaan periytymisen olevan privatetyyppistä. HUOM! Tällä kurssilla laadittavissa ohjelmissa periytyminen on aina julkista eikä toteutustavan periytymiseen enää palata. 4

Moniperiytyminen C++:ssa luokka voi periä useamman kuin yhden kantaluokan. Kantaluokat merkitään pilkulla erotettuna seuraavasti class Ali: public EkaKanta, public TokaKanta // jne Normaalitapauksessa C++-kielessä moniperiytyminen on erottelevaa moniperiytymistä. Tämä konkretisoituu, kun luokka perii saman kantaluokan kahta eri kautta esimerkiksi seuraavasti: class Kanta // JNE class EkaSolmu: public Kanta // JNE class TokaSolmu: public Kanta // JNE class Ali: public EkaSolmu, public TokaSolmu // JNE Nyt luokka Ali perii luokan Kanta sekä luokan EkaSolmu että luokan TokaSolmu kautta. Tällöin Ali-luokan oliolla on kaksi eri ilmentymää Kanta-luokassa esitellyistä tiedoista. Tämä voi joskus aiheuttaa ongelmia. Tilanteen muuttamiseksi C++:ssa voidaan käyttää ns. yhdistävää eli virtuaalista moniperiytymistä seuraavaan tapaan: class Kanta // JNE class EkaSolmu: public virtual Kanta // JNE class TokaSolmu: public virtual Kanta // JNE class Ali: public EkaSolmu, public TokaSolmu // JNE Nyt luokat EkaSolmu ja TokaSolmu perivät Kanta-luokan virtuaalisesti, jolloin luokat perivään Ali-luokkaan tulee vain yksi ilmentymä Kanta-luokasta. Moniperiytyminen on monessa tilanteessa kätevä ominaisuus, mutta sitä kannattaa käyttää harkiten. Tällä kurssilla moniperiytymistä harrastetaan ainoastaan rajapintaperiytymisenä. Tämä tarkoittaa sitä, että luokan periessä useamman eri luokan, yhtä lukuun ottamatta perittävät luokat ovat rajapintoja, ts. niiden kaikki metodit ovat puhtaita virtuaalimetodeja. Tällaisiin metodeihin tutustutaan tämän dokumentin kohdassa 2.2 Uudelleenmäärittely. 2. Monimuotoisuus Monimuotoisuus (polymorfismi) tarkoittaa löyhästi sanottuna sitä, että tilanteesta riippuen oliot voivat reagoida samaan viestiin eri lailla. Termiä käytetään eri yhteyksissä eri tavoin, joten termi itsekin on tavallaan polymorfinen. Nimenomaan monimuotoisuus erottaa olio-ohjelmoinnin selvimmin muista ohjelmointiparadigmoista. Olio-ohjelmoinnissa monimuotoisuus ilmenee neljällä eri tavalla: metodien (funktioiden) ylikuormittamisena, metodien uudelleen määrittelemisenä, muuttujien monimuotoisuutena ja geneerisyytenä. Kaikki nämä ilmenemismuodot edistävät omalla tavallaan koodin uudelleenkäyttöä. Seuraavassa käsitellään kolmea ensimmäistä ilmenemismuotoa, geneerisyys jätetään myöhemmäksi. Esitettävät asiat sisältyvät pääosin teoksen [Bud] lukuihin 14-17. 5

2.1. Ylikuormittaminen Ylikuormittamisessa metodin nimi on monimuotoinen, toisin sanoen samannimisellä metodilla (tai yleisemmin funktiolla) on useita eri toteutuksia, joista kutsuttaessa valitaan tilanteeseen sopiva. Tyypillisesti tämä tapahtuu siten, että eri toteutuksien parametrilistat ovat erilaiset ja kutsuttaessa metodia päätellään syöteparametrien määrästä ja tyypeistä, mikä toteutus valitaan. Valinta voidaan tehdä staattisesti eli käännösaikana, koska kaikki tarvittava informaatio kutsun sitomiseksi on jo silloin saatavana. Huomaa, että ylikuormittaminen ei edellytä oliomaisuutta; monessa ohjelmointikielessä ylikuormittaminen on mahdollista, vaikka oliotukea ei olisikaan. Hyvin usein luokan olioita voidaan luoda eri parametriyhdistelmien perusteella; tällöin on tavallista ylikuormittaa luokan muodostin. Monesti tarvitaan esimerkiksi oletusmuodostin, ts. muodostin, jota voidaan kutsua ilman parametreja ja joka luo olion joillakin oletusarvoilla. Esimerkiksi Java-kielen merkkijonoa mallintavalla String-luokalla on mm. seuraavat konstruktorit: String() Initializes a newly created String object so that it represents na empty character sequence. String(char[] value) Allocates a new String so that it represents the sequence of characters currently contained in the character array argument. String(String original) Initializes a newly created String object so that it represents the same sequence of characters as the argument; in other words, the newly created string is a copy of the argument string. Näin ollen uusi String-olio voidaan luoda Java-ohjelmassa esimerkiksi seuraavilla tavoilla: String s = new String(); // s = char merkit[] = h, e, i ; String t = new String(merkit); // t = hei String u = new String(t); // u = hei Oletetaan, että C++-kielellä on kirjoitettu luokka, joka mallintaa tason pisteitä. Kahden pisteen yhteenlasku on luonnollista toteuttaa laskemalla pisteiden x- ja y-koordinaatit yhteen, jolloin tulokseksi saadaan uusi piste. Luettavuuden ja yhtenäisyyden kannalta voisi olla toivottavaa käyttää +-operaattoria esimerkiksi seuraavaan tapaan: Piste p(2,3); Piste q (4,5); Piste r = p+q; // r = (6,8); Automaattisesti tämä ei kuitenkaan toimi, sillä itse laaditun luokan olioihin ei voi soveltaa +- operaattoria, ellei ohjelmoija itse ylikuormita operaattoria sopivasti ottamaan parametreinaan ko. luokan olioita. Jos operaattoria ei ylikuormiteta, pitäisi luokkaan kirjoittaa oma metodi pisteiden yhteenlaskua varten. Tämä saattaa kuitenkin heikentää ohjelmakoodin intuitiivista ymmärtämistä, joten operaattorin toteuttaminen on perusteltua. C++-kielessä voidaankin lähes mikä tahansa operaattori ylikuormittaa, koska operaattorit on toteutettu funktioina, joiden ylikuormittaminen on sallittua. 6

Esimerkiksi Piste-luokkaan voitaisiin lisätä operaattori += ja ylikuormittaa operaattori + seuraavasti: class Piste private: double x_coord; double y_coord; ; Piste(double x,double y); Piste(const Piste& p); ~Piste(); Piste& operator+=(const Piste &p); Piste operator+(const Piste& p1,const Piste& p2); Operaattorit voitaisiin toteuttaa seuraavaan tapaan: Piste::Piste(double x,double y):x_coord(x),y_coord(y) Piste::Piste(const Piste& p) x_coord = p.x_coord; y_coord = p.y_coord; Piste::~Piste() Piste& Piste::operator+=(const Piste &p) x_coord = x_coord+p.x_coord; y_coord = y_coord+p.y_coord; return *this; Piste operator+(const Piste& p1,const Piste& p2) Piste p(p1); p += p2; return p; Nyt ohjelmassa esiintyvä koodi Piste p1(2.1,3.2); Piste p2(0.5,1.1); Piste p3 = p1+p2; toimisi asianmukaisesti. Kaikilla olio-ohjelmointikielillä vastaavaa ominaisuutta ei voi toteuttaa; esimerkiksi Javalla tämä ei onnistu, sillä Javassa ei ole mahdollista kuormittaa operaattoreita. Javassa tulisi siis toteuttaa luokkaan oma metodi pisteiden yhteenlaskuun. Ylikuormitus voi perustua myös näkyvyysalueisiin. Koska luokka muodostaa oman näkyvyysalueensa, kahdessa eri luokassa voi olla täsmälleen samanniminen metodi samoin parametrein, eikä hämmennystä aiheudu. Tällöin ei myöskään välttämättä oleteta, että metodeilla olisi semanttista yhteyttä. 7

Seuraavassa kappaleessa käsitellään metodien uudelleen määrittelyllä aikaan saatavaa monimuotoisuutta. 2.2. Uudelleenmäärittely Aliluokan metodin sanotaan uudelleenmäärittelevän (override) sen yliluokan metodin, mikäli aliluokassa on toteutettu metodi, jolla on täsmälleen sama parametrilista ja paluuarvo kuin yliluokan metodilla. Sanotaan myös että aliluokan metodi korvaa yliluokan metodin. Tällöin kutsuttaessa aliluokan olion metodia yliluokan toteutus peittyy, ja käytetään aliluokassa määriteltyä toteutusta. Uudelleenmääritellyn metodin näkyvyyden muuttamisen säännöt ovat kielikohtaisia, esimerkiksi Javassa yliluokan public-tyyppistä metodia ei voi aliluokassa uudelleen määritellä private-tyyppiseksi. Sen sijaan C++-kielessä näin voi tehdä. Yleensä uudelleenmääriteltyjen metodien näkyvyyttä ei ole syytä muutella. Jos metodin kutsu sidotaan staattisesti käännösaikana, ei voida uudelleenmäärittelyn yhteydessä vielä puhua monimuotoisuudesta. Uudelleenmäärittelystä tuleekin vasta todella hyödyllinen, kun käytetään hyväksi kahta ominaisuutta: 1. Metodin kutsu sidotaan dynaamisesti ajon aikana 2. Aliluokan oliota voidaan käsitellä yliluokan oliona Jälkimmäistä ominaisuutta sanotaan alityyppiperiaatteeksi: Aliluokan olio voi esiintyä kaikissa yhteyksissä, joissa yliluokan olio esiintyy. Tämä periaate tunnetaan myös nimellä Liskov substitution principle, koska Barbara Liskov ja John Guttag käsittelivät periaatteen ideaa vuonna 1986 teoksessaan Abstraction and Specification in Program Development. Java-kielen aliluokat ovat aina yliluokkansa alityyppejä. C++-kielessä näin ei välttämättä ole, koska siinä voidaan käyttää erilaisia perinnän muotoja, jotka rikkovat alityyppiperiaatteen. Käytettäessä julkista yksinkertaista periytymistä myös C++-kielessä on alityyppiperiaate voimassa. Alityyppiperiaate aiheuttaa uudelleenmäärittelyyn semanttisen ongelman: Aliluokka voi määritellä yliluokan metodin uudelleen miten tahansa. Mikään ei takaa, että uudelleenmääritelty metodi muistuttaisi toiminnaltaan alkuperäistä, vaan se jää ohjelmoijan vastuulle. Dynaamisesti sidottavaa metodia sanotaan virtuaalioperaatioksi. Javassa kaikki metodit ovat virtuaalioperaatioita, sen sijaan C++-kielessä ohjelmoijan on erikseen määriteltävä halutut luokan metodit virtuaalisiksi, muuten ne sidotaan staattisesti. Mikäli virtuaalioperaatiolla on toteutus sekä yli- että aliluokassa, sanotaan operaatiota kiinnitetyksi. Tällöin yliluokassa toteutettu metodi on oletusoperaatio, joka voidaan syrjäyttää aliluokissa. Mikäli yliluokassa operaatio on esitelty, mutta sillä ei ole toteutusta, kyseessä on avoin virtuaalioperaatio eli abstrakti metodi. C++:ssa abstrakti metodi määritellään syntaktisesti hieman oudolla tavalla kirjoittamalla metodin esittelyn perään sijoitus =0. Esimerkiksi dokumentin lopussa olevassa esimerkissä esiintyvän Esine-luokan kaikki metodit ovat abstrakteja. On selvää, että tällaisesta luokasta ei voi luoda olioita, koska sillä olisi metodi, jota ei voi kutsua. Tällaista luokkaa sanotaan abstraktiksi luokaksi. Staattisesti tyypitetyissä kielissä abstrakteilla metodeilla on tärkeä asema, koska niiden avulla luokkahierarkian kantaluokkaan voi liittää metodeja, joiden toteuttaminen pakotetaan kaikissa hierarkian ei-abstrakteissa luokissa. Joissakin kielissä (esimerkiksi Javassa) luokan voi pakottaa abstraktiksi jollakin avainsanalla. C++-kielessä näin ei ole, vaan luokasta tulee abstrakti täsmälleen silloin, kun se sisältää vähintään yhden abstraktin metodin. 8

Uudelleenmäärittely voidaan ohjelmointikielissä toteuttaa periaatteessa kahdella tavalla: korvaamalla (replacement) tai tarkentamalla (refinement). Korvaamista kutsutaan joskus amerikkalaiseksi semantiikaksi. Siinä aliluokan metodi korvaa yliluokan metodin siten, että ainoastaan aliluokan metodi suoritetaan. Useimmissa nykyisissä ohjelmointikielissä käytetään kumpaakin muotoa, mutta enimmäkseen korvaamista. Tarkentaminen tunnetaan myös skandinaavisen semantiikan nimellä. Tarkentamismenetelmässä suoritetaan ensin yliluokan metodi ja sitten aliluokan metodi. Yleisesti käytettävissä kielissä tarkentamista käytetään vain muodostimissa automaattisesti, esimerkiksi sekä Javassa että C++:ssa oliota luotaessa kutsutaan aina jotakin sen yliluokan muodostinta. Muita metodeja suoritettaessa tarkentaminen voidaan saada aikaan kutsumalla erikseen yliluokan metodia. Kuten edellä mainittiin, C++-kielessä metodin dynaamista sidontaa ei tehdä automaattisesti vaan ainoastaan erikseen kantaluokassa virtual-määreellä esitellyille metodeille. Kun kantaluokassa metodi on määritelty virtuaaliseksi, se on dynaamisesti sidottava läpi koko luokkahierarkian, ominaisuutta ei voi poistaa periytymisessä. Milloin ohjelmakoodissa oleva metodikutsu voi johtaa siihen, että se sidotaan eri toteutuksiin? C++:ssa olio voi esiintyä normaalina muuttujana tai siihen osoittava muuttuja voi olla viite- tai osoitintyyppinen. Jos olio on normaali muuttuja, metodikutsu sidotaan aina ko. olion luokkaan, sen sijaan viite- tai osoitintyyppinen muuttuja voi olla kantaluokan tyyppinen, mutta osoittaa johonkin aliluokan olioon. Tällöin metodikutsu voi sitoutua eri toteutuksiin, jos metodi on määritelty virtual-tyyppiseksi. Tarkastellaan alla olevaa esimerkkiohjelmaa: #include <iostream> using namespace std; class Kanta virtual void dynamet(); void statmet(); virtual ~Kanta() ; void Kanta::dynaMet() cout << "Kanta dynamet() kutsuttu " << endl; void Kanta::statMet() cout << "Kanta statmet() kutsuttu " << endl; class Lapsi : public Kanta void dynamet(); void statmet(); ~Lapsi() ; void Lapsi::dynaMet() cout << "Lapsi dynamet() kutsuttu " << endl; void Lapsi::statMet() cout << "Lapsi statmet() kutsuttu " << endl; 9

int main() Kanta* ptr = new Lapsi(); ptr->dynamet(); ptr->statmet(); delete ptr; ; return 0; Kanta-luokan metodi dynamet on määritelty virtuaaliseksi, kun taas statmet ei. Pääohjelmassa Kanta-luokan tyyppinen osoitin osoittaa Lapsi-luokan olioon; osoittimen kautta kutsutaan kumpaakin yllämainittua metodia. Koska dynamet on virtuaalisena dynaamisesti sidottava, sen toteutus haetaan Lapsi-luokasta. Sen sijaan statmet on staattisesti sidottava ja sen kutsu sidotaan jo käännösaikana Kanta-luokan metodiin. Näin ollen ohjelma tulostaa Lapsi dynamet() kutsuttu Kanta statmet() kutsuttu Metodin uudelleenmäärittelyllä aikaan saatava monimuotoisuus toteutetaan siis C++:ssa käyttämällä virtual-tyyppisiä metodeja ja osoittimia tai viitteitä olioihin. Luokkahierarkian kantaluokassa on syytä määritellä ainakin hajotin virtuaaliseksi, muussa tapauksessa ajaudutaan helposti resurssivuotoihin. Lisätään edelliseen esimerkkiohjelmaan luokkien hajottimiin tulostuskomennot seuraavasti: class Kanta virtual void dynamet(); void statmet(); virtual ~Kanta() cout << Kanta-luokan hajotin << endl; ; class Lapsi : public Kanta void dynamet(); void statmet(); ~Lapsi() cout << Lapsi-luokan hajotin << endl; ; Tällöin ohjelma tulostaa Lapsi dynamet() kutsuttu Kanta statmet() kutsuttu Lapsi-luokan hajotin Kanta-luokan hajotin Toisin sanoen oliota tuhottaessa kutsutaan ensin aliluokan ja sitten kantaluokan hajotinta, niin kuin asiaan kuuluukin. Jos kuitenkin poistetaan virtual-määre Kanta-luokan hajottimesta: 10

class Kanta virtual void dynamet(); void statmet(); ~Kanta() cout << Kanta-luokan hajotin << endl; ; niin tulostuu Lapsi dynamet() kutsuttu Kanta statmet() kutsuttu Kanta-luokan hajotin Nyt hajotin on staattisesti sidottava ja koska pääohjelmassa delete-operaatio kohdistetaan Kantaluokan osoittimeen (vaikka osoitin osoittaakin Lapsi-luokan olioon), kutsutaan ainoastaan Kantaluokan hajotinta. Mikäli Lapsi-luokan hajottimessa tehtäisiin tärkeitä resurssin vapauttamisia, ne jätettäisiin tässä tapauksessa suorittamatta. Kirjoita siis aina luokkaan virtuaalinen hajotin, jos on mahdollista, että luokka peritään! Joissakin tilanteissa voi olla tarpeellista estää metodin uudelleenmäärittely. Kaikissa kielissä se ei ole mahdollista, mutta esimerkiksi Javassa se onnistuu määrittelemällä luokan metodi finaltyyppiseksi. Tällöin sitä ei voi aliluokissa uudelleen määritellä. Menetelmällä saavutetaan ainakin se etu, että tällöin metodin kutsu voidaan sitoa staattisesti, jolloin suorituskyky paranee. C++-kielessä uudelleenmäärittelyä ei voi estää, mutta jättämällä kantaluokassa metodi määrittelemättä virtuaaliseksi varmistetaan, että metodi sidotaan aina staattisesti. Yleensä uudelleenmäärittelyssä aliluokan metodin tyypin oltava täsmälleen sama kuin aliluokan, ts. sekä paluuarvon että parametrilistan muuttujien tyyppien on oltava täsmälleen samat. Oletetaan esimerkiksi, että luokkaan nimeltä Kanta on toteutettu metodi vertaa, joka ottaa kantaluokan tyyppisen viiteparametrin. Luokka peritään Kovar-nimiseen luokkaan, jossa yritetään uudelleen määritellä vertailumetodi. Kovar-luokan metodin parametrin tulisi olla Kovar-tyyppinen, koska yleensä yhtäsuuruutta voi vertailla vain kahden saman luokan olion välillä. Onnistuuko tällainen uudelleenmäärittely? Kuvatun kaltaista tilannetta, jossa parametrin tyyppi muuttuu samaan suuntaan luokkahierarkian kanssa, sanotaan kovarianssiksi (covariant change). Testataan asiaa yksinkertaisella ohjelmalla: 11

#include <iostream> class Kanta virtual bool vertaa(kanta &k) std::cout << "Vertaillaan kahta Kantaluokan oliota" << std::endl; return true; ; class Kovar: public Kanta Kovar(double d):dup(d) bool vertaa(kovar &k) std::cout << "Vertaillaan kahta Kovarluokan oliota" << std::endl; return dup == k.dup; private: double dup; ; int main(int argv, char** args) Kovar *k1 = new Kovar(1.1); Kanta *k2 = new Kovar(1.5); if (k2->vertaa(*k1)) std::cout << "TOTTA!" << std::endl; else std::cout << "SE OLI VALHETTA VAIN!" << std::endl; delete k1; delete k2; return 0; Mikäli luokan Kovar metodi vertaa korvaisi Kanta-luokan metodin vertaa, kutsuttaisiin ohjelmassa luokan Kovar metodia. Ohjelmaa suoritettaessa huomataan kuitenkin sen tulostuksesta, että siinä kutsutaan metodia vertaa Kanta-luokasta, joten metodia ei ole uudelleenmääritelty vaan metodit tulkitaan kahdeksi eri metodiksi. Yleensä kovarianssi ei olekaan mahdollinen metodin parametrien suhteen. Sen sijaan paluuarvon suhteen kovarianssi on semanttisesti mahdollista toteuttaa. C++:ssakin ominaisuus on sallittu. 12

Esimerkki: #include <iostream> class Yli virtual void nimi() std::cout << "Mahtava Yliluokan olio" << std::endl; ; class Ali: public Yli void nimi() std::cout << "Kurja Aliluokan olio" << std::endl; ; class Kovar virtual Yli* teeolio() return new Yli(); ; class KovarAli: public Kovar Ali* teeolio() return new Ali(); ; int main(int argv, char** args) Kovar *k1 = new Kovar(); Kovar *k2 = new KovarAli(); Yli *y = k1->teeolio(); y->nimi(); Yli *yy = k2->teeolio(); yy->nimi(); delete y; delete yy; delete k1; delete k2; return 0; Ohjelma voidaan kääntää ja suorittaa. Ohjelman tulostuksesta huomataan, että osoitin y osoittaa luokan Yli olioon ja osoitin yy luokan Ali olioon. Näin ollen metodi teeolio määritellään uudelleen luokassa KovarAli. Tällainen uudelleenmäärittely on kuitenkin sallittu vain jos korvaavan metodin paluuarvo on korvattavan metodin paluuarvon alityyppi. Jos luokka Ali ei periytyisi luokasta Yli, ohjelma ei kääntyisi. Samoin, jos hierarkia oli toisinpäin, ts. jos luokka Yli perisi luokan Ali, uudelleenmäärittely luokissa Kovar ja KovarAli ei olisi sallittua. 13

Mikäli parametrit muuttuvat luokkahierarkiassa päinvastaiseen suuntaan kuin luokkahierarkia, tilannetta sanotaan kontravarianssiksi (contravariant change). Tällöin siis aliluokan metodin parametrit ovatkin yleisempää tyyppiä kuin vastaavan metodin sen yliluokassa. Yllä olevasta esimerkistä kävi jo ilmi, että kontravarianssi ei ole yleensä sallittua metodien paluuarvoissa. Sen sijaan parametreissa kontravarianssi ei aiheuta kääntäjälle ongelmaa. Esimerkiksi edellinen ohjelma voitaisiin muokata seuraavanlaiseksi (luokat Yli ja Ali kuten ennenkin): class Kontravar virtual void tulostanimi(ali &a) std::cout << "Kontravar-luokan metodi" << std::endl; a.nimi(); ; class KontravarAli: public Kontravar void tulostanimi(yli &y) std::cout << "KontravarAli-luokan metodi" << std::endl; y.nimi(); ; int main(int argv, char** args) KontravarAli *k = new KontravarAli(); Ali a; Yli y; k->tulostanimi(a); k->tulostanimi(y); delete k; return 0; Ohjelma voidaan kääntää ja suorittaa ja tulostuksesta huomataan, että ohjelma kutsuu luokan KontravarAli metodia myös, kun parametri on Ali-luokan tyyppinen. Kuitenkaan metodi tulostanimi ei ole uudelleenmääritelty aliluokassa, vaan itse asiassa ylikuormitettu. Tämän havaitsee muuttamalla pääohjelman muotoon int main(int argv, char** args) Kontravar *k = new KontravarAli(); Ali a; k->tulostanimi(a); delete k; return 0; Mikäli metodi tulostanimi olisi uudelleenmääritelty, sitä kutsuttaisiin luokasta KontravarAli. Tulostuksesta huomataan kuitenkin, että metodia kutsutaan luokasta Kontravar, joten metodia ei ole uudelleenmääritelty. Joissakin kielissä kontravarianssi parametreissa on 14

eräissä tilanteissa mahdollinen (tällainen kieli on esimerkiksi Eiffel), edellisen testin perusteella ei kuitenkaan C++:ssa. 2.3. Monimuotoiset muuttujat Dynaamisesti tyypitetyissä kielissä mikä tahansa muuttuja voi säilyttää minkä tahansa tyyppistä tietoa. Monet skriptikielet ovat dynaamisesti tyypitettyjä. Näissä kielissä kaikki muuttujat ovat monimuotoisia. Sen sijaan staattisesti tyypitetyissä kielissä, kuten Java ja C++, kaikilla muuttujilla on hyvin määritelty tyyppi, joka annetaan muuttujan esittelyn yhteydessä. Näissä kielissä muuttujan monimuotoisuus tarkoittaa sitä, että muuttuja voi viitata tyyppinsä alityypin mukaiseen arvoon. Yleensä aliluokka on yliluokkansa alityyppi, jolloin yliluokan tunnuksella voidaan viitata sen alapuolella luokkahierarkiassa oleviin olioihin. C++:ssa tällainen muuttuja on välttämättä joko viittaus- tai osoitintyyppinen. Javassa kaikki oliomuuttujat ovat viitetyyppisiä. Monimuotoisia muuttujia käytetään yleisesti monimuotoisten säiliöiden laatimisessa. Säiliöön tallennetaan viitteitä jonkin luokkahierarkian kantaluokkaan, mutta viitteet voivat tosiasiassa olla viitteitä joihinkin hierarkiassa syvemmällä sijaitseviin olioihin. Kun säiliöstä otetaan tällaisia olioita, ne ovat eräässä mielessä menettäneet identiteettinsä: ohjelmoija ei tiedä minkä luokan oliota käsitellään. Ohjelmoitaessa tätä ei aina tarvitsekaan tietää. Varsinkin, jos kantaluokka sisältää vähintään esittelyt kaikista olioihin sovellettavista metodeista, näitä voidaan kutsua tarkemmin selvittämättä olion todellista luokkaa. Joskus kuitenkin on tiedettävä, onko jokin tietty metodi käytettävissä, mikä vaatii luokan tuntemisen ainakin jollakin tarkkuudella. Usein oliokielissä on olemassa tällainen mekanismi. C++-kielessä monimuotoiset säiliöt toteutetaan tallentamalla kantaluokan osoittimia säiliöön; olio varataan kekodynaamisesti ja kantaluokan osoitin laitetaan osoittamaan varattuun olioon. Edellä mainittua ominaisuutta, jonka avulla voi selvittää jonkin olion luokan ominaisuuksia, sanotaan reflektioksi. C++-kielessä reflektion toteutus on varsin vaatimaton, mutta perusominaisuudet voidaan saavuttaa ajonaikaisen tyyppitunnistuksen mekanismilla (RTTI, Run-Time Type Identification). Mekanismi perustuu kahteen operaattoriin: 1. typeid, joka palauttaa olion tyyppiä kuvaavan olion, 2. dynamic_cast, jonka avulla osoittimen tai viitteen tyyppiä voidaan muuttaa luokkahierarkiassa turvallisesti. Jotta mekanismi toimisi asianmukaisesti, on sen kohteena olevan luokkahierarkian oltava monimuotoinen, ts. hierarkian kantaluokassa on oltava ainakin yksi virtuaalinen metodi. Yleensä perittävään luokkaan onkin syytä kirjoittaa ainakin virtuaalinen hajotin, joten mekanismi soveltuu useimpiin käytännössä esiintyviin luokkahierarkioihin. Käytännön ohjelmoinnissa dynamic_cast-operaattori esiintyy huomattavasti useammin kuin typeid. Operaattoria dynamic_cast voidaan soveltaa sekä viitteeseen että osoittimeen, jolloin tyypinmuunnoksen epäonnistuminen johtaa erilaisiin toimintoihin: kun kohteena on osoitin, osoittimen arvoksi tulee nolla, mutta viitteen tapauksessa ohjelmassa aiheutetaan poikkeus. Seuraavassa esimerkissä sovelletaan operaattoria osoittimeen. 15

Oletetaan, että ohjelmassa käytetään seuraavia luokkia (Henkilo, Ajattelija, Koomikko ja Kirjailija): #include <iostream> #include <string> using namespace std; class Henkilo private: string nimi; Henkilo(const string &ihmisnimi); void muutanimi(const string &uusinimi); string annanimi() const; void tulostanimi() const; virtual ~Henkilo(); ; Henkilo::Henkilo(const string &ihmisnimi):nimi(ihmisnimi) void Henkilo::muutaNimi(const string &uusinimi) nimi = uusinimi; string Henkilo::annaNimi() const return nimi; void Henkilo::tulostaNimi() const cout << "Nimeni on " +nimi << endl; class Ajattelija virtual string ajattele() = 0; ; class Koomikko: public Ajattelija private: string ajatus; Koomikko(const string &nimi, const string &aatos); string ajattele(); ; Koomikko::Koomikko(const string &nimi, const string &aatos): ajatus(aatos) string Koomikko::ajattele() return ajatus; 16

class Kirjailija: public Henkilo, public Ajattelija private: string aforismi; Kirjailija(const string &nimi, const string &aatos); string ajattele(); ; Kirjailija::Kirjailija(const string &nimi, const string &aatos): Henkilo(nimi),aforismi(aatos) string Kirjailija::ajattele() return aforismi; Henkilo-luokasta todettakoon, että henkilöllä on attribuuttina nimi, joka saadaan kutsumalla metodia annanimi. Luokat Koomikko ja Kirjailija perivät kumpikin Ajattelija-luokan, mutta ainoastaan Kirjailija perii Henkilo-luokan. Jos säiliöön tallennetaan sekä Koomikko- että Kirjailija-luokkien olioita Ajattelija-luokan olioina (ts. säiliöön tallennetaan osoittimia, joiden kohdetyyppi on Ajattelija), niin voidaan olla varmoja, että niistä löytyy metodi ajattele. Sen sijaan ei voida tietää, onko mahdollista kutsua metodia annanimi, joka on olemassa vain Kirjailija-luokassa. Siten mikäli ko. metodia halutaan kutsua säiliöstä otettaessa on selvitettävä voidaanko oliota kohdella myös Henkilo-luokan oliona esimerkiksi seuraavasti: void tulostaajatukset(ajattelija* viisaat[], int length ) for(int i=0; i< length; i++) Henkilo *h; h = dynamic_cast<henkilo *>(viisaat[i]); if( h!= 0) // Tyypinmuunnos onnistui cout << h->annanimi() + " sanoo:" << endl; cout << viisaat[i]->ajattele() << endl; 17

int main(int argv, char** args) string ajatus = "En voisi kuulua kerhoon, joka ottaa minunlaisiani ihmisiä jäsenikseen."; string aforismi = "Vain itseoppineet ovat oppineita. \nkaikki muut ovat opetettuja."; Koomikko *gm = new Koomikko("Groucho Marx",ajatus); Kirjailija *ep = new Kirjailija("Erno Paasilinna",aforismi); Ajattelija** filosofit = new Ajattelija*[2]; filosofit[0] = gm; filosofit[1] = ep; tulostaajatukset(filosofit,2); Ohjelmassa yritetään tehdä tyyppimuunnos Henkilo-luokan osoittimeksi käyttäen dynamic_castoperaattoria, minkä jälkeen tarkistetaan osoittimen arvosta, onko tyyppimuunnos onnistunut. Ellei muunnos onnistu, osoitin saa arvon nolla ja tällöin ei metodia annanimi voi kutsua. Jos arvo on erisuuri kuin nolla, tyyppimuunnos on laillinen ja voidaan kutsua metodia annanimi. Tyyppimuunnosta, jolla tyyppi vaihdetaan luokkahierarkiassa alaspäin, kutsutaan nimellä downcasting. Tällaista tyyppimuunnosta käytettäessä on aina varmistettava, että muunnos on laillinen. Jos monimuotoisuutta käytetään oikein, downcasting- tyyppimuunnoksia ei ohjelmassa pitäisi juuri koskaan esiintyä. Joskus ne ovat kuitenkin tarpeen. Javassa on huomattavasti kehittyneempi reflektiomekanismi kuin C++:ssa. Siihen ei kuitenkaan paneuduta tässä. Lopuksi perehdytään esimerkkiin, jossa yhdistellään edellä esiintyneitä monimuotoisuuden muotoja. Esimerkki. Kuten alussa mainittiin, ylikuormittaminen tehdään staattisesti käännösaikana. Usein eteen tulee kuitenkin tilanteita, joissa olisi hyödyllistä kutsua metodia siten, että sen parametrien tyypit määräytyvät vasta ajon aikana. Kääntäjä ei kuitenkaan hyväksy tätä. Halutaan esimerkiksi tehdä kivi-paperi-sakset -peli. Kivi, paperi ja sakset periytyvät Esine-luokasta: #include <iostream> #include <cstdlib> #include <string> using namespace std; class Esine ; class Kivi: public Esine ; class Paperi : public Esine ; class Sakset : public Esine ; 18

Esine* arvoesine() switch(rand()%3)) default: case 0: return new Sakset(); case 1: return new Paperi(); case 2: return new Kivi(); string vertaa(kivi &k, Kivi &k2) return "Tasapeli"; string vertaa(kivi &k, Paperi &p) return "Tappio"; string vertaa(kivi &k, Sakset &s) return "Voitto"; string vertaa(paperi &p, Kivi &k) return "Voitto"; string vertaa(paperi &p, Paperi &p2) return "Tasapeli"; string vertaa(paperi &p, Sakset &s) return "Tappio"; string vertaa(sakset &s, Kivi &k) return "Tappio"; string vertaa(sakset &s, Paperi &p) return "Voitto"; string vertaa(sakset &s, Sakset &s2) return "Tasapeli"; int main(int args, char** argv) Esine *pa, *pb; pa = arvoesine(); pb = arvoesine(); cout << vertaa(*pa,*pb) << endl; delete pa; delete pb; Jokaiselle parille on tehty vertailufunktio. Pääohjelmassa arvotaan kaksi esinettä, joita vertaillaan keskenään. Metodin vertaa() kutsu ei kuitenkaan kelpaa kääntäjälle, koska käännösvaiheessa ei voida tietää, mitä esinetyyppejä parametrit ovat. Tämä voidaan ratkaista niin sanotulla double-dispatch-tekniikalla. Olio itse tietää oman tyyppinsä, joten perustetaan oikean metodin kutsuminen luokan metodin kutsuun. Jokaiseen luokkaan kirjoitetaan vertailumetodi 19

kantaluokalle ja perityille luokille; lisäksi kantaluokkaan tarvitaan ainakin prototyypit samoista metodeista. Vertailumetodissa kutsutaan toisen parametriolion metodia, joka taas tuntee oman tyyppinsä. Tällöin ohjelma muuttuu seuraavaan muotoon: #include <iostream> #include <cstdlib> #include <string> using namespace std; // Luokkien esittelyt Esine-luokkaa varten class Kivi; class Sakset; class Paperi; class Esine ; virtual string name()=0; virtual string vertaa(esine &e)=0; virtual string vertaakiveen(kivi &k)=0; virtual string vertaapaperiin(paperi &p)=0; virtual string vertaasaksiin(sakset &s)=0; class Kivi: public Esine string name() return "Kivi"; string vertaa(esine &e) return e.vertaakiveen(*this); string vertaakiveen(kivi &k) return "Tasapeli"; string vertaapaperiin(paperi &p) return "Tappio"; ; string vertaasaksiin(sakset &s) return "Voitto"; 20

class Sakset : public Esine string name() return "Sakset"; string vertaa(esine &e) return e.vertaasaksiin(*this); string vertaakiveen(kivi &k) return "Tappio"; string vertaapaperiin(paperi &p) return "Voitto"; ; string vertaasaksiin(sakset &s) return "Tasapeli"; class Paperi : public Esine string name() return "Paperi"; string vertaa(esine &e) return e.vertaapaperiin(*this); string vertaakiveen(kivi &k) return "Voitto"; string vertaapaperiin(paperi &p) return "Tasapeli"; ; string vertaasaksiin(sakset &s) return "Tappio"; Esine* arvoesine() switch(rand()%3) default: case 0: return new Sakset(); case 1: return new Paperi(); case 2: return new Kivi(); string vertaa(esine &e1, Esine &e2) return e2.vertaa(e1); 21

int main(int args, char** argv) Esine *pa, *pb; pa = arvoesine(); pb = arvoesine(); cout << "pa = " << pa->name() << " pb = " << pb->name() << endl; cout << "Tulos: " << vertaa(*pa,*pb) << endl; delete pa; delete pb; Huomaa, että edellisessä esimerkissä on toimimaton ylikuormittaminen korvattu metodien uudelleen määrittelyllä aikaan saatavalla monimuotoisuudella. Esine-tyyppiset osoitinmuuttujat ovat monimuotoisia muuttujia. Lähteet [Bud] Budd, Timothy A: An Introduction to Object-Oriented Programming, Addison-Wesley 2002 [Eck] Eckel, Bruce: Thinking in C++, 2 nd edition, Volume 1, 2000. Saatavissa osoitteesta: http://mindview.net/books/ticpp/thinkingincpp2e.html [Hie] Hietanen, P.: C++ ja olio-ohjelmointi, 3. laitos, Docendo 2004 22