Polymorfismi
osaamistavoitteet
- Ymmärrät polymorfismin perusajatuksen
- Osaat korvata yliluokan metodin aliluokassa sekä estää korvaamisen
final-avainsanalla - Osaat kirjoittaa pienen ohjelman, jossa hyödynnetään polymorfismia
- Tunnistat Object-luokan korvattavia metodeja, kuten
toString()

Polymorfismi viittaa olio-ohjelmoinnissa kykyyn käsitellä erilaisia olioita yhtenäisellä tavalla. Kun metodia kutsutaan, päätös siitä, mikä metodi tosiasiallisesti suoritetaan, tehdään ajon aikana olion todellisen tyypin perusteella. Polymorfismi mahdollistaa joustavan koodin kirjoittamisen, jossa uusia olioita voidaan lisätä ilman, että olemassa olevaa koodia tarvitsee muuttaa.
Polymorfismi jaetaan yleensä kahteen päätyyppiin: (1) käännösaikaiseen polymorfismiin, jota kutsutaan myös dynaamiseksi sidonnaksi (engl. dynamic binding) ja (2) ajon aikaiseen polymorfismiin. Käännösaikaisella polymorfismilla tarkoitetaan Javassa aliohjelman kuormitusta (engl. method overloading). Asiaa on käsitelty Ohjelmointi 1 -kurssilla, emmekä sitä tässä käsittele tarkemmin, mutta lyhyesti: aliohjelman kuormitus tarkoittaa sitä, että aliohjelmalla voi olla useita samannimisiä toteutuksia, jotka eroavat toisistaan parametrien lukumäärän, parametrien tyyppien tai aliohjelman paluuarvon perusteella. Lue lisää Ohjelmointi 1 -kurssin materiaalista. (TODO: Linkki)
Tämä kaikki saattaa kuulostaa hitusen abstraktilta, joten otetaanpa konkreettinen esimerkki!
Metodin korvaaminen ja dynaaminen sidonta
Kuvitellaan tilanne, jossa ohjelmassa on erilaisia soittimia: Kitara, Piano ja Rumpusetti. Haluamme, että soittimia voi soittaa. Yksi mahdollisuus olisi kirjoittaa jokaiselle soittimelle oma metodi soittamista varten, kuten:
Kitara kitara = new Kitara();
kitara.soitaKitaraa();
Piano piano = new Piano();
piano.soitaPianoa();
Rumpusetti rumpusetti = new Rumpusetti();
rumpusetti.soitaRumpuja();
Tämä lähestymistapa ei ole laajennettavissa. Jos yrittäisimme käsitellä soittimia yhtenäisenä joukkona, esimerkiksi listana, joutuisimme tekemään hankalia ja virheherkkiä tyyppitarkastuksia vain saadaksemme selville, mitä soittometodia kutsua. Ratkaisu tähän on löytää yhteinen nimittäjä kaikille soittimille. Sekä kitara että piano ovat loppujen lopuksi Soittimia. Luodaan yliluokka Soitin, joka sisältää toiminnon, jonka jokaisen soittimen pitäisi pystyä tekemään: soita().
huomautus
Soitin-luokka määritellään tässä tavallisena luokkana, mutta se voisi olla myös abstrakti luokka, ja se olisikin tässä tapauksessa luontevaa. Koska abstrakti luokka käsitellään vasta luvussa 3.3 Abstraktit luokat, määrittelemme Soittimen tässä tavallisena luokkana.
public class Soitin {
// Kaikilla soittimilla on soita()-metodi
public void soita() {
IO.println("Tuntematon soitin soi."); // Oletusarvoinen toteutus
}
}
Nyt voimme määritellä Kitara- ja Piano-luokat perimään Soitin-luokan.
public class Kitara extends Soitin {
// ...
}
public class Piano extends Soitin {
// ...
}
Nyt meillä on kyllä yhtenäinen tapa kutsumista varten, mutta jos kutsuisimme nyt Kitara- tai Piano-olion soita()-metodia, ne molemmat suorittaisivat yliluokan (Soitin) oletustoteutuksen: "Tuntematon soitin soi." Tämä ei riitä! Haluamme, että kukin soitin soi itselleen ominaisella tavalla. Tätä varten aliluokassa voidaan korvata (engl. override) yliluokan soita()-metodi omalla, spesifillä toteutuksellaan.
public class Kitara extends Soitin {
// Korvataan yliluokan Soitin.soita()
@Override
public void soita() {
IO.println("Kitara soi ja kieliä näppäillään.");
}
}
public class Piano extends Soitin {
// Korvataan yliluokan Soitin.soita()
@Override
public void soita() {
IO.println("Piano soi ja koskettimia painellaan.");
}
}
Perintä antoi meille yhteisen tyypin (Soitin). Metodin korvaaminen antoi meille mahdollisuuden toteuttaa toiminto olioittain. Nyt nämä kaksi mekanismia yhdessä mahdollistavat polymorfismin (nk. monimuotoisuuden). Kun kutsumme metodia yliluokan tyyppiä käyttäen, ohjelma valitsee automaattisesti oikean, korvatun metodin sen perusteella, mikä on olion todellinen tyyppi suoritusajankohdalla.
Tämä mahdollistaa yhtenäisen käsittelyn, jota lähdimme hakemaan:
void main() {
ArrayList<Soitin> orkesteri = new ArrayList<>();
orkesteri.add(new Kitara());
orkesteri.add(new Piano());
// Rumpusetti voitaisiin toteuttaa samoin
// orkesteri.add(new Rumpusetti());
// Kutsumme kaikille samaa soita()-metodia...
for (Soitin soitin : orkesteri) {
soitin.soita();
}
}
TODO: Lisää tähän väliin UML-kaavio.
is-a-suhde
Perintäsuhteesta käytetään myös englanninkielistä termiä is-a-suhde. Voimmekin
sanoa, Piano on Soitin ja Kitara on Soitin -- nimen omaan näin päin.
Palataan vielä hetkeksi edelliseen opintotietojärjestelmä-esimerkkiimme,
siinäkin voimme sanoa että Opiskelija on Henkilo, Opettaja on
Henkilo ja Sihteeri on Henkilo. Edelleen, myös TutkintoOpiskelija on
Henkilo, koska se perii Opiskelija-luokan, joka puolestaan perii
Henkilo-luokan.
Kuten edellä opimme, polymorfismin ansiosta voimme käsitellä Opiskelija,
Opettaja ja Sihteeri-olioita koodissamme Henkilo-luokan olioina. Lisätään
kaikki tekemämme oliot Henkilo-taulukkoon:
Opiskelija opiskelija = new Opiskelija();
Opettaja opettaja = new Opettaja();
Sihteeri sihteeri = new Sihteeri();
Henkilo[] henkilot = {opiskelija, opettaja, sihteeri};
metodit kirjaudu() ja kirjauduUlos(). Nyt siis kaikki henkilöt perivät nämä
Jotta esimerkkimme olisi vähän mielekkäämpi, lisätään vielä Henkilo-luokkaan
metodit.
class Henkilo {
// HIGHLIGHT_GREEN_BEGIN
private boolean kirjautunut;
// HIGHLIGHT_GREEN_END
public Henkilo(String nimi) {
// ...
// HIGHLIGHT_GREEN_BEGIN
this.kirjautunut = false;
// HIGHLIGHT_GREEN_END
// ..
}
// HIGHLIGHT_GREEN_BEGIN
void kirjaudu() {
this.kirjautunut = true;
IO.println(this.getNimi() + " kirjautui sisään.");
}
void kirjauduUlos() {
this.kirjautunut = false;
IO.println(this.getNimi() + " kirjautui ulos.");
}
// HIGHLIGHT_GREEN_END
}
Voimme nyt kutsua vaikkapa kirjauduUlos()-metodia kaikille henkilot-taulukon
olioille ilman, että meidän tarvitsee tietää tarkasti, minkä tyyppisiä olioita
taulukossa on:
for (Henkilo henkilo : henkilot) {
henkilo.kirjauduUlos();
}
Huomionarvoista on is-a-suhteen suunta; Opettaja ei ole Sihteeri, vaikkakin molemmat perivät Henkilo-luokan.
Lisätään yllä olevaan Opiskelija-esimerkkimme attribuutti boolean opintoOikeusVoimassa, joka ilmaisee, onko opiskelijalla voimassa oleva opinto-oikeus. Jos opinto-oikeus ei ole voimassa, opiskelija ei voi kirjautua järjestelmään. Korvataan kirjaudu()-metodi Opiskelija-luokassa tarkistamaan tämä ehto ennen kirjautumista.
class Opiskelija extends Henkilo {
// ...
boolean opintoOikeusVoimassa;
@Override
void kirjaudu() {
if (opintoOikeusVoimassa) {
super.kirjaudu(); // Kutsutaan yliluokan kirjaudu-metodia
} else {
IO.println("Opinto-oikeus ei ole voimassa. Et voi kirjautua.");
}
}
}
Muissa Henkilo-luokan aliluokissa, kuten Opettaja ja Sihteeri, kirjaudu()-metodi toimii edelleen alkuperäisellä tavalla, koska niitä ei ole korvattu.
Metodin korvaamiseen liittyy pari sääntöä:
- Korvaaminen koskee aina hierarkiassa lähintä yliluokan metodia.
- Kun aliluokan olion metodia kutsutaan, kutsu viittaa aina hierarkiassa lähimpään korvattuun versioon.
Alla oleva koodi havainnollistaa korvaamista ja kutsujen välittymistä luokkahierarkiassa.
public class KokeillaanKorvaamista {
public static void main(String args[]) {
C c = new C();
c.hei(); // Kutsuu A-luokan hei()-metodia
c.moikka(); // Kutsuu B-luokan moikka()-metodia
c.huhhuh(); // Kutsuu C-luokan huhhuh()-metodia
}
}
Tämän esimerkin UML-kaavio näyttäisi seuraavalta.
Esimerkki: Muoto-luokka
Otetaan vielä yksi esimerkki. Tarkastellaan Muoto-luokkaa, jolla on metodi laskeAla().
public class Muoto {
public double laskeAla() {
return 0.0;
}
}
Huomaamme, että laskeAla()-metodin toteutus on vähän hassu. Tämä johtuu siitä, että ei ole oikeastaan mitään ns. yleistä muotoa, vaan Muoto-luokan edustajan tulee aina olla jokin konkreettinen muoto, kuten suorakulmio tai ympyrä, joilla on omat tavat laskea pinta-ala. Kuten jo Soitin-esimerkissä mainitsimme, palaamme tähän dilemmaan osassa 3.3 Abstraktit luokat.
Tehdään nyt aliluokat Suorakulmio ja Ympyra. Koska näiden muotojen pinta-alat ovat luonnollisesti keskenään erilaisia, tulee kummallakin olla oma toteutus laskeAla()-metodille.
public class Suorakulmio extends Muoto {
private double leveys;
private double korkeus;
public Suorakulmio(double leveys, double korkeus) {
this.leveys = leveys;
this.korkeus = korkeus;
}
@Override
public double laskeAla() {
return leveys * korkeus;
}
}
Nyt voimme kirjoittaa koodia, joka käsittelee Muoto-olioita ilman, että tarvitsee tietää, onko kyseessä Suorakulmio vai Ympyra.
public class Main {
public static void main()
{
Muoto muoto1 = new Ympyra(5);
Muoto muoto2 = new Suorakulmio(5, 7);
IO.println(muoto1.laskeAla());
IO.println(muoto2.laskeAla());
}
}
Miksi polymorfismia tarvitaan?
Polymorfismi mahdollistaa monin tavoin joustavan ja laajennettavan koodin kirjoittamisen. Olio-ohjelmoinnissa polymorfismia tarvitaan erityisesti siksi, että sen avulla voimme tarjota yhtenäisen tavan käsitellä keskenään hyvinkin erilaisia olioita.
Kun useat luokat perivät saman yliluokan (tai toteuttavat saman rajapinnan; paneudumme rajapintoihin luvussa 4.1 Rajapinta), ne voidaan käsitellä yhden yhteisen tyypin kautta. Tämä mahdollistaa sen, että ohjelma voi käsitellä joukkoa erilaisia olioita kuten:
- kaikkia soittimia (
Soitin), kuten kitarat, pianot ja rummut - kaikkia ajoneuvoja (
Ajoneuvo), vaikka ne olisivatkin erilaisia, kuten autoja, polkupyöriä ja lentokoneita - kaikkia eläimiä (
Elain), kuten koiria, kissoja ja lintuja - kaikkia graafiseen käyttöliittymään piirrettäviä komponentteja (
Piirrettava), kuten painikkeita, tekstikenttiä ja kuvia - kaikkia maksutapoja (
Maksutapa), kuten luottokortti, PayPal ja käteinen
Javassa on mahdollista kiertää yhtenäistä käsittelyä tutkimalla instanceof-operaattorin avulla,
onko olio tietyn luokan ilmentymä. Esimerkiksi:
if (soitin instanceof Kitara) {
((Kitara) soitin).soitaKitaraa();
} else if (soitin instanceof Piano) {
((Piano) soitin).soitaPianoa();
}
Tällä kurssilla vältämme instanceof-operaattoria, ellei siihen erikseen
ohjeisteta. On nimittäin niin, että instanceof-operaattorin käyttö tarkoittaa
varsin usein sitä, ettei perintää ja polymorfismia ole hyödynnetty
optimaalisella tavalla, josta seuraa yllä olevan esimerkin mukainen
ehtolause-hässäkkä. Tällöin menetetään olio-ohjelmoinnin keskeinen etu, eli se,
että olioiden erilaiset toteutukset voidaan piilottaa niiden käyttäjiltä.
instanceof-operaattorin käyttö voi olla oikeutettua joissain
erityistilanteissa, kuten
- kun emme hallitse olemassa olevaa luokkahierarkiaa,
- kun koodi toimii rajalla, kuten parsittaessa tietoa ulkoisesta lähteestä, integroiduttaessa toiseen järjestelmään tai työskenneltäessä reflektiolla, tai
- jos vaihtoehto olisi huonompi, kuten monimutkaisen luokkahierarkian tai toisteisen koodin kirjoittaminen.
Esimerkki instanceof-operaattorin käytöstä
Tarkastellaan tilannetta, jossa ohjelma vastaanottaa viestejä (tekstiviesti, kuvaviesti) ulkoisesta järjestelmästä (esim. JSON-rajapinta, verkko, kolmannen osapuolen kirjasto). Viestien luokkia ei voi muuttaa, ja niillä on vain yhteinen ylityyppi.
interface Viesti { }
// Konkreettiset viestityypit (ulkoisesta kirjastosta)
class TekstiViesti implements Viesti {
String teksti;
}
class KuvaViesti implements Viesti {
byte[] data;
}
Ohjelman täytyy käsitellä viestit eri tavoin niiden todellisen ajonaikaisen tyypin perusteella.
void kasittele(Viesti v) {
if (v instanceof TekstiViesti t) {
IO.println("Teksti: " + t.teksti);
} else if (v instanceof KuvaViesti k) {
IO.println("Kuvan koko: " + k.data.length);
} else {
throw new IllegalArgumentException("Tuntematon viestityyppi");
}
}
Tämä on harvoja tilanteita, joissa instanceof on aidosti oikea ratkaisu:
- Luokkahierarkiaa ei voi muuttaa: Viestiluokat tulevat ulkoisesta kirjastosta → niihin ei voi lisätä metodeja.
- Polymorfia ei ole käytettävissä: Ei voida määritellä esimerkiksi metodia
kasittele()rajapintaanViesti. - Käsittely riippuu konkreettisesta tyypistä: Tekstiviesti ja kuvaviesti vaativat luonteeltaan eri logiikan.
- Kyseessä on järjestelmän rajapinta: Tällainen koodi kuuluu tyypillisesti I/O-, integraatio- tai adapterikerrokseen.
Object-luokan metodien korvaaminen
Javassa kaikilla luokilla on yhteinen yliluokka nimeltä Object. Tämä tarkoittaa, että kaikki luokat perivät automaattisesti Object-luokan ominaisuudet ja metodit, ellei toisin määritellä. Object-luokassa on useita hyödyllisiä metodeja, joita voidaan korvata aliluokissa.
Object-luokasta löytyy esimerkiksi toString()-metodi, joka tarjoaa olion merkkijonoesityksen. Oletusarvoisesti metodi palauttaa olion luokan nimen ja sen hajautusarvon, mikä ei välttämättä ole kovin informatiivista. Voimme korvata tämän metodin omassa luokassamme, jotta se palauttaa juuri meidän tarpeisiimme sopivan merkkijonoesityksen.
Tehdään vaikkapa Vektori3D-luokka, joka edustaa kolmiulotteista vektoria. Tehdään pääohjelmassa muutama Vektori3D-olio ja tulostetaan niiden arvot.
public class Main {
public static void main(String[] args) {
Vektori3D v1 = new Vektori3D(1.0, 2.0, 3.0);
Vektori3D v2 = new Vektori3D(4.0, 5.0, 6.0);
IO.println("Vektori 1: (" + v1.getX() + ", " + v1.getY() + ", " + v1.getZ() + ")");
IO.println("Vektori 2: (" + v2.getX() + ", " + v2.getY() + ", " + v2.getZ() + ")");
}
}
Vaikka tulostaminen kyllä toimii, olisi varsin mukavaa, jos voisimme yksinkertaisesti kirjoittaa IO.println("Vektori 1: " + v1); ilman, että meidän tarvitsee erikseen hakea koordinaatteja ja yhdistellä String-olioita toisiinsa. Tätä varten voimme korvata toString()-metodin Vektori3D-luokassa.
public class Main {
public static void main(String[] args) {
Vektori3D v1 = new Vektori3D(1.0, 2.0, 3.0);
Vektori3D v2 = new Vektori3D(4.0, 5.0, 6.0);
IO.println("Vektori 1: " + v1);
IO.println("Vektori 2: " + v2);
}
}
Pääohjelma näyttää nyt huomattavasti siistimmältä.
Tutki omatoimisesti muita Object-luokan metodeja Javan dokumentaatiosta.
Perimisen tai korvaamisen estäminen (final-avainsana)
Luokan periminen tai metodin korvaaminen voidaan estää käyttämällä final-avainsanaa. Kun luokka on merkitty final-avainsanalla, sitä ei voi periä. Vastaavasti, kun metodi on merkitty final-avainsanalla, sitä ei voi korvata aliluokassa.
Ehkä hieman hämäävästi final-avainsanaa voidaan käyttää myös muuttujien yhteydessä, jolloin se tarkoittaa, että muuttujan arvoa ei voi muuttaa sen alustamisen jälkeen. Tällä ei ole kuitenkaan tekemistä perinnän kanssa.
Tehtävät
EDIT 29.1.2026: Kiitos palautteestanne. Poistin selityksen puhelimen tietojen
tulostamisesta. Tämän tehtävän tavoitteena on harjoitella toString()-metodin
ylikirjoittamista. Jos vielä on epäselvyyksiä, niin älkää epäröikö laittaa
palauteboksiin kommenttia tai sähköpostia.
EDIT 29.1.2026: Luokan SahkoAuto nimi muutettu Sahkoauto-muotoon, kuten
TIMissäkin oli.
Luokissa Tuote, Elektroniikka ja Puhelin ylikirjoita metodi toString(),
jossa kutsut ensimmäisenä yliluokan toString()-metodia, ja sen jälkeen yhdistä
merkkijonoon luokan omista attribuuteista tietoja.
Tehtäväsivulla on valmiiksi annettuna pääohjelma, jota voit käyttää luokkiesi testaamiseen.
Avaa tästä, mitä ohjelma voisi esimerkiksi tulostaa.
Tietokone HighPower: 899.0 €
Takuuta laitteessa alunperin: 24 kk
Aifoun42: 888.0 €
Takuuta laitteessa alunperin: 37 kk
Käyttöjärjestelmä: AiOS
Yhteystyyppi: 5G
----------------------------
--- UUSI KAUPAN TUOTE ---
Light Bulb: 67000.0 €
Takuuta laitteessa alunperin: 73 kk
Akun kunto: 100.00
Toimintasäde: 404.00 km
--- KÄYTTÖÖNOTTO JA LATAUS ---
Ladataan autoa Light Bulb...
Ladataan autoa Light Bulb...
Ladataan autoa Light Bulb...
Ladataan autoa Light Bulb...
Ladataan autoa Light Bulb...
--- TILANNE LATAUSTEN JÄLKEEN ---
Light Bulb: 67000.0 €
Takuuta laitteessa alunperin: 73 kk
Akun kunto: 99.50
Toimintasäde: 401.98 km
Laajenna luokkahierarkiaa edelleen. Lisää Sahkoauto-luokka, joka perii
Elektroniikka-luokan.
Lisää luokkaan
- attribuutit
- vakio
TOIMINTASADE_MAX, joka ilmaisee maksimietäisyyden kilometreinä, jonka sähköauto voi kulkea yhdellä latauksella. private double akunKunto(prosentteina; väliltä 0-100)
- vakio
- metodit
lataa(), joka heikentää akun kuntoa 0.1%:lla jokaisella latauskerralla.toString(), joka kutsuu ensin yliluokan metodiatoSTring(), jonka jälkeen tulostaa akun kunnon prosentteina ja sitten toimintasäteen kilometreinä, jonka laskemiseen hyödynnetään kaavaa: (akunkunto / 100 * TOIMINTASADE_MAX).
Tee luokka Ajoneuvo, jolla on attribuutti String merkki ja konstruktori joka
asettaa tämän arvon. Lisää myös metodi liiku(), joka ei tee mitään.
Peri Ajoneuvo-luokasta luokat Auto ja Lentokone. Tee Auto- ja
Lentokone-luokkiin liiku()-metodi, joka ylikirjoittaa Ajoneuvo-luokan
liiku()-metodin. Auto-olio tulostaaa "Auto <merkki> ajaa maantiellä
renkaat vinkuen.", ja Lentokone-olio "Lentokone <merkki> nousee kiitotieltä
ja lentää pilvien päällä.".
Tee pääohjelma, jossa luot kaksi Ajoneuvo-muuttujaa (ei siis Auto- tai
Lentokone-tyyppisiä), ja sijoitat niihin Auto-olion ja Lentokone-olion.
Kutsu kummankin olion liiku()-metodia.
Lisää Auto-luokkaan attribuutti int ajokilometrit. Lisää
Lentokone-luokkaan attribuutti int lentotunnit. Lisää kummallekin luokalle
uusi konstruktori, jossa nämä attribuutit asetetaan. Muokkaa aiempaa
konstruktoria niin, että nämä attribuutit saavat arvon 0.
Muuta liiku()-metodeja siten, että ne kasvattavat näitä arvoja. Auto-luokan
liiku()-metodi kasvattaa ajokilometrit-attribuuttia 10:llä ja
Lentokone-luokan liiku()-metodi kasvattaa lentotunnit-attribuuttia 1:llä.
Ylikirjoita vielä Ajoneuvo-luokassa metodi toString(), joka palauttaa
tekstin "Ajoneuvon <merkki> tiedot: ". Ylikirjoita tämä metodi edelleen
Auto- ja Lentokone-luokissa siten, että ne palauttavat lisäksi
ajokilometrit tai lentotunnit.