Opiskelumateriaali - Abstraktit tietotyypit ja olio-ohjelmointi
Transcription
Opiskelumateriaali - Abstraktit tietotyypit ja olio-ohjelmointi
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi Abstraktit tietotyypit ja olio-ohjelmointi Edellisessä osassa käsiteltiin aliohjelmia prosessiabstraktion välineenä. Prosessiabstraktio onkin vanhimpia ohjelmointikielten suunnittelukäsitteitä ja sisältynyt ohjelmointikieliin niiden alkutaipaleelta asti. Vähääkään edistyneempi ohjelmointi ei ole mahdollista ilman prosessiabstraktiota, koska vasta algoritmien ja operaatioiden abstrahointi tekee mahdolliseksi hallita suuria ohjelmakokonaisuuksia. Data-abstraktio seurasi luonnollisella tavalla prosessiabstraktiota, kun havaittiin, että on tarpeellista yhdistää tietotyyppeihin määrättyjä operaatioita, joiden yksityiskohtia ei tietotyypin käyttäjän kuitenkaan tarvitse tuntea. Data-abstraktio toteutetaan abstrakteja tietotyyppejä käyttämällä; nämä tietotyypit puolestaan johtivat olioohjelmoinnin syntyyn. Tässä osassa käsitellään sekä abstrakteja tietotyyppejä että olioohjelmointia. 1. Abstraktit tietotyypit Abstrakti tietotyyppi (abstract data type, ADT) on tietotyyppi, joka toteuttaa seuraavat ehdot ([Seb], kappale 11.2.2): 1. Tyypin määrittelyn ja operaatioiden yksityiskohdat eivät näy tietotyypin ulkopuoliselle käyttäjälle. 2. Tyypin esittely samoin kuin tietotyyppiin liittyvät operaatioiden esittelyt sijaitsevat yhdessä syntaktisessa yksikössä. Muut ohjelmayksiköt voivat muodostaa tämän tyypin muuttujia. Ensimmäistä ehtoa sanotaan tiedon kätkennäksi (information hiding) tiedon ja toista kapseloinniksi (encapsulation). Kätkentään katsotaan kuuluvaksi myös se tietotyypin ominaisuus, että tyyppiin voidaan kohdistaa suoraan ainoastaan sellaisia operaatioita, jotka tyypin esittely sallii. Kapseloinnilla erotetaan tyypin rajapinta ja toteutuksen yksityiskohdat toisistaan: tyypin sisäiseen esitykseen ja operaatioihin voidaan tehdä muutoksia muiden tyypin käyttäjien kärsimättä, kunhan tyypin rajapinta ei muutu. Kapselointi helpottaa suuren ohjelman loogista ja teknistä hallintaa. Periaatteena on Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi koota data ja sitä käsittelevät operaatiot yhteen. Kun kootaan loogisesti yhteenkuuluvat operaatiot samaan yksikköön, myös niiden mentaalinen hallinta helpottuu. Teknisesti kapselointi helpottaa ohjelman kääntämistä, koska yhdestä syntaktisesta yksiköstä voidaan muodostaa erikseen käännettävä käännösyksikkö (compilation unit). Näin ollen voidaan tehdä muutoksia jonkin tietotyypin koodiin joutumatta kääntämään koko ohjelmaa erikseen. Samaan kokonaisuuteen liittyvien abstraktien tietotyyppien ja aliohjelmien sanotaan muodostavan moduulin, joka voidaan siis kääntää itsenäisesti ja esimerkiksi sijoittaa kirjastoon uudelleen käytettäväksi ohjelmayksiköksi. Sellaisissa ALGOL- pohjaisissa kielissä (esimerkiksi Pascal), joissa noudatetaan alisteista sisäkkäisiin ohjelman osiin perustuvaa ohjelmarakennetta, kapseloinnin toteuttaminen on ongelmallista. Tiedon kätkentä on erittäin tärkeä periaate tietotyypin luotettavuuden kannalta, sillä ainoastaan tietotyypin sisäiset operaatiot voivat väliaikaisesti rikkoa olion tilan, mutta palauttavat sen jälleen kuntoon ennen kontrollin siirtymistä tietotyypin käyttäjälle. Lisäksi koodin ylläpito helpottuu, mikäli tietotyypin joihinkin operaatioihin on tehtävä muutoksia, koska ulospäin näkyvä rajapinta säilyy silti samana. Huomaa, että myös kielen sisäiset tietotyypit voivat tämän määritelmän mukaan olla abstrakteja. Tässä käsitteellä tarkoitetaan kuitenkin käyttäjän määrittelemää abstraktia tietotyyppiä. Kirjan [Har] luvussa 9 käsitellään myös kapselointimekanismeja ja abstrakteja tietotyyppejä. 1.1. Abstraktit tietotyypit eri ohjelmointikielissä Abstraktit tietotyypit sisällytettiin ensimmäiseksi SIMULA 67 - kieleen ([Seb], kappale 11.4). Abstrakti tietotyyppi määriteltiin luokkana ja se sisälsi luokan muuttujat, aliohjelmien esittelyt sekä niiden koodin. Luokkien ilmentymät eli oliot luotiin pinodynaamisesti ja niihin voitiin viitata ainoastaan osoitinmuuttujan välityksellä. Luokassa määriteltyjä muuttujia ei piilotettu ilmentymän luovalta sovellukselta, joten tiedon kätkennän toteutus oli puutteellinen. Abstraktien tietotyyppien käyttö yleistyi ohjelmointikielissä vasta useita vuosia SIMULA 67-kielen julkaisun jälkeen. Useimmissa nykyisissä korkean tason ohjelmointikielissä voidaan käyttää abstrakteja tietotyyppejä; erityisesti tämä koskee olio-ohjelmointikieliä. Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi Adassa abstrakteja tietotyyppejä voidaan simuloida käyttämällä pakkauksia (packages), joiden avulla tietoja voidaan kapseloida. Pakkaukset koostuvat kahdesta osasta, joita molempia myös kutsutaan pakkauksiksi. Nämä osat ovat määrittelyosa (specification package) ja runkoon (body packege), jotka molemmat esitellään samannimisinä pakkauksina, mutta runko-osassa lisätään määre body pakkauksen nimen eteen. Pakkausta käyttävä sovellus näkee aina vain määrittelyosan, mutta ei runkoa. Pakkauksella ei välttämättä olekaan runko-osaa lainkaan. Tiedon kätkentä voidaan Adassa toteuttaa jakamalla määrittelyosa julkiseen ja yksityiseen osaan. Julkinen osa on kokonaisuudessaan näkyvissä pakkauksen käyttäjille, mutta yksityinen, private -määreellä merkitty osa on käyttäjältä piilotettu. Jollei määrettä ole annettu, tieto on julkista. Esimerkiksi linkitetyn listan määrittelevä tietotyyppi package LINKITETTY_LISTA is type SOLMU; type OSOITIN is access SOLMU; type SOLMU is record DATA: INTEGER; LINKKI: OSOITIN; end record; end; package body LINKITETTY_LISTA is -- Paketin runko end; sallisi pääsyn myös SOLMU -tyypin muuttujiin DATA ja LINKKI. Mikäli SOLMU haluttaisiin näkyviin paketin ulkopuolelle, mutta piilottaa sen yksityiskohdat, voitaisiin pakkaus määritellä seuraavasti: package LINKITETTY_LISTA is Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi type SOLMU is private; -- Listan käsittelymetodit private type SOLMU; type OSOITIN is access SOLMU; type SOLMU is record DATA: INTEGER; LINKKI: OSOITIN; end record; end LINKITETTY_LISTA; package body LINKITETTY_LISTA is -- Paketin runko end LINKITETTY_LISTA; Nyt SOLMU -tyypin esitys on piilotettu, mutta tyyppi näkyy kuitenkin pakkauksen ulkopuolelle. Pakkaus otetaan käyttöön ohjelmassa käyttämällä with ja use -lauseita: with LINKITETTY_LISTA; use LINKITETTY_LISTA; procedure LISTA_OHJELMA is OMA_SOLMU: SOLMU; begin -- listan käsittelyä end LISTA_OHJELMA; Tässä with -lause sisällyttää ulkoisen pakkauksen ohjelmaan, ja use -lauseella poistetaan tarve viitata pakkaukseen muuttujiin ja operaatioihin pakkauksen nimellä, Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi ts. voidaan käyttää nimeä SOLMU nimen LINKITETTY_LISTA.SOLMU asemasta. Reino Kurki-Suonion kirjan [Kur] luvussa 6 on hieman tarkemmin käsitelty Adan pakkauksia. Pascalissa ja C-kielessä on mahdollista määritellä omia tyyppejä, mutta niihin ei voi kytkeä operaatioita, joten abstraktien tietotyyppien toteuttaminen näissä kielissä on mahdotonta. Sen sijaan C++ -kielessä samoin kuin Javassa voidaan luokkien avulla määritellä abstrakteja tietotyyppejä. Seuraavassa perehdytään hieman C++:n ja Javan luokkien käyttöön nimenomaan data-abstraktion toteuttajana. Varsinaiseen olioohjelmointiin palataan tuonnempana. 1.2. Abstraktit tietotyypit C++ -kielessä C++ rakennettiin liittämällä C-kieleen oliotuki; data-abstraktio toteutetaan luokkien avulla (ks. [Strou], luku 10). Erona Adan abstraktiomalliin on, että C++ -kielen luokat ovat tietotyyppejä toisin kuin Adan pakkaukset, jotka sisältävät tietotyyppien määrittelyjä. C++:ssa voidaan siis määritellä luokkatyyppisiä muuttujia; luokkien julkisiin jäsenmuuttujiin voidaan viitata ainoastaan luokan instanssin kautta, kun taas Adassa sisällytetyn pakkauksen muuttujiin voidaan viitata suoraan (kunhan käytetään use-lausetta). C++:n luokka on C-kielen tietueen eli struct-tyypin laajennus. C-kielen structia voidaan pitää luokan erikoistapauksena, jossa ei ole lainkaan operaatioita ja kaikki jäsenmuuttujat ovat julkisia. Luokkien operaatioita kutsutaan jäsenfunktioiksi. C++ kielen luokkamalli muistuttaa SIMULA 67:n mallia, jossa luokka oli myös tietotyyppi. Luokan jäsenmuuttujat (member variables)ja -funktiot (member functions) voivat olla julkisia (public), suojattuja (protected) tai yksityisiä (private). Julkinen jäsenmuuttuja tai -funktio on näkyvissä ulkopuolelle, ts. luokan instanssin (olion) haltija voi suoraan viitata olion jäsenmuuttujaan tai kutsua jäsenfunktiota. Yksityiset jäsenmuuttujat ja funktiot näkyvät ainoastaan luokan sisällä ja luokan ystäväluokissa ja -funktioissa. Ystäväfunktio on luokan ulkopuolella määritelty funktio, jolla on pääsy luokan yksityiseen alueeseen. Tällaisia funktioita tarvitaan määriteltäessä luokkien välistä yhteistä rajapintaa tilanteessa, jossa olisi määriteltävä sama metodi useampiin eri Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi luokkiin. Ystäväfunktio on siis luokan ulkopuolinen funktio, jota ei kutsuta luokan jäsenfunktiona. Kokonainen luokkakin voidaan määritellä toisen luokan ystäväksi: tällöin ystäväluokan jäsenfunktiot voivat käsitellä luokan yksityisiä osia kuten omiaan. Ystävyys ei ole automaattisesti molemminpuolista. Jos halutaan antaa luokalle pääsy sen ystäväluokan yksityisiin osiin, on luokka määriteltävä ystäväksi toiselle luokalle. Sisäkkäisten luokkien tapauksessa on useimmiten syytä määritellä ulompi luokka sisemmän luokan ystäväksi, oletusarvoisesti ulommalla luokalla ei ole pääsyä sisemmän luokan yksityisiin osiin. Suojattujen osien näkyminen liittyy periytymiseen; tähän palataan olio-ohjelmointia koskevassa osassa. Jos jäsenmuuttujat määritellään staattisiksi, ne luodaan jo käännösvaiheessa ja niiden elinaika päättyy vasta ohjelman loputtua. Luokan staattisten jäsenmuuttujien näkyvyyttä voidaan säädellä samoilla määreillä kuin ei-staattisten muuttujienkin. Luokan ulkopuolella niihin voidaan viitata (silloin, kun se näkyvyyden kannalta on sallittua) luokan nimen ja näkyvyysoperaattorin (::) avulla. Myös funktiot voidaan määritellä staattisiksi, jolloin ne voivat käsitellä vain staattisia tietotyyppejä. Tällaisia funktioita voidaan kutsua, vaikka luokasta ei olisikaan luotu instanssia. Staattisten funktioiden näkyvyys määräytyy kuten muidenkin jäsenfunktioiden. Luokan staattiset tietotyypit tulee alustaa luokan ulkopuolella, jotta niitä voidaan käyttää luokan funktioissa. C++ -kielen funktioiden joukossa on kaksi erikoisasemassa olevaa funktiotyyppiä: muodostimet (konstruktorit, constructors) ja hajottimet (destruktorit, destructors). Muodostinta kutsutaan olion luomisen ja hajotinta olion tuhoamisen yhteydessä. Muodostimen nimi on aina sama kuin luokan nimi eikä sillä ole paluuarvoa; muodostinfunktioita voi olla useita (ts. niitä voidaan ylikuormittaa) jolloin olion luomisen yhteydessä parametrilistan perusteella päätellään, mitä muodostinta kutsutaan. Hajotin on sen sijaan yksikäsitteinen ja sen muoto on ~LuokkaNimi() Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi Hajotinta kutsutaan automaattisesti, kun olio tuhotaan. Yleensä hajottimeen on syytä kirjoittaa vapautusoperaatiot dynaamisesti varatulle muistille jne. Seuraavassa esimerkissä on toteutettu pinorakenne kokonaisluvuille: class Pino { private: // Jäsenmuuttujat int *pino_osoitin; int koko; int huippu; public: // Muodostin Pino (int pinonkoko) { pino_osoitin = new int [pinonkoko]; koko = pinonkoko; huippu = -1; } // Hajotin ~Pino () { delete [] pino_osoitin; } // Muut metodit void push(int luku) { /* funktion runko …*/ … } int pop( ) { /* funktion runko …*/ } int top( ) { /* funktion runko …*/ } int empty( ) { /* funktion runko …*/ } }; Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi Tässä versiossa jäsenfunktioiden koodit on kirjoitettu esittelyn yhteyteen. Toinen tapa kirjoittaa luokan määrittelyyn ainoastaan funktioiden otsikot ja kirjoittaa funktioiden rungot erikseen seuraavasti: // Tiedostossa Pino.h class Pino { private: int *pino_osoitin; int koko; int huippu; public: Pino (int pinonkoko); ~Pino (); void push(int luku); int pop( ); int top( ); int empty( ); }; // Tiedostossa Pino.cpp #include "Pino.h" Pino::Pino (int pinonkoko) { pino_osoitin = new int [pinonkoko]; koko = pinonkoko; huippu = -1; } Pino::~Pino () { delete [] pino_osoitin; } void Pino::push(int luku) { /*funktion runko …*/} int Pino::pop( ) { /* funktion runko …*/ } int Pino::top( ) { /* funktion runko …*/ } int Pino::empty( ) { /* funktion runko …*/ } Usein koodi kirjoitetaan niin, että hyvin lyhyet funktiot kirjoitetaan kokonaan määrittelyyn ja pitempien funktioiden toteutukset erilliseen kooditiedostoon. Sekä muodostinta että hajotinta voidaan periaatteessa myös eksplisiittisesti kutsua koodissa, mutta useimmiten siihen ei ole mitään syytä. C++ -ohjelmassa olioille voidaan varata muisti kaikilla samoilla tavoilla kuin muuttujillekin. Oliot voivat siis olla staattisia Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi muuttujia (myös näkyvyydeltään globaaleja), pinodynaamisia tai kekodynaamisia, esimerkiksi // Globaali olio Pino pino_glob; int main() { // Pinodynaaminen olio Pino pino_stack; pino_stack.push(12); int luku=pino_stack.pop(); // Kekodynaaminen olio Pino *pino_heap = new Pino(); pino_heap->push(12); luku=pino_heap->pop(); … delete pino_heap; } Kekomuistista varattavat oliot on ohjelmoijan itsensä vapautettava deleteoperaattorilla. 1.3. Abstraktit tietotyypit Javassa Javan luokkamalli muistuttaa C++ -kielen mallia, mutta joitakin merkittäviä poikkeuksia on. Javan oliot ovat aina kekodynaamisia ja ne luodaan new-operaattorilla, pinosta ei koskaan varata olioita. Javassa ohjelmoija ei itse voi suoraan vapauttaa muistia, vaan roskien keruu huolehtii tästä. Ohjelmoija voi kuitenkin vaikuttaa siihen esimerkiksi asettamalla olioviitteen arvoon null, jolloin roskien kerääjä aikanaan vapauttaa varatun muistin, ellei olioon ole muita viitteitä. Roskienkerääjän voi myös ohjelmallisesti aktivoida (ks. [Arn], kappale 15.2). Javassa voidaan tehdä olion tuhoamisen yhteydessä tarpeelliset operaatiot ylikirjoittamalla finalize() -metodi, jonka kaikki oliot perivät Object-luokalta. Java-koodissa tätä tarvitsee vain harvoin kuitenkaan tehdä, koska muistivuotoja ei tapahdu. (Javan luokkiin voi tutustua esimerkiksi teoksen [Arn] luvusta 2). Javan olioihin viitataan niiden nimillä, jotka ovat viittaustyypin (referenssityypin) muuttujia. Javassa ei käytetä osoitinoperaattoreita, mikä merkitsee, ettei Javan Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi primitiivisten tietotyyppien muistiosoitteita voida käsitellä, koska ne ovat arvotietotyyppejä. Olioiden jäseniin viitataan pistenotaatiolla. Javassa ei ole luokan ulkopuolista elämää: kaikkien metodien ja tietotyyppien on kuuluttava johonkin luokkaan. Myös metodien määrittely toteutetaan samassa luokassa, jossa metodi esitellään (paitsi abstraktien metodien tapauksessa; tähän palataan olio-ohjelmoinnin yhteydessä). Javan luokissa voidaan C++-kielen tapaan määritellä sekä staattisia että eistaattisia jäsenmuuttujia ja -metodeja. Luokan staattiset jäsenet allokoidaan käännösvaiheessa. Erotuksena C++-kieleen staattiset jäsenmuuttujat voidaan myös alustaa luokassa, jolloin niitä voidaan suoraan käyttää luokan metodeissa. Luokan ei-staattisia jäseniä käytetään luokan ilmentymän kautta. Jokaisella ilmentymällä on omat kopionsa luokassa määritellyistä ei-staattisista jäsenmuuttujista. Staattisista jäsenmuuttujista, luokkamuuttujista (class variable), on kustakin vain yksi kopio, jota kaikki ilmentymät käyttävät. Luokan jäsenmetodit, luokkametodit (class method) allokoidaan myös staattisesti. Myös luokkametodeja voidaan käyttää ilman dynaamisesti luotua oliota luokan nimen ja pisteoperaattoria avulla. Javan tiedonkätkentä perustasolla on hieman monimutkaisempi kuin C++ -kielessä. Javassa voidaan käyttää samoja näkyvyysmääreitä (public, protected ja private) kuin C++ -kielessäkin ja niiden merkityskin on sama. Java-luokassa voidaan kuitenkin jättää näkyvyysmääre myös antamatta, jolloin käytetään oletusnäkyvyytenä pakkausnäkyvyyttä (package scope), joka poikkeaa kolmen luetellun määreen näkyvyydestä ([Arn], kappale 10.2). Javassa voidaan nimittäin luokkia koota pakkauksiksi (ja juuri näistä liitetään ohjelmaan luokkia kaikille Java-ohjelmoijille tutulla import-lauseella). Oletusnäkyvyyden jäsenet näkyvät pakkauksen sisällä tai, ellei pakkausta ole määritelty, samassa hakemistossa. Tällä mekanismilla on korvattu C++ kielen ystäväluokat ja -funktiot, joita Javassa ei ole toteutettu. Mikäli luokan halutaan näkyvän pakkauksen ulkopuolella, se on määriteltävä public-tyyppiseksi ja se on sijoitettava tiedostoon, jonka nimi on sama kuin luokan nimi (lisättynä java tarkenteella). Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi 2. Olio-ohjelmointi 1980 -luvun aikana havaittiin, että ohjelmistokehityksessä tuottavuutta voidaan parhaiten lisätä ohjelmien uudelleenkäytöllä. Abstraktit tietotyypit ovat ominaisuuksiensa (datan kapselointi ja tiedon kätkentä) ansiosta sopivia yksiköitä kierrättämiseen. Yleensä olemassaolevat tietotyypit eivät kuitenkaan täysin sovi uuteen käyttökohteeseensa, vaan niihin olisi tehtävä pieniä muutoksia ja lisäyksiä. Tämä voi olla työläs ja hankala operaatio, eivätkä abstraktit tietotyypit sinänsä muodosta välttämättä hierarkkista rakennetta, jota saatetaan tarvita ohjelman rakentamisessa. Tämä ongelma voidaan ratkaista, mikäli abstraktit tietotyypit voivat periä aiemman tietotyypin ominaisuudet ja datan. Näin saadaan aiempi koodi uudelleen käytettyä ja sen ominaisuuksia voidaan muokata sekä lisätä uuteen tietotyyppiin uusia ominaisuuksia. Tämä on olio-ohjelmoinnin perusta. Oliokielten abstrakteja tietotyyppejä kutsutaan luokiksi ja luokkien instansseja olioiksi. Olio-ohjelmointi keksittiin jo 1960 -luvulla, mutta yleistyi vasta 1980 -luvulla ja kohosi sittemmin johtavaksi ohjelmointiparadigmaksi. Oliotuki on lisätty myöhemmin moniin sellaisiin kieliin, joista se on alun perin puuttunut. Olio-ohjelmoinnin opettelu vaatii prosessisuuntautuneen ohjelmoinnin koulukunnan edustajalta muutosta ajattelutavassa. Prosessisuuntautuneessa ajattelumallissa ohjelman aktiiviset operaatiot (sijoituslauseet ja aliohjelmat) käsittelevät passiivisia tietoalkioita. Olio-ohjelmoinnissa tietoalkiot eli oliot ovat itse aktiivisia ja ohjelman toiminta koostuu olioiden toisilleen lähettämistä viesteistä ja niihin saaduista vastauksista. Viestit voivat muuttaa olion tilaa, millä tarkoitetaan olion jäsenmuuttujien kulloisiakin arvoja. Olio-ohjelmoinnin ihanteisiin kuuluu, että toiset oliot eivät voi yleensä muuttaa tai lukea suoraan olion tilaa, vaan siihen käytetään erillisiä saanti- ja asetusmetodeja. Tässä esityksessä luokkien jäsenfunktioita kutsutaan metodeiksi; terminologiassa saattaa esiintyä vaihtelua eri lähteissä, esimerkiksi Stroustrup ([Strou], s. 310) tarkoittaa metodilla luokan virtuaalista jäsenfunktiota. Tässä esitellään aluksi olio-ohjelmoinnin peruskäsitteitä, kuten periytyvyys, monimuotoisuus ja dynaaminen sidonta, minkä jälkeen tarkastellaan, kuinka kyseiset Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi ominaisuudet on toteutettu C++- ja Java -kielissä. Näistä poikkeavia oliokieliä on runsaasti, mutta niihin ei tässä yhteydessä perehdytä. Sebestan ([Seb]) luvussa 12 on jonkin verran käsitelty myös muita oliokieliä. Myös Harsu esittelee kirjansa [Har] luvussa 10 joitakin olio-ohjelmointikieliä. 2.1. Periytyvyys Periytyvyys (inheritance) on olio-ohjelmoinnin keskeisin käsite, kuten jo aiemmin on käynyt ilmi; periytyvyys yhdessä koostamisen kanssa kuvaa olio-ohjelmoinnin abstraktien tietotyyppien eli luokkien väliset suhteet. Luokkaa, joka peritään, kutsutaan usein kantaluokaksi (base class) tai yliluokaksi (superclass) ja perivää luokkaa sen aliluokaksi (subclass) tai johdetuksi luokaksi (derived class). Joskus myös perittävää luokkaa sanotaan vanhemmaksi ja perivää luokkaa sen lapseksi. Periytymisessä kantaluokan olioiden käyttäytyminen siirtyy osaksi aliluokan olioiden käyttäytymistä. Aliluokka voi sekä laajentaa (extend) että erikoistaa (specialize) kantaluokan käyttäytymistä. Laajentamisella tarkoitetaan uusien jäsenmuuttujien ja metodien liittämistä aliluokkaan. Erikoistaminen tarkoittaa, että aliluokassa korvataan perittyjä määrittelyjä luokan omilla määrittelyillä (yleensä tämä tarkoittaa kantaluokan metodin korvaamista aliluokan omalla versiolla). Tyypin T alityypiksi kutsutaan sellaista tietotyyppiä S, johon voidaan soveltaa mitä tahansa tyyppiin T sovellettavaa operaatiota. Tällöin tyyppiä T sanotaan tyypin S ylityypiksi. Tärkeä kysymys oliokielessä on, ovatko aliluokat kantaluokan alityyppejä. Mikäli aliluokalle sallitaan periytymisessä ainoastaan yllä mainitut laajentaminen ja erikoistaminen, aliluokka on kantaluokan alityyppi ja siihen soveltuu ns. Alityyppiperiaate: Alityypin olio voi ohjelmassa esiintyä missä tahansa sen ylityypin odotetaan esiintyvän. Tämä on yleensä voimassa olio-ohjelmointikielissä. Joissakin kielissä voidaan kuitenkin myös rajoittaa periytyviä ominaisuuksia, jolloin aliluokka ei ole enää kantaluokan alityyppi. Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi Näkyvyysmääreet säätelevät, mitkä kantaluokan ominaisuudet ovat aliluokan käytettävissä. Usein julkisten jäsenten lisäksi vain suojatut jäsenet ovat näkyvissä aliluokassa. Näkyvyysmääreitä on käsitelty yleisesti aiemmin; tuonnempana tutustutaan näkyvyysmääreiden käyttöön C++ ja Java -kielissä. Mikäli luokalla voi olla monta kantaluokkaa, sanotaan että ohjelmointikieli tukee moniperiytymistä (multiple inheritance). Jos moniperiytymistä ei sallita, luokkahierarkia voidaan esittää puurakenteena, moniperiytymisen tapauksessa hierarkia muodostaa verkon. C++ tukee moniperiytymistä, Javassa peritään aina ainoastaan yksi luokka. Jos aliluokassa määritellään jäsenmuuttuja, jonka nimi on sama kuin periytymisen yhteydessä saadulla muuttujalla, piilottaa määrittely yliluokan vastaavan muuttujan. Aliluokan metodi, jolla on sama otsikko kuin kantaluokan metodilla, määrittelee uudelleen (override) perityn metodin. Yliluokan piilotettuihin muuttujiin ja uudelleenmääriteltyihin metodeihin voidaan yleensä viitata yliluokan nimen ja tarkoitukseen määrätyn operaattorin avulla. Tyypillisiä uudelleenmääriteltäviä metodeja ovat olion tulostamiseen liittyvät metodit. 2.2. Monimuotoisuus ja dynaaminen sidonta Monimuotoisuus (polymorfismi, polymorphism) tarkoittaa saman operaattorin tai funktion sitomista erilaisiin toteutuksiin tilanteesta riippuen. Olio-ohjelmoinnissa tämä tarkoittaa yleensä monimuotoisuutta, joka saavutetaan viestin dynaamisella sidonnalla metodin määrittelyyn. Tämä mekanismi yhdessä edellä mainitun alityyppiperiaatteen kanssa sallii muodostaa monimuotoisia muuttujia, ts. muuttujia joiden tyyppi on kantaluokka, mutta sen muuttujat voivat olla jonkin aliluokan olioita. Mikäli aliluokan metodi määrittelee uudelleen kantaluokassa olevan metodin ja monimuotoisen muuttujan metodia kutsutaan, kutsu sidotaan oikean luokan metodiin. Dynaaminen sidonta on varsin hyödyllinen ominaisuus ohjelmiston ylläpidon kannalta. Kun muodostetaan uusia luokkia, kantaluokkien koodiin ei tarvitse tehdä perustoiminnoissa muutoksia. Esimerkiksi olion tulostaminen tapahtuu dynaamisen Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi sidonnan avulla aina oikein, vaikka perusluokka ei sisältäisikään tulostusmahdollisuutta uudelle luokalle, koska ohjelmassa kutsutaan oikean luokan tulostusmetodia, vaikka olioon viittaava muuttuja olisi kantaluokan tyyppinen. Dynaaminen sidonta tapahtuu luonnollisesti ohjelman suorituksen aikana (muuten ei mahdollista tietää, minkä tyyppiseen olioon muuttuja viittaa), joten se on tehottomampaa kuin staattinen, käännösaikana tapahtuva sidonta. Tästä syystä joissakin oliokielissä kaikki sidonta ei automaattisesti ole dynaamista, vaan ohjelmoija voi itse säätää, mitkä metodit sidotaan dynaamisesti. Edelleen, dynaaminen sidonta asettaa haasteita kielen tyypintarkistukselle, sillä monimuotoinen muuttuja voi osoittaa tietotyyppiin, joka on tietotyypin määrittelemän tyypin alityyppi, joten on päätettävä, missä vaiheessa dynaamisesti sidottavan metodin tyypintarkistus tehdään. Mikäli kieli on vahvasti tyypitetty, tyypintarkistus olisi tehtävä staattisesti. Metodikutsujen yhteydessä tarvitaan kahdentyyppistä tarkistusta: parametrien tyyppien vastaavuus parametrilistaan ja paluuarvon tyypin vastaavuus odotettuun paluuarvotyyppiin. Toinen mahdollisuus on luopua staattisesta tyypintarkistuksesta ja tarkistaa tyypit dynaamisesti metodikutsun yhteydessä. Yleensä oliohierarkian kantaluokka kannattaa suunnitella niin, että se sisältää täsmälleen kaikkien aliluokkien tarvitsemat operaatiot. Joissakin tapauksissa tällainen kantaluokka on niin yleistä muotoa, ettei siitä ole tarkoituksenmukaista muodostaa olioita. Tällöin luokasta voidaan tehdä abstrakti luokka, josta ei voi luoda instansseja. Abstrakti luokka voi sisältää abstrakteja metodeja, joille on määritelty ainoastaan prototyyppi, mutta ei lainkaan runkoa. (Joissakin esityksissä abstrakteja metodeja kutsutaan [puhtaasti] virtuaalisiksi metodeiksi.) Tyypillinen esimerkki tämän kaltaisesta tapauksesta olisi ohjelma, jossa käsiteltäisiin yksinkertaisia tasokuvioita. Kaikilla tasokuvioilla on pinta-ala ja kaikki tarvitsevat metodin, joka piirtää kuvion näytölle. Muut ominaisuudet saattavat vaihdella kuvion tyypistä riippuen; tällöin voisi olla tarkoituksenmukaista määritellä abstrakti kantaluokka, jossa on jäsenmuuttuja kuvaamaan pinta-alaa ja metodi piirtämiselle. Kutakin tasokuviotyyppiä kohti kirjoitettaisiin oma luokka, joka perii kantaluokan ja määrittelee uudelleen piirtometodin. Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi 2.3. Oliokielten suunnittelukysymyksiä Edellä esitettyjen asioiden yhteenvetona voidaan todeta, että olio-ohjelmointikielen suunnittelijan on otettava kantaa ainakin seuraaviin kysymyksiin. 1. Onko kielessä muita tietotyyppejä kuin olioita? Esimerkiksi Smalltalk on puhtaasti oliokieli, kun taas Javassa ja C++:ssa esiintyy perustietotyyppejä, joiden ilmentymät eivät ole olioita. 2. Ovatko aliluokat aina perittyjen luokkien alityyppejä? 3. Sallitaanko moniperiytyminen? 4. Miten hoidetaan olioiden muistinvaraaminen ja -vapauttaminen? Esimerkiksi Javassa olio voidaan varata ainoastaan kekomuistista, kun C++:ssa olio ei eroa muista muuttujista tältäkään osin. 5. Sidotaanko metodit aina dynaamisesti vai onko staattinen sidonta mahdollista? 6. Sallitaanko sisäkkäisiä luokkia? 3. Olio-ohjelmointi C++ ja Java -kielissä Tässä osassa tarkastellaan edellä mainittuja käsitteitä konkreettisesti tutkimalla, miten ne ilmenevät C++- ja Java-kielissä. Vaikka oliomalli molemmissa kielissä on perustaltaan samankaltainen, on kielten välillä myös huomattavia eroja. Muihin oliokieliin lukija voi perehtyä esimerkiksi Loudenin teoksesta ([Lou], luku 9) ja Sebestan kirjasta ([Seb], luku 12). 3.1. C++ Jo abstraktien tietotyyppien yhteydessä esiteltiin C++ -kielen luokkamalli. Tässä perehdytään periytymisen ja myöhäisen sidonnan toteutukseen C++:ssa. Asiaa on mahdotonta käsitellä kattavasti tässä yhteydessä, kiinnostunut lukija voi perehtyä C++:n oliomalliin syvällisemmin esimerkiksi Stroustrupin ([Strou]) kirjan osasta II. Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi C++-kielessä periytyminen kirjoitetaan seuraavasti: class derived_class_name: acces_mode base_class_name { <data members> <member functions> }; Tässä access_mode voi olla jokin näkyvyysmääre, ts. public, protected tai private, joista yleisin käytännön ohjelmoinnissa on public. C++-kielen näkyvyysmääreillä säädellään, miten kantaluokan jäsenet näkyvät aliluokassa. Normaalisti (access_mode on public) aliluokassa näkyvät kaikki public- ja protected-tyyppiset muuttujat ja funktiot ja niiden näkyvyysmääre on sama kuin kantaluokassa. Sen sijaan private-tyyppisiin jäseniin aliluokalla ei ole pääsyä. Tällöin aliluokka on kantaluokan alityyppi ja tämän tyypin ilmentymä voi esiintyä missä tahansa yhteydessä, jossa kantaluokkaa käytetään. Toisaalta private-näkyvyysmäärettä käyttämällä ainoastaan aliluokan jäsenet ja ystävät saavat pääsyn kantaluokan public- ja protected-tyyppisiin jäseniin. Käytettäessä protected-määrettä, lisäksi aliluokasta perityt luokat ja niiden ystävät saavat pääsyn kantaluokan public- ja protected -tyyppisiin jäseniin. Kummassakaan tapauksessa aliluokka ei ole kantaluokan alityyppi. Esimerkki. Olkoon ohjelmassa määritelty luokat class Kanta { public: void kanta_julkinen_funktio(){ cout << "Olen kantaluokan julkinen funktio" << endl; } }; class AliLuokka:public Kanta { }; ja funktio Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi void kutsuja(Kanta k) { k.kanta_julkinen_funktio(); } Nyt ohjelma AliLuokka ak; ak.kanta_julkinen_funktio(); kääntyy, koska AliLuokka on Kanta -luokan alityyppi ja sen olion kautta funktion kanta_julkinen_funktio näkyvyys on public. Näin sitä voidaan kutsua. Jos kuitenkin vaihdetaan aliluokka muotoon class AliLuokka:private Kanta { }; tai class AliLuokka:protected Kanta { }; koodi ei käänny, koska AliLuokka-luokassa perityn funktion näkyvyys ei ole public. Näin ollen AliLuokka-luokka ei enää olekaan Kanta-luokan alityyppi. Samasta syystä koodi AliLuokka ak; kutsuja(ak); kääntyy ensimmäisessä tapauksessa, mutta kahdessa jälkimmäisessä koodi ei ole sallittua, vaikka jätettäisiin funktion kutsuja runko tyhjäksikin (AliLuokka -luokan oliota ei voi muuttaa Kanta -luokan olioksi). C++ -kielessä moniperiytyminen on mahdollinen, mistä johtuu, että luokkahierarkia on verkkomainen. Moniperiytyminen aiheuttaa myös sen, että aliluokassa voi tulla nimitörmäyksiä, kun samanniminen jäsen peritään useammasta luokasta. Samoin Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi dynaamisen sidonnan toteuttaminen on hieman hankalampaa kielessä, jossa moniperiytyminen on sallittu. C++ -kielessä ei noudateta automaattisesti dynaamista sidontaa, vaan kun esimerkiksi laaditaan luokkarakenne: class Kanta { public: void kanta_funktio() { cout << "Olen Kanta -luokan funktio" << endl; } }; class AliKanta: public Kanta { public: void kanta_funktio() { cout << "Olen AliKanta -luokan funktio" << endl; } }; class Alimmainen:public AliKanta { public: void kanta_funktio() { cout << "Olen Alimmainen -luokan funktio" << endl; } }; ja kirjoitetaan funktio void kutsu_funktio(Kanta *k) { k->kanta_funktio(); } koodi Alimmainen alin; kutsu_funktio(&alin); Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi tulostaa Olen Kanta -luokan funktio koska funktiokutsussa olion osoite välittyy parametrina osoittimena Kanta -luokan olioon ja funktiota ei sidota dynaamisesti oikeaan luokkaan. Sama koskee viitetyypin muuttujia, ts. jos funktio on void kutsu_funktio(Kanta &k) { k.kanta_funktio(); } ja ohjelmakoodi Alimmainen alin; kutsu_funktio(alin); tulos on sama. Jos sen sijaan muutettaisiin Kanta-luokan funktio virtuaaliseksi seuraavasti: class Kanta { public: virtual void kanta_funktio() { cout << "Olen Kanta -luokan funktio" << endl; } }; tulostuisi Olen Alimmainen -luokan funktio koska virtuaalisiksi määritellyt metodit sidotaan dynaamisesti. C++:ssa kantaluokan virtuaaliset metodit periytyvät virtuaalisina koko hierarkian läpi, ts. ne ovat virtuaalisia kaikissa aliluokissa, näiden aliluokissa jne., vaikka aliluokissa niitä ei erikseen määriteltäisi virtuaalisiksi. Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi Joskus kantaluokasta halutaan tehdä abstrakti luokka. Tämä onnistuu C++ -kielessä sisällyttämällä luokkaan vähintään yksi puhtaasti virtuaalinen funktio, jolle ei anneta runkoa vaan merkitään se nollaksi. Esimerkiksi yllä Kanta -luokan funktio voitaisiin määritellä class Kanta { public: virtual void kanta_funktio() = 0; }; Tällöin luokasta tulee abstrakti, eikä siitä voi luoda oliota. Mikäli luokan aliluokista halutaan konkreettisia, niissä on aina ylikirjoitettava puhtaasti virtuaaliset metodit. 3.2. Java Lopuksi tarkastellaan periytymistä ja dynaamista sidontaa Java-kielessä ja vertaillaan sitä C++-kieleen. Vaikka Javakaan ei ole puhtaasti oliokieli (primitiiviset tietotyypit eivät ole olioita), se on sitä voimakkaammin kuin C++, joka sisältää esimerkiksi luetellut tietotyypit, jotka eivät ole olioita. Samoin C++:n taulukot eivät ole olioita. Javassa kaikki muu kuin primitiiviset tietotyypit perustuu olioihin. Edelleen, Javan kaikki luokat perivät Object -luokan, C++-luokka ei oletusarvoisesti peri mitään. Näin ollen C++ -kielessä on mahdollista kirjoittaa luokka, jolla ei ole lainkaan jäseniä, mikä ei onnistu Javassa. (Ks. [Arn], luku 3) Javassa jokainen luokka perii (extends) täsmälleen yhden luokan (ellei luokan ilmoiteta perivän toista luokkaa, se perii Object -luokan). Näin ollen Javan luokkahierarkia on puumainen. Moniperiytymisen puutetta korvaamaan on Javassa otettu rajapinta (interface). Rajapinta on eräänlainen puhtaasti abstrakti luokka, joka sisältää ainoastaan metodien esittelyt. Jokainen konkreettinen luokka, joka toteuttaa (implements) rajapinnan, on velvollinen toteuttamaan rajapinnan kaikki metodit. Abstrakti luokka voi sen sijaan jättää rajapinnan metodin toteuttamatta. Luokka voi toteuttaa rajoittamattoman määrän rajapintoja. Rajapinnat voivat myös periä toisia rajapintoja. Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi Javassa luokka pitää tarvittaessa erikseen määritellä abstraktiksi ja tällainen luokka voi sisältää abstrakteja metodeja (vaikka luokka voidaan määritellä abstraktiksi siitä huolimatta, että sillä ei ole abstrakteja metodeja). Abstrakti luokka voidaan kirjoittaa esimerkiksi seuraavasti: public abstract class MessagePasser { public abstract void send(Object o); public void send(int i) { send(new Integer(i)); } public void send(double d) { send(new Double(d)); } public abstract Object receive(); public int receiveInt { return(((Integer)this.receive()).intValue()); } public double receiveDbl { return(((Double)this.receive()).doubleValue()); } } Mikäli halutaan muodostaa konkreettinen luokka, joka perii ylläolevan luokan, on metodit send(Object o) ja receive() toteutettava. Tällöin saadaan automaattisesti käyttöön metodin send muut versiot ja metodit receiveInt() sekä receiveDbl() . Javan periytymisessä ei voida näkyvyyttä säätää kuten C++:ssa. Javan periytyminen vastaa C++:n public-tyyppistä periytymistä. Siis aliluokka perii kaikki public- ja protected -tyyppiset jäsenet ja niiden näkyvyydet säilyvät samana aliluokassa. Private-tyyppiset jäsenet eivät näy aliluokassa. Tästä seuraa myös se, että aliluokka on aina kantaluokan alityyppi, mikä helpottaa ohjelmointia. Javassa voidaan perimisketju katkaista määrittelemällä luokka final-tyyppiseksi. Tällöin luokkaa ei voi enää periä aliluokkiin. Myös luokan metodi voidaan määritellä final-tyyppiseksi, mikä tarkoittaa sitä, että metodia ei voi enää aliluokissa määritellä uudelleen. Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi Javassa kaikki sidonta on dynaamista, ts. ohjelmoija ei voi itse valita sidonnan tyyppiä kuten C++:ssa. Tämä toisaalta tekee ohjelmoinnin helpommaksi ja luotettavammaksi; koska sidonta toimii aina samalla lailla, ei voi tehdä vahingossa koodia, jossa kutsuttaisiin väärän luokan metodia funktiokutsun seurauksena, mikäli vain tuntee oletussidonnan. Näin ollen class Kanta { public void kanta_funktio() { System.out.println("Olen Kanta -luokan funktio"); } } class AliKanta extends Kanta { public void kanta_funktio() { System.out.println("Olen AliKanta -luokan funktio"); } } class Alimmainen extends AliKanta { public void kanta_funktio() { System.out.println("Olen Alimmainen -luokan funktio"); } } public class DynBind { static void kutsuja(Kanta k) { k.kanta_funktio(); } public static void main(String[] args) { Alimmainen alin = new Alimmainen(); kutsuja(alin); } } tulostaa Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 815338A Ohjelmointikielten periaatteet: Abstraktit tietotyypit ja olio-ohjelmointi Olen Alimmainen -luokan funktio Yhteenvetona voidaan todeta, että vaikka Javan luokkamalli pohjautuukin C++-kielen malliin, sitä on yksinkertaistettu monin tavoin. Moniperiytyminen on poistettu ja korvattu rajapintojen käytöllä. Kaikki luokat perivät yhteisen perusluokan (Object). Edelleen kaikki sidonta on dynaamista. Erityisesti periytymiseen liittyvät näkyvyysmääreet on yksinkertaistettu C++:n vaikeasti hallittavasta järjestelmästä huomattavasti selkeämmäksi ja helppokäyttöisemmäksi mekanismiksi. Myös C++ kielen sallimasta ystäväjärjestelmästä on luovuttu. Näistä ominaisuuksista ainoastaan moniperiytymisen puuttumista voidaan vakavasti arvostella ohjelmoijan työtä hankaloittavana seikkana. Sekä C++ että Java sallivat sisäkkäisten luokkien määrittelyn, mitä jotkut teoreetikot ovat arvostelleet epäonnistuneena ratkaisuna. Lähteet [Arn] Arnold, Ken Gosling, James. The Java Programming Language, Second Edition, Addison-Wesley 1998. [Har] Harsu, Maarit. Ohjelmointikielet, Periaatteet, käsitteet, valintaperusteet, Talentum 2005. [Kur] Kurki-Suonio Reino. Ada-kieli ja ohjelmointikielten yleiset perusteet. MODEEMI ry Tampere 1983. [Lou] Louden, Kenneth C. Programming Languages, Principles and Practice, PWS-KENT 1993. [Seb] Sebesta, Robert W. Concepts of Programming Languages 10th edition, Pearson 2013. [Strou] Stroustrup, Bjarne. The C++ Programming Language, 3rd edition, Murray Hill 1997.