TIEP111 Ohjelmointi 2
Tämä on Jyväskylän yliopiston järjestämän TIEP111 Ohjelmointi 2 -opintojakson oppimateriaali.
Voit palauttaa tehtäviä vain, jos olet ilmoittautunut opintojaksolle Sisu- tai Ilpo-järjestelmässä. Oman etenemisesi tilanteen (harjoitustehtävien pisteet, harjoitustyön hyväksyminen, tenttitulokset) näet TIM-järjestelmästä.
Tietoja opintojaksosta
Opintojaksolla opit
- oliopohjaisen ohjelmoinnin perusteita ja periaatteita,
- tuottamaan pieniä ja keskisuuria oliopohjaisia ohjelmia,
- graafisen käyttöliittymän suunnittelua ja kehittämistä,
- ohjelman testaamista,
- erilaisia ohjelmoijan työkaluja ja tekniikoita.
Tarkemmat tiedot löydät opintojakson Sisu-esitteestä.
Uutiset
1. tammikuuta 2026: Kurssimateriaalia uudistetaan keväällä 2026
Teemme kokonaisvaltaisen uudistuksen oppimateriaaliin sekä tehtäviin kevään 2026 aikana. Osa materiaalista julkaistaan kurssin edetessä. Uudistamisesta johtuen sisällössä voi olla myös keskeneräisyyksiä ja virheitä. Pahoittelemme tästä mahdollisesti aiheutuvaa haittaa. Pyydämme, että ilmoitat virheistä tai parannusehdotuksista GitHubin kautta (katso tämän sivun alareuna) tai suoraan opettajien sähköpostiin ohj2-opet@jyu.onmicrosoft.com.
Ohjaukset ja tuki
Kevään 2026 on 12. tammikuuta – 24. huhtikuuta välisenä aikana tarjolla lähiohjausta Agoralla, etäohjausta Teamsin kautta, sekä sähköpostitukea.
Pääsiäistauon aikana (30.3.-6.4.) ei kuitenkaan ole ohjausta tarjolla.
Sisu vaatii ilmoittautumisen yhteydessä valitsemaan ohjausryhmän. Voit kuitenkin täysin vapaasti käyttää kaikkia ohjausaikoja ja -kanavia riippumatta siitä, mihin ohjausryhmään olet ilmoittautunut.
| Tukikanava | Aika | Paikka/Linkki |
|---|---|---|
| Lähiohjaus | ke 8-18, to 8-18, pe 8-14 | Agoralla luokat Ag B212.1 Finland ja Ag B211.1 Sovjet |
| Etäohjaus | ke 8-18, to 8-18, pe 8-14 | Ohjelmointi 2 Teams-kanava |
| Vastuuopettajien ja tuntiopettajien sähköpostiosoite | Jatkuva | ohj2-opet@jyu.onmicrosoft.com |
Ohjaukset ovat yhteisiä TIEP111 Ohjelmointi 2, ITKP102 Ohjelmointi 1- ja ITKA2004 Tietokannat ja tiedonhallinta -opintojaksojen kanssa. Ohjaajat auttavat kaikkien kolmen kurssin opiskelijoita.
Ohjausaikoja saatetaan lisätä tai poistaa kysynnän mukaan; kerro aikatoiveistasi opettajille sähköpostitse.
24.4. jälkeen ohjausta on saatavilla ajanvarauksella. Linkki ajanvaraukseen tulee myöhemmin saataville.
Haluatko ohjausaikoja näkyviin Sisun opintokalenteriin? (Avaa ohje klikkaamalla)
-
Kirjaudu Sisuun
-
Jos olet jo ilmoittautunut kurssille, klikkaa ylhäällä välilehteä Opintokalenteri tai klikkaa sitä hampurilaisvalikosta
-
Selaa oikealla oikea kurssi näkyville, eli tässä tapauksessa Ohjelmointi 2
-
Klikkaa oikealla olevaa oikealle osoittavaa väkästä Ohjelmointi 2 -kurssin kohdalla
-
Skrollaa alaspäin, kunnes tulee alaotsikko Pääteohjaus
-
Jos ei vielä näy, niin skrollaa alaspäin, kunnes näkyy Muiden ryhmien tiedot ja klikkaa sitä
-
Nyt voit skrollaamalla alaspäin haluamiesi pääteohjauksien kohdalta klikata nappulaa Näytä tapahtumat kalenterissa.

-
Nyt kyseisen ryhmän ohjausajat näkyvät sinulla automaattisesti. Tarvittaessa voit poistaa ryhmän tapahtumia viikkokohtaisesti Tapahtumakalenterista.
Ohjeet Teams-ohjauksiin liittymiseksi (JY tutkinto-opiskelijat)
-
Kirjaudu yliopiston tunnuksellasi Microsoft Teamsiin osoitteessa https://teams.microsoft.com. Käyttäjätunnus on muotoa
käyttäjätunnus@jyu.fi(esim.mameikal@jyu.fi). Tunnuksen muotostudent.jyu.fiei käy. Tunnuksen toimiminen vaatii, että olet hyväksynyt Office 365 -palvelut OMA-palvelussa (https://sso.jyu.fi). -
Lataa Teams-sovellus (suositus) tai käytä nettiversiota. Saatavilla on myös mobiilisovellus. Jos selaimella liittymisessä on ongelmia, tarkista ensin tukeeko Microsoft sitä täältä.
-
Teams-sovelluksessa klikkaa Teams Join or create team Join a team with a code
-
Syötä koodi
nnobn49 -
Testaa kaverin kanssa, että puhelu ja ruudun jakaminen toimii. Sinun tulee tarvittaessa sallia oikeudet käyttöjärjestelmäsi asetuksista.
Ohjeet Teams-ohjauksiin liittymiseksi (JY Avoin yliopisto, erilliset opinto-oikeudet)
Lähetä sähköpostilla alla oleva pyyntö osoitteeseen ohj2-opet@jyu.onmicrosoft.com.
Hei,
opiskelen Ohjelmointi 2 -kurssilla ei-tutkintoon johtavassa koulutuksessa.
Pyydän liittämään minut opintojakson Teams-ryhmään vieraana.
Teamsissa käyttämäni sähköpostiosoite on: [oma sähköposti tähän].
Terveisin, [oma nimi]
Liitämme sinut viimeistään seuraavana arkipäivänä.
Etäohjauksiin osallistuminen ilman Teamsia
Jos et millään onnistu kirjautumaan Teamsiin tai et halua olla Teams-kanavalla, voit pyytää etäohjausta Zoomin kautta seuraavasti:
- Asenna Zoom sovellus koneellesi osoitteesta https://zoom.us/download (muut kuin tutkinto-opiskelijat) tai https://jyufi.zoom.us (tutkinto-opiskelijat; Valitse Download Client ihan alhaalta)
- Kirjaudu Zoomiin valitsemallasi tilillä, esim. Google-kirjautumista käyttäen
(muut kuin tutkinto-opiskelijat) tai Single Sign-on / SSO -toiminnolla
(tutkinto-opiskelijat; käytä company domainia
jyufi) - Aloita kokous New meeting toiminnolla
- Testaa Audio Test speaker & mikrofone toiminnolla että äänet pelittää
- Ota kokouslinkki talteen Participants Copy invite link
- Avaa ohjauspyyntölomake: https://forms.gle/5QULUPBHjjqS4ndf6
- Täytä omat tietosi ja HUOM Pasteta lisätietokenttään kohdassa 5 kopioimasi linkki
- Odota, että ohjaaja tulee huoneeseesi. Saatat joutua hyväksymään hänen sisäänpääsyn (riippuu kokoushuoneesi asetuksista)
Navigointi tässä materiaalissa
Tässä muutama pikavinkki tässä materiaalissa navigoimiseen.
- Sisällysluettelon saat auki ja kiinni sivupalkki-kuvakkeesta .
- Voit selata materiaalia eteen- ja taaksepäin nuolikuvakkeista sivun vasemmassa ja oikeassa laidassa (tai ihan sivun alalaidassa, jos käytät mobiililaitetta) .
- Hakutoiminnon saat auki suurennuslasista oikeasta yläreunasta tai painamalla S-kirjainta näppäimistöltä .
Palaute ja kehittäminen
Olemme erittäin kiitollisia kaikesta palautteesta, joka auttaa meitä kehittämään opintojaksoa edelleen! Voit antaa palautetta ja kehitysehdotuksia opintojaksosta kolmella tavalla:
-
Jyväskylän yliopiston tutkinto-opiskelijat voivat antaa jatkuvaa palautetta opintojakson aikana Norppa-järjestelmässä. Nyt, kun olemme kehittämässä opintojakson sisältöjä ja toteutusta, tämä jatkuva palaute on erityisen tärkeää.
-
Kaikki opiskelijat voivat ilmoittaa havaitsemistaan virheistä, epäselvyyksistä, tai muista ongelmista tässä oppimateriaalissa. Raportoi havaintosi GitHubissa klikkaamalla kunkin sivun alareunassa olevia linkkejä. Voit myös ilmoittaa puutteista suoraan opettajille sähköpostitse osoitteeseen
ohj2-opet@jyu.onmicrosoft.com. -
Opintojakson lopuksi kaikki Sisussa (tai Ilpo-portaalissa) ilmoittautuneet (tutkinto, avoin, erilliset opinto-oikeudet, lukiolinjat) saavat henkilökohtaisen linkin kurssipalautekyselyyn, jossa voit antaa anonyymisti palautetta koko opintojaksosta.
Tekijät ja lisenssi
Ohjelmointi 2 oppimateriaali © 2025 by Denis Zhidkikh, Sami Sarsa, Antti-Jussi Lakanen, Rauli Ruokokoski, Karri Sormunen.
Kiitos Jonne Itkoselle palautteesta ja parannusehdotuksista.
Materiaali on julkaistu CC-BY-SA-4.0-lisenssillä. Tarkemmat tiedot löydät materiaalin GitHub-sivulta.
Suorittaminen
Voit valita kolmesta suoritustavasta itsellesi sopivimman. Kaikki suoritustavat sisältävät harjoitustyön tekemisen.
Suoritustapojen yksityiskohdat eroavat aikaisemmista toteutuksista jonkin verran, joten jos olet aiemmin yrittänyt suorittaa kurssia, lue tämä osio huolellisesti läpi.
Suoritustapa 1
Harjoitustehtävät, harjoitustyö aikataulussa sekä tentti
Suoritus koostuu seuraavista osasuorituksista (kaikista on saatava hyväksytty):
- Keräät jokaisesta osasta vähintään 50% siitä pistemäärästä mitä harjoitustehtävien perustehtävistä voi saada1
- Teet harjoitustyön aikataulussa
- Teet tentin hyväksytysti
1 Harjoitustehtävät muodostuvat perustehtävistä ja lisätehtävistä. "100%" tarkoittaa tässä yhteydessä sitä pistemäärää, mitä perustehtävistä voi saada. Voit kuitenkin tehdä myös lisätehtäviä kartuttaaksesi prosenttiosuuttasi ja siten tehdä jopa >= 100% pisteistä.
Arvosana muodostuu harjoitustehtävien ja tentin painotettuna keskiarvona (40/60). Tarkempi kuvaus on alla.
Harjoitustehtävien pisterajat:
| Kerättyjä pisteitä enintään | Harjoitustehtävien arvosana |
|---|---|
| <50% | 0 |
| 50% | 1 |
| 60% | 2 |
| 70% | 3 |
| 80% | 4 |
| >=90% | 5 |
Ensimmäisen kuuden osan kohdalla on mahdollista saada myös ns. DL-BONUS-pisteitä: Jos teet osasta vähintään 50%2 osan takarajaan mennessä, lisätään kyseisen osan harjoitustehtävien pistemäärään 0,5 pistettä. Niinpä DL-BONUS-pisteitä voi saada maksimissaan 6 * 0,5 = 3 pistettä. Karkeasti ottaen, kun teet yhden osan takarajaan mennessä, se vaikuttaa noin 1%-yksikön verran kokonaisprosenttiisi.
2 Edelleen, "50%" tarkoittaa tässä yhteydessä puolet siitä pistemäärästä, mitä kyseisen osan perustehtävistä voi saada.
Osien takarajat DL-BONUS-pisteiden saamiseksi ovat seuraavat:
| Osa | Takaraja DL-BONUS-pisteille |
|---|---|
| 1 | ma 19.1.2026 klo 11:59 (keskipäivä) |
| 2 | ma 26.1.2026 klo 11:59 (keskipäivä) |
| 3 | ma 2.2.2026 klo 11:59 (keskipäivä) |
| 4 | ma 9.2.2026 klo 11:59 (keskipäivä) |
| 5 | ma 16.2.2026 klo 11:59 (keskipäivä) |
| 6 | ma 23.2.2026 klo 11:59 (keskipäivä) |
Näet etenemissivulla kerättyjen tehtäväpisteidän määrän ja prosenttiosuuden sekä DL-BONUS-pisteet erikseen.
Lopullinen arvosana muodostuu harjoitustehtävien arvosanan ja tentin arvosanan painotettuna keskiarvona, pyöristäen lähimpään kokonaislukuun. Harjoitustehtävistä saatua arvosanaa painotetaan 40% ja tentistä saatua arvosanaa painotetaan 60%. Sekä harjoitustehtävistä että tentistä täytyy saada vähintään arvosana 1, jotta kurssista voi saada hyväksytyn arvosanan.
| Esim1 | Esim2 | Esim3 | |
|---|---|---|---|
| Osuus harjoitustehtävistä (sis DL-BONUS) | 70% | 90% | 70% |
| Harjoitustehtävien arvosana | 3 | 5 | 3 |
| Tentin arvosana | 1 | 4 | 5 |
| Painotettu keskiarvo | 1.8 | 4.4 | 4.6 |
| Pyöristetty arvosana | 2 | 4 | 5 |
Harjoitustehtävistä saatu arvosana otetaan lukuun kolmeen ensimmäiseen tenttiin, jotka opiskelija suorittaa, ja enintään yhden vuoden sisällä opintojakson viimeisestä suorituspäivästä.
Suoritustapa 2
105% harjoitustehtävistä, suullinen kuulustelu harjoitustyöstä, harjoitustyö aikataulussa
- Keräät vähintään 100% pistettä harjoitustehtävistä (kaikista osista yhteensä) JA keräät kaikki DL-BONUS-pisteet (6 * 0.5 = 3 pistettä)
- Teet harjoitustyön aikataulussa
- Osallistut suulliseen kuulusteluun, jossa ohjaaja arvioi harjoitustyösi
Arvosanasi on tällöin 1, jota voit vapaaehtoisesti korottaa tentillä.
Suoritustapa 3
Harjoitustyö ja loppukoe.
- Teet harjoitustyön
- Teet tasokokeen tapaisen loppukokeen (tämä on eri asia kuin tentti)
Arvosana muodostuu loppukokeen arvosanasta.
Eettiset ohjeet
Olet vastuussa kaikista palauttamistasi töistä. Kopioiminen tai toisen henkilön työn esittäminen omanaan on kiellettyä. Ryhmätyö on sallittua, mutta jokaisen ryhmän jäsenen tulee antaa panoksensa työhön, ymmärtää tekemänsä asiat ja osata selittää ne tarvittaessa. Ryhmätyönä tehty osa tulee aina merkitä selvästi palautettuun työhön, esimerkiksi koodin kommenttien avulla.
Noudatamme Jyväskylän yliopiston ohjeita ja linjauksia tekoälypohjaisten sovellusten käytössä opiskelussa. Alla olevat ohjeet täydentävät näitä linjauksia.
Generatiivisten tekoälytyökalujen käyttö koodin luomisessa on kiellettyä. Ohjelmoinnin opiskelun eräinä keskeisinä osaamistavoitteina on ongelmanratkaisun ja päättelyn oppiminen, ja tekoälytyökalujen käyttö vääristää näitä osaamistavoitteita. Kiellettyjä generatiivisia työkaluja ovat esimerkiksi GitHub Copilot, ChatGPT, Bard ja vastaavat chat- ja agenttisovellukset. Myös IDE-työkalujen sisäänrakennetut tekoälyavusteiset koodinluontiominaisuudet kuuluvat kiellettyjen työkalujen piiriin.
Generatiivista tekoälyä voi käyttää apuvälineenä esimerkiksi käsitteiden selittämiseen, tehtävänantojen ymmärtämiseen tai materiaalissa annettujen esimerkkien selittämiseen. Tekoälytyökalulle annettavassa kehotteessa tulee huomioida, että tekoäly ei saa tuottaa suoria vastauksia tai koodia opintojakson tehtäviin.
Menettely vilppiepäilytilanteessa on kuvattu Jyväskylän yliopiston opintoja ohjaavissa säädöksissä ja määräyksissä.
Työkaluohjeet
Ohjelmointi 2 -opintojaksolla käytämme seuraavia työkaluja:
-
Java Development Kit (JDK) - ohjelmistokehityspaketti, joka sisältää muun muassa Java-kääntäjän sekä virtuaalikoneen Java-ohjelmien ajamista varten.
-
Git - versiohallintaohjelma (engl. Version Control Software, VCS), joka mahdollistaa koodin versioinnin ja yhteistyön koodaajien välillä.
-
IntelliJ IDEA - integroitu kehitysympäristö (engl. Integrated Development Environment, IDE), jolla voi kehittää ja debugata muun muassa Java-ohjelmia. Käytämme IntelliJ IDEAn ilmaista Community Edition -versiota.
-
ComTest - työkalu dokumentaatiotestien kirjoittamiselle ja ajamiselle.
Yllä olevat ohjelmat löytyvät valmiiksi asennettuna Agoran mikroluokissa (Alban puoleinen pääty, 1. ja 2. kerros). Jos sinulla on oma tietokone, suosittelemme vahvasti, että asennat ohjelmat myös siihen. Erityisesti harjoitustyön tekeminen on helpompaa, kun kaikki tarvittavat ohjelmat on myös omalla tietokoneella.
tärkeää
Tämän sivun ohjeet vaativat komentorivin käyttöä. Voit tarvittaessa kerrata komentorivin perusteita seuraavista linkeistä:
Kurssilla virallisesti tuettuja käyttöjärjestelmiä ovat Windows, macOS ja Linux. Työkalujen asentaminen ChromeOS:ään saattaa olla mahdollista, mutta emme valitettavasti voi tarjota tukea kyseiseen käyttöjärjestelmään. Tästä syystä emme suosittele ChromeOS:n käyttöä.
Valitse käyttöjärjestelmäsi alta:
Esivalmistelut
Git
IntelliJ IDEA
Java Development Kit (JDK)
Avaa IntelliJ IDEA ja odota, kunnes pääset Welcome to IntelliJ IDEA -näkymään.
Klikkaa ikkunan keskellä tai ylädassa olevaa New Project -painiketta:
Avautuneesta ikkunasta klikkaa JDK-alasvetolaatikkoa ja valitse Download JDK... -painike:
Aseta avautuneessa ikkunassa asetukset seuraavasti:
- Version: 25
- Vendor: Oracle OpenJDK Älä muuta Location-kohdassa olevaa polkua! Paina lopuksi Select-painiketta.
Jätä muut projektin asetukset sellaiseksi kuin ne ovat. Paina oikeassa alalaidassa olevaa Create-painiketta ja anna projektin latautua.
Tämä avaa IntelliJ IDEA -kehitysympäristön käyttöliittymän.
JDK:n lataamisessa voi mennä aikaa. Odota rauhassa, kunnes kaikki virheet ja punaiset tekstit häviää.
Kun projekti on latautunut eikä virheitä näy, kokeile ajaa projekti painamalla oikeassa ylälaidassa olevaa Play-painiketta:
Odota, kunnes ohjelma kääntyy. Jos kaikki toimii, ikkunan alapuolelle pitäisi ilmestyä konsoli-ikkuna, jossa näet seuraavan tekstin:
Hello and welcome! i = 1 i = 2 i = 3 i = 4 i = 5Voit nyt sulkea IntelliJ IDEA:n.
ComTest
Avaa IntelliJ IDEA ja odota, kunnes pääset Welcome to IntelliJ IDEA -näkymään.
Jos sinulle avautui jokin vanha projekti, klikkaa yläpalkista tai hampurilaisvalikosta File Close project. Tämä vie sinut takaisin Welcome to IntelliJ IDEA -näkymään.
Klikkaa ikkunan vasemmassa alalaidassa oleva Configure (Ratas-ikoni) Settings.
Valitse vasemmalla puolella olevista asetusnäkymistä Plugins
Valitse Marketplace-välilehti ja hae hakusanalla
ComTestValitse Comtest Runner -pluginin kohdalta Install
Paina Save
Sulje IntelliJ IDEA
Mitä seuraavaksi?
Onneksi olkoon! Sinulla on seuraavaksi kaikki tarvittavat kurssityökalut. Voit jatkaa tästä varsinaisiin materiaaleihin.
Yleiset ongelmat ja ratkaisut
Saan IDEAssa Java-projektia ajaessa virheen error: illegal character: '\ufeff'
Virhe voi mahdollisesti johtua siitä, että toit Rider-työkalun asetukset IDEAan. Riderin asetukset eivät ole täysin yhteensopivia Javan kehityksen kanssa eikä IDEA osaa korjata ongelmaa.
Tee seuraavasti:
-
Avaa IntelliJ IDEA ja odota, kunnes pääset Welcome to IntelliJ IDEA -näkymään.
Jos sinulle avautui jokin vanha projekti, klikkaa yläpalkista tai hampurilaisvalikosta File Close project. Tämä vie sinut takaisin Welcome to IntelliJ IDEA -näkymään.
-
Klikkaa ikkunan vasemmassa alalaidassa oleva Configure (Ratas-ikoni) Settings.
-
Valitse vasemmalla puolella olevista asetusnäkymistä Editor File Encodings
-
Aseta Create UTF-8 files -asetuksen arvoksi with no BOM.
-
Paina Save.
-
Tee uusi projekti ja kokeile ajaa yksinkertainen ohjelma.
Internal error: com.intellij.platform.ide.bootstrap... Process "C:\...idea64.exe" is still running and does not respond
Tämä virhe voi johtua siitä, että Rider on jostain syystä jumittunut taustaprosessina käyttöjärjestelmässä.
- Sulje IntelliJ IDEA kokonaan.
- Avaa Rider.
- Sulje Rider.
- Käynnistä IntelliJ IDEA uudelleen.
Harjoitustyö
Harjoitustyöhön liittyvät tiedot julkaistaan erikseen opintojakson aikana.
Tentti
Lukuvuonna 2025-2026 tenttejä järjestetään seuraavasti
| Tentti | Päivämäärä | Aika | Paikka | Ilmoittautumislinkki |
|---|---|---|---|---|
| Kevät 1 | pp.kk.2025 | klo xx-xx | Agora / Zoom | Ilmoittaudu |
| Kevät 2 | pp.kk.2025 | klo xx-xx | Agora / Zoom | Ilmoittaudu |
| Kevät 3 | pp.kk.2025 | klo xx-xx | Agora / Zoom | Ilmoittaudu |
| Kesä 1 | pp.kk.2025 | klo xx-xx | Agora / Zoom | Ilmoittaudu |
| Kesä 2 | pp.kk.2025 | klo xx-xx | Agora / Zoom | Ilmoittaudu |
| Syksy 1 | pp.kk.2025 | klo xx-xx | Agora / Zoom | Ilmoittaudu |
| Syksy 2 | pp.kk.2025 | klo xx-xx | Agora / Zoom | Ilmoittaudu |
| Syksy 3 | pp.kk.2025 | klo xx-xx | Agora / Zoom | Ilmoittaudu |
Voit osallistua tenttiin joko Agoran luentosalissa tai etänä Zoomin kautta.
Tenttiin tulee ilmoittautua viimeistään 72 tuntia ennen tentin alkuhetkeä. Ilmoittautumisen yhteydessä opiskelijan tulee hyväksyä sekä tentin säännöt (kuvattu alla). Ilmoittautumisen yhteydessä opiskelija valitsee myös tentin suoritustavan (salitentti tai etätentti).
Opiskelijan on todistettava henkilöllisyytensä ennen tentistä poistumista. Henkilöllisyyden todistamiseksi hyväksytään passi, henkilökortti tai ajokortti. Vain tunnistettujen opiskelijoiden suorituksen arvostellaan.
Tenttiaika on 4 tuntia. Ilmoita ilmoittautumisen yhteydessä, mikäli sinulle on myönnetty lisäaikaa tentteihin yksilöllisenä järjestelynä.
Ennen kuin ilmoittaudut tenttiin, lue huolellisesti (i) Jyväskylän yliopiston ohjeet verkkotenttien suorittamiseen ja (ii) Tarkentavat ohjeet opintojakson Ohjelmointi 1 (ITKP102) -tenttiin ennen tenttiin ilmoittautumista. Jos yliopiston ohjeiden ja opintojakson tarkentavien ohjeiden välillä on ristiriita, opintojakson ohjeet pätevät.
Tentin aikana ilmenevät tekniset ongelmat
Teknisissä ongelmissa käytä jotakin seuraavista yhteydenottotavoista:
- Salissa: Nosta kätesi ylös ja odota, että valvoja tulee luoksesi.
- Etänä: Jätä avunpyyntö lomakkeella: Avunpyyntö etätentissä (linkki tulee tähän hyvissä ajoin ennen tenttiä)
- Vaihtoehtoisesti laita viesti osoitteeseen ohj2-opet@jyu.onmicrosoft.com
- Muista, että ongelman sattuessa ei ole kiirettä tai syytä paniikkiin. Vastuuopettaja voi tarvittaessa myöntää lisäaikaa tenttiin.
Tyyliopas
Tämä dokumentti on kesken.
Usein kysyttyä
Olen aloittanut Ohjelmointi 2 -kurssin aiemmin. Voinko jatkaa siitä, mihin jäin?
Jos sinulta puuttuu aiemmalta toteutukselta vain harjoitustyö, ja muut osasuoritukset ovat enintään 3 vuotta vanhoja, voit viimeistellä harjoitustyön.
Jos sinulta puuttuu jotain tai joitain muita osasuorituksia, sinun tulee suorittaa opintojakso uudelleen. Tämä johtuu siitä, että opintojakson materiaaleja päivitettiin merkittävästi keväällä 2026.
Luennot
Keväällä 2026 luennot järjestetään paikan päällä Agoralla. Luennot samanaikaisesti striimataan YouTube-palveluun. Luentonauhoitteet julkaistaan YouTubessa ja Moniviestimessä.
| Luento | Päivämäärä | Sijainti | Striimi ja nauhoite | Materiaalit |
|---|---|---|---|---|
| Luento 1: Java-kielen perusteet | ma 12.1.2026 klo 10.15 | Ag Auditorio 3 | YouTube, Moniviestin | Kalvot |
| Luento 2: Olio-ohjelmoinnin perusteet | ma 19.1.2026 klo 14.15 | Ag Auditorio 3 | YouTube, Moniviestin | Kalvot, Koodit |
| Luento 3: Perintä, polymorfismi | ma 26.1.2026 klo 14.15 | Ag Auditorio 3 | YouTube, Moniviestin | Kalvot, Koodit |
| Luento 4: Rajapinta, geneeriset luokat | ma 2.2.2026 klo 14.15 | Ag Auditorio 3 | YouTube, Moniviestin | Kalvot, Koodit |
| Luento 5: Tietorakenteita ja algoritmeja | ma 9.2.2026 klo 14.15 | Ag Auditorio 3 | YouTube, Moniviestin | |
| Luento 6: Hyödyllisiä menetelmiä Javassa | ma 16.2.2026 klo 14.15 | Ag Auditorio 3 | YouTube, Moniviestin | |
| Luento 7 | ma 23.2.2026 klo 14.15 | Ag Auditorio 3 | YouTube, Moniviestin | |
| Luento 8 | ma 2.3.2026 klo 14.15 | Ag Auditorio 3 | YouTube, Moniviestin | |
| Luento 9 | ma 9.3.2026 klo 14.15 | Ag Auditorio 3 | YouTube, Moniviestin | |
| Luento 10 | ma 16.3.2026 klo 14.15 | Ag Auditorio 3 | YouTube, Moniviestin | |
| Luento 11 | ma 23.3.2026 klo 14.15 | Ag Auditorio 3 | YouTube, Moniviestin |
Kyselytunnit
Kyselytunti järjestetään joka viikko luentojen jälkeen klo 16.00–17.00 alkaen maanantaista 19.1.2026.
Kyselytunnilla vastuuopettajat päivystävät luentosalissa ja opintojakson Teams-kanavalla ja vastaavat opiskelijoiden kysymyksiin. Tilaisuus on luonteeltaan vapaamuotoinen, eli ennalta määrättyä ohjelmaa ei ole, eikä tilaisuutta nauhoiteta. Voit tulla paikalle kysyäksesi mieltäsi askarruttavista asioista liittyen luentoihin, harjoitustehtäviin tai harjoitustyöhön. Kyselytunti ei ole kuitenkaan tarkoitettu harjoitustyön hyväksymiseen tai harjoitustehtävien pistekorjauksiin. Näissä kysymyksissä ota yhteyttä opettajiin sähköpostitse.
Java-kielen perusteet
osaamistavoitteet
- Tutustut Java-kielen perussyntaksiin, muuttujiin, ja ohjausrakenteisiin.
- Opit kirjoittamaan yksinkertaisia Java-ohjelmia ja ymmärrät miten koodia käännetään ja suoritetaan.
Materiaalin käytöstä
Materiaali on jaettu 12 osaan, jotka sisältävät tekstiä, esimerkkejä ja tehtäviä.
Yläpalkista löydät seuraavat toiminnot:
- Sisällysluettelo: Avaa ja sulkee vasemman laidan sisällysluettelon.
- Haku (tai paina s-painiketta): Etsi sisältöä koko materiaalista hakusanalla.
- Teema: Vaihda ulkoasua (vaalea, tumma, automaattinen).
- Tulosta: Tulosta koko materiaali.
Koodiesimerkit
Ajettavissa koodiesimerkeissä on käytössä seuraavat painikkeet:
- Aja koodi: Suorittaa koodiesimerkin ja näyttää tulosteen.
- Näytä koko koodi: Paljastaa piilotetut rivit, jotka eivät ole esimerkin ymmärtämisen kannalta keskeisiä, mutta vaaditaan koodin suorittamiseen.
Jotkin koodiesimerkit saattavat olla muokattavia. Voit tarvittaessa palauttaa esimerkin alkuperäiseen tilaan Peruuta muutokset-painikkeella ().
Tehtävät
Jokainen osa sisältää joukon harjoitustehtäviä. Löydät kuhunkin alalukuun liittyvät tehtävät alaluvun lopusta. Löydät kaikki osan tehtävät koostettuna myös "Osan kaikki tehtävät" -alaluvusta.
Tehtävät palautetaan TIM-järjestelmään. Linkki kunkin tehtävän palautuslaatikkoon löytyy aina tehtävänannon yhteydessä. Voit seurata edistymistäsi kurssin TIM-kotisivulla. Kirjaudu sisään TIM-järjestelmään Haka-kirjautumisella käyttäen yliopistotunnuksesi. Ohjeet Haka-kirjautumiselle löytyvät TIM-ohjeista.
Hei, Java!
osaamistavoitteet
- Tutustut Java-kielen perusteisiin
- Tiedät, miten Java-ohjelma käännetään ja ajetaan
- Tiedät, mikä on (J)VM ja miten kääntäminen eroaa tulkkauksesta
- Tunnet Java-kielen vastineita yleisimmille I/O-operaatioille
Ohjelmointi 2 -kurssilla käytämme Java-ohjelmointikieltä. Java on yleiskäyttöinen olio-ohjelmointia tukeva kieli, joka on tarkoitettu alustasta riippumattomien ohjelmien kirjoittamiseen. Java on suosittujen ohjelmointikielten kärkilistoilla (ks. esim. TIOBE index, StackOverflow 2025 developer survey, suosituimmat kielet GitHub-palvelussa). Javan syntaksi muistuttaa paljon Ohjelmointi 1 -kurssilla käytettyä C#-kieltä.
Java-kielen perusteet
Lähdetään liikkeelle perinteisellä 'Hei, maailma' -esimerkillä Javalla:
/* 1 */ void main() {
/* 2 */ IO.println("Hei, maailma!");
/* 3 */ }
Käydään läpi ohjelma rivi riviltä:
-
Java-ohjelman suoritus alkaa
main-nimisestä aliohjelmasta.voidtarkoittaa, että aliohjelma ei palauta mitään arvoja. Koska pääohjelma ei ota parametreja,main-sanan perässä olevat kaarisulkeet voidaan jättää tyhjäksi. Aliohjelman runko alkaa avaavalla aaltosululla{. -
Tekstin tulostaminen komentorivi-ikkunaan onnistuu
IO.println-metodilla. Javassa lause loppuu yleensä puolipisteeseen;, kuten tässäkin. -
Aliohjelman runko lopetetaan aaltosululla
}. Ohjelman suoritus päättyy automaattisesti, kunmain-aliohjelma on suoritettu loppuun.
Vaikka ohjelma on hyvin yksinkertainen, se on silti aivan toimiva Java-ohjelma. Opintojakson aikana teemme paljon juuri komentorivi-ikkunaan kirjoittavia ja sieltä lukevia ohjelmia. Opintojakson loppupuoliskolla painopiste siirtyy graafisten käyttöliittymien parissa työskentelyyn.
Javan koodauskäytänteistä
Monesti ohjelmointikielessä on joukko kyseiseen kieleen vakiintuneita koodauskäytänteitä. Näin on myös Javassa. Tutustumme erilaisiin käytänteisiin tämän materiaalin edetessä. Mainittakoon, että Javan koodauskäytänteet poikkeavat hieman C#-kielen käytänteistä muun muassa nimeämisessä ja koodin muotoilussa.
Tässä olennaisimmat Javan koodauskäytänteet, joita on tässä vaiheessa hyvä pitää mielessä:
-
Aliohjelman runkoa aloittava aaltosulku
{laitetaan yleensä samalle riville kuin aliohjelman määrittely. Sama pätee muihin rakenteisiin, joissa käytetään aaltosulkuja, kutenif-,for-,while- jado-while-rakenteille. -
Muuttujien ja aliohjelmien nimeämisessä käytetään camelCasing-tyyliä, eli ensimmäinen kirjain on pienaakkonen ja seuraavat sanat aloitetaan suuraakkosella. Esimerkiksi
tamaOnFunktionNimi. -
Tiedostojen ja luokkien nimeämisessä käytetään PascalCasing-tyyliä, eli ensimmäinen kirjain on suuraakkonen ja seuraavat sanat aloitetaan isolla kirjaimella:
HeiMaailma.java,public class Opiskelija, jne. Samoin monissa muissa myöhemmin opittavissa rakenteissa, kuten rajapinnoissa ja enumeraatioissa käytetään PascalCasing-tyyliä.
Opas: Java-ohjelmien kääntäminen ja ajaminen
tärkeää
Tässä osiossa tarvitset opintojakson työkaluja. Käy ensin asentamassa kaikki työkalut Työkaluohjeesta.
Vaikka monet yllä olevan tapaiset pienet ohjelmat voidaan periaatteessa tehdä nettiselaimessa, on ohjelmia kehittäessä varsin käytännöllistä käyttää erillistä kehitysympäristöä. Tässä materiaalissa käytämme IntelliJ IDEA -kehitysympäristöä Java-ohjelmien luomiseen, ajamiseen ja virheenjäljitykseen.
Luo uusi projekti
Luodaan seuraavaksi yksinkertainen projekti IDEAssa. Projekti on IDEA-kehitysympäristön tapa koostaa lähdekooditiedostoja, testejä, kirjastoja ja muita lisätiedostoja yhteen kokonaisuuteen.
Tee seuraavasti:
-
Avaa IntelliJ IDEA ja avaa uuden projektin dialogi.
Jos sinulle avautui Welcome to IntelliJ IDEA, valitse New Project.
Jos sinulle avautui jokin valmis Java-projekti, valitse yläpalkissa File New Project.
Yläpalkin valinnat saattavat olla hampurilaisvalikkopainikkeen () takana.
-
Uuden projektin dialogissa aseta seuraavat tiedot:
-
Valitse vasemmalla puolella olevasta listasta projektityypiksi Empty Project.
-
Aseta projektin nimeksi Name-kenttään
HelloWorld. Projektien nimet kirjoitetaan yleensä ilman välilyöntejä. -
Aseta projektin sijainti Location-kenttään. Klikkaa kentän oikealla puolella olevaa kansiokuvaketta () ja valitse projektille sopiva kansio. Valitse sellainen kansio, jonka löydät tulevaisuudessakin helposti omalta tietokoneelta.
-
Laita ruksi Create Git repository pois päältä. Emme tarvitse versiohallintaa vielä tässä vaiheessa.
Yllä olevien valintojen jälkeen tuloksen pitäisi näyttää seuraavalta (Location-kenttä voi olla erilainen riippuen käyttöjärjestelmästäsi):
-
-
Paina Create. Tämän jälkeen IDEA luo uuden projektin valitsemaasi kansioon.
Käydään pikaisesti läpi IDEAn olennaisimmat osat:
- Koodialue: projektissa olevien tiedostojen sisällöt näkyvät tässä, kun ne avataan. Kukin avattu tiedosto avautuu omaan välilehteen.
- Projektiselain: projektissa olevat kansiot ja tiedostot näkyvät tässä. Selaimen kautta voidaan lisätä, poistaa, siirtää tai uudelleennimetä tiedostoja ja kansioita.
- Projektin ajaminen ja debuggaus: tässä näkyy ajettavan Java-ohjelman nimi, ohjelman ajopainike () ja debuggauspainike ().
- Valikot ja näkymät: IDEAssa on erilaisia näkymiä, jotka ovat oletuksella piilossa. Sivupalkin avulla voidaan avata ja piilottaa näkymät tarpeen mukaan. Esimerkiksi projektiselaimen voi piilottaa painamalla sivupalkissa olevasta kansiokuvakkeesta ().
Luo Java-moduuli
Oletuksella uusi projekti on tyhjä. IDEA:ssa ohjelmakoodi kirjoitetaan
moduuleihin (engl. module). Samaan projektiin voi lisätä useita
moduuleita, joita voi kehittää ja ajaa erikseen.
Esimerkiksi, voit näin tehdä yhden projektin tietyn viikon tehtäville
(esim. Viikko1 tai Viikon1Tehtavat) ja lisätä
kunkin tehtävän omana moduulina.
Luodaan seuraavaksi alimoduuli nimeltään HelloProgram, lisätään
siihen uusi ohjelma ja kokeillaan ajaa se.
Tee seuraavasti:
-
Klikkaa hiiren toissijaisella painikkeella projektin nimeä projektinäkymässä (
HelloWorld) ja valitse New Module. -
Avautuvassa New Module -dialogissa aseta seuraavat tiedot:
-
Valitse vasemmalla puolella olevasta listasta projektityypiksi Java.
-
Aseta projektin nimeksi Name-kenttään
HelloProgram. Projektien nimet kirjoitetaan yleensä ilman välilyöntejä. -
Jätä Location-kenttä sellaisenaan. IDEA automaattisesti valitsee moduulille oikean sijainnin.
-
Valitse Build system-rivillä IntelliJ. Tutustumme muihin projektien rakennusjärjestelmiin myöhemmissä osissa.
-
Varmista, että JDK-kentässä on sama JDK-versio kuin minkä olet asentanut Työkaluohjeissa.
-
Laita ruksi Add sample code pois päältä. Lisäämme kooditiedoston itse.
Yllä olevien valintojen jälkeen tuloksen pitäisi näyttää seuraavalta (Location-kenttä voi olla erilainen riippuen käyttöjärjestelmästäsi):
Paina lopuksi Create. Tämä luo uuden
HelloProgram-kansion, joka sisältääsrc-kansion. -
Luo lähdekooditiedosto
IntelliJ-projektissa moduuliin kuuluva koodi laitetaan src-kansioon.
Jos projektissa on useampi moduuli, jokaisella moduulilla on oma src-kansio.
Luodaan seuraavaksi yksinkertainen Java-lähdekooditiedosto, johon voidaan kirjoittaa koodi.
-
Klikkaa projektiselaimessa olevaa
HelloProgram-moduulin alasvetopainiketta. Sen jälkeen klikkaa toissijaisella hiiren painikkeellasrc-kansiosta ja valitse New Java Compact File. -
Anna lähdekooditiedoston nimeksi
Ohjelmaja paina Enter.
IDEA luo uuden Ohjelma.java-nimisen tiedoston src-kansioon. IDEA myös lisää
automaattisesti main-aliohjelman määrittelyn lähdekooditiedostoon. Samalla
IDEA avaa lähdekooditiedoston koodialueelle. Voit jatkossa avata tiedoston myös
tuplaklikkaamalla sitä.
Kirjoita ohjelma
Kirjoitetaan seuraavaksi yksinkertainen "Hei, maailma"-ohjelma alusta alkaen
juuri luotuun Ohjelma.java-tiedostoon.
Tee seuraavasti:
-
Poista kaikki koodi
Ohjelma.java-tiedostosta.IDEA lisää yleensä valmista pohjakoodia uusiin lähdekooditiedostoihin. Tätä harjoitusta varten kirjoitamme kuitenkin koodia itse.
-
Kirjoita seuraava koodi
Ohjelma.java-tiedostoon:void main() { IO.println("Hei, maailma!"); }Vältä kopioimasta koodia, vaan kirjoita se itse. Kirjoittaminen itse usein auttaa muistamaan, mistä eri ohjelmoinnissa käytettävät merkit, kuten aaltosulut, kaarisulut ja puolipiste löytyvät.
Bonus: IDEAn täydennysominaisuuksien käyttäminen
IDEA tarjoaa erilaisia aikaa säästäviä täydennysominaisuuksia, joiden käyttöä on hyvä harjoitella.
Kokeile ainakin seuraavia ominaisuuksia.
-
main-pääohjelman automaattinen lisääminen: Aloita kirjoittamallamain. Paina sen jälkeen Ctrl+Space (macOS: ⌘+Space). Valitse nuolinäppäimillämain-pohja ja paina Enter:IDEA sisältää erilaisia valmiita pohjia, jotka nopeuttavat koodin kirjoittamista ja helpottavat yleisempien rakenteiden muistamista. Näet kaikki koodipohjat painamalla Ctrl+J (macOS: ⌘+J).
-
println-aliohjelman automaattinen täydentäminen: Siirrä kursorimain-pääohjelmaan tyhjälle riville.
Kirjoita alkuun kirjain I, minkä jälkeen IDEA automaattisesti näyttää
kaikki kursorin kohdalle sopivat rakenteet, jotka alkavat kirjaimella I.
Valitse nuolipainikkeilla IO ja paina Enter. Tämä
täydentää IO kursorin kohdalle.
Kirjoita sen jälkeen . (piste), minkä jälkeen IDEA automaattisesti näyttää
kaikki IO-luokassa olevat aliohjelmat. Siirry listassa nuolipainikkeilla
println-aliohjelman kohdalle ja paina Enter.
Tämä täydentää println-tekstin kursorin kohdalle.
Kirjoita sen jälkeen kaarisulku (. IDEA automaattisesti täydentää
lopettavan kaarisulun ). Siirry nuolipainikkeilla kaarisulkujen väliin
ja kirjoita "Hei, maailma!". Lopuksi siirry rivin loppuun painamalla End
tai nuolinäppäimiä käyttäen ja lisää loppuun puolipiste ;.
IDEA osaa automaattisesti siis ehdottaa luokkien ja aliohjelmien nimiä kontekstin perusteella. Voit myös aina erikseen avata automaattisen täydennyksen painamalla
Ohjelman ajaminen
Kooditiedostoja, jotka sisältävät main-aliohjelman, voidaan suorittaa.
Suorittaminen onnistuu ajopainikkeella (), joka
sijaitsee main-aliohjelman vieressä sekä IDEA:n yläpalkissa.
Tee seuraavasti:
-
Klikkaa
main-aliohjelman vasemmalla puolella olevaa ajopainiketta ().IDEA ensin kääntää ohjelmasi. Kun ohjelma on käännetty, IDEA ajaa ohjelmasi, ja editorin alapuolelle avautuu Run-ikkuna, jossa näkyy tekstiä. Ensimmäinen rivi on se komento, jota IDEA käytti käännetyn tiedoston ajamiseksi. Seuraavalla rivillä on oman ohjelmamme tuottama tuloste
Hei, maailma!. Viimeinen rivi kertoo, että ohjelman suoritus päättyi ilman virheitä. -
Kokeile vielä ohjelman ajamista luodulla ajokonfiguraatiolla.
Kun ajat kooditiedoston ensimmäistä kertaa, IDEA luo ajokonfiguraation. Ajokonfiguraatio on pieni tiedosto, johon tallentuu koodin suorittamiseen liittyviä asetuksia, kuten käytettävä JDK-versio, mahdolliset komentoriviparametrit ja työhakemisto. Oletusarvoisesti tämä tiedosto syntyy projektin juurikansioon
.ideaworkspace.xml.Kun ajokonfiguraatio on luotu ensimmäisen ajon jälkeen, voit jatkossa ajaa koodin aina IDEA:n yläpalkissa olevalla ajopainikkeella. Tällä tavoin voit helposti ajaa ohjelmia ilman, että kooditiedostoa tarvitsee erikseen avata.
IDEAn yläpalkissa pitäisi nyt näkyä
Ohjelma-ajokonfiguraation nimi, jonka vieressä on ajopainike (). Kokeile sulkeaOhjelma.javaja suorittaa ohjelma yläpalkin kautta.Ajokonfiguraatioiden avulla voit ajaa eri moduuleissa kirjoitettuja ohjelmia. Myöhemmin materiaalissa tutustumme lisäksi Gradle-hallintatyökaluun, jonka avulla teemme muun muassa erillisiä ajokonfiguraatioita projektin ajamiselle, testaamiselle ja kääntämiselle.
vinkki
Tutustu yleisimpiin pikanäppäinkomentoihin
Näppäinkomennot nopeuttavat kehitysympäristön käyttöä, ja pienellä harjoittelulla ohjelmointi voi sujua kokonaan hiirtä käyttämättä. Näppäinkomennot riippuvat käyttöjärjestelmästä ja valituista näppäinasetuksista. IDEA kuitenkin näyttää näppäinkomennot valikoissa sekä vihjeteksteissä, mikä helpottaa komentojen oppimista.
Voit myös muokata näppäinkomentoja asetuksista kohdassa File Settings Keymap. Voit myös ladata muiden kehitysympäristöjen, kuten Visual Studio Coden, näppäinasetuksia laajennoskaupasta kohdassa File Plugins.
Kääntäminen ja ajaminen komentoriviltä
Ennen kuin IDEA varsinaisesti ajaa ohjelman, se käännetään ajettavaan muotoon. IDEAssa tämä tapahtuu taustalla automaattisesti klikkaamalla ajopainiketta, debuggauspainiketta tai Build-valikon kautta. Java-lähdekoodin voi kuitenkin tarvittaessa myös kääntää ja ajaa itse komentoriviltä. On hyvä tietää, miten tämä tapahtuu, jotta ymmärrät paremmin, mitä IDEA taustalla tekee.
Tutkitaan nyt, miten ohjelma käännetään ja ajetaan komentoriviltä.
Miten voin seurata mukana?
Tee alkuun yksinkertainen ohjelma yllä olevan oppaan mukaisesti.
Sen jälkeen avaa IDEA:n vasemmasta näkymäpalkista komentorivi painamalla komentorivipainikkeesta (). Tämä avaa käyttöjärjestelmän komentorivin (zsh macOS:lla, PowerShell Windowsilla, oletuskomentorivi Linuxilla).
Jos latasit Java-kehitysympäristön seuraamalla työkaluohjeita, komentorivi ei löydä mitään Javan kääntämiseen tarkoitettuja työkaluja. Ota työkalut käyttöön kopioimalla ja liittämällä alla oleva komento:
Yllä oleva komento mahdollistaa JDK:n työkalujen käyttöä komentoriviltä väliaikaisesti. Komentorivi palautuu ennalleen, kun suljet ja avaat komentorivin uudelleen.
Avataan nyt komentorivi ja siirrytään alkuun projektikansioon. Tarkastellaan vielä, mitä tiedostoja projektista löytyy:
Koska käytössämme on IntelliJ-projekti, sieltä löytyy vain muutama olennainen tiedosto ja kansio:
HelloWorld.imlon projektin asetustiedosto, jolla IDEA tunnistaa kansion olevan Java-projektiHelloProgramon lähdekoodikansio, jossa kaikki lähdekooditiedostot sijaitsevatouton kansio, joka sisältää käännetyt ohjelmat
Siirrytään nyt kansioon HelloProgram ja tarkastellaan sen sisältö:
Yksittäisestä moduulista löytyvät vastaavasti seuraavat tiedostot ja kansiot:
HelloProgram.imlon moduulin asetustiedosto, jolla IDEA tunnistaa kansion olevan Java-moduulisrcon lähdekoodikansio, joka sisältää ohjelman lähdekoodin
Siirrytään vastaavasti kansioon src ja tarkastellaan sen sisältö:
.java-tiedostopäätettä käytettävät tiedostot ovat Javan
lähdekooditiedostoja. Ne sisältävät ohjelman lähdekoodia tekstinä eivätkä ne
ole vielä suoraan ajettavissa.
Jotta ohjelma voidaan ajaa, se pitää kääntää. Kuten mainitsimme, IDEA tekee
tämän automaattisesti kun käynnistämme tekemämme ohjelman, mutta lähdekoodin
kääntäminen onnistuu myös komentoriviltä käyttäen Java-kehitysympäristön mukana
tullutta javac-kääntäjäohjelmaa. Kokeillaan kääntää Ohjelma.java:
Jos kääntäminen onnistui, javac-komento ei tulosta oletuksena mitään.
Tutkitaan vielä kansion rakenne ls-komennolla:
Kääntämisen seurauksena syntyy .class-päätteinen tiedosto. Tämä tiedosto
sisältää niin sanottua tavukoodia (engl. bytecode), joka on tiedoston
käännetty muoto. Tavukoodi ei ole suoraan prosessorilla ajettava ohjelma, vaan
eräänlainen välivaihe. Tavukoodia voidaan kuitenkin suorittaa Javan
virtuaalikoneella (JVM, Java Virtual Machine), joka on erillinen ohjelma, joka
osaa tulkita ja suorittaa tavukoodia. Vaikka tämä voi kuulostaa turhan
monimutkaiselta, hyöty on siinä, että ohjelma joka on käännetty
Java-tavukoodiksi voidaan nyt ajaa eri alustoilla (Windows, macOS, Linux, jne.),
kunhan JVM on toteutettu kyseisellä alustalla. JVM voi puolestaan optimoida
tavukoodia juuri alustalle ja prosessorille sopivaan muotoon. Javalla onkin
iskulause: "Write Once, Run Anywhere", jolla viitataan tähän periaatteeseen.
JDK:n kanssa tulee myös valmiiksi Java-virtuaalikone sekä Javan ajoympäristö
(JRE, Java Runtime Environment), joka sisältää yleisempiä toimintoja, joita
Java-ohjelma saattaa käyttää. Tavukooditiedosto voidaan ajaa JVM:llä käyttäen
java-komentoa:
Huomaa, että java-komentoa antaessa kirjoitetaan tavukooditiedoston nimi
ilman .class-päätettä.
Myöhemmin materiaalissa tutustumme Gradle-projektinhallintaohjelmaan, jolla
pystyy kääntämään useita lähdekooditiedostoja yhteen .jar-tiedostoon, johon
voidaan pakata kaikki ohjelman ajamiseen tarvittavat tiedostot.
Myös .jar-tiedostot voidaan suorittaa java-komennolla.
vinkki
Alkaen Javan versiosta 11 java-komento osaa myös automaattisesti kääntää
ja suorittaa .java-lähdekooditiedostot ilman erillistä javac-kääntäjän
ajamista.
Lisäksi tässä materiaalissa käytämme pääosin IDEA-kehitysympäristöä, joka hoitaa lähdekooditiedostojen kääntämisen automaattisesti ja tehokkaasti.
Bonus: jshell-tulkkiohjelma
Vaikka Java lasketaan käännettäväksi kieleksi, toisinaan voi olla hyödyllistä
kokeilla Java-ohjelmien kirjoittamista interaktiivisesti ilman jatkuvaa
kääntämistä.
Interaktiivisuus tässä tarkoittaa, että voit kokeilla eri komentoja rivi/lohko
kerrallaan ilman erillistä kääntämistä ja ajamista, luokkia tai main-pääohjelmaa.
Tätä varten JDK sisältää jshell-ohjelman, joka on Java-ohjelman
komentorivitulkki eli ns. REPL-tulkki (read-evaluate-print-loop).
jshell tarjoaa useita hyödyllisiä toimintoja, kuten:
- Luokkien ja aliohjelmien nimien täydennys ja haku Tab-painikkeella
- Lausekkeiden suorittaminen ilman tarvetta
main-aliohjelmalle
jshell-ohjelmasta voi poistua /exit-komennolla.
Tekstin tulostaminen ja syötteen lukeminen komentorivi-ikkunassa
Jatkossa voi olla hyödyllistä tulostaa erilaisia asioita komentorivin avulla ja
toisaalta lukea tietoa sieltä. Javan IO-luokka tarjoaa kolme perustoimintoa
tekstin tulostamiseen ja lukemiseen komentorivillä:
| Aliohjelma | Esimerkki | Selitys |
|---|---|---|
println | IO.println("Moi!");
| Tulostaa parametrina annetun arvon ja lisää loppuun rivinvaihdon |
IO.println();
| Tulostaa rivin rivinvaihdolla | |
print | IO.print("Samalla rivillä!");
| Tulostaa parametrina annetun arvon ilman rivinvaihtoa |
readln | IO.readln();
| Lukee syöterivin käyttäjältä (ts. Enterin painallukseen saakka). Jos käyttäjä lopettaa ohjelman antamatta syötettä, palauttaa ns. null-viitteen |
IO.readln("Anna sana > ");
| Sama kuin readln, mutta tulostaa ensin annetun tekstin ennen syötteen lukemista. |
Katsotaan vielä näiden yhteistoimintaa. Voit muokata alla olevaa esimerkkiä vapaasti ja kokeilla, miten erilainen tulostus toimii.
void main() {
String nimi = IO.readln("Anna nimesi: > ");
IO.println();
IO.println("Moi, " + nimi + "!");
IO.print("Tämä teksti");
IO.print(" menee samalle");
IO.print(" riville");
IO.println(); // Kokeile ottaa tämä pois ja katso, mitä tapahtuu
IO.println("Tervetuloa Ohjelmointi 2 -kurssille!");
}
Muuttujat ja tietotyypit
osaamistavoitteet
- Muistat, mitä ovat muuttujat ja vakiot
- Muistat, mitä ovat merkkijonot ja listat
- Tunnet Javan alkeistietotyypit
- Tunnet, miten merkkijonoja, taulukoita ja listoja käytetään Javassa
Ohjelmat käsittelevät muistiin tallennettua tietoa. Korkean tason kielissä, kuten Javassa käytetään selkokielisiä nimiä viitattaessa muistiin tallennettuun tietoon. Tällaista nimeä, joka viittaa muistissa olevaan tietoon, kutsutaan muuttujaksi (engl. variable). Ohjelmoijan tarvitsee muistaa vain nimi; käyttöjärjestelmä ja tietokoneen sisäinen logiikka huolehtivat tiedon todellisesta sijainnista muistissa.
Ennen käyttämistä muuttuja tulee määritellä.
tyyppi muuttujanNimi;
Muuttujan tyyppi määritellään muuttujan nimen edessä, ja se kertoo, millaista tietoa muuttuja voi sisältää, esimerkiksi kokonaisluvun, desimaaliluvun tai totuusarvon. Muuttujan nimi on ohjelmoijan valitsema tunniste, jonka avulla muuttujaan viitataan. Nimi voi sisältää kirjaimia ja alaviivoja. Muuttujan nimi ei kuitenkaan voi olla Java-kielessä varattu avainsana eikä muuttujan nimi saa alkaa numerolla.
Muuttujan määrittelyn jälkeen muuttujaan voi sijoittaa lausekkeiden arvoja:
muuttujanNimi = lauseke;
Yhtäsuuruusmerkin oikealla puolella olevan lausekkeen (engl. expression) arvo tallennetaan sen vasemmalla puolella nimettyyn muuttujaan. Jos muuttujissa oli aiemmin jotain muita arvoja, ne korvataan uusilla.
void main() {
double korkokerroin; // Muuttujan määrittely, double = desimaaliluku
double paaoma; // Muuttujan määrittely
korkokerroin = 0.05; // Arvon sijoitus muuttujaan
paaoma = 150.0; // Arvon sijoitus muuttujaan
IO.println("korkokerroin = " + korkokerroin);
IO.println("paaoma = " + korkokerroin);
}
Muuttujaa ei voi käyttää ennen kuin siihen sijoittaa arvon ainakin kerran. Tätä varten voi käyttää yhdistettyä määrittely- ja sijoituslausetta, joka yhdistää muuttujan määrittelyn ja alkuarvon sijoittamisen samalle riville:
double paaoma = 0.05;
// Yllä oleva on sama kuin:
double paaoma;
paaoma = 0.05;
Muuttuja voi olla myös lausekkeen osana, ja siten sen arvoa voidaan käyttää osana sijoitettavaa lauseketta.
void main() {
double korkokerroin = 0.05;
double paaoma = 150.0;
double paaomaKorolla = (1 + korkokerroin) * paaoma;
IO.println("korkokerroin = " + korkokerroin);
IO.println("paaoma = " + korkokerroin);
IO.println("paaomaKorolla = " + paaomaKorolla);
}
Ohjelmoinnissa sijoitus on lause, eli yksittäinen suoritettava käsky.
Esimerkiksi lausetta double paaoma = 150.0; voi ajatella tarkoittavan:
"tallenna luku 150.0 muistiin paikkaan, jota kutsutaan tästä eteenpäin nimellä
paaoma". Muuttujan arvo pysyy samana kunnes jokin toinen lause muokkaa
muuttujan arvoa.
Javan tietotyypit voidaan jakaa kahteen pääryhmään: alkeistietotyyppeihin (engl. primitive data types) ja viitetietotyyppeihin (engl. reference data types). Kaikki tieto tallennetaan tietokoneen muistiin binäärilukuina (nollien ja ykkösten sarjana), ja tietotyypit eroavat toisistaan siinä, kuinka paljon muistia ne varaavat, millaista dataa ne esittävät ja millä säännöillä dataa voi käsitellä. Alkeistietotyypit sisältävät yksinkertaisia arvoja, kuten kokonaislukuja ja totuusarvoja, kun taas viitetietotyypit sisältävät monimutkaisempia rakenteita, kuten olioita, taulukoita ja merkkijonoja.
Alkeistietotyypit
Javassa on kahdeksan sisäänrakennettua alkeistietotyyppiä, joita voi karkeasti jakaa neljään kategoriaan: kokonaisluvut, liukuluvut, merkit ja totuusarvot.
Kokonaisluvut
Kokonaisluvuille on olemassa neljä tyyppiä, jotka eroavat toisistaan
lukualueen ja muistinkulutuksen perusteella. Yleisimmin käytetty
kokonaislukutyyppi on int.
| Tyyppi | Koko (tavua /bittiä) | Lukualue (suuntaa antava) |
|---|---|---|
byte | 1 tavu (8 bittiä) | -128 ... 127 |
short | 2 tavua (16 bittiä) | -32 768 ... 32 767 |
int | 4 tavua (32 bittiä) | n. -2 miljardia ... 2 miljardia |
long | 8 tavua (64 bittiä) | n. +/- 9 * 10^18 |
Huomautus: Muuttujan tyyppi ei vaihdu lennosta
Useissa dynaamisissa ohjelmointikielissä, kuten Pythonissa tai JavaScriptissa, kokonaisluvuille ei välttämättä ole suurinta arvoa: suurille luvuille varataan joko lisää tilaa tai vähemmän merkitseviä numeroita pyöristetään. Tämä ei päde Javassa. Jos laskutoimituksen tuloksena kokonaisluku ylittää muuttujan tyypin lukualueen, luku vuotaa yli (engl. overflow) ja "pyörähtää lukualueen ympäri":
void main() {
int suuriLuku = 2000000000;
IO.println("suuriLuku = " + suuriLuku);
suuriLuku += 1000000000;
IO.println("suuriLuku = " + suuriLuku);
}
Siispä mahdollinen lukualue on otettava huomioon ohjelmaa kirjoittaessa. Jos on mahdollisuus, että laskutoimitus ylittää tyypin lukualueen, on syytä vaihtaa toiseen tietotyyppiin.
Javasta löytyy myös suuria lukuja käsittelevä
BigInteger
-tyyppi, jota ei tällä opintojaksolla käsitellä.
Liukuluvut
Desimaaliluvuille käytetään liukulukutyyppejä. Yleisin näistä on
double.
| Tyyppi | Koko (tavua) | Tarkkuus |
|---|---|---|
| float | 4 tavua (32 bittiä) | n. 7 merkitsevää numeroa |
| double | 8 tavua (64 bittiä) | n. 15 merkitsevää numeroa |
Huomautus: Liukuluvut ovat epätarkkoja!
Java käyttää liukulukuja desimaalilukujen double ja float
esittämiseen. Liukulukuja voidaan ajatella esittävän desimaalilukujen
likiarvoja. Liukulukujen tarkka toiminta on standardoitu (IEEE 754 -standardi);
vaikka ne on tarkoitettu desimaalilukujen esittämiseen, niillä on silti joitain
mielenkiintoisia ja kenties yllättäviä eroja tavallisiin desimaalilukuihin:
void main() {
// Laskutoimitukset voivat erota desimaaliluvuista pyöristysvirheiden takia
double pyoristysVirhe = 0.1 + 0.2;
IO.println("pyoristysVirhe = " + pyoristysVirhe);
// Jakaminen nollalla on määritelty eikä aiheuta virhettä
double negatiivinenAarettomyys = -1.0 / 0.0;
IO.println("negatiivinenAarettomyys = " + negatiivinenAarettomyys);
double positiivinenAarettomyys = 1.0 / 0.0;
IO.println("positiivinenAarettomyys = " + positiivinenAarettomyys);
// Jakolasku 0/0 ei aiheuta virhettä
double nan = 0.0 / 0.0;
IO.println("nan = " + nan);
}
Liukuluvuille on siten erikseen määritelty ja NaN =
Not A Number. Lisäksi liukulukujen väliset laskutoimitukset
voivat sisältää pieniä virheitä johtuen liukulukujen esitystavasta ja
pyöristysvirheistä.
Hyvin tarkkoja laskuja vaativille ohjelmille löytyy myös
BigDecimal
-tyyppi, jota ei tällä opintojaksolla käsitellä.
Merkit
Yksi merkki tallennetaan char-tyyppiseen muuttujaan, joka käyttää
2 tavua muistia.
Totuusarvot
Totuusarvoja varten on boolean-tyyppi, jolla on kaksi
mahdollista arvoa: true (tosi) tai false (epätosi).
Viitetietotyypit
Toisin kuin alkeistietotyypit,
viitetietotyyppinen muuttuja sisältää varsinaisen tiedon sijaan vain pienen,
kiinteän kokoisen arvon eli viitteen (engl. reference).
Viitteen avulla ohjelma pääsee käsiksi varsinaiseen dataan,
jonka kokoa tai sisältöä ei välttämättä tiedetä ennen kuin ohjelma ajetaan.
Javassa käytännössä kaikki muut tietotyypit kuin alkeistietotyypit ovat
viitetietotyyppejä. Esimerkiksi String on viitetietotyyppi, kuten myös kaikki
taulukot ja listat. Alkaen luvusta 2 tutustumme olio-ohjelmointiin; Javassa
kaikki oliot ovat viitetietotyypit.
Kaikkiin viitetyyppimuuttujiin on sallittua sijoittaa erikoisarvo null.
Tämä niin sanottu null-viite merkitsee, että muuttuja ei sisällä viitettä
mihinkään tietoon. Yritys muokata tai lukea muuttujaa, jonka arvo on null,
tuottaa yleensä virheen ohjelman ajon aikana:
String teksti = null;
String tekstiIsolla = teksti.toUpperCase();
IO.println(tekstiIsolla);
java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "<local1>" is null
at main.main(main.java:3)
Koska virhe tapahtuu vasta ajon aikana, Javassa on yleensä ohjelmoijan vastuulla varmistaa, että muuttujan tai funktion parametrin arvona ei ole null-viite. Varmistus voidaan tehdä esimerkiksi ehtorakenteella, jotka esitetään seuraavassa alaluvussa.
Valinnaista lisätietoa: Miksi viitetietotyyppejä on olemassa?
On useita syitä sille, miksi nämä kaksi eri kategoriaa tietotyypeille on olemassa.
Ensimmäinen liittyy suorituskykyyn ja muistin hallintaan. Jos kaikki muuttujat
olisivat arvopohjaisia (kuten alkeistietotyypit), se aiheuttaisi valtavasti
turhaa muistin kulutusta ja hidastaisi ohjelman suorituskykyä, erityisesti
suurten tietorakenteiden kohdalla. Jos meillä olisi vaikkapa kirja, joka
sisältäisi 1000 sivua tekstiä, niin joka ikinen kerta kun haluamme käsitellä
kirja-muuttujaa, meidän pitäisi kopioida kaikki 1000 sivua muistissa. Tämä
olisi erittäin tehotonta. Sen sijaan viitetietotyypit mahdollistavat sen, että
vain viittaamme kirja-olioon, joka sijaitsee jossakin muualla muistissa,
ilman että tarvitsee kopioida koko kirjaa joka kerta.
Toinen syy liittyy jaettuun tilaan. Usein haluamme, että useampi ohjelman osa
muokkaa samaa tietoa. Esimerkiksi on järkevää, että pankkitili-olio on jaettu
useiden eri toimintojen kesken, kuten talletus, nosto ja tilin saldo.
Arvopohjaisessa maailmassa joutuisimme kopioimaan pankkitili-olion joka kerta,
kun tililtä halutaan nostaa rahaa, tehdä tilisiirto tai vaikkapa tarkistaa
saldo. Tämä johtaisi helposti siihen, että eri kopiot olisivat eri tilassa, mikä
saattaisi aiheuttaa virheitä.
Kolmas syy on dynaaminen koko. Viitetietotyypit mahdollistavat dynaamisesti kasvavien ja kutistuvien tietorakenteiden, kuten linkitettyjen listojen, pinojen ja jonojen, luomisen. Näitä rakenteita ei voida helposti toteuttaa arvopohjaisina, koska arvopohjaisten muuttujien koko on kiinteä käännösaikana.
Neljäs syy liittyy erityisesti olio-ohjelmointiin, ja liittyy osittain myös kolmanteen kohtaan. Javassa viitteet mahdollistavat polymorfismin. Koska muuttuja on vain viite, se voi osoittaa mihin tahansa, joka "näyttää" oikealta tyypiltä.
Elain lemmikki = new Koira();
lemmikki = new Kissa();
Jos nämä olisivat puhtaita arvotyyppejä, Elain-tyyppiselle muuttujalle pitäisi
varata kiinteä määrä muistia. Jos Kissa sitten tarvitsisikin enemmän muistia
kuin Elain on varannut, koodi hajoaisi. Viitteiden avulla muuttujan koko on
aina sama (viitteen koko), riippumatta siitä kuinka valtava olio viitteen päässä on.
Literaalit
Literaali (engl. literal) tarkoittaa ohjelmakoodiin kirjoitettua kiinteää arvoa. Eri tietotyypeillä on omat kirjoitussääntönsä literaaleille.
- Merkit (
char): Kirjoitetaan yksittäisen lainausmerkin sisään, esimerkiksi'A','*'ja'x'. Erikoismerkit alkavat kenoviivalla:'\n'(rivinvaihto),'\u03A9'(kreikkalainen iso omega) ja'\t'(tabulaattori). - Kokonaisluvut (
byte,short,int,long): Kirjoitetaan suoraan numerona, esimerkiksi42,-7ja0.long-luvun literaali päättyy isoon tai pieneen kirjaimeenLtail, esimerkiksi12345678901L. - Liukuluvut (
float,double): Kirjoitetaan desimaalipisteellä erotettuna, esimerkiksi3.14,-0.001ja2.0. Voidaan käyttää myös tieteellistä muotoa:1.5e3(eli 1.5 × 10³ = 1500) ja2.0E-4(eli 2.0 × 10⁻⁴ = 0.0002). Oletuksena desimaaliluvut ovatdouble-tyyppiä. Jos haluat luodafloat-luvun, literaalin tulee päättyä isoon tai pieneen kirjaimeenFtaif, esimerkiksi3.14f. - Totuusarvot (
boolean): Kirjoitetaan avainsanoinatruejafalse.
void main() {
char merkki = 'A';
IO.println("merkki = " + merkki);
int luku = 123;
IO.println("luku = " + luku);
long isoLuku = 12345678901L;
IO.println("isoLuku = " + isoLuku);
double liukuluku = -2.0;
IO.println("liukuluku = " + liukuluku);
float pieniLiukuluku = 2.0f;
IO.println("pieniLiukuluku = " + pieniLiukuluku);
double tieteellinenMuoto = 1.5e-2;
IO.println("tieteellinenMuoto = " + tieteellinenMuoto);
boolean totuusarvo = true;
IO.println("totuusarvo = " + totuusarvo);
}
Käärijäluokat
Javassa kullekin alkeistietotyypille on olemassa niin sanottu käärijäluokka
(engl. wrapper class). Käärijäluokka "käärii" alkeistietotyypin arvon olion
sisään, jolloin alkeistietotyypin arvoa voidaan käsitellä oliona. Esimerkiksi
int-tyypin käärijäluokka on Integer. Käärijäluokista löytyy hyödyllisiä
metodeja, kuten toString() sekä vakioita, kuten MAX_VALUE. Alkeistietotyypit
ja niitä vastaavat käärijäluokat on esitetty alla olevassa taulukossa.
| Alkeistietotyyppi | Käärijäluokka |
|---|---|
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| char | Character |
| boolean | Boolean |
Alla olevassa esimerkissä käytetään käärijäluokkien MAX_VALUE-vakioita,
tulostetaan primitiivityyppien käärittyjä arvoja, sekä havainnollistetaan
kokonaisluvun lukualueen ylitystä.
void main() {
byte tavu = Byte.MAX_VALUE;
short kaksiTavua = Short.MAX_VALUE;
IO.println(tavu);
IO.println(kaksiTavua);
IO.println(Short.toString(kaksiTavua).charAt(0));
int maksimi = Integer.MAX_VALUE;
IO.println(maksimi + " on suurin luku, jonka voi tallettaa int-tyyppiseen muuttujaan" );
int ylivuoto = Integer.MAX_VALUE + 1;
IO.println("Ylitetään lukualue:");
IO.println(ylivuoto);
}
Merkkijonot
Javassa merkkijonoja ei lasketa alkeistietotyypiksi.
Java-kieli tarjoaa kuitenkin merkkijonoille oman syntaksin: uuden merkkijonon
voi luoda kirjoittamalla merkkejä lainausmerkkien " väliin:
void main() {
String jono = "Opiskelen ohjelmointia!";
IO.println("jono = " + jono);
}
Javassa merkkijono on muuttumaton. Jos yrität suorittaa jonkin operaation merkkijonolle, saat tulokseksi uuden merkkijonon, eikä alkuperäinen merkkijono muutu. Katsotaan tästä esimerkki:
void main() {
String muuttumaton = "Tämä on muuttumaton.";
IO.println("muuttumaton = " + muuttumaton);
muuttumaton.concat("Vai onko sittenkään?");
IO.println("muuttumaton = " + muuttumaton);
}
Metodin concat() palauttamaa uutta merkkijonoa ei nyt tallenneta mihinkään,
ja alkuperäinen merkkijono pysyy ennallaan. Toisin sanoen, merkkijonomuuttujien
arvoa voi muuttaa vain sijoittamalla:
void main() {
String muuttumaton = "Tämä on muuttumaton.";
IO.println("muuttumaton = " + muuttumaton);
// HIGHLIGHT_GREEN_BEGIN
muuttumaton = muuttumaton.concat("Vai onko sittenkään?");
// HIGHLIGHT_GREEN_END
IO.println("muuttumaton = " + muuttumaton);
}
String-tyyppi sisältää lukuisia hyödyllisiä metodeja. Alla on lueteltu joitain
niistä.
| Metodi | Selitys |
|---|---|
jono.charAt(paikka) | Palauttaa jonossa olevan yksittäisen merkin indeksistä paikka. |
jono.length() | Palauttaa jonossa olevien merkkien määrän eli jonon pituuden. |
jono.trim() | Palauttaa kopion jonosta ilman alussa ja lopussa olevia ylimääräisiä tyhjiä merkkejä. |
jono.replace(mitä, millä) | Palauttaa kopion jonosta, jossa jonot/merkit mitä on korvattu jonolla/merkillä millä. |
jono.split(haku) | "Pilkkoo" jonon osiin haku-jonon esiintymien kohdalla ja palauttaa taulukon pilkotuista osajonoista. Huomaa, että haku on ns. säännöllinen lauseke. |
jono.contains(etsittävä) | Palauttaa true, jos etsittävä löytyy jonosta. |
jono.indexOf(etsittävä) | Palauttaa indeksin, jossa etsittävä esiintyy ensimmäistä kertaa. |
jono.substring(mistä, mihin) | Palauttaa osan jonosta alkaen indeksistä mistä päättyen indeksiin mihin. |
String.join(merkki, jonot) | Palauttaa jonon, jossa taulukossa jonot olevat jonot ovat peräkkäin yhdistettynä merkillä merkki). |
Kaikki metodit ja niiden tarkat selitykset löytyvät JavaDocs-sivulta (ks. Class
String).
Katsotaan vielä, miten yllä olevia esimerkkejä voi käyttää:
void main() {
String mjono = "Opiskelen ohjelmointia java-kielellä.";
IO.println("mjono = " + mjono);
IO.println("Ensimmäinen merkki: " + mjono.charAt(0));
IO.println("Jonon pituus: " + mjono.length());
IO.println(); // Lisää ylimääräisen rivivaihdon
// Merkkijonojen yhdistäminen onnistuu + operaattorilla
mjono = mjono + " Hei maailma!";
IO.println("mjono (lisäyksen jälkeen) = " + mjono);
// Tekstin korvaaminen merkkijonossa
mjono = mjono.replace("java", "Java");
IO.println("mjono (korvattu java -> Java) = " + mjono);
IO.println();
// "Pilkkoo" jonon kahteen osajonoon viivan kohdalla
// HUOM: split-metodissa merkit \^$.|?*+()[]{}
// vaativat, että niiden eteen laitetaan kaksi kenoviivaa \\
// Eli jos haluttaisiin pilkkoa pisteen kohdalla, tulisi kirjoittaa
// mjono.split("\\.") eikä mjono.split(".");
// jälkimmäinen versio on ns. säännöllinen lauseke (regular expression),
// joka pilkkoo jonon jokaisen merkin kohdalla
String[] lauseet = mjono.split("\\.");
IO.println("lauseet = " + Arrays.toString(lauseet));
IO.println();
// indexOf etsii indeksin, jossa annettu jono löytyy
int ohjelmointiaPaikka = mjono.indexOf("ohjelmointia");
IO.println("ohjelmointiaPaikka = " + ohjelmointiaPaikka);
// substring palauttaa osajonon annetusta jonosta indeksin perusteella
String osajono = mjono.substring(ohjelmointiaPaikka, ohjelmointiaPaikka + 12);
IO.println("osajono = " + osajono);
IO.println();
// Operaatio "String + lauseke" muuntaa lausekkeen arvon merkkijonoksi
String toinenJono = "1/2 = " + (1.0 / 2);
IO.println("toinenJono = \"" + toinenJono + "\"");
}
Luvun parsiminen merkkijonosta
Merkkijono voidaan muuntaa luvuksi käyttämällä käärijäluokkien parse-alkuisia
metodeja. Esimerkiksi Integer.parseInt muuntaa merkkijonon kokonaisluvuksi ja
Double.parseDouble muuntaa merkkijonon desimaaliluvuksi.
void main() {
String kokonaislukuJono = "42";
int kokonaisluku = Integer.parseInt(kokonaislukuJono);
IO.println("kokonaisluku = " + kokonaisluku);
String desimaalilukuJono = "3.14";
double desimaaliluku = Double.parseDouble(desimaalilukuJono);
IO.println("desimaaliluku = " + desimaaliluku);
}
StringBuilder
Käytä StringBuilder-luokkaa, kun tarvitset muunneltavan merkkijonon. Se
tarjoaa menetelmiä merkkijonon muokkaamiseen ilman uusien merkkijono-olioiden
luomista, mikä tehostaa muistin käyttöä.
Alla on lueteltu joitain StringBuilder-luokan hyödyllisiä metodeja.
| Metodi | Selitys |
|---|---|
sb.charAt(paikka) | Palauttaa jonossa olevan yksittäisen merkin indeksistä paikka. |
sb.length() | Palauttaa jonossa olevien merkkien määrän eli jonon pituuden. |
sb.append(arvo) | Lisää arvon nykyisen jonon loppuun. |
sb.toString() | Palauttaa kopion tästä jonosta String-merkkijonona. |
Kaikki toiminnot ja niiden tarkat selitykset löytyvät JavaDocs-sivulta (ks. Class
StringBuilder).
Alla on esimerkkejä metodien käytöstä.
void main() {
StringBuilder muuttuva = new StringBuilder("Tämä on muuttuva");
IO.println("muuttuva = " + muuttuva);
IO.println("muuttuva.length() = " + muuttuva.length());
IO.println();
muuttuva.append(" merkkijono.");
IO.println("muuttuva = " + muuttuva);
IO.println("muuttuva.length() = " + muuttuva.length());
IO.println();
String muuttumatonKopio = muuttuva.toString();
IO.println("muuttumatonKopio = " + muuttumatonKopio);
}
Taulukot
Taulukkoja (engl. array) käytetään tallentamaan joukkoa samantyyppisiä alkioita muuttujaan. Tämä helpottaa datan organisointia.
Uuden taulukon määrittely ja luominen Javassa onnistuu seuraavasti:
Tyyppi[] nimi = new Tyyppi[koko];
Tässä new Tyyppi[koko] luo taulukon, joka sisältää koko kappaletta alkioita,
joiden tyyppi on Tyyppi. Taulukon luomisen jälkeen alkioiden arvoja voi
asettaa käyttäen sijoituslausetta seuraavasti:
void main() {
int[] arvosanat = new int[4];
arvosanat[0] = 4;
arvosanat[1] = 2;
arvosanat[2] = 2;
arvosanat[3] = 5;
IO.println("arvosanat = " + Arrays.toString(arvosanat));
}
Sijoituslauseessa [numero] tarkoittaa alkion paikkaa eli indeksiä
taulukossa. Javassa indeksointi alkaa nollasta, eli ensimmäinen alkio on
indeksissä 0, toinen indeksissä 1, ja niin edelleen. Taulukon viimeisen alkion
indeksi on aina taulukko.length - 1.
Jos alkioiden arvot tunnetaan etukäteen, taulukon voi myös luoda seuraavasti.
void main () {
int[] arvosanat = new int[] {4, 2, 2, 5};
IO.println("arvosanat = " + Arrays.toString(arvosanat));
}
Yhdistetyssä muuttujan määrittely- ja sijoituslauseessa new Tyyppi[]-osa
on myös sallittua pudottaa pois, jolloin yllä olevaa voi vielä hieman tiivistää.
void main () {
int[] arvosanat = {4, 2, 2, 5};
IO.println("arvosanat = " + Arrays.toString(arvosanat));
}
Javassa taulukkojen kokoa ei voi muuttaa taulukon luomisen jälkeen. Arvon sijoittaminen indeksiin, jota ei ole taulukossa, aiheuttaa ajonaikaisen virheen.
int[] arvosanat = new int[] {4, 2, 2, 5};
arvosanat[5] = 3;
java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 4
at main.main(main.java:3)
Javassa taulukon pituuden voi aina tarkistaa length-attribuutilla.
Lisäksi taulukon voi tulostaa käyttämällä Arrays.toString-metodia (ks.
JavaDocs)
void main() {
int[] arvosanat = new int[] {4, 2, 2, 5};
IO.println("Taulukon pituus: " + arvosanat.length);
IO.println("Taulukon sisältö: " + Arrays.toString(arvosanat));
}
Moniulotteiset taulukot
Toisin kuin esimerkiksi C#-kielessä, Javassa ei ole erillisiä moniulotteisia
taulukkoja. Sen sijaan Javassa voi luoda taulukon, jonka alkioina
ovat taulukot, eli Tyyppi[][]:
void main() {
int[][] taulukko2D = new int[][] {
new int[] {1, 2, 3},
new int[] {4, 5, 6, 7},
new int[] {8, 9, 0},
};
// Taulukon pituus on tässä siten taulukkojen lukumäärä, eli "rivien" määrä.
IO.println("Taulukossa on " + taulukko2D.length + " taulukkoa.");
// Indeksointi toimii normaalisti; alkiona on nyt kokonainen taulukko
IO.println("taulukko2D[0] on taulukko: " + Arrays.toString(taulukko2D[0]));
IO.println("taulukko2D[1] on taulukko: " + Arrays.toString(taulukko2D[1]));
IO.println("taulukko2D[2] on taulukko: " + Arrays.toString(taulukko2D[2]));
// Myös yksittäisen taulukon indeksointi toimii normaalisti,
// mutta huomaa syntaksi!
int ensimmainenAlkio = taulukko2D[0][0];
IO.println("Ensimmäisen rivin ensimmäinen alkio: " + ensimmainenAlkio);
IO.println("Rivillä 2 (indeksi 1) kolmas alkio (indeksi 2) on: " + taulukko2D[1][2]);
}
Huomaa, että yllä olevassa esimerkissä taulukko2D[0][0] viittaa
ensimmäisen taulukon (taulukko2D[0]) ensimmäiseen alkioon
(taulukko2D[0])[0]. Yllä oleva taulukko ja siinä olevat alkiot voisi kuvata
siis seuraavasti:
Huomaa erityisesti, että "rivitaulukkojen" ei tarvitse olla välttämättä samanpituuisia.
Vakiot
Muuttuja, jolle voidaan sijoittaa arvo vain alustuksen yhteydessä, määritellään
käyttämällä final-avainsanaa. Javan koodauskäytänteisiin kuuluu, että
final-muuttujat kirjoitetaan suuraakkosin ja sanat erotellaan toisistaan
alaviivalla.
Javassa final-avainsanaa voi käyttää sekä alkeistietotyyppien että
viitetietotyyppien kanssa. On kuitenkin huomattava, että viitetietotyyppisen
muuttujan tapauksessa final-sana tarkoittaa, että viitettä ei voi muuttaa
osoittamaan uuteen dataan. Viitteen päässä olevaa dataa voi silti muuttaa, jos
tietotyyppi sallii sen.
final int PAIVIA_VIIKOSSA = 7;
final int[] PAIVIA_KUUKAUDESSA_KARKAUSVUOSI = new int[] {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
// PAIVIA_VIIKOSSA = 8; // Tämä aiheuttaa käännösvirheen
PAIVIA_KUUKAUDESSA_KARKAUSVUOSI[0] = 30; // Tämä on sallittu
Vakioita tarvitaan mm. koodin lukemisen helpottamiseksi, toisteisen koodin vähentämiseksi, luotettavuuden parantamiseksi ja suorituskyvyn parantamiseksi.
Listat
Lista on tietorakenne, joka voi kasvaa ja kutistua tarpeen mukaan. Kuten taulukko, lista voi sisältää vain yhden tyypin mukaisia alkioita. Listan koko ei ole kiinteä, mikä tekee siitä joustavamman tilanteisiin, joissa alkioiden määrä ei ole etukäteen tiedossa. Javan listat vastaavat siten JavaScriptin taulukkoja.
Javan yleisin listan tyyppi on ArrayList<T>, jossa T on listassa olevien
alkioiden tyyppi. Jotta listoja voi käyttää koodissa, tulee ne ensin tuoda
kääntäjän näkyville lisäämällä tiedoston ensimmäiselle riville import-määre:
import java.util.*;
// Varsinainen ohjelma
void main() ...
import-määre kertoo kääntäjälle, että ohjelmassa käytetään tyyppejä, jotka
löytyvät java.util-pakkauksesta. Lyhyesti, pakkaus on tapa järjestellä luokkia
yhteisen nimittäjän alle. Jos olet käyttänyt C#:a aiemmin, likimain vastaava käsite
siellä on nimiavaruus (namespace). Pakkauksiin palataan tarkemmin myöhemmissä
osissa; tässä vaiheessa riittää tiedostaa, että kääntäjä ei välttämättä tiedä
tyyppien olemassaolosta ellei ne tuo näkyviin import-määreellä.
Kun java.util-pakkauksen sisältö on tuotu ohjelmaan, voidaan listoja alustaa
seuraavasti:
import java.util.*;
void main() {
// Tapa 1: Tyhjä lista ilman alkioita
List<Integer> arvosanat = new ArrayList<Integer>();
// Lisätään alkiot yksi kerrallaan
arvosanat.add(4);
arvosanat.add(2);
arvosanat.add(2);
arvosanat.add(5);
IO.println("arvosanat = " + arvosanat);
// Tapa 2: Lista, jossa alkiot annettu valmiiksi
List<Integer> arvosanatValmis = new ArrayList<Integer>(List.of(4, 2, 2, 5));
IO.println("arvosanatValmis = " + arvosanatValmis);
}
Javan rajoituksista johtuen listan alkioiden tyypin on aina oltava viitetyyppi.
Niinpä esimerkiksi int-alkioita sisältävää listaa ei voi kirjoittaa muodossa
ArrayList<int>:
List<int> lista = new ArrayList<int>();
error: unexpected type
List<int> lista = new ArrayList<int>();
^
required: reference
found: int
Jos tarvitset listoja, jonka alkioina ovat alkeistietotyypit, käytä alkioiden
tyyppinä alkeistietotyyppien käärijäluokat, jotka ovat
viitetyyppejä, mutta toimivat kuten niitä vastaavat alkeistietotyypit. Toisin
sanoen, ArrayList<Integer> on sallittu, kun taas ArrayList<int> ei ole.
Puolestaan ArrayList<String> on sallittu, koska merkkijono on
viitetietotyyppi.
huomautus
Javan koodauskäytänteisiin kuuluu, että listamuuttujien tyyppinä käytetään
List<T>, kun taas muuttujien arvojen alustuksessa käytetään
tarkempaa tyyppiä, kuten ArrayList<T>.
Toisin sanoen, vaikka alla oleva on sallittu
void main() {
ArrayList<String> nimet = new ArrayList<String>(List.of("Matti", "Teppo"));
IO.println("nimet = " + nimet);
}
koodauskäytänteiden mukaisesti on yleisempää esittää muuttuja seuraavasti:
void main() {
List<String> nimet = new ArrayList<String>(List.of("Matti", "Teppo"));
IO.println("nimet = " + nimet);
}
Lisäksi, jos alkioiden tyyppi on muuttujan määrittelyrivin perusteella selvä, alkioiden tyyppi saatetaan usein jättää pois listan alustuksesta:
void main() {
// Kääntäjä päättelee, että ArrayList<> = ArrayList<String>
// muuttujan tyypin perusteella
List<String> nimet = new ArrayList<>(List.of("Matti", "Teppo"));
IO.println("nimet = " + nimet);
}
Palaamme tarkemmin List<T> ja ArrayList<T> -tyyppien välisiin eroihin
osassa 5. Voit tässä vaiheessa kuitenkin miettiä,
että List<T> on yleinen tyyppi listoille, kun taas ArrayList<T> on
(eräs) Javan tarjoama tapa esittää lista.
Katsotaan vielä listojen eräitä hyödyllisiä metodeja.
| Metodi | Selitys |
|---|---|
size() | Palauttaa listassa olevien alkioiden lukumäärän. |
add(lisättävä) | Lisää alkion listan loppuun. |
add(indeksi, lisättävä) | Lisää alkion listan indeksiin indeksi siirtäen loput alkiot yhden paikan eteenpäin. |
get(indeksi) | Palauttaa indeksissä indeksi olevan alkion. |
remove(poistettava) | Poistaa listasta poistettava:n ensimmäisen esiintymän siirtäen loput alkiot yhden paikan taaksepäin. |
remove(indeksi) | Poistaa listasta paikassa indeksi olevan alkion. |
Löydät lisää metodeja JavaDocs-sivustolla (ks. Class ArrayList<E>).
import java.util.*;
void main () {
// Luodaan tyhjä merkkijonolista
List<String> nimet = new ArrayList<>();
// Lisätään alkioita listaan
nimet.add("Matti");
nimet.add("Teppo");
nimet.add("Liisa");
// Tulostetaan listan koko
IO.println("Listan koko: " + nimet.size());
IO.println("------");
// Haetaan alkio indeksistä 1 (toinen alkio)
String toinen = nimet.get(1);
IO.println("Toinen alkio: " + toinen);
IO.println("------");
// Poistetaan alkio indeksistä 0 (ensimmäinen alkio)
nimet.remove(0);
IO.println("Poistettiin ensimmäinen alkio.");
IO.println("------");
// Tulostetaan kaikki alkiot
IO.println("nimet = " + nimet);
IO.println("------");
// Tulostetaan listan koko
IO.println("Listan koko: " + nimet.size());
// Kaksi esimerkkiä siitä, kuinka luoda listaan heti sisältöä
List<String> elaimet = new ArrayList<>(List.of("koira", "kissa", "kala"));
List<String> varit = Arrays.asList("punainen", "sininen", "keltainen");
IO.println("elaimet = " + elaimet);
IO.println("varit = " + elaimet);
}
Huomaa ainakin nämä erot Javan, C# ja Pythonin välillä listoja käytettäessä.
Muuttuja i viittaa listan indeksiin.
| Toiminto | Java | C# | Python |
|---|---|---|---|
| Lukeminen tietystä paikasta | list.get(i) | list[i] | list[i] |
| Listan koko | list.size() | list.Count | len(list) |
| Poistaminen | list.remove(i) | list.RemoveAt(i) | list.pop(i) |
| Onko lista tyhjä? | list.isEmpty() tai list.size() == 0 | list.Count == 0 | if not list: tai len(list) == 0 |
Javan tyyppijärjestelmä
Java on staattisesti tyypitetty kieli, mikä tarkoittaa, että muuttujien tyypit määräytyvät käännösaikana, ei ohjelman ajon aikana. Jos yrität sijoittaa muuttujaan väärän tyyppistä tietoa, ohjelma ei käänny, ja kääntäjä antaa virheilmoituksen.
Käytännössä Javassa eri tietotyyppejä ei voi käyttää toistensa sijaan, ellei
kieli nimenomaisesti salli sitä. Esimerkiksi totuusarvoa (boolean) ei voi
käyttää lukuarvona, eikä viitetyyppistä arvoa voi käsitellä kokonaislukuna. Jos
ohjelmoija yrittää rikkoa näitä sääntöjä, seurauksena on käännösvirhe.
void main() {
boolean totuusarvo = false;
totuusarvo = 1;
}
error: incompatible types: int cannot be converted to boolean
totuusarvo = 1;
^
1 error
error: compilation failed
Yllä oleva käännösvirhe kertoo, että kokonaislukua (int) ei voida muuntaa
totuusarvoksi (boolean). Tämä on selkeä ero dynaamisesti tyypitettyihin
kieliin, kuten Pythoniin tai JavaScriptiin, jossa tyyppi määräytyy
ohjelman ajon aikana, mahdollistaen erityyppisten arvojen sijoittamisen samaan
muuttujaan:
// Tämä on sallittu koodi JavaScriptissa
let totuusarvo = true;
console.log(`totuusarvo = ${totuusarvo}`);
totuusarvo = 1;
console.log(`totuusarvo = ${totuusarvo}`);
On kuitenkin väistämätöntä, että ohjelmassa tulee käsitellä useita erityyppisiä arvoja. Tätä varten Javassa on valmiiksi määritelty joitain automaattisia sääntöjä, joiden perusteella kääntäjä osaa tehdä implisiittisen tyyppimuunnoksen sijoituksissa ja lausekkeissa. Esimerkiksi
- kokonaislukuja (
int) voidaan automaattisesti muuntaa desimaaliluvuksi (double), - pienempiä kokonaislukuja (esim. 8-bittinen kokonaisluku
byte) voidaan "laajentaa" suurempiin kokonaislukuihin (esim. 32-bittinen kokonaislukuint).
Tyyppimuunnossääntöjä on paljon lisää; yleisperiaate on, että jos muunnos ei aiheuta tiedon menetystä, sille on todennäköisesti olemassa implisiittinen muunnos.
void main() {
int kokonaisluku = 23;
IO.println("kokonaisluku = " + kokonaisluku);
double desimaaliluku = kokonaisluku; // OK: int -> double muunnos on implisiittinen
IO.println("desimaaliluku = " + desimaaliluku);
// HUOM: jakolasku on int / int => desimaalit häviävät
double puoletVirhe = 1 / 2;
IO.println("puoletVirhe = " + puoletVirhe);
// OIKEIN: jakolasku on int / double -> double / double
double puoletOikein = 1 / 2.0;
IO.println("puoletOikein = " + puoletOikein);
}
Lisäksi ohjelmoija voi erikseen pakottaa ns. eksplisiittisen tyyppimuunnoksen
käyttämällä syntaksia (uusiTyyppi)muuttujanNimi. Tämä soveltuu tilanteisiin,
jossa muunnos ei olisi mahdollista implisiittisesti:
void main() {
long sairaanIsoLuku = 40000000000L; // long = 64-bittinen luku
IO.println("Iso, long-tyyppinen luku: " + sairaanIsoLuku);
// long -> int ei ole implisiittinen, mutta se onnistuu eksplisiittisesti
int katkaistu = (int)sairaanIsoLuku; // int = 32-bittinen luku
IO.println("int-luku eksplisiittisen tyyppimuunnoksen jälkeen: " + katkaistu);
}
Staattinen tyypitys tarkoittaa Javassa käytännössä sitä, että tyyppeihin liittyvät virheet pyritään estämään jo ennen ohjelman suorittamista. Kääntäjä toimii eräänlaisena turvaverkkona, joka varmistaa, että arvot, muuttujat ja operaatiot ovat keskenään yhteensopivia.
Ohjausrakenteet ja perustietorakenteet
osaamistavoitteet
- Ehtolauseet (
if,switch) - Toistolauseet (
for,while,do-while), ja listatyyppiset tietorakenteet - Tiedostat, että Javassa merkkijonoja verrataan
equals-aliohjelmalla eikä==-operaattorilla
Ohjelmointi on harvoin pelkkää koodirivien suorittamista peräkkäin. Jotta ohjelmista saadaan hyödyllisiä, niiden täytyy pystyä tekemään päätöksiä, toistamaan asioita ja hallinnoimaan tietoa järkevästi. Tässä luvussa käymme läpi Javan logiikan, toistorakenteet sekä kaksi tapaa säilöä tietoa: perinteiset taulukot ja joustavat listat.
Vertailuoperaattorit
Ennen kuin voimme opettaa ohjelmaa tekemään valintoja ("jos tämä, niin tuo"), meidän täytyy ymmärtää, miten tietokone näkee maailman. Tietokoneen logiikka on binääristä: väittämät ovat joko totta (true) tai epätotta (false). Vertailuoperaattorit ovat kuin kysymyksiä, jotka palauttavat vastaukseksi totuusarvon. Tässä ovat yleisimmät vertailuoperaattorit Javassa.
| Operaattori | Merkitys | Esimerkki (kun x=5, y=3) | Tulos |
|---|---|---|---|
| == | Yhtä suuri kuin | x == y | false |
| != | Eri suuri kuin | x != y | true |
| > | Suurempi kuin | x > y | true |
| < | Pienempi kuin | x < 4 | false |
| >= | Suurempi tai yhtä suuri | x >= 5 | true |
| <= | Pienempi tai yhtä suuri | y <= 3 | true |
Usein päätökset riippuvat useammasta kuin yhdestä asiasta. Esimerkiksi: "Menen ulos, JOS ei sada JA minulla on vapaa-aikaa". Tätä varten tarvitsemme loogisia operaattoreita yhdistämään ehtoja.
-
&&(JA / AND): Lauseke on tosi vain, jos molemmat ehdot ovat tosi. -
||(TAI / OR): Lauseke on tosi, jos edes toinen ehdoista on tosi. -
!(EI / NOT): Kääntää totuusarvon päinvastaiseksi (tosi muuttuu epätodeksi).
varoitus
Älä sekoita toisiinsa sijoitusoperaattoria = ja
vertailuoperaattoria ==.
if (x = 5)yrittää asettaa x:n arvoksi 5 (virhe)if (x == 5)kysyy, onko x:n arvo 5 (oikein)
Viitetietotyyppisten muuttujien vertailu
Toisin kuin primitiivityypeillä (int, double, jne.), Javassa
==-operaattori vertaa viitetyyppien kohdalla viitteitä, eikä sisältöä. Tästä
syystä merkkijonojen ja muiden
viitetyyppimuuttujien sisällön vertailuun tulee käyttää equals()-metodia.
void main() {
String mjono1 = "Slush";
String mjono2 = new String("Slush"); // Luodaan pakolla uusi merkkijono
// VÄÄRIN: Vertaa viitteitä -> tulostaa false
IO.println(mjono1 == mjono2);
// OIKEIN: Vertaa sisältöjä -> tulostaa true
IO.println(mjono1.equals(mjono2));
}
Vertailu null-viitteeseen
Kuitenkin null-viitteen tarkistus voidaan tehdä ==-operaattorilla, koska kyseessä on juuri viitteiden vertailu:
void main() {
String mjono = null;
IO.println(mjono == null);
}
Aliohjelmat usein käyttävät null-viitettä esittämään arvon puuttumista.
Esimerkiksi IO.readln() voi
JavaDoc-dokumentaation
perusteella palauttaa null, jos syötettä ei voitu lukea. Näin voi käydä, jos
esimerkiksi käyttäjä lopettaa ohjelman suorituksen kesken. Silloin voi olla
järkevää tarkistaa, että muuttujan arvo ei ole null ennen muuttujan käyttöä:
void main() {
String syote = IO.readln("Anna syöte");
if (syote == null) {
// Tehdään jotain, jos syötettä ei olekaan annettu
// Esimerkiksi: poistutaan ohjelmasta
return;
}
// Tässä ollaan varmoja, että syote-muuttujassa on edes jokin merkkijono
IO.println("Teksti huudettuna: " + syote.toUpperCase());
}
Ehtolauseet
Ehtolauseilla ohjataan ohjelman kulkua. Ehtolauseiden muodostamiseen tarvitaan aina yksi tai useampi totuusarvoinen ehtolauseke.
If-rakenne
Perusmuotoinen ehtolause on if. Sen sisällä oleva koodilohko suoritetaan vain, jos sulkeissa oleva totuuslauseke on tosi (true). Usein tarvitsemme myös vaihtoehtoisia reittejä, jolloin käytämme else if ("muuten jos") ja else ("muuten") -rakenteita.
If-lauseiden syntaksi Javassa on seuraavanlainen:
if (pisteet >= 90) {
IO.println("Arvosana: 5");
} else if (pisteet >= 50) {
IO.println("Arvosana: Läpi");
} else {
// Suoritetaan, jos mikään yllä olevista ei toteutunut
IO.println("Arvosana: Hylätty");
}
Switch-rakenne
Kun halutaan verrata yhden muuttujan sisältämää arvoa useisiin yksittäisiin arvoihin (esimerkiksi valikon valinta), switch-rakenne voi olla selkeämpi kuin pitkä if-else-ketju.
int valinta = 2;
switch (valinta) {
case 1:
IO.println("Valitsit vaihtoehdon 1");
break; // Tärkeä: lopettaa suorituksen tässä lohkossa
case 2:
IO.println("Valitsit vaihtoehdon 2");
break;
default:
IO.println("Tuntematon valinta");
}
Kolmiarvoinen operaattori
Yksinkertaisissa "joko-tai"-tilanteissa, joissa halutaan sijoittaa arvo
muuttujaan ehdon perusteella, voidaan käyttää kolmiarvoista operaattoria
(ternary operator) ?. Se tiivistää koodia merkittävästi.
Syntaksi: (ehto) ? arvo_jos_tosi : arvo_jos_epätosi;
Koodiesimerkki:
void main() {
int luku1 = 5;
int luku2 = 8;
// Luetaan: Jos luku1 on suurempi kuin luku2,
// sijoita suurempi-muuttujaan luku1, muuten luku2
int suurempi = (luku1 > luku2) ? luku1 : luku2;
IO.println("Suurempi luvuista on: " + suurempi);
}
Silmukat
Silmukoita tarvitaan, kun halutaan suorittaa asioita toistuvasti. Javassa on
neljä päätapaa toteuttaa toistorakenteita: for, for-each, while ja
do-while.
For
Käytä for-silmukkaa, kun tiedät etukäteen toistojen määrän tai tarvitset indeksiä (järjestysnumeroa) toiston aikana. Rakenne on seuraava.
for (alustus; toistoehto; päivitys) {
// silmukan runko
}
Alla on esimerkki summan laskemisesta for-silmukassa.
void main () {
int[] luvut = {1, 2, 3, 4};
int summa = 0;
// Käydään taulukko läpi indeksien 0, 1, 2, 3 avulla
for (int i = 0; i < luvut.length; i++) {
summa += luvut[i];
}
IO.println("Summa on: " + summa);
}
Alustus, toistoehto ja päivitys voidaan periaatteessa jättää jopa tyhjiksi, mutta puolipisteiden on pakko olla paikallaan. For-silmukalla voidaan tehdä ikuinen silmukka jättämällä toistoehto tyhjäksi, joskin tämä on harvoin tarkoituksenmukaista.
For-Each
For-each-silmukka on usein luettavin ja myös turvallisin tapa käydä läpi koko tietorakenne. Kun et tarvitse indeksiä etkä aio muokata rakenteen kokoa, käytä for-each-silmukkaa.
For-each-silmukassa on joitain rajoituksia: Et tiedä monennessako alkiossa olet menossa, etkä voi tehdä muutoksia tietorakenteeseen, kuten poistaa tai lisätä alkioita.
void main () {
int[] luvut = {1, 2, 3, 4};
int summa = 0;
// "Jokaiselle luvulle taulukossa luvut..."
for (int luku : luvut) {
summa += luku;
}
IO.println(summa);
}
While
While-silmukka on hyvä valinta silloin, kun et tiedä etukäteen, kuinka monta kertaa toisto pitää suorittaa. Se jatkuu niin kauan kuin ehto on tosi. Tyypillinen esimerkki on tietojen lukeminen tiedostosta rivi riviltä tai pelisilmukka.
void main() {
String syote = "";
IO.println("Tervetuloa peliin! (Kirjoita 'lopeta' poistuaksesi)");
// Huomaa "!" (EI-operaattori) ja .equals() merkkijonolle
// Silmukka jatkuu niin kauan kuin
// syöte EI puutu (eli ei ole null-viite) JA syöte EI ole "lopeta"
while (syote != null && !syote.equals("lopeta")) {
syote = IO.readln("> "); // Pysähtyy odottamaan käyttäjän kirjoitusta
IO.println("Kaiku: " + syote);
}
IO.println("Peli päättyi.");
}
Do-While
Tämä toimii kuten while, mutta yhdellä merkittävällä erolla: silmukan runko suoritetaan aina vähintään kerran, koska ehto tarkistetaan vasta lopussa.
Do-while on ainoa silmukka, jonka lopettavaan sulkeeseen tulee puolipiste.
Alla pseudokoodina esimerkki, jossa omenan sijainti arvotaan uudestaan, jos se on liian lähellä pelaajaa.
void main () {
Vector2D pelaajanSijainti = new Vector2D(0, 0);
Vector2D omenanSijainti = new Vector2D(0, 0);
do {
// Arvo omenalle uusi sijainti
omenanSijainti.x = Math.random() * 10;
omenanSijainti.y = Math.random() * 10;
// Jos omena on liian lähellä pelaajaa, arvotaan uudestaan
} while (omenanSijainti.distanceTo(pelaajanSijainti) < 2.0);
}
Silmukoiden suorituksen ohjaaminen
Tarvittaessa silmukan suoritusta voi ohjata seuraavilla lauseilla:
break: lopettaa silmukan suorittamisen ja siirtyy suorittamaan silmukan jälkeistä koodiacontinue: päättää tämänhetkisen silmukan toiston ja siirtyy silmukan päivityslauseeseen ja toistoehtoon
Esimerkiksi alla olevassa silmukassa tulostetaan luvut 1, 2, 3, 4, 6, 7. Luku 5 jätetään välistä ja luvun 8 kohdalla silmukka lopetetaan.
void main() {
for (int i = 1; i <= 10; i++) {
if (i == 5) {
continue; // Siirrytään seuraavaan kierrokseen
}
if (i == 8) {
break; // Lopetetaan silmukan suoritus
}
IO.println(i);
}
}
Ohjausrakenteiden yhdistäminen
Silmukkarakenne voi sisältää muita rakenteita, kuten ehto- ja silmukkarakenteita. Sisäkkäisiä rakenteita käytettäessä on huomioitavaa muuttujien näkyvyys: sisemmässä rakenteessa määritelty muuttuja ei näy ulommassa rakenteessa, kun taas ulommassa rakenteessa määritelty muuttuja näkyy sisemmässä rakenteessa.
Esimerkiksi, ehtorakenteen sisällä määritelty muuttuja ei ole käytettävissä ehtorakenteen ulkopuolella:
void main() {
int luku = 10;
if (luku > 5) {
int luku2 = luku + 1; // OK, luku määritelty ulommassa rakenteessa
luku2 *= 5;
}
IO.println(luku2); // VIRHE: luku2 on määritelty sisemmässä rakenteessa
}
error: cannot find symbol
IO.println(luku2);
^
symbol: variable luku2
Tällaisissa tapauksissa eräs korjaus on siirtää muuttujan määrittely ulompaan rakenteeseen:
void main() {
int luku = 10;
// Lisätään muuttujan määrittely ja alustetaan väliaikaisella arvolla
// HIGHLIGHT_GREEN_BEGIN
int luku2 = 0;
// HIGHLIGHT_GREEN_END
if (luku > 5) {
// OK: Nyt luku ja luku2 määritelty ulommassa rakenteessa
// HIGHLIGHT_YELLOW_BEGIN
luku2 = luku + 1;
// HIGHLIGHT_YELLOW_END
luku2 *= 5;
}
IO.println(luku2); // OK: luku2 on määritelty samassa rakenteessa kuin tämä lause
}
Sisäkkäiset silmukat
Sisäkkäiset silmukat tarkoittaa yhden tai useamman silmukkarakenteen kirjoittamista toisen silmukkarakenteen sisään. Tällöin jokaista ulomman silmukan toistokertaa kohden suoritetaan tietty määrä lisää toistoja.
Yleinen käyttötapaus sisäkkäisille silmukoille on moniulotteisten taulukoiden
T[][] käsittely.
void main() {
int[][] taulu2D = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
for (int riviNro = 0; riviNro < taulu2D.length; riviNro++) {
IO.println("Rivi " + riviNro + ":");
for (int sarakeNro = 0; sarakeNro < taulu2D[riviNro].length; sarakeNro++) {
int alkio = taulu2D[riviNro][sarakeNro];
IO.println(" Sarake " + sarakeNro + ": " + alkio);
}
}
}
Muista, että int[][] tarkoittaa, että kyseessä on taulukko, jonka alkioina
ovat kokonaislukutaulukot. Siispä yllä olevassa esimerkissä:
taulu2D.lengthantaa taulukossa olevien taulukoiden lukumäärän, eli ns. "rivien" lukumäärän;taulu2D[riviNro]antaa tietyssä indeksissä olevanint[]-taulukon, joka sisältää kaikki rivillä olevat alkiot;taulu2D[riviNro][sarakeNro]antaataulu2D[riviNro]-taulukossa olevan alkion indeksistäsarakeNro.
Aliohjelmat
osaamistavoitteet
- Osaat määritellä aliohjelman
- Osaat käsitellä tietoa aliohjelmien avulla
- Ymmärrät Javan perustietotyyppien ja viitetyyppien eron aliohjelmaa kutsuttaessa
- Osaat dokumentoida aliohjelman
Aliohjelma on ohjelman osa, joka suorittaa tietyn tehtävän. Aliohjelmat helpottavat ohjelman jäsentämistä, sillä niiden avulla ohjelma voidaan jakaa pienempiin, hallittavampiin osiin. Aliohjelmat helpottavat myös uudelleenkäyttöä, sillä samaa aliohjelmaa voidaan kutsua useita kertoja eri kohdissa ohjelmaa ilman, että koodia tarvitsee kirjoittaa uudelleen.
Aliohjelmia kutsutaan joskus myös funktioiksi, ja olio-ohjelmoinnin yhteydessä myös metodeiksi. Nimeäminen riippuu kontekstista, mutta tässä yhteydessä käytämme termiä aliohjelma.
Aliohjelma voi ottaa vastaan syötteitä, joita sanotaan parametreiksi.
Tehtävän suoritettuaan aliohjelma voi palauttaa tuloksen. Kutsutaan alla
keskiarvo-aliohjelmaa, joka laskee kokonaislukujen joukon keskiarvon. Tässä
siis parametrina annetaan yksi kokonaislukutaulukko, ja aliohjelma palauttaa
keskiarvon double-tyyppisenä arvona.
void main () {
int[] luvut = {4, 8, 15, 16, 23, 42};
double keskiarvo = keskiarvo(luvut);
IO.println("Lukujen keskiarvo on: " + keskiarvo);
}
double keskiarvo(int[] luvut) {
if (luvut.length == 0) {
return 0;
}
double summa = 0;
for (int luku : luvut) {
summa += luku;
}
return summa / luvut.length;
}
Aliohjelman määrittely
Yllä oleva keskiarvo-aliohjelma koostuu monesta pienestä palasesta (1)
paluuarvo, (2) nimi, (3) parametrit ja (4) runko-osa.
- (1) Paluuarvon tyyppi (tässä
double): Kertoo, minkä tyyppistä tietoa aliohjelma palauttaa. Jos aliohjelma ei palauta mitään, tyyppi onvoid. - (2) Aliohjelman nimi (tässä
keskiarvo): Kertoo millä nimellä sitä kutsutaan. - (3) Parametrit (tässä
int[] luvut): Sulkeiden sisään määritellään muuttujat, jotka ottavat vastaan aliohjelmalle annettavat syötteet. Parametreja voi olla nolla tai useampia, ja ne erotetaan toisistaan pilkulla. Jokaisella parametrilla on oma tyyppi ja nimi.
Kutsutaan näitä kolmea palasta yhdessä aliohjelman esittelyriviksi. Luvussa 2 tutustutaan myös olio-ohjelmointiin liittyviin määreisiin (engl. modifier), joita esittelyriville voi lisätä.
Esittelyrivin jälkeen kirjoitetaan aaltosulkeiden sisään aliohjelman runko-osa. Se sisältää varsinaisen koodin, joka suoritetaan, kun aliohjelmaa kutsutaan.
Kuten kaikessa lähdekoodissa muutenkin, myös aliohjelmien ja parametrien nimien tulee olla kuvaavia ja noudattaa Javan nimeämiskäytäntöjä ja olla tämän opintojakson tyylioppaan mukaisia.
Paluuarvot ja datan käsittely
Aliohjelmaa voi ajatella mustana laatikkona: sinne syötetään raaka-ainetta
(parametrit), laatikon sisällä tapahtuu prosessointia, ja lopuksi ulos tulee
valmis tuote (paluuarvo). Avainsana return lopettaa aliohjelman suorituksen
välittömästi ja palauttaa arvon kutsujalle. Arvon tyypin on vastattava aliohjelman
määrittelyssä annettua tyyppiä.
Toisen tekemää aliohjelmaa käytettäessä emme välttämättä tiedä, mitä laatikon sisällä tapahtuu, vaan luotamme siihen, että se toimii määritellyllä tavalla. Tämä on tyypillistä ohjelmoinnissa, jossa käytämme valmiita kirjastoja ja aliohjelmia.
Void-aliohjelma
Joskus aliohjelmaa tarvitaan vain tekemään jokin toimenpide, kuten tulostamaan
tekstiä ruudulle, tai aiheuttamaan muu sivuvaikutus. Tällaisessa tapauksessa
aliohjelman ei tarvitse palauttaa arvoa. Tällöin paluuarvon tyypiksi merkitään
void.
Parametrin välitys: alkeistietotyypit ja viitetyypit
On tärkeää ymmärtää, mitä itse asiassa annamme aliohjelmalle parametrina kun kutsumme sitä. Javassa kaikki parametrit välitetään arvona (ns. pass-by-value), mutta välitettävän arvon luonne riippuu parametrin tyypistä.
- Jos parametrin tyyppi on alkeistietotyyppi (kuten
int,double,char,boolean), aliohjelmalle annetaan kopio alkuperäisestä arvosta. - Jos parametrin tyyppi on viitetyyppi (kuten taulukko), aliohjelmalle annetaan kopio viitteestä alkuperäiseen dataan.
Kun alkeistietotyyppi (ks. Luku
1.2) annetaan
parametrina, kyseisen muuttujan arvo kopioidaan ja välitetään
kutsuttavalle aliohjelmalle. Jos aliohjelma muuttaa tätä kopiota, alkuperäinen
muuttuja ei muutu. Alla esimerkki, jossa annamme int-tyyppisen muuttujan
parametrina.
void yritaMuuttaa(int luku) {
luku = 99; // Muutetaan vain kopiota
IO.println("Metodissa: " + luku);
}
void main() {
int x = 10;
yritaMuuttaa(x);
IO.println("Mainissa: " + x); // Tulostaa edelleen 10
}
Kun viitetyyppi (ks. Luku 1.2) annetaan parametrina aliohjelmalle, kopioidaan viite eikä alkuperäistä dataa. Aliohjelma saa siis käyttöönsä kopion viitteestä, joka osoittaa samaan dataan kuin pääohjelman muuttuja. Jos aliohjelma sitten muokkaa muuttujan osoittamaa dataa (esim. taulukon alkioita), muutos näkyy myös pääohjelmassa.
Voit ajatella viitteen ikään kuin "kaukosäätimenä", jolla ohjataan "televisiota" eli dataa. Vaikka aliohjelmalle viedään kopio "kaukosäätimestä", voi sillä ohjata samaa "televisiota".
Alla esimerkki tällaisesta tilanteesta, jossa annamme
int[]-tyyppisen taulukon parametrina, ja aliohjelma muokkaa taulukon alkioita.
void nollaaTaulukko(int[] taulukko) {
// Tämä muutos tapahtuu alkuperäiselle taulukolle!
// Koska "taulukko"-muuttuja viittaa samaan taulukkoon.
for (int i = 0; i < taulukko.length; i++) {
taulukko[i] = 0;
}
}
void main() {
int[] luvut = {1, 2, 3};
nollaaTaulukko(luvut);
// Alkuperäinen taulukko on muuttunut
IO.println(luvut[0]); // Tulostaa 0
}
Oleellista on kuitenkin ymmärtää, että aliohjelman kutsussa annoimme nimenomaisesti kopion viitteestä, emme alkuperäistä viitettä. Jos aliohjelma yrittäisi muuttaa viitettä osoittamaan muuhun dataan (esim. toiseen taulukkoon), tämä muutos ei vaikuttaisi alkuperäiseen viitteeseen pääohjelmassa. Alla esimerkki tästä tilanteesta:
void main() {
int[] luvut = {1, 2, 3};
muutaViite(luvut);
// Alkuperäinen taulukko ei ole muuttunut
IO.println(luvut[0]); // Tulostaa edelleen 1
}
void muutaViite(int[] taulukko) {
// Tämä muutos ei vaikuta alkuperäiseen viitteeseen!
taulukko = new int[] {9, 9, 9};
}
Joissain kielissä, kuten C++:ssa, on mahdollista välittää parametrina alkuperäinen muuttuja viitteenä (ns. pass-by-reference). Javassa tällaista mekanismia ei kuitenkaan ole, vaan kaikki parametrit välitetään arvona, kuten yllä on selitetty.
Aliohjelma ja sivuvaikutukset
Aliohjelmaa, joka muokkaa parametrina annettua dataa, sanotaan usein
aiheuttavan sivuvaikutuksia.
Esimerkiksi, jos alla oleva aliohjelma kaanna kääntää parametrina annetun
taulukon alkiot käänteiseen järjestykseen:
void kaanna(int[] taulu) {
// Toteutus piilotettu tilan säästämiseksi
for (int i = 0; i < taulu.length / 2; i++) {
int tmp = taulu[i];
taulu[i] = taulu[taulu.length - 1 - i];
taulu[taulu.length - 1 - i] = tmp;
}
}
void main() {
int[] taulukko = {1, 2, 3, 4, 5};
IO.println("taulukko = " + Arrays.toString(taulukko));
kaanna(taulukko);
IO.println("taulukko = " + Arrays.toString(taulukko));
}
Huomaa, että kaanna ei palauta mitään, mutta sen sivuvaikutuksena
on, että parametrina annettu taulukko kääntyy.
Yllä oleva aliohjelma voitaisiin toteuttaa myös ilman sivuvaikutuksia
muuttamalla palautusarvoa:
int[] kaanna(int[] taulu) {
// Toteutus piilotettu tilan säästämiseksi
int[] tulos = new int[taulu.length];
for (int i = 0; i < taulu.length; i++) {
tulos[i] = taulu[taulu.length - 1 - i];
}
return tulos;
}
void main() {
int[] taulukko = {1, 2, 3, 4, 5};
IO.println("taulukko = " + Arrays.toString(taulukko));
int[] kaannetty = kaanna(taulukko);
IO.println("kaannetty = " + Arrays.toString(kaannetty));
}
Sivuvaikutukset voivat olla hyödyllisiä muun muassa muistin käytön optimoinnin
kannalta, mutta ne voivat myös tehdä ohjelmasta
vaikeammin ymmärrettävän. Yllä olevassa esimerkissä
aliohjelman void kaanna(int[] taulu) sivuvaikutus ei ole ilmiselvää
ellei katso aliohjelman lähdekoodia. Sivuvaikutuksilla voikin esimerkiksi
tiedostamatta tuhota tai muokata dataa.
Siksi on erittäin tärkeää
olla tietoinen siitä, miten aliohjelmat käsittelevät parametreja.
Kommentointi ja dokumentointi
Lähdekoodiin voi kirjoittaa tekstiä, joka ei ole varsinaista koodia, vaan selittää sitä. Tällaista selitystekstiä on kahdentyyppisiä: (1) koodin sekaan kirjoitettavia kommentteja (nimitetään näitä lyhyesti kommenteiksi) sekä (2) dokumentaatiokommentteja.
Kommenttien tarkoitus on palvella kehityksen aikaista tekemistä. Ne näkyvät sisäisesti, eli ohjelmoijalle itselleen. Dokumentaatiokommenttien tarkoitus on palvella kaikkia, jotka käyttävät koodia. Ne näkyvät paitsi ohjelmoijalle itselleen, myös niille, jotka hyödyntävät koodia esimerkiksi API:n (application programming interface) kautta.
Yhden rivin kommentointi
Yhden rivin kommentteja, jonka syntaksi on // voidaan käyttää esimerkiksi
merkitsemään TODO-kohtia koodissa:
void main() {
// TODO: Tarkista millaisia ongelmia tästä ratkaisusta voi tulla
String syote = IO.readln();
IO.println("Kirjoitit: " + syote);
}
Yleisesti hyvä periaate on, että ohjelmoija pyrkii kirjoittamaan sellaista koodia, joka selittää itse itseään. Muuttujat, luokat, aliohjelmat ja muut nimet, johon ohjelmoija pystyy vaikuttamaan, pyritään nimeämään mahdollisimman kuvaavasti, jolloin yksittäisten rivien kommentointi ei välttämättä ole tarpeen. Asiaa on kuvattu myös opintojakson tyylioppaassa. TODO: Linkki.
Joskus yhden rivin kommenteilta ei voi välttyä, jos jotakin operaatiota ei voida olettaa itsestäänselväksi tai muuttujan nimestä tulisi kohtuuttoman pitkä:
void main() {
int n = 9;
// Pyöristää alaspäin lähimpään neljällä jaolliseen lukuun
int pyoristetty = n & ~3;
IO.println(pyoristetty);
}
Nyt muuttujan pyoristetty tilalla voisi olla pyoristaaAlaspainLahimpaanNeljallaJaolliseenLukuun, joka ei sekään ole oikein
järkevä vaihtoehto.
Monirivinen kommentti
Javassa monirivinen kommentti tulee /* ja */ väliin. Tällaista suositellaan
käytettäväksi, kun jokin monimutkaisempi logiikka vaatii tarkempaa avaamista
ja/tai on järkevää selittää miksi juuri kyseinen ratkaisu on valittu. Tätä
kommenteissa olevaa tarkempaa avaamista ei kuitenkaan ole tarkoitus näyttää
koodin käyttäjille.
if (kayttaja.kayttaaVanhaa) {
/*
* Vanhat käyttäjät (rekisteröityneet ennen vuotta 2022) käyttävät
* toistaiseksi vanhaa käyttöoikeusmallia.
* Älä poista tarkistusta, ennen kuin kaikki tilit on siirretty.
*/
return kaytaVanhojaKayttooikeuksia(kayttaja);
}
Dokumentaatiokommentti
Dokumentaatiokommentti tarkoittaa sellaista kommenttia, josta voidaan automaattisesti luoda erilaisia koodin käyttäjille tarkoitettuja dokumentaatiomuotoja. Tällaisia dokumentaatiomuotoja voivat olla esimerkiksi HTML-muotoiset API-dokumentaatiot, jotka kertovat miten koodia käytetään, tai IDE:ssä näkyvät työkaluvihjeet aliohjelmien käytöstä.
Dokumentaatiokommenttien sijainti on ennen dokumentoitavaa koodia, kuten aliohjelmaa tai luokkaa.
Javassa dokumentaatiokommentit kirjoitetaan erityisellä syntaksilla, joka eroaa
tavallisista kommenteista. Dokumentaatiokommentit alkavat /** ja päättyvät
*/, eli ovat syntaksiltaan hyvin lähellä monirivistä kommenttia.
void main() {
IO.println("summa(1, 2) ==> " + summa(1, 2));
}
/**
* Laskee kahden kokonaisluvun summan.
*
* @param a Ensimmäinen luku
* @param b Toinen luku
* @return Lukujen summa
*/
int summa(int a , int b) {
return a + b;
}
Aliohjelman dokumentaatiokommentin runko syntyy automaattisesti
IDEA-kehitysympäristössä, kun aliohjelman esittelyrivin yläpuolelle kirjoittaa
merkit /** ja painaa Enter.
Bonus: miltä Javan dokumentaatio näyttää?
Oletetaan nyt, että tallennat yllä olevan tiedostoon Summa.java ja ajat sen
jälkeen komennon javadoc Summa.java. Nyt voit avata luodun index.html
-tiedoston selaimessa, klikata selaimessa luokkaa Summa ja pääset
seuraavanlaiseen näkymään:

Näyttääkö tutulta? Vertaa esimerkiksi Javan dokumentaatioon IO-luokasta
vinkki
Kannattaa vilkaista materiaalissa linkattuihin Javan virallisiin dokumentaatioihin! Linkin tunnistaa tekstissä olevasta "JavaDoc"-tekstiä sisältävästä linkistä.
Varaudu kuitenkin siihen, että dokumentaation lukeminen on haastavaa ennen kuin siihen tottuu! Virallinen dokumentaatio myös sisältää usein paljon termejä ja syntaksia, joita ei ole vielä käsitelty ja on vaikea ymmärtää. Tästä ei tarvitse olla huolissaan ja kaikki mitä kurssilla tarvitsee osata käydään läpi näissä kurssimateriaaleissa ellei erikseen toisin mainita.
Dokumentaation lukutaito on kuitenkin tärkeä taito ohjelmoijalle, joten kannattaa pyrkiä totuttelemaan siihen pikkuhiljaa — myös tekoälyn aikana sillä ne eivät välttämättä anna viimeisintä tai edes onnistu toistamaan dokumentaation kertomaa totuudenmukaisesti. Jos oppii luottamaan pelkästään tekoälyyn, siinä vaiheessa kun asiat menee vaikeiksi tulee ongelmia. Tekoäly (erityisesti generatiivinen), toimii nimittäin parhaiten kun ongelmat ovat yleisiä ja tunnettuja eivätkä vaadi suurta asiantuntemusta tai tarkkaa silmää.
Osan kaikki tehtävät
huomautus
Jos palautat tehtävät ennen osion takarajaa (ma 19.1.2026 klo 11:59 (keskipäivä)), voit saada DL-BONUS-pisteitä harjoitustehtäviin. Lue lisää suorittaminen-sivulta.
Ennen kuin aloitat tehtävien tekemisen, on syytä tutustua opintojakson eettisiin ohjeisiin.
Tee uusi IDEA-projekti nimeltään Viikko1 (ks. Luo uusi
projekti ohje). Lisää projektiin moduuli
Tehtava11 (ks. Luo Java-moduuli -ohje).
Lisää projektiin Java Compact File -tiedosto nimeltään OmatTiedot.java.
Kirjoita tiedostoon ohjelma, joka tulostaa kullekin eri riville
- nimesi,
- puhelimesi merkin
- puhelimesi mallin
Tee ohjelma, joka tulostaa konsolisyötteenä annetun merkkijonon sisältämien sanojen määrän. Voit olettaa, että sanat on eroteltu yhdellä välilyönnillä. Muita välimerkkejä ei tarvitse huomioida.
Ohjelman pitää tulostaa "Ei sanoja", jos syöte on tyhjä merkkijono.
Ohjelma lopettaa voi toimintansa tulosteen näyttämisen jälkeen. Ohjelman ei tarvitse kysyä syötettä jatkuvasti.
Testaa ohjelmaasi lukemalla käyttäjältä merkkijono ja tulostamalla sanojen määrä, esimerkiksi:
Sanojen määrä: 5
Tee ohjelma, joka lukee käyttäjältä ei-negatiivisia kokonaislukuja yksi rivi kerrallaan
silmukassa käyttäen
IO.readln()-metodia, kunnes käyttäjä antaa tyhjän merkkijonon.
Tallenna nämä luvut listaan.
Tee sitten aliohjelmat summa, keskiarvo, pienin ja suurin, jotka
laskevat listan pienimmän luvun, suurimman luvun, lukujen summan ja lukujen
keskiarvon. Tulosta aliohjelmien palauttamat arvot käyttäjälle.
Lisää aliohjelmiin sopiva käsittely tyhjille listoille ja dokumentoi ne.
Voit olettaa, että käyttäjä kirjoittaa vain ei-negatiivisia kokonaislukuja syötteenä. Jos aliohjelma saa syötteenä tyhjän listan, sen tulee palauttaa arvo -1.
Valmiiden Collections-luokan metodien, kuten Collections.min() ja
Collections.max(), käyttö on kielletty.
Vinkki
Voit muuntaa merkkijonon numeroksi käyttäen
Integer.parseInt(luku)-metodia.
Tee aliohjelma pakkaaMerkkijono,
joka ottaa parametrina String-merkkijonon
ja palauttaa uuden String-merkkijonon tiivistettynä siten,
että peräkkäiset samat merkit ilmoitetaan merkillä ja niiden lukumäärällä.
Esimerkiksi:
pakkaaMerkkijono("aaaabbbccd")palauttaisi merkkijonon"a4b3c2d1",pakkaaMerkkijono("00666663332222222")palauttaisi merkkijonon"02653327",pakkaaMerkkijono("Niiiiiin")palauttaisi merkkijonon"N1i6n1",pakkaaMerkkijono("nnNNnnnN")palauttaisi merkkijonon"n2N2n3N1",pakkaaMerkkijono("ohjelmointi")palauttaisi merkkijonon"o1h1j1e1l1m1o1i1n1t1i1".
Kirjoita aliohjelmalle myös sopiva dokumentaatiorivi ja lisää
ainakin yksi toimintaesimerkki main()-pääohjelmaan.
Vinkki 1
Käytä
StringBuilder-merkkijonoa
ja sen metodeja merkkijonon rakentamiseen.
Vinkki 2
Kokeile ratkaista ongelma ensin paperilla käsin lyhyelle jonolle
(esim. "aabb"). Mistä asioista on pidettävä kirjaa (muuttujat)?
Vinkki 3
Voi olla helpompaa aloittaa toisesta merkistä ja verrata se aina edeltävään:
Tee funktio onkoSalasanaVahva, joka tarkastaa parametrina
saadun salasanan (String)
vahvuuden. Salasanan tulee olla vähintään 8 merkkiä pitkä, sisältää yhden luvun,
sekä vähintään yhden suuren ja pienen kirjaimen.
Funktio palauttaa true, jos merkkijonona annettu salasana täyttää yllä
olevat vaatimukset, muuten se palauttaa false.
Kirjoita aliohjelmalle myös sopiva dokumentaatiorivi ja lisää
ainakin yksi toimintaesimerkki main()-pääohjelmaan.
Vinkki
Katso Character-luokan metodi
isDigit.
Esimerkiksi kutsumalla Character.isDigit(merkki) saat paluuarvona true,
jos merkki on numero, ja vastaavasti false, jos merkki ei ole numero.
Vastaavalla periaatteella toimivat myös metodit isUpperCase ja isLowerCase,
jotka löytyvät myös samasta luokasta.
Tee aliohjelma tulostaMatriisi, joka tulostaa kaikki parametrina
annetun kokonaislukumatriisin luvut samalle riville.
Aliohjelma ei palauta mitään.
Esimerkki aliohjelman toiminnasta
int[][] matriisi = {
{42, 2, 3},
{4, 67, 6},
{7, 8, 9}
};
tulostaMatriisi(matriisi);
Tulostuksen pitäisi silloin olla:
42 2 3 4 67 6 7 8 9
Tee ohjelma, joka kysyy käyttäjältä syötteen ja tulostaa, kuinka monta kertaa kukin numero (0–9) esiintyy syötteessä.
Esimerkiksi, jos käyttäjä antaa syötteenä 12223, ohjelma tulostaa:
1: 1 kpl
2: 3 kpl
3: 1 kpl
Vastaavasti, jos syötteenä annetaan 10002244412, ohjelma tulostaa.
0: 3 kpl
1: 2 kpl
2: 3 kpl
4: 3 kpl
Tulostamisen jälkeen ohjelma kysyy käyttäjältä uuden syötteen. Jos käyttäjä antaa tyhjän syötteen, ohjelman suoritus päättyy.
Vinkki
Yksittäisen merkin saa muunnettua kokonaisluvuksi
yhdistämällä
Character.toString
ja
Integer.parse:
int numeroLukuna = Integer.parseInt(Character.toString(a));
Huomaa, että Integer.parseInt olettaa, että annettu merkkijono on
todellakin numero; jos se sisältää jotain muuta kuin numeroa,
funktio heittää virheen.
Voit tarkistaa, että onko yksittäinen merkki numero käyttämällä
Character.isDigit-funktiota.
Tee funktio int puuttuvaLuku(int[] taulukko).
Funktiolle annetaan parametriksi taulukko, joka sisältää
luvut 1-N satunnaisessa järjestyksessä, mutta yksi luvuista puuttuu.
Funktion on palautettava luku, joka puuttuu.
Funktio ei saa aiheuttaa sivuvaikutuksia.
Esimerkiksi:
puuttuvaLuku(new int[] { 1, 6, 3, 4, 5 })palauttaa2puuttuvaLuku(new int[] { 8, 2, 4, 1, 3, 5, 6 })palauttaa7puuttuvaLuku(new int[] { })palauttaa1puuttuvaLuku(new int[] { 2 })palauttaa1
Voit käyttää alla olevaa apufunktiota satunnaisen taulukon luomiseksi:
int[] annaSyote(int maxLuku) {
Random r = new Random();
List<Integer> luvut = new ArrayList<>(IntStream.range(1, r.nextInt(2, maxLuku + 1)).boxed().toList());
Collections.shuffle(luvut);
luvut.remove(r.nextInt(luvut.size()));
return luvut.stream().mapToInt(Integer::intValue).toArray();
}
Voit käyttää aliohjelmaa seuraavasti:
// Tekee taulukon luvuista 1-10, mutta yksi luku puuttuu
int[] syote = annaSyote(10);
// Selvitetään, mikä luvuista puuttuu
int puuttuva = puuttuvaLuku(syote);
Tulosta kaikki alkuluvut väliltä 1-100.
Alkuluku on lukua 1 suurempi kokonaisluku, joka on jaollinen vain itsellään ja luvulla 1.
Vinkki
Tee ensin funktio onAlkuluku, joka saa parametrinaan kokonaisluvun ja
palauttaa totuusarvon, joka kertoo onko luku alkuluku vai ei. Käytä tätä
funktiota apuna pääohjelmassa, jossa tulostat silmukkaa apuna käyttäen kaikki
alkuluvut väliltä 1-100.
Olio-ohjelmoinnin perusteet
osaamistavoitteet
- Ymmärrät olio-ohjelmoinnin perusidean ja kuinka se eroaa proseduraalisesta ohjelmoinnista.
- Osaat hahmottaa olion kokonaisuutena, jossa tieto ja toiminta yhdistyvät.
- Ymmärrät luokan ja olion suhteen, ja miten luokka toimii mallina olioiden luomisessa.
- Osaat määritellä oman luokan ja sen jäsenet, eli attribuutit, metodit ja konstruktorit.
- Ymmärrät olioiden elinkaaren ja osaat käyttää olioita ohjelman osina.
- Tiedät, mitä
static-määrite merkitsee luokan jäsenten osalta. - Ymmärrät kapseloinnin periaatteet ja hyödyt, ja osaat erottaa olion sisäisen toteutuksen sen ulkoisesta käytöstä.
Kohti olio-ohjelmointia
osaamistavoitteet
- Otat askeleen proseduraalisen ohjelmoinnin data ja funktio -ajattelumallista kohti olio-ohjelmoinnin tila ja metodi -ajattelumallia.
- Ymmärrät olion käsitteen; tieto ja toiminnallisuus yhdessä paketissa.
- Ymmärrät, miten käsitteiden mallintaminen olioina voi helpottaa ohjelman rakentamista.
Tähän mennessä olemme tehneet enimmäkseen ohjelmia, joissa dataa tallennetaan ohjelman muuttujiin ja käsitellään funktioiden avulla. Tällaista ohjelmointityyliä kutsutaan proseduraaliseksi ohjelmoinniksi. Tutustumme tällä kurssilla toiseen ohjelmointityyliin: olio-ohjelmointiin.
Olio-ohjelmointi on hyvin laaja aihe, ja käymme tällä kurssilla läpi olio-ohjelmoinnin teoriaa valikoidusti erityisesti tämän opintojakson tarpeita ajatellen. Syvemmin teoriaan perehdytään esimerkiksi opintojaksolla TIEA1130 Oliosuuntautunut suunnittelu ja ohjelmointi.
Perusidea
Olio-ohjelmoinnissa ideana on luoda olioiksi kutsuttuja rakenteita, jotka sisältävät datan sekä toiminnallisuudet sen muokkaamiseen. Oliot voivat olla samanlaisia, mutta jokaisella oliolla on oma tila, joka voi muuttua ohjelman suorituksen aikana. Olion tila tallentuu sen omiin muuttujiin eli attribuutteihin.
Oliolla voi olla myös omia aliohjelmia, joita kutsutaan metodeiksi. Metodi on oliolle kuuluva aliohjelma, joka voi tarkastella ja muuttaa sen omistavan olion tilaa. Ennen olion luontia täytyy ensin määrittää luokka eli class, jossa kuvaillaan olion rakenne.
Olioiden tehtävä on olla vastuussa oman vastuualueensa toiminnallisuuksista. Olioiden välinen yhteistoiminta ja kommunikointi metodikutsujen kautta on olio-ohjelmoinnissa keskeisessä osassa. Joissain ohjelmointikielissä tätä saatetaan kutsutaan viestinvälitykseksi.
Minimaalinen, olioita hyödyntävä ohjelma voisi näyttää esimeriksi tältä:
class Kissa {
private String nimi;
public String getNimi() {
return nimi;
}
public void setNimi(String uusiNimi) {
this.nimi = uusiNimi;
}
}
void main() {
Kissa kissa1 = new Kissa();
kissa1.setNimi("Miuku");
Kissa kissa2 = new Kissa();
kissa2.setNimi("Katti");
IO.println(kissa1.getNimi());
IO.println(kissa2.getNimi());
}
Esimerkissä määritellään ensin Kissa-luokka ja luodaan sitten pääohjelmassa
sen pohjalta olioita, jotka sisältävät attribuuttina merkkijonon nimi sekä
kaksi metodia. Olion nimeä voidaan muuttaa kutsumalla sen setNimi-metodia ja
se voidaan pyytää vastaavasti getNimi-metodilla. Molemmilla olioilla on oma
tilansa - eli oma nimi. Yhden olion tila ei vaikuta toisen olion tilaan.
Tästä yksinkertaisesta esimerkistä näemme, kuinka data voidaan ryhmitellä
olioiden sisälle. Tässä tapauksessa olion ainoa attribuutti nimi on suoraan
käsiteltävissä metodien kautta. Käytännössä tällainen suora manipulointi on
harvinaista tuotantokoodissa, mutta palaamme tähän myöhemmin.
Yhtä muuttujaa varten tuskin kannattaa tehdä eri olioita, mutta olio-ohjelmoinnin hyödyt tulevat nopeasti esille, kun tallennettavan tiedon määrä lisääntyy. Mietitään esimerkiksi tilannetta, jossa haluaisimme tallentaa tietoa kilpailussa mukana olevista kilpailijoista. Kilpailijoita voi olla useita ja jokaisesta pitäisi tallentaa ainakin nimi, kilpailijanumero ja pisteet. Olioiden avulla voimme pitää yhden kilpailijan tiedot ja niiden muokkaamiseen liittyvät toiminnallisuudet saman rakenteen sisällä, mikä helpottaa näiden tietojen käsittelyä.
class Kilpailija {
private String nimi;
private int numero;
private int pisteet;
public Kilpailija(String nimi, int numero, int pisteet) {
this.nimi = nimi;
this.numero = numero;
this.pisteet = pisteet;
}
// ...
public void tulostaTiedot() {
IO.println(String.format("%d: %s, %d pistettä", numero, nimi, pisteet));
}
}
void main() {
Kilpailija[] kilpailijat = {
new Kilpailija("A", 2, 20),
new Kilpailija("B", 4, 15),
new Kilpailija("C", 6, 10)
};
for (Kilpailija kilpailija : kilpailijat) {
kilpailija.tulostaTiedot();
}
}
Vastaavan ohjelman tekeminen ilman olio-ohjelmointia vaatii vähän aivojumppaa. Yksi tapa olisi käyttää kolmea erillistä taulukkoa: Yksi sisältäisi nimet, toinen numerot ja kolmas pisteet. Tällöin täytyy kuitenkin huolehtia, että kilpailijan tiedot löytyvät aina samoista indekseistä kaikissa taulukoissa, mikä on käytännössä hankalaa ja altistaa virheille. Olioiden avulla tiedot ja toiminnallisuudet voidaan paketoida selkeämmin yhteen.
Yhteen kuuluvan tiedon ja toiminnallisuuden järjestely saman rakenteen sisälle tekee ohjelman koodista helpommin ymmärrettävää ja laajennettavaa, mutta se ei toki ole olio-ohjelmoinnin ainoa etu; voimme myös piilottaa osan luokan toteutusyksityiskohdista vain luokan sisäiseen käyttöön, jolloin luokkaa käyttävä ohjelmoija saa käyttöönsä vain tarkkaan määritetyn julkisen rajapinnan eikä hänen tarvitse miettiä luokan sisäistä toimintaa. Tutustumme 3. osassa myös polymorfismiin, perintään ja rajapintoihin, jolloin olio-ohjelmoinnin hyödyt tulevat todella esille.
Pääohjelma Javassa
Java on erityisesti olio-ohjelmointiin suuntautunut ohjelmointikieli, jossa myös pääohjelman on historiallisesti täytynyt olla luokan sisällä. Javan uudemmissa versioissa tähän on tullut muutoksia; yksinkertaisen ohjelman kirjoittamista on pyritty helpottamaan niin, että pääohjelma ei tarvitsisi ympärilleen luokkaa. Näistä muutoksista voi lukea lisää Java 21:n dokumentaatiosta sekä vuoden 2025 syksyllä julkaistun Java 25:n dokumentaatiosta.
Nykyään riittää näissä materiaaleissa käytetty suoraviivaisempi pääohjelma:
void main() {
IO.println("Hei maailma!");
}
Aiemmin minimaalinen Java-ohjelma saattoi näyttää tältä:
public class HeiMaailma {
public static void main(String[] args) {
IO.println("Hei maailma!");
}
}
Vanhemman tyylin esimerkissä on muutama uusi käsite, joihin tutustumme tässä
osassa tarkemmin. Ohjelmassa määritetään ensiksi class-avainsanaa käyttäen
luokka, jonka nimeksi on esimerkissä annettu HeiMaailma. Pääohjelma main
sijoitetaan tämän luokan sisälle. Vanhassa tyylissä pääohjelmassa täytyy olla
static-määrite, jota katsomme tarkemmin pian.
Koska suoraviivaisempi pääohjelma on verrattain uusi ominaisuus, valtaosa verkosta ja kirjoista löytyvistä esimerkeistä käyttää yhä alkuperäistä tyyliä, eli luokan sisään upotettua pääohjelmaa. Tämä on hyvä tiedostaa tietoa etsiessä.
Luokka ja olio
osaamistavoitteet
- Ymmärrät luokan ja olion suhteen, ja miten luokka toimii mallina olioiden luomisessa.
- Osaat määritellä oman luokan ja sen jäsenet, eli attribuutit, metodit ja konstruktorit.
- Ymmärrät olioiden elinkaaren ja osaat käyttää olioita ohjelman osina.
- Ymmärrät, miten
this-viite toimii olion metodeissa. - Tiedät, mitä
static-määrite merkitsee luokan jäsenten osalta.
Luokka
Ensimmäinen askel olio-ohjelmointiin on luokan määritteleminen
class-avainsanaa käyttäen. Luokkaa voi ajatella kaavana tai muottina, jonka
pohjalta olioita luodaan. Luokka kertoo, mitä tietoja olio sisältää
(attribuutit) ja mitä se voi tehdä (metodit). Luokassa määriteltyjä
attribuutteja ja metodeja kutsutaan myös luokan jäseniksi (engl. class
member).
Tehdään pieni ajatusharjoitus: mietitään hetki talonrakennusta. Arkkitehdin
piirtämän yhden rakennuspiirustuksen pohjalta voidaan rakentaa monta rakennusta.
Ne olisivat rakenteeltaan samanlaisia, sillä ne ovat saman kaavan mukaan tehty,
mutta jokaisella rakennuksella olisi kuitenkin oma tila; eri omistaja, väri,
sisustus, ja niin edelleen. Rakennuspiirustusta voi (ainakin etäisesti) ajatella
olio-ohjelmoinnin luokkana, kun taas rakennukset ovat sen pohjalta tehtyjä
olioita. Luokan nimi kertoo, mikä olio on, joten jos tekisimme luokan
rakennuksille, sen nimeksi sopisi Rakennus.
Huomaa, että Javassa on tapana aloittaa luokkien nimet aina isolla kirjaimella.
Määritellään aluksi tyhjä luokka Rakennus, jota lähdemme täydentämään.
class Rakennus {
// Luokan sisällä määritellään rakenne, jota luokasta
// tehdyt oliot vastaavat.
}
Luokasta luodaan ilmentymiä eli olioita käyttämällä avainsanaa new. Tämä
varaa muistista oliolle sopivan tilan, valitsee ja suorittaa sopivan
muodostajan, ja palauttaa viitteen juuri luotuun olioon. Sijoitamme tämän
viitteen muuttujaan, jotta pääsemme olioon sitä kautta käsiksi. Luokan nimi on
muuttujan tyyppi ja siten kertoo kääntäjälle, millainen olio muistisijainnissa
täytyy olla.
void main() {
// Lauseke 'new Rakennus()' luo olion ja palauttaa viitteen siihen.
// Sijoitamme tämän viitteen muuttujaan 'rakennus'.
Rakennus rakennus = new Rakennus();
}
huomautus
Viitemuuttuja ja olio ovat kaksi eri asiaa. Viitemuuttuja on kuin nuoli, joka
voi osoittaa olioon. Viitemuuttujan ei kuitenkaan ole pakko viitata mihinkään,
jolloin sen arvo on null. Olio on vastaavasti mahdollista luoda ilman siihen
viittaavaa muuttujaa, mutta jos olioon osoittavia viitteitä ei ole, siihen ei
päästä käsiksi ja se merkitään automaattisesti roskaksi. Useampi viitemuuttuja
voi viitata samaan olioon, mutta viitemuuttuja voi osoittaa vain yhteen olioon
kerrallaan.
Attribuutit
Attribuutti on luokan sisällä, aliohjelmien ulkopuolella määritelty muuttuja, joka edustaa olion ominaisuutta, piirrettä tai tilaa. Luokasta tehdyt oliot sisältävät aina luokassa määritellyt attribuutit. Siinä missä luokka määrittelee, mitä tietoja olioilla voi olla, attribuutit tallentavat kunkin yksittäisen olion konkreettiset arvot. Kuten muuttujat yleensä, attribuutit voivat olla alkeistietotyyppejä tai viitteitä. Niiden nimeämisessä käytetään myös samoja käytänteitä. Huomaa, että metodien sisällä esitellyt muuttujat eivät ole attribuutteja; ne ovat metodin paikallisia eli lokaaleja muuttujia. Lokaalit muuttujat eivät ole osa olion tilaa.
Lisätään nyt muutama attribuutti Rakennus-luokkaamme.
public class Rakennus {
// Nämä muuttujat ovat olion attribuutteja. Jokaisella rakennuksella
// on omistaja ja väri, mutta ne eivät välttämättä ole kaikilla
// olioilla arvoiltaan samat.
private String omistaja;
// Attribuutille voidaan asettaa oletusarvo. Tässä värin oletusarvo on
// sininen, jolloin kaikilla tämän luokan pohjalta tehdyillä olioilla
// on aluksi tämä arvo, ellei sitä muuteta.
private String väri = "sininen";
}
Attribuutit poikkeavat paikallisista muuttujista siten, että niiden näkyvyyttä voidaan hallita näkyvyysmääreiden avulla. Paikalliset muuttujat ovat olemassa ja nähtävillä vain aliohjelman sisällä sen suorituksen ajan, mutta attribuutit ovat olemassa koko olion eliniän ajan ja näkyvät kaikille muille jäsenille luokan sisällä. Ne voidaan asettaa näkyväksi myös olion ulkopuolelta, joskin tämä on yleisesti ottaen huono idea. Palaamme näkyvyysmääreisiin myöhemmin tässä osassa.
huomautus
Paikallisella muuttujalla voi olla sama nimi kuin attribuutilla, jolloin se
peittää attribuutin. Tätä kutsutaan varjostamiseksi (engl. shadowing). Jos
attribuutilla ja paikallisella muuttujalla on sama nimi, käytetään
lausekkeissa ensisijaisesti lokaalia muuttujaa. Tässäkin tilanteessa olion
metodeista voi yhä päästä käsiksi sen attribuuttiin käyttämällä
this-viitettä, johon palaamme hyvin pian.
Attribuutille voidaan luokassa antaa oletusarvo, jolloin luokasta luodut oliot saavat sen myös oman attribuuttinsa alkuarvoksi. Jos attribuutilla ei ole oletusarvoa, sen arvo voidaan määrittää olion luomisen yhteydessä, myöhemmin metodien avulla, tai jopa jättää määrittämättä.
public class Rakennus {
// Olion attribuutti, jolla on oletusarvo.
private String väri = "sininen";
public void tulosta() {
// Tämä lokaali muuttuja peittää saman nimen omaavan attribuutin
// aliohjelman sisällä.
String väri = "punainen";
// Tulostaa "punainen". Tunniste 'väri' viittaa ensisijaisesti
// lokaaliin muuttujaan, jos sellainen on näkyvissä.
IO.println(väri);
// Tulostaa "sininen". Voimme viitata olion metodin sisältä sen
// attribuuttiin 'this'-viitteen avulla.
IO.println(this.väri);
}
}
Metodit
Luokassa määriteltyjä aliohjelmia kutsutaan metodeiksi. Siinä missä attribuuttia voisi kuvailla niin, että se muodostaa olion sisäisen tilan, metodia voisi kuvailla olion kyvyksi tehdä jotain. Javassa erikoista on se, että kaikki aliohjelmat ovat itse asiassa aina jonkin luokan sisällä. Ehkä tästä syystä Java-kielessä tupataan kutsumaan kaikkia aliohjelmia metodeiksi.
Metodien määrittely ei syntaksiltaan eroa muista aliohjelmista, ja niiden nimeämisessä käytetään myös samanlaisia käytänteitä. Metodeja voidaan myös kuormittaa. Metodien näkyvyyttä luokan ulkopuolelle voidaan hallita attribuuttien tapaan näkyvyysmääreiden avulla.
Kuten yleensä aliohjelmia tehdessä, metodin tulisi suorittaa tehtävä, jota sen nimi kuvastaa. Liian suuret tehtävät on hyvä jakaa pienempiin osiin eli useammaksi metodiksi.
Voidaksemme kutsua olion metodia, meillä täytyy olla olio ja siihen viite.
Lisätään nyt Rakennus-luokkaamme pari yksinkertaista metodia olion tilan
käsittelyyn ja kutsutaan näitä. Tällaisia metodeja kutsutaan usein
saantimetodeiksi.
public class Rakennus {
private String omistaja;
private String väri;
// Olion metodi, joka ottaa vastaan merkkijonon ja sijoittaa
// sen 'väri'-attribuuttiin.
public void setVäri(String väri) {
// Parametrilla ja attribuutilla on sama nimi, joten käytämme
// this-viitettä.
this.väri = väri;
}
// Olion metodi, joka palauttaa 'väri'-attribuutin arvon kutsujalle.
public String getVäri() {
return this.väri;
}
}
Voisimme lisätä myös vastaavat metodit luokan toista attribuuttia varten.
Keskustelemme myöhemmin tässä osassa siitä, miten olion tilaa voidaan käsitellä
hieman järkevämmin, mutta tässä vaiheessa tällaiset yksinkertaiset get- ja
set-metodit riittävät.
This-viite
Avainsana this viittaa olioon itseensä. Se toimii viitteenä "tähän olioon",
jonka kontekstissa koodia suoritetaan.
Käytimme tämän osan alun esimerkeissä this-viitettä lukeaksemme olion
attribuutteja näin:
public class Rakennus {
private String väri;
// ...
public String getVäri() {
return this.väri;
}
}
Tämä viite on automaattisesti käytettävissä aina, kun kutsutaan jonkin olion
metodia. Metodikutsun yhteydessä this asetetaan osoittamaan metodin
suorituksen sisällä siihen olioon, jonka metodia kutsuttiin, eikä sitä voi
muuttaa. Viitteen kautta metodi pääsee käsiksi oikean olion tilaan sekä
metodeihin. Olion metodien sisällä this-viite on pakko kirjoittaa vain, jos
näkyvyysalueella on samanniminen jäsen, kuten lokaali muuttuja. Meidän ei siis
tarvitse kirjoittaa aina attribuutin tai metodikutsun eteen this, sillä
kääntäjä osaa päätellä sen itse, jos konfliktia ei ole. Joissain
ohjelmointikielissä tällaista viitettä kutsutaan myös nimellä self.
Katsotaan muutama esimerkki this-viitteen käytöstä.
public class Rakennus {
private String omistaja;
private String väri;
public void setVäri(String väri) {
// Käytämme tässä this-viitettä, sillä attribuutilla ja parametrilla
// on sama nimi. Lokaalia muuttujaa käytettäisiin muuten ensisijaisesti.
this.väri = väri;
}
public String getVäri() {
// Tässä this on vapaaehtoinen. Samalla näkyvyysalueella ei ole muita
// "väri" nimisiä muuttujia, joten sekaannusta ei tapahdu.
// this-viitteen käyttö on kuitenkin täysin sallittua.
return this.väri;
}
}
Metodien sisällä voimme käyttää this-viitettä kuin muitakin viitemuuttujia.
Voimme esimerkiksi välittää sen toiselle aliohjelmalle parametrina. Tämä ei ole
tarpeen olion omia metodeja kutsuessa, mutta voimme näin välittää viitteen
olioon myös luokan ulkopuolisille aliohjelmille.
public class Rakennus {
private String omistaja;
private String väri;
public String getOmistaja() {
return omistaja;
}
public void setOmistaja(String omistaja) {
this.omistaja = omistaja;
}
public String getVäri() {
return väri;
}
public void setVäri(String väri) {
this.väri = väri;
}
// Olion metodi.
public String kaunista() {
// Välitetään omistava olio toisen luokan metodille.
return Kaunistaja.MuotoileKauniisti(this);
}
}
Muodostaja eli konstruktori
Muodostaja eli konstruktori on luokan erikoismetodi, jota käytetään uuden olion luomisen yhteydessä sen tilan alustamiseen. Muodostajan nimi on aina sama kuin luokan nimi ja se kirjoitetaan isolla alkukirjaimella, mikä poikkeaa muiden metodien nimeämistyylistä. Muodostajalle ei määritetä paluuarvon tyyppiä, vaan muodostaja palauttaa aina viitteen muodostettuun olioon.
Luokassa täytyy olla ainakin yksi muodostaja. Olemme kuitenkin tähän asti luoneet olioita määrittelemättä luokkaan muodostajaa. Tämä onnistuu Javassa, sillä jos luokkaan ei ole tehty yhtään muodostajaa, kääntäjä luo automaattisesti parametrittoman muodostajan. Automaattisesti luotu parametriton muodostaja on toteutukseltaan tyhjä siinä mielessä, että se ei sisällä yhtään lausetta. Parametritonta muodostajaa ei luoda automaattisesti, jos määrittelemme luokkaan yhdenkin muodostajan itse.
Muodostajan nimi on aina sama kuin luokan, joten teemme siis muodostajia lisätessämme metodin kuormitusta erilaisilla parametreilla. Kääntäjä valitsee oikean muodostajan automaattisesti olion luomisen yhteydessä annettujen argumenttien perusteella.
Käytimme aikaisemmassa esimerkissä Rakennus-luokkaa määrittelemättä
muodostajaa. Otetaan metodit hetkeksi pois selkeyden vuoksi ja katsotaan, mitä
olioita luodessa tapahtuu.
public class Rakennus {
private String omistaja;
private String väri;
}
Tässä tapauksessa olio muodostetaan automaattista oletusmuodostajaa käyttäen, sillä yhtään muodostajaa ei ole määritelty. Voimme tehdä vastaavan muodostajan itsekin seuraavasti.
public class Rakennus {
private String omistaja;
private String väri;
// Parametriton muodostaja, joka vastaa oletusmuodostajaa.
public Rakennus() {
// Voisimme täällä alustaa olion tilan jollain tavalla,
// asettamalla attribuuteille alkuarvot.
}
}
Olemme tähän asti muodostaneet olioita ilman kunnollista alustusta. Tämä ei ole hyvä käytäntö, joten korjataan tilanne seuraavaksi.
Rakennukset ovat heti luomisen jälkeen tilassa, jossa niillä ei ole omistajaa
tai väriä. Tällä hetkellä vasta luodun olion attribuuttien arvot ovat null.
Oliosta ja sen tarkoituksesta riippuen tällaiset arvot voivat olla sallittuja,
mutta jos rakennus on olemassa, sillä täytynee olla jokin omistaja ja väri.
Olisi parempi, että olio olisi heti luonnin jälkeen käyttökelpoinen. Voisimme luoda olion suoraan oikeaan tilaan määrittelemällä muodostajan, joka ottaa olion tilan alustamiseen tarvittavat tiedot vastaan parametreina ja alustaa attribuutit oikein.
Lisätään nyt Rakennus-luokalle muodostaja, joka ottaa omistajan ja värin
vastaan parametreina ja alustaa näiden avulla olion attribuutit. Näin meidän ei
tarvitse asettaa attribuutteja erikseen olion luomisen jälkeen pääohjelmassa.
Voimme nyt myös poistaa parametrittoman muodostajan, jotta sitä ei voi enää
käyttää luomaan olioita, joilla on virheellinen alkutila.
public class Rakennus {
private String omistaja;
private String väri;
public Rakennus(String omistaja, String väri) {
// Alustetaan olion tila parametreilla. Huomaa tässä this-viitteen
// käyttö, sillä parametrien ja attribuuttien nimet ovat samat.
this.omistaja = omistaja;
this.väri = väri;
}
}
Pari huomiota: Emme voi luoda rakennusta yhdellä parametrilla, esimerkiksi
Rakennus rakennus2 = new Rakennus("JYU");, sillä määrittelemämme muodostaja
tarvitsee kaksi parametria. Emme voi myöskään luoda rakennusta ilman
parametreja, esimerkiksi Rakennus rakennus3 = new Rakennus();, koska
oletusmuodostajaa ei enää ole.
Lisätään vielä lopuksi hieman erikoisempi muodostaja, joka ottaa vastaan toisen saman luokan olion ja kopioi sen tilan muodostettavalle oliolle. Tällaisesta muodostajasta puhuttaessa käytetään usein nimitystä copy constructor.
public class Rakennus {
private String omistaja;
private String väri;
public Rakennus(String omistaja, String väri) {
this.omistaja = omistaja;
this.väri = väri;
}
// Muodostaja, joka ottaa vastaan toisen saman luokan olion ja
// kopioi sen arvot muodostettavalle oliolle.
public Rakennus(Rakennus kopioitava) {
this.omistaja = kopioitava.omistaja;
this.väri = kopioitava.väri;
}
}
Voimme lisäksi käyttää muodostajassa this-avainsanaa kuin metodia, jos
haluamme siirtää muodostamisen toiselle saman luokan muodostajalle. Voimme usein
välttää näin turhaa toistoa.
Muutetaan luokkaa nyt niin, että parametriton muodostaja käyttää parametrillista muodostajaa antamaan attribuuteille alkuarvot.
public class Rakennus {
private String omistaja;
private String väri;
public Rakennus(String omistaja, String väri) {
this.omistaja = omistaja;
this.väri = väri;
}
public Rakennus(Rakennus kopioitava) {
// Siirrämme muodostamisen yllä olevalle muodostajalle sitä
// vastaavilla parametreilla
this(kopioitava.omistaja, kopioitava.väri);
}
}
Tee luokka Kello, jolla on attribuutit minuutit ja tunnit
kokonaislukuina.
Lisää luokkaan metodit setMinuutit ja setTunnit, jotka ottavat parametrina
kelloon minuutit ja tunnit. Minuuttien täytyy olla välillä 0-59, muuten
tulostetaan virheilmoitus, ja aika pysyy entisellään. Samoin tuntien täytyy olla
välillä 0-23, muuten tulostetaan virheilmoitus.
Tee myös metodi naytaAika, joka tulostaa ajan muodossa "HH:MM".
Voit testata luokan toimintaa valmiin pääohjelman avulla.
Tee luokka Ajastin, jolla on attribuutit minuutit ja sekunnit
kokonaislukuina.
Lisää luokkaan metodit lisaaMinuutteja ja lisaaSekunteja, jotka ottavat
parametrina ajastimeen lisättävät minuutit ja sekunnit. Lisää myös metodi
annaMerkkijono, joka antaa ajastimen minuutit ja sekunnit merkkijonona.
Minuutteja voi olla kuinka monta tahansa, mutta sekuntien täytyy olla välillä 0-59. Jos sekunnit ylittävät rajan, muutetaan ne minuuteiksi. Sekunnit voi muuttaa minuuteiksi + sekunneiksi esimerkiksi näin:
int sekunteja = 75; // Esimerkki parametrina tulevasta arvosta
// Tämä antaa this.minuutteja-attribuuttiin _lisättävät_ minuutit
int lisattaviaMinuutteja = (this.sekunteja + sekunteja) / 60;
// Tämä antaa this.sekunteja-attribuuttiin _sijoitettavat_ sekunnit
int jaljelleJaavatSekunnit = (this.sekunteja + sekunteja) % 60;
Voit testata luokan toimintaa valmiin pääohjelman avulla.
Valitse jokin tosimaailman esine tai käsite ja tee siitä oma yksinkertainen luokka.
Lisää luokkaan seuraavat:
- Vähintään kaksi sille sopivaa attribuuttia.
- Vähintään kaksi metodia, jotka muuttavat tai tarkastelevat olion tilaa jollain tavalla.
- Vähintään kaksi erilaista muodostajaa, jotka alustavat olion järkevään tilaan. Se mikä on järkevä tila riippuu omasta oliostasi.
Voit käyttää materiaalin esimerkkejä tarvittaessa inspiraationa attribuutteja ja metodeja keksiessä.
Luo olio ja kokeile sen tilan muuttamista pääohjelmassa. Tulosta olion tietoja ennen tilan muuttamista ja sen jälkeen.
Static
Luokan jäsenet voidaan määritellä kuuluvaksi olion sijaan luokalle
static-määritettä käyttämällä. Tällaisia attribuutteja ja metodeja kutsutaan
luokan attribuuteiksi (engl. class attribute) ja luokan metodeiksi (engl.
class method).
Sana static voi olla hieman harhaanjohtava; staattisuus ei tässä tarkoita,
että nämä luokan jäsenet ovat pysyviä tai muuttumattomia. Käytämme kuitenkin
tässä materiaalissa sanaa staattinen kuvaamaan luokan jäseniä.
Siinä missä attribuutit ja metodit liittyvät olioon -- olion attribuutit ja metodit pääsevät olion tilaan käsiksi -- luokan attribuutti ei ole osa minkään olion tilaa ja sillä on vain yksi arvo, joka on jaettu kaikkien luokan olioiden kesken. Jos yksi olio muuttaa oman luokkansa attribuutin arvoa, muutos näkyy kaikissa saman luokan olioissa.
Samalla tavalla voimme ajatella myös luokan metodien olevan jaettu kaikkien
luokan olioiden kesken; ne eivät liity mihinkään olioon, eivätkä siten pääse
minkään olion tilaan suoraan käsiksi. Oliot voivat kutsua oman luokkansa
metodia, siis staattista metodia, mutta tämä kutsu ei mahdollista olion tilan
käsittelyä. Koska staattiset metodit eivät liity mihinkään olioon, niiden
sisällä ei myöskään voi käyttää this-viitettä.
Voidaksemme kutsua olion metodia, meillä täytyy olla olio ja siihen viite. Staattista luokan metodia sen sijaan voimme kutsua suoraan luokan kautta ilman olion luomista. Staattista metodia on mahdollista kutsua myös olioviitteen kautta, mutta tämäkään ei mahdollista olion tilan tarkastelua metodin sisältä.
Tehdään aluksi luokka, jossa ei ole staattisia jäseniä.
public class Henkilo {
private String etunimi;
private String sukunimi;
public Henkilo(String etunimi, String sukunimi) {
this.etunimi = etunimi;
this.sukunimi = sukunimi;
}
public String annaNimi() {
return etunimi + " " + sukunimi;
}
}
Koska annaNimi on olion metodi, meidän täytyy ensin luoda olio. Emme voi
kutsua tätä metodia staattisesti luokan kautta, esimerkiksi
Henkilo.annaNimi().
Lisätään nyt luokan attribuutti taysiIkaisyydenRaja, joka kuvastaa kaikkien
henkilöiden täysi-ikäisyyden rajaa. Lisätään luokkaan myös metodi
onkoTaysiIkainen, joka tarkistaa, onko annettu ikä täysi-ikäinen. Huomaa, että
kyseinen metodi ei liity mihinkään olioon, vaan se tarkistaa vain annetun iän.
public class Henkilo {
private String etunimi;
private String sukunimi;
private static int taysiIkaisyydenRaja = 18;
public Henkilo(String etunimi, String sukunimi) {
this.etunimi = etunimi;
this.sukunimi = sukunimi;
}
public String annaNimi() {
return etunimi + " " + sukunimi;
}
public static boolean onkoTaysiIkainen(int ika) {
return ika >= taysiIkaisyydenRaja;
}
}
Nyt meillä on luokan metodi onkoTaysiIkainen, jota voimme kutsua ilman oliota.
Voisimme kutsua tätä luokan metodia myös olion kautta, mutta se ei ole tarpeen
ja kääntäjä varoittaakin siitä.
Staattinen jäsen on hyödyllinen, kun haluamme määritellä jonkin toiminnallisuuden, joka ei liity mihinkään yksittäiseen olioon, mutta liittyy luokkaan yleisesti.
Staattiseen jäseneen pääsee käsiksi myös olion metodin sisältä. Voimme esimerkiksi tarkistaa, onko henkilö täysi-ikäinen.
// Olion metodi voi myös käyttää staattista muuttujaa
public boolean onkoHenkiloItseTaysiIkainen() {
return this.ika >= taysiIkaisyydenRaja;
}
Selkeyden vuoksi on hyvä käyttää luokan nimeä, kun viitataan staattiseen jäseneen
luokan sisälläkin, esimerkiksi Henkilo.taysiIkaisyydenRaja. Tämä auttaa erottamaan
staattiset jäsenet olion jäsenistä.
Yllä oleva esimerkki on sikäli varoittava, että täysi-ikäisyyden rajaa pystyy
muuttamaan mistä tahansa koodista, joka pääsee käsiksi Henkilo-luokkaan.
Tällaisen "globaalin" tilan käyttäminen koodissa on yleensä erittäin huono idea.
Yksi esimerkki staattisesta metodista, jota käytämme usein, on IO.println().
Tämä on
IO-luokan
staattinen metodi. Sen käyttäminen on helpompaa, kun meidän ei tarvitse joka
kerta luoda IO-oliota ja kutsua sen println-metodia.
Muuta esimerkin Henkilo-luokkaa niin, että jokainen luotu henkilö saa
automaattisesti oman järjestysnumeron, joka on kokonaisluku.
Ensimmäiseksi luodun henkilön numero on 1, seuraava saa numeroksi 2, jne.
Numeroa ei saa antaa henkilölle sen ulkopuolelta esimerkiksi kutsumalla sen metodia.
Toteuta luokkaan myös metodi annaNumero, joka palauttaa tietyn olion numeron
kokonaislukuna. Muita metodeja ei tarvitse lisätä.
Vinkki
Tarvitset tässä tehtävässä staattisia luokan jäseniä.
Mieti aluksi seuraavia kysymyksiä:
- Milloin olio saa numeron?
- Mistä olio tietää, mikä sen numeron pitäisi olla?
- Mikä tieto on jaettua olioiden kesken ja mikä on oliokohtaista?
Olion elinkaari
Ohjelman ajon aikana luokasta luodaan ilmentymä eli olio. Jotta olioon voidaan päästä käsiksi, luodaan sitä varten viitemuuttuja. Javan kääntäjä tarkistaa käännöksen yhteydessä, että muuttuja on yhteensopiva luodun olion kanssa, ja asettaa viitteen osoittamaan luotuun olioon.
Olion luonnin yhteydessä Java varaa sille sopivan tilan virtuaalikoneensa kekomuistista. Kun olioon ei enää ole yhtään viitettä olemassa, se tuhoutuu. Javassa ohjelmoijan ei tarvitse itse pitää huolta muistin varaamisesta tai vapauttamisesta. Tuhoutuneiden olioiden varaama muisti vapautetaan lopulta Javan automaattisen roskienkeräyksen toimesta.
Käydään vielä olion koko elinkaari läpi esimerkkien avulla. Tarvitsemme olioiden
luomista varten ensimmäiseksi luokan. Käytetään esimerkkinä taas
Henkilo-luokkaa, mutta lisätään nyt muutama hyvin yksinkertainen metodi olion
tilan muuttamiseen.
class Henkilo {
private String etunimi;
private String sukunimi;
public Henkilo(String etunimi, String sukunimi) {
this.etunimi = etunimi;
this.sukunimi = sukunimi;
}
public String annaNimi() {
return etunimi + " " + sukunimi;
}
public void asetaEtunimi(String etunimi) {
this.etunimi = etunimi;
}
public void asetaSukunimi(String sukunimi) {
this.sukunimi = sukunimi;
}
}
Pääohjelmassa luodaan nyt olio eri muodostajia käyttäen ja katsotaan samalla viitteiden toimintaa.
Kun olemme luoneet olioita, voimme tarkastella ja muokata niiden tilaa ohjelman suorituksen aikana.
void main() {
Henkilo h1 = new Henkilo("Joni", "Mäkinen");
IO.println(h1.annaNimi());
h1.asetaSukunimi("Korhonen");
IO.println(h1.annaNimi());
}
Tarkastellaan lopuksi olioiden elinkaaren loppua, eli niiden tuhoutumista. Kun olioon ei enää ole yhtään viitettä, se merkitään "roskaksi", jonka Javan automaattinen roskienkeräys (engl. garbage collection) voi aikanaan poistaa muistista vapauttaen sitä varten varten varatun tilan.
void main() {
// Luomme taas kaksi oliota.
Henkilo h1 = new Henkilo("Joni", "Mäkinen");
Henkilo h2 = new Henkilo("Anna", "Korhonen");
// Muuttuja h1 on tällä hetkellä ainoa viite ensimmäiseen olioon.
// Jos sijoitamme h1-muuttujaan jonkin muun viitten tai asetamme sen arvoksi
// null, olio merkitään tuhottavaksi, sillä siihen ei ole enää viitteitä.
h1 = null;
// Aliohjelman päättyessä kaikki sen sisällä luodut lokaalit
// muuttujat (h1 ja h2) tuhoutuvat. Tässä tapauksessa olioihin ei ole
// enää muita viitteitä, joten nekin tuhoutuvat.
}
Emme tällä kurssilla perehdy kovin syvällisesti Javan automaattiseen roskienkeräykseen tai muistin hallintaan. Tämän kurssin kannalta riittää, että tiedämme milloin olio muuttuu roskaksi ja tuhoutuu. Jos haluat tutustua aiheeseen hieman tarkemmin, voit aloittaa lukemalla täältä täältä lisää kekomuistista ja sen varaamisesta sekä täältä Javan roskienkeräyksestä suhteellisen helposti lähestyttävässä muodossa.
Tehtävät
Tee luokka Puhelin, jolla on attribuutit merkki (merkkijono) ja akunVaraus
(kokonaisluku, joka kuvaa akun varausta prosentteina väliltä 0-100). Lisää
luokkaan seuraavat metodit:
lahetaViesti(String henkilo, String viesti): tulostaa viestin muodossa "Lähetetään viesti henkilölle <henkilo>: <viesti>". Viestin lähettäminen vähentää akkua 5 prosenttiyksikköä.soita(String henkilo, int minuutit): tulostaa viestin muodossa "Soitetaan puhelu henkilölle <henkilo>, kesto: <minuutit> minuuttia". Soittaminen vähentää akkua 1 prosenttiyksikköä per minuutti.lataa(int prosentteja): lisää akun varausta annetun määrän, mutta akun varaus ei voi ylittää 100 prosenttia.tulostaTiedot(): tulostaa puhelimen merkin ja akun varauksen muodossa "Puhelimen <merkki> akun varaus on <akku>%".
Korvaa kulmasulkeissa olevat kohdat sopivilla attribuuttien / parametrien arvoilla.
Akun varaus ei voi mennä alle 0%.
Jos akun varaus on 0%, viestiä ei voida lähettää eikä voi soittaa "Akku tyhjä. Viestiä ei voida lähettää / ei voi soittaa.".
Testaa sovellustasi luomalla Puhelin-olio, lähettämällä viesti, soittamalla
puhelu, lataamalla akkua ja tulostamalla puhelimen tiedot.
Valinnainen lisätehtävä: Jos akku loppuu kesken
Muokkaa lahetaViesti- ja soita-metodeja siten, että jos akun varaus ei riitä
koko viestin lähettämiseen tai puhelun soittamiseen, niin lähetetään/soitetaan
niin kauan kuin akku riittää. Jos akku loppuu kesken puhelun / viestin, tulosta
kauanko puhelu onnistui / kuinka paljon viestistä saatiin lähetettyä ennen akun
loppumista. Esimerkiksi "Akku tyhjä. Sait lähetettyä 60% viestistä." tai "Akku
loppui 3 minuutin jälkeen.
Toteuta luokka Kirja, joka pitää kirjaa yksittäisistä kirjoista, mutta myös seuraa kirjaston tilastoja globaalisti.
Luo luokalle seuraavat muuttujat:
Oliomuuttujat:
String nimi: Kirjan nimi.String kirjoittaja: Kirjan kirjoittaja.boolean onLainassa: Kertoo, onko kyseinen kirja tällä hetkellä lainassa.
Luokkamuuttujat (static):
static int kirjojenMaara: Kuinka monta kirjaa on luotu yhteensä.static int lainassaOlevat: Kuinka monta kirjaa on tällä hetkellä lainassa.
Muodostajan tulee ottaa vastaan nimi ja kirjoittaja. Aina kun uusi kirja luodaan, kirjojenMaara-muuttuja kasvaa yhdellä.
Tee oliometodit lainaa() ja palauta: Nämä muuttavat kirjan onLainassa-tilaa ja päivittävät staattisen lainassaOlevat-laskurin.
Tee staattinen metodi tulostaTilastot(), joka tulostaa ruudulle kirjaston tilastot: "Kirjasto sisältää X kirjaa, joista Y on lainassa."
Saat valmiina pääohjelman, jota voit käyttää ohjelmasi testaamiseen
Kapselointi
osaamistavoitteet
- Tiedät, mitä näkyvyysmääreet kuten
publicjaprivatetarkoittavat. - Ymmärrät, että metodit ovat olioiden pääasiallinen tapa viestiä.
- Ymmärrät kapseloinnin periaateet ja hyödyt, ja miten olion sisäinen toteutus eroaa sen ulkoisesta käytöstä.
- Osaat toteuttaa ohjelman, jossa oliot toimivat yhdessä niin, että ne eivät ole riippuvaisia toistensa sisäisestä toteutuksesta.

Näkyvyysmääreet
Java tarjoaa kolme pääasiallista näkyvyysmäärettä: public, protected ja
private. Näkyvyysmääreet määrittelevät, mistä luokan jäseniin voidaan päästä
käsiksi.
Javassa oletuksena luokan jäsenet ovat ns. package-private-näkyvyydellä, mikä
tarkoittaa, että ne ovat näkyvissä vain samassa pakkauksessa oleville luokille.
Alla olevassa taulukossa on yhteenveto eri näkyvyysmääreiden vaikutuksista;
Oletus-rivi viittaa package-private-näkyvyyteen.
| Luokka | Pakkaus | Aliluokka | Muu maailma | |
|---|---|---|---|---|
public | Kyllä | Kyllä | Kyllä | Kyllä |
protected | Kyllä | Kyllä | Kyllä | Ei |
| oletus | Kyllä | Kyllä | Ei | Ei |
private | Kyllä | Ei | Ei | Ei |
Ensimmäinen sarake ("luokka") ilmaisee, onko luokan oliolla itsellään pääsy määritellyn näkyvyystason jäseneen. Kuten näet, oliolla on aina pääsy omiin jäseniinsä. Toinen sarake ("pakkaus") ilmaisee, onko muilla samassa pakkauksessa olevilla oliolla pääsy jäseneen. Kolmas sarake ("aliluokka") ilmaisee, onko luokasta perityillä aliluokan olioilla, jotka sijaitsevat pakkauksen ulkopuolella, pääsy jäseneen. Neljäs sarake ilmaisee, onko millä tahansa oliolla pääsy jäseneen.
Kun muut ohjelmoijat tai sinä itse käyttävät tekemääsi luokkaa, näkyvyysmääreet auttavat varmistamaan, että luokkaasi käytetään sillä tavalla, jolla olet suunnitellut sen käytettävän. Pääsääntö on, että ohjelmoijan tulisi käyttää mahdollisimman rajoittavaa näkyvyysmäärettä, ellei ole erityistä syytä käyttää jotain muuta. Tämä auttaa suojaamaan luokan sisäistä tilaa ja estämään tahalliset tai tahattomat väärinkäytökset luokan jäseniin. Erityisesti julkisten attribuuttien kohdalla tulisi pohtia tarkkaan, onko niille todella tarvetta, sillä ne altistavat luokan sisäisen tilan suoraan ulkopuolisille. Tällä opintojaksolla pyrimme suunnittelemaan ohjelmat niin, ettei julkisia attribuutteja -- poislukien vakiot -- tarvita.
huomautus
Tässä materiaalissa saatetaan hetkittäin käyttää esimerkinomaisesti julkisia attribuutteja. Tämä voi auttaa havainnollistamaan joitakin kohtia tiiviisti, mutta sitä ei suositella tuotantokoodissa.
Attribuutille tai metodille voi antaa näkyvyysmääreen lisäämällä sen esittelyriville. Luokalle voidaan myös asettaa näkyvyysmääre.
class Henkilo {
// Näkyvyysmääre 'private' piilottaa attribuutin niin, että sitä ei voi
// tarkastella luokan ulkopuolelta.
private String etunimi;
private String sukunimi;
// Näkyvyysmääre 'public' mahdollistaa metodin kutsumisen luokan ulkopuolelta.
public String annaMerkkijono() {
return etunimi + " " + sukunimi;
}
}
Kapselointi ja koheesio
Kapselointi (engl. encapsulation) on yksi keskeisimmistä käsitteistä olio-ohjelmoinnissa. Se tarkoittaa luokkien suunnittelua mahdollisimman itsenäiseksi ja modulaariseksi. Jokaisella luokalla on oma tehtävänsä, jota varten tarvittavat tiedot ja toiminnallisuudet kapseloidaan olion sisälle. Osa näistä tiedoista ja toiminnallisuuksista voidaan piilottaa vain olion sisäistä käyttöä varten. Olioiden tilan käsittelyä varten luokka tarjoaa käyttäjälleen tavallisesti julkisista metodeista koostuvan rajapinnan, mikä parantaa ohjelman muokattavuutta ja laajennettavuutta vähentämällä luokan sisäisistä muutoksista johtuvia sivuvaikutuksia.
Yhteen kuuluvan tiedon ja toiminnallisuuden sijoittaminen saman rakenteen sisälle on kapseloinnin ensimmäinen periaate. Olemmekin jo tehneet näin määritellessämme luokkia ja niille sopivia attribuutteja ja metodeja. Luokan koheesio (engl. cohesion) kuvastaa sitä, kuinka hyvin luokan attribuutit ja metodit sopivat yhteen luokan tarkoituksen kanssa. Luokkien suunnittelun tavoitteena on mahdollisimman korkea koheesio; luokan jäsenten tulisi olla sopivia sen tarkoitukseen.
Jos tekisimme esimerkiksi luokan kuvaamaan autoa, ei ehkä olisi kovin järkevää lisätä tähän luokkaan jäseneksi auton omistajan nimeä, osoitetta, puhelinnumeroa, jne. Omistajan tiedot ja niihin liittyvät toiminnallisuudet voi olla parempi laittaa omaan luokkaan.
Tehdään nyt luokka Auto, johon voimme soveltaa kapseloinnin periaatteita.
Lisätään aluksi vain attribuutit ja yksinkertaiset muodostajat.
class Auto {
String malli;
String valmistenumero;
double ajetutKilometrit;
public Auto(String malli, String valmistenumero, double ajetutKilometrit) {
this.malli = malli;
this.valmistenumero = valmistenumero;
this.ajetutKilometrit = ajetutKilometrit;
}
}
Haluaisimme tallentaa myös tietoja auton eri osista. Moottorista voisimme
tallentaa esimerkiksi mallin ja nykyisen kierrosluvun. Renkaista olisi hyvä
tietää ainakin malli, rengaspaine ja ehkä myös tyyppi, jolla voimme erottaa
kesä- ja talvirenkaat. Nämä voisi lisätä suoraan Auto-luokkaan
attribuuttina, mutta attribuuttien määrä kasvaisi aika suureksi, sillä
jokaisella renkaalla on omat tietonsa. Jos käyttäisimme taulukoita tai listoja,
tarvitsisimme niitäkin useita. Voi myös olla, että autossa ei välttämättä aina
ole moottoria kiinni. Renkaatkin on mahdollista ottaa irti tai niiden lukumäärä
voisi vaihdella automallien välillä.
Auton ei oikeastaan tarvitse olla tietoinen sen moottorin tai renkaiden sisäisestä toiminnasta, joten meidän on parempi tehdä useampi luokka, joista jokainen on vastuussa omista tiedoistaan ja toiminnoistaan.
Lisätään nyt Moottori ja Rengas -luokat ja määritellään näille sopivia
attribuutteja sekä muodostajat.
class Moottori {
String malli;
double kierrosluku;
public Moottori(String malli, double kierrosluku) {
this.malli = malli;
this.kierrosluku = kierrosluku;
}
}
Lisätään lopuksi moottori ja lista renkaista Auto-luokan attribuuteiksi. Nämä
sisältävät viitteet moottori- ja rengasolioihin. Muistetaan, että viitteet eivät
oletuksena osoita mihinkään, vaan meidän täytyy luoda myös oliot ja asettaa
viitteet osoittamaan niihin. Auto sisältää nyt muiden luokkien olioita, jotka
hoitavat omat vastuualueensa auton kokonaisuudessa. Tällaista rakennetta
kutsutaan kompositioksi (engl. composition). Yksi etu tässä on se, että auton
moottorin tai renkaat voisi vaihtaa helposti uusin asettamalla viiteattribuutit
osoittamaan uuteen olioon.
Lisätään myös pieni pääohjelma, jossa voimme luoda yhden auton oletusarvoilla ja tulostaa sen tietoja. Käytämme tässä vielä olion attribuuttien arvoja suoraan, mikä ei ole hyvän tavan mukaista. Teemme pian tämän paremmin.
class Moottori {
String malli;
double kierrosluku;
public Moottori(String malli, double kierrosluku) {
this.malli = malli;
this.kierrosluku = kierrosluku;
}
}
Kapseloinnin toinen periaate on luokan sisäisen tiedon piilottaminen ja sen käsittelyn rajoittaminen niin, että siihen päästään käsiksi vain tarkkaan määritetyn julkisen rajapinnan kautta. Oliolla voi olla sen tilan tallentamista varten paljon sisäistä tietoa, jonka ei ole tarkoitus olla tarkasteltavissa tai muokattavissa olion ulkopuolelta. Itse asiassa kaikki olion attribuutit tavallisesti piilotetaan, jotta oliota ei olisi mahdollista vahingossa saattaa attribuutteja suoraan muuttamalla virheelliseen tilaan. Oliolla voi myös olla sisäiseen käyttöön apumetodeja, joita ei ole tarkoitus voida kutsua ulkopuolelta.
Olion sisäisen tilan muokkaamista varten luokkaan määritellään julkisia metodeja, joita voidaan kutsua muualta ohjelmasta. Nämä metodit muodostavat edellä mainitun julkisen rajapinnan ja kaikki muutokset olion tilaan tapahtuvat niiden kautta, jolloin virheellisiin muutoksiin voidaan reagoida metodin sisällä sopivalla tavalla.
Se, mitä attribuutteja luokalla on tai miten niitä käsitellään luokan sisällä ovat yleensä toteutusyksityiskohtia, joita luokkaa käyttävän ohjelmoijan ei tarvitse tietää. Tällaisten toteutusyksityiskohtien piilottamisen tavoite on helpottaa ohjelmoijan työtä; kun luokan toteutusyksityiskohdat ovat piilotettuja ja tilaa käsitellään vain julkisen rajapinnan kautta, luokan sisäiseen toimintaan voidaan helpommin tehdä muutoksia niin, että luokan käyttäjä ei edes huomaa niiden tapahtuneen. Tämä on yksi kapseloinnin suurimmista höydyistä.
Lisätään Auto-luokalle muutama metodi yksinkertaista julkista rajapintaa
varten. Piilotetaan myös attribuutit, että emme voi muuttaa auton tilaa enää
suoraan. Luokan jäsenten näkyvyyttä luokan ja sen olioiden ulkopuolelle voidaan
muuttaa käyttämällä niiden esittelyn yhteydessä näkyvyysmääreitä kuten
public ja private. Moottori ja Rengas pysyvät vielä samana.
import java.util.ArrayList;
public class Auto {
private String malli;
private String valmistenumero;
private double ajetutKilometrit;
private Moottori moottori;
private ArrayList<Rengas> renkaat = new ArrayList<>();
public Auto(String malli, String valmistenumero, double ajetutKilometrit) {
this.malli = malli;
this.valmistenumero = valmistenumero;
this.ajetutKilometrit = ajetutKilometrit;
// Lisätään autolle moottori luomalla uusi moottori-olio.
moottori = new Moottori("M100", 0);
// Lisätään autolle neljä rengasta luomalla näille oliot ja lisäämällä
// viitteet renkaat-listaan.
renkaat.add(new Rengas("RR", "nastarengas", 100.0));
renkaat.add(new Rengas("RR", "nastarengas", 100.0));
renkaat.add(new Rengas("RR", "nastarengas", 100.0));
renkaat.add(new Rengas("RR", "nastarengas", 100.0));
}
public void aja(double kilometrit) {
if (kilometrit < 0) return;
this.ajetutKilometrit += kilometrit;
}
public void lisaaMoottori(Moottori moottori) {
this.moottori = moottori;
}
public void lisaaRengas(Rengas rengas) {
this.renkaat.add(rengas);
}
public void tulostaTiedot() {
IO.println("Malli: " + malli);
IO.println("Valmistenumero: " + valmistenumero);
IO.println("Ajetut kilometrit: " + ajetutKilometrit);
// Lisätään moottorin ja renkaiden tulostus seuraavaksi.
}
}
Piilotimme Auto-luokan attribuutit private-näkyvyymääritteellä ja auton
tilaa käsitellään nyt yksinkertaisten saantimetodien avulla. Siirsimme myös
tulostamisen luokan vastuulle. Meidän tulisi tehdä vielä samanlaiset muutokset
Moottori ja Rengas -luokille, jotta voimme käyttää niiden julkisia
rajapintoja Auto-luokan sisällä.
import java.util.ArrayList;
public class Auto {
private String malli;
private String valmistenumero;
private double ajetutKilometrit;
private Moottori moottori;
private final int maxRenkaat = 4;
private ArrayList<Rengas> renkaat = new ArrayList<>();
public Auto(String malli, String valmistenumero, double ajetutKilometrit) {
this.malli = malli;
this.valmistenumero = valmistenumero;
this.ajetutKilometrit = ajetutKilometrit;
}
public void aja(double kilometrit) {
if (kilometrit < 0) return;
this.ajetutKilometrit += kilometrit;
}
public void lisaaMoottori(Moottori moottori) {
this.moottori = moottori;
}
public void lisaaRengas(Rengas rengas) {
if (renkaat.size() < maxRenkaat) {
this.renkaat.add(rengas);
}
}
public void tulostaTiedot() {
IO.println("Auton malli: " + malli);
IO.println("Auton valmistenumero: " + valmistenumero);
IO.println("Ajetut kilometrit: " + ajetutKilometrit);
// Välitetään tulostuskomento moottorille, joka tulostaa itse omat tietonsa.
IO.println();
IO.println("Moottori:");
IO.println();
if (moottori != null) {
moottori.tulostaTiedot();
}
// Välitetään tulostuskomento myös jokaiselle renkaalle.
IO.println();
IO.println("Renkaat:");
for (Rengas rengas : renkaat) {
IO.println();
rengas.tulostaTiedot();
}
}
}
Nyt Auto-luokkamme ei enää ole riippuvainen Moottori tai Rengas -luokkien
sisäisistä toteutusyksityiskohdista.
Vastuu olion tilasta kuuluu oliolle itselleen
Joissain tähän mennessä nähdyissä esimerkeissä attribuutteja piilotettiin, mutta niitä voitiin edelleen muokata lähes suoraan metodin kautta. Tämä ei ole täysin olio-ohjelmoinnin tavoitteiden mukaista; julkisen rajapinnan ei ole tarkoitus olla vain väliaskel attribuutin muuttamisessa. Julkisen rajapinnan idea on välittää oliolle käsky tehdä jotain, jolloin olio suorittaa käskyn itse parhaaksi näkemällään tavalla.
Hyvä esimerkki tästä voisi olla esimerkiksi pelihahmo, jolla on sijaintia varten
attribuutteina koordinaatit X ja Y. Sen sijaan, että muuttaisimme pelihahmon
sijaintia suoraan yksinkertaisilla setX tai setY -metodeilla, voisimme
määritellä hahmolle liiku-metodin, joka ottaa tavoitekoordinaatit vastaan ja
sallii pelihahmon itse suorittaa tavoitekoordinaatteihin liikkumisen oman
sisäisen toteutuksensa ja rajoitteidensa mukaisesti. Tällöin vastuu
liikkumisesta kuuluu pelihahmolle itselleen. Jos pelihahmo ei esimerkiksi
kykenisi sillä hetkellä liikkumaan, metodi voi käsitellä tilanteen itse, jolloin
metodin kutsujan ei tarvitse ottaa tällaisia erikoistilanteita huomioon.
Todellisuudessa yksinkertaisia saantimetodeja käytetään usein myös tuotantokoodissa, sillä olioiden hyvä suunnittelu vie paljon aikaa ja vaivaa. Joskus voi olla myös ihan perusteltua muuttaa yksinkertaisen attribuutin arvoa suoraan metodin kautta. Metodi tuntuu tässä tapauksessa aika turhalta, mutta sen olemassaolo kuitenkin mahdollistaa sen, että luokan sisäistä toteutusta voidaan muuttaa ilman, että luokkaa käyttävä ohjelmakoodi hajoaa.
Tällä kurssilla emme valitettavasti ehdi käydä oliosuunnittelun teoriaa perusteellisesti läpi. Suosittelemme olio-ohjelmoinnin teorian oppimiseen tämän osan alussa mainittua kurssia.
Olioiden yhteistoiminta
Kerrataan vielä esimerkin avulla olioiden ja niiden yhteistyön suunnittelua tässä osassa opittuja käsitteitä käyttäen. Olioiden hyödyt tulevat paremmin esille, kun alamme rakentamaan ohjelmaan useampia luokkia, jotka tekevät yhteistyötä. Nyt kun olemme myös oppineet kapseloinnin periaatteista, voimme käyttää niitä organisoimaan ohjelmakoodia fiksummin.
Oliot voivat toimia yhdessä eri tavoin. Oliot voivat esimerkiksi sisältää toisia olioita - tai tarkemmin ilmaistuna viitteitä toisiin olioihin. Kun olio koostuu olioista, joista jokainen tuo oman toiminnallisuutensa, tätä kutsutaan usein kompositioksi. Oliot voivat kutsua toistensa metodeja ja näin delegoida tehtäviä toiselle oliolle, jolle tehtävän vastuu kuuluu, tai kommunikoida esimerkiksi tapahtumien yhteydessä. Oliot voivat myös sisältää kokoelmia olioista; esimerkiksi Javan sisäänrakennetut tietorakenteet ovat olioita, jotka sisältävät kokoelman olioista.
Katsotaan esimerkkiä, jossa haluamme mallintaa ohjelmallamme rakennuksia, niissä sijaitsevia tiloja sekä tiloihin tehtäviä varauksia. Jokaisessa rakennuksessa voi olla monta tilaa ja jokaisessa tilassa voi olla monta varausta. Emme välitä vielä tässä esimerkissä siitä, voiko varauksia olla samassa tilassa päällekkäin. Pidetään myös luokat vielä suhteellisen yksinkertaisina.
Aloitetaan määrittelemällä tarvittavat luokat Rakennus, Tila ja Varaus.
Tehdään näille myös muodostajat, jotka alustavat olion heti käyttökelpoiseen
tilaan.
import java.util.*;
public class Rakennus {
private String nimi;
private List<Tila> tilat = new ArrayList<>();
public Rakennus(String nimi) {
this.nimi = nimi;
}
}
Lisätään nyt rakennukselle sopivat metodit tilojen lisäämiseen. Haluaisimme löytää oikean tilan myöhemmin sen nimen perusteella, joten rakennuksessa ei saisi olla saman nimisiä tiloja. Tarvitsemme siis myös keinon hakea tila sen nimen perusteella. Tilan lisäämiseen tarvitsemme vain tilan nimen.
import java.util.*;
public class Rakennus {
private String nimi;
private List<Tila> tilat = new ArrayList<>();
public Rakennus(String nimi) {
this.nimi = nimi;
}
public Tila haeTila(String tilanNimi) {
for (Tila tila : tilat) {
if (tila.getNimi().equals(tilanNimi)) {
return tila;
}
}
return null;
}
public void lisaaTila(String tilanNimi) {
Tila tila = haeTila(tilanNimi);
if (tila != null) {
IO.println("Rakennus " + nimi + " sisältää jo tilan " + tilanNimi);
return;
}
tilat.add(new Tila(tilanNimi));
}
}
Tilaan pitäisi voida tehdä varauksia. Lisätään tämä ominaisuus, mutta pidetään esimerkki yksinkertaisena tekemällä varaukset tasatunneille. Ohjelman tila voisi kuvastaa esimerkiksi seuraavan päivän tilavarauksia.
Tarvitsemme varauksen tekemiseen varaajan nimen sekä varauksen alkamistunnin ja
keston. Lisätään oheinen metodi Tila-luokkaan.
public void lisaaVaraus(String varaaja, int ajankohta, int kesto) {
varaukset.add(new Varaus(varaaja, ajankohta, kesto));
}
Lisätään vielä mahdollisuus lisätä varauksia rakennuksen kautta niin, että
käyttäjän ei tarvitse edes tietää, että Tila-luokka on olemassa. Se miten
Rakennus tallentaa tiedot tiloista jää sen toteutusyksityiskohdaksi.
Tarvitsemme varauksen lisäämiseen tilan ja varaajan nimet sekä varauksen alkamistunnin ja keston.
Lisätään myös yksinkertaiset tulosta-metodit kaikkiin luokkiin, jotta voimme
nähdä kaikki rakennuksen tilat ja varaukset helposti.
import java.util.*;
public class Rakennus {
private String nimi;
private List<Tila> tilat = new ArrayList<>();
public Rakennus(String nimi) {
this.nimi = nimi;
}
public Tila haeTila(String tilanNimi) {
for (Tila tila : tilat) {
if (tila.getNimi().equals(tilanNimi)) {
return tila;
}
}
return null;
}
public void lisaaTila(String tilanNimi) {
Tila tila = haeTila(tilanNimi);
if (tila != null) {
IO.println("Rakennus '" + nimi + "' sisältää jo tilan '" + tilanNimi + "'");
return;
}
tilat.add(new Tila(tilanNimi));
}
public void lisaaVaraus(String tilanNimi, String varaaja, int ajankohta, int kesto) {
Tila tila = haeTila(tilanNimi);
if (tila == null) {
IO.println("Rakennuksessa '" + nimi + "' ei ole tilaa '" + tilanNimi + "'");
return;
}
tila.lisaaVaraus(varaaja, ajankohta, kesto);
}
public void tulosta() {
IO.println(nimi);
for (Tila tila : tilat) {
tila.tulosta();
}
}
}
Voimme nyt käyttää Rakennus-luokkaa niin, että meidän ei tarvitse olla
tietoisia tilojen tai varausten toiminnasta. Vastaavasti Rakennus ei riipu
suoraan siitä, miten Tila tallentaa varausten tietoja.
Tässä vaiheessa meiltä puuttuu vielä osa olio-ohjelmoinnin tärkeimmistä työkaluista; perintä, polymorfismi ja rajapinnat. Tutustumme näihin seuraavassa osassa.
Tehtävät
Toteuta luokka Ovi, joka mallintaa ovea, joka voi olla joko lukossa tai auki.
Attribuutit:
private boolean lukossaprivate String avainkoodi
Muodostaja saa oven avainkoodin parametrina: Ovi(String avainkoodi)
Muodostajan pitää asettaa avainkoodi ja asettaa ovi aluksi lukkoon.
Metodit:
boolean avaa(String koodi): avaa oven vain, jos koodi on oikein ja ovi on lukossa. Palauttaatrue, jos ovi avattiin, muutenfalse.boolean lukitse(): lukitsee oven vain, jos se on auki. Palauttaatrue, jos lukitseminen onnistui, muutenfalse.boolean vaihdaKoodi(String vanha, String uusi): vaihtaa avainkoodin uuteen, jos vanha koodi on oikein ja ovi on auki. Uusi koodi ei voi olla tyhjä merkkijono. Palauttaatrue, jos vaihto onnistui, muutenfalse.String tila(): palauttaa merkkijonon "Ovi on lukossa" tai "Ovi on auki"
Vain tila() saa tulostaa jotain. Muut metodit eivät.
Kirjoita pääohjelma, jossa
- luot oven
- lukitset oven
- yrität avata ovea väärällä koodilla
- avaat oven oikealla koodilla
- yrität avata jo avointa ovea
- yrität lukita jo lukittua ovea
- yrität vaihtaa koodia kun ovi on lukossa
- vaihdat koodin väärällä vanhalla koodilla
- vaihdat koodin oikealla vanhalla koodilla
- tulostat oven tilan
Toteuta luokka Saastolipas, jonka tarkoituksena on säilyttää rahaa.
Attribuutit:
private double saldo: Säästölippaan nykyinen rahamäärä.private String omistaja: Lippaan omistajan nimi.private final String SALASANA: Salasana, joka tarvitaan rahojen nostamiseen.
Konstruktori: Ottaa vastaan omistaja ja SALASANA -arvot. Asettaa
alkusaldoksi 0.0.
Metodit:
public void talleta(double maara): Lisää rahaa vain, jos maara on positiivinen.public double nosta(double maara, String annettuSalasana): Tarkistaa, onko annettuSalasana oikein. Tarkistaa, onko lippaassa tarpeeksi rahaa. Jos molemmat täyttyvät, vähentää saldon ja palauttaa nostetun määrän. Muuten palauttaa 0.0 ja tulostaa virheilmoituksen.public void tulostaSaldo(): Tulostaa "Hei <omistaja>, lippaasi saldo on <saldo> euroa.".
Korvaa kulmasulkeissa olevat kohdat sopivilla attribuuttien / parametrien arvoilla.
Tee pääohjelma, jossa luot Saastolipas-olion ja testaat sen toiminnallisuutta
eri tilanteissa, kuten tallettaminen, onnistunut nosto ja epäonnistuneet nostot
(väärä salasana, liian suuri nostettava määrä).
Tässä tehtävässä rakennat järjestelmän, joka valvoo rakennusten sähkönkulutusta ja estää sulakkeiden palamisen. Tehtävä koostuu vaiheista.
Vaihe 1: Sähkölaite
Tee luokka Sahkolaite. Laitteilla on kaksi muuttumatonta ominaisuutta: nimi ja
virrankulutus ampeereina.
Lisää attribuutit: private final String NIMI ja private final double VIRTA.
Oletetaan, että virrankulutus on aina positiivinen luku.
Tee konstruktori, joka asettaa nämä arvot.
Lisää private boolean kytketty, joka kertoo, onko laite päällä.
Tee metodit kytke() ja irrota(), jotka muuttavat kytketty-muuttujan tilaa.
Tee myös getVirta()-metodi, joka palauttaa laitteen virrankulutuksen,
vastaavasti getNimi().
Kokeile luokkaasi pääohjelmassa luomalla muutama laite ja kytkemällä niitä päälle ja pois.
Vaihe 2: Keskus ja oliolista
Luo Sahkokeskus-luokka. Tämän luokan tarkoituksena on hallita sähkölaitteita.
Lisää luokkaan attribuutti final double SULAKKEEN_KOKO. Sulakkeen koko voi
olla esimerkiksi 16 ampeeria tai 35 ampeeria. Lisää myös private boolean sulakePaalla, joka kertoo, onko sulake ehjä (true) vai palanut (false). Aluksi
sulake on päällä.
Luo List<Sahkolaite> paallaOlevatLaitteet (käytä ArrayListia).
Tee metodi double laskeNykyinenVirta(), joka käy listan läpi ja laskee laitteiden VIRTA-arvojen summan.
Vaihe 3: Valvova logiikka ja tilan hallinta
Keskuksen on päätettävä, saako laitteen kytkeä päälle.
Tee metodi boolean kytke(Sahkolaite laite). Metodin tulee tarkistaa, onko nykyinen virta + uuden laitteen virta <= sulakekoko. Jos on, laite lisätään listaan ja sille kutsutaan laite.kytke(). Muussa tapauksessa sulake palaa: aseta sulakePaalla = false, sammuta kaikki listan laitteet (irrota()) ja tyhjennä päälläolevien laitteiden lista.
Tee myös metodi void irrota(Sahkolaite laite), joka poistaa laitteen listasta ja kutsuu laite.irrota().
Vaihe 4: Globaali seuranta
Sähköyhtiö haluaa seurata kaikkien keskusten tilannetta.
Lisää Sahkokeskus-luokkaan static double kokonaisKulutusValtakunnassa.
Päivitä tätä muuttujaa aina, kun jokin laite missä tahansa keskuksessa kytketään päälle, irrotetaan sähkökeskuksesta tai kun sulake palaa.
Lisää static-metodi tulostaValtakunnanTilanne(), joka tulostaa kokonaiskulutuksen.
Voit testata ohjelmaasi TIMissä olevalla valmiilla pääohjelmalla, tai voit kirjoittaa oman testiohjelmasi.
Muokkaa esimerkin Rakennus, Tila ja Varaus -luokat sisältävää ohjelmaa
niin, että ohjelma ei anna lisätä samaan tilaan päällekkäisiä varauksia. Jos
tilassa on jo varaus, joka olisi päällekkäin uuden varauksen kanssa, uutta
varausta ei luoda.
Lisää myös tarkistukset, jotka estävät virheellisten varausten luomisen. Varauksen keston täytyy olla vähintään 1 tunti ja alkuajankohdan täytyy olla välillä 0-23.
Virhetilanteet voi tässä tehtävässä käsitellä tulostamalla virheilmoituksen.
Ennen tehtävän aloittamista kannattaa miettiä hetki, mitkä vastuut kuuluvat millekin oliolle.
Voit testata ohjelman toimintaa valmiiksi annetulla pääohjelmalla.
Osan kaikki tehtävät
huomautus
Jos palautat tehtävät ennen osion takarajaa (ma 26.1.2026 klo 11:59 (keskipäivä)), voit saada DL-BONUS-pisteitä harjoitustehtäviin. Lue lisää suorittaminen-sivulta.
Tee luokka Kello, jolla on attribuutit minuutit ja tunnit
kokonaislukuina.
Lisää luokkaan metodit setMinuutit ja setTunnit, jotka ottavat parametrina
kelloon minuutit ja tunnit. Minuuttien täytyy olla välillä 0-59, muuten
tulostetaan virheilmoitus, ja aika pysyy entisellään. Samoin tuntien täytyy olla
välillä 0-23, muuten tulostetaan virheilmoitus.
Tee myös metodi naytaAika, joka tulostaa ajan muodossa "HH:MM".
Voit testata luokan toimintaa valmiin pääohjelman avulla.
Tee luokka Ajastin, jolla on attribuutit minuutit ja sekunnit
kokonaislukuina.
Lisää luokkaan metodit lisaaMinuutteja ja lisaaSekunteja, jotka ottavat
parametrina ajastimeen lisättävät minuutit ja sekunnit. Lisää myös metodi
annaMerkkijono, joka antaa ajastimen minuutit ja sekunnit merkkijonona.
Minuutteja voi olla kuinka monta tahansa, mutta sekuntien täytyy olla välillä 0-59. Jos sekunnit ylittävät rajan, muutetaan ne minuuteiksi. Sekunnit voi muuttaa minuuteiksi + sekunneiksi esimerkiksi näin:
int sekunteja = 75; // Esimerkki parametrina tulevasta arvosta
// Tämä antaa this.minuutteja-attribuuttiin _lisättävät_ minuutit
int lisattaviaMinuutteja = (this.sekunteja + sekunteja) / 60;
// Tämä antaa this.sekunteja-attribuuttiin _sijoitettavat_ sekunnit
int jaljelleJaavatSekunnit = (this.sekunteja + sekunteja) % 60;
Voit testata luokan toimintaa valmiin pääohjelman avulla.
Valitse jokin tosimaailman esine tai käsite ja tee siitä oma yksinkertainen luokka.
Lisää luokkaan seuraavat:
- Vähintään kaksi sille sopivaa attribuuttia.
- Vähintään kaksi metodia, jotka muuttavat tai tarkastelevat olion tilaa jollain tavalla.
- Vähintään kaksi erilaista muodostajaa, jotka alustavat olion järkevään tilaan. Se mikä on järkevä tila riippuu omasta oliostasi.
Voit käyttää materiaalin esimerkkejä tarvittaessa inspiraationa attribuutteja ja metodeja keksiessä.
Luo olio ja kokeile sen tilan muuttamista pääohjelmassa. Tulosta olion tietoja ennen tilan muuttamista ja sen jälkeen.
Muuta esimerkin Henkilo-luokkaa niin, että jokainen luotu henkilö saa
automaattisesti oman järjestysnumeron, joka on kokonaisluku.
Ensimmäiseksi luodun henkilön numero on 1, seuraava saa numeroksi 2, jne.
Numeroa ei saa antaa henkilölle sen ulkopuolelta esimerkiksi kutsumalla sen metodia.
Toteuta luokkaan myös metodi annaNumero, joka palauttaa tietyn olion numeron
kokonaislukuna. Muita metodeja ei tarvitse lisätä.
Vinkki
Tarvitset tässä tehtävässä staattisia luokan jäseniä.
Mieti aluksi seuraavia kysymyksiä:
- Milloin olio saa numeron?
- Mistä olio tietää, mikä sen numeron pitäisi olla?
- Mikä tieto on jaettua olioiden kesken ja mikä on oliokohtaista?
Tee luokka Puhelin, jolla on attribuutit merkki (merkkijono) ja akunVaraus
(kokonaisluku, joka kuvaa akun varausta prosentteina väliltä 0-100). Lisää
luokkaan seuraavat metodit:
lahetaViesti(String henkilo, String viesti): tulostaa viestin muodossa "Lähetetään viesti henkilölle <henkilo>: <viesti>". Viestin lähettäminen vähentää akkua 5 prosenttiyksikköä.soita(String henkilo, int minuutit): tulostaa viestin muodossa "Soitetaan puhelu henkilölle <henkilo>, kesto: <minuutit> minuuttia". Soittaminen vähentää akkua 1 prosenttiyksikköä per minuutti.lataa(int prosentteja): lisää akun varausta annetun määrän, mutta akun varaus ei voi ylittää 100 prosenttia.tulostaTiedot(): tulostaa puhelimen merkin ja akun varauksen muodossa "Puhelimen <merkki> akun varaus on <akku>%".
Korvaa kulmasulkeissa olevat kohdat sopivilla attribuuttien / parametrien arvoilla.
Akun varaus ei voi mennä alle 0%.
Jos akun varaus on 0%, viestiä ei voida lähettää eikä voi soittaa "Akku tyhjä. Viestiä ei voida lähettää / ei voi soittaa.".
Testaa sovellustasi luomalla Puhelin-olio, lähettämällä viesti, soittamalla
puhelu, lataamalla akkua ja tulostamalla puhelimen tiedot.
Valinnainen lisätehtävä: Jos akku loppuu kesken
Muokkaa lahetaViesti- ja soita-metodeja siten, että jos akun varaus ei riitä
koko viestin lähettämiseen tai puhelun soittamiseen, niin lähetetään/soitetaan
niin kauan kuin akku riittää. Jos akku loppuu kesken puhelun / viestin, tulosta
kauanko puhelu onnistui / kuinka paljon viestistä saatiin lähetettyä ennen akun
loppumista. Esimerkiksi "Akku tyhjä. Sait lähetettyä 60% viestistä." tai "Akku
loppui 3 minuutin jälkeen.
Toteuta luokka Kirja, joka pitää kirjaa yksittäisistä kirjoista, mutta myös seuraa kirjaston tilastoja globaalisti.
Luo luokalle seuraavat muuttujat:
Oliomuuttujat:
String nimi: Kirjan nimi.String kirjoittaja: Kirjan kirjoittaja.boolean onLainassa: Kertoo, onko kyseinen kirja tällä hetkellä lainassa.
Luokkamuuttujat (static):
static int kirjojenMaara: Kuinka monta kirjaa on luotu yhteensä.static int lainassaOlevat: Kuinka monta kirjaa on tällä hetkellä lainassa.
Muodostajan tulee ottaa vastaan nimi ja kirjoittaja. Aina kun uusi kirja luodaan, kirjojenMaara-muuttuja kasvaa yhdellä.
Tee oliometodit lainaa() ja palauta: Nämä muuttavat kirjan onLainassa-tilaa ja päivittävät staattisen lainassaOlevat-laskurin.
Tee staattinen metodi tulostaTilastot(), joka tulostaa ruudulle kirjaston tilastot: "Kirjasto sisältää X kirjaa, joista Y on lainassa."
Saat valmiina pääohjelman, jota voit käyttää ohjelmasi testaamiseen
Toteuta luokka Ovi, joka mallintaa ovea, joka voi olla joko lukossa tai auki.
Attribuutit:
private boolean lukossaprivate String avainkoodi
Muodostaja saa oven avainkoodin parametrina: Ovi(String avainkoodi)
Muodostajan pitää asettaa avainkoodi ja asettaa ovi aluksi lukkoon.
Metodit:
boolean avaa(String koodi): avaa oven vain, jos koodi on oikein ja ovi on lukossa. Palauttaatrue, jos ovi avattiin, muutenfalse.boolean lukitse(): lukitsee oven vain, jos se on auki. Palauttaatrue, jos lukitseminen onnistui, muutenfalse.boolean vaihdaKoodi(String vanha, String uusi): vaihtaa avainkoodin uuteen, jos vanha koodi on oikein ja ovi on auki. Uusi koodi ei voi olla tyhjä merkkijono. Palauttaatrue, jos vaihto onnistui, muutenfalse.String tila(): palauttaa merkkijonon "Ovi on lukossa" tai "Ovi on auki"
Vain tila() saa tulostaa jotain. Muut metodit eivät.
Kirjoita pääohjelma, jossa
- luot oven
- lukitset oven
- yrität avata ovea väärällä koodilla
- avaat oven oikealla koodilla
- yrität avata jo avointa ovea
- yrität lukita jo lukittua ovea
- yrität vaihtaa koodia kun ovi on lukossa
- vaihdat koodin väärällä vanhalla koodilla
- vaihdat koodin oikealla vanhalla koodilla
- tulostat oven tilan
Toteuta luokka Saastolipas, jonka tarkoituksena on säilyttää rahaa.
Attribuutit:
private double saldo: Säästölippaan nykyinen rahamäärä.private String omistaja: Lippaan omistajan nimi.private final String SALASANA: Salasana, joka tarvitaan rahojen nostamiseen.
Konstruktori: Ottaa vastaan omistaja ja SALASANA -arvot. Asettaa
alkusaldoksi 0.0.
Metodit:
public void talleta(double maara): Lisää rahaa vain, jos maara on positiivinen.public double nosta(double maara, String annettuSalasana): Tarkistaa, onko annettuSalasana oikein. Tarkistaa, onko lippaassa tarpeeksi rahaa. Jos molemmat täyttyvät, vähentää saldon ja palauttaa nostetun määrän. Muuten palauttaa 0.0 ja tulostaa virheilmoituksen.public void tulostaSaldo(): Tulostaa "Hei <omistaja>, lippaasi saldo on <saldo> euroa.".
Korvaa kulmasulkeissa olevat kohdat sopivilla attribuuttien / parametrien arvoilla.
Tee pääohjelma, jossa luot Saastolipas-olion ja testaat sen toiminnallisuutta
eri tilanteissa, kuten tallettaminen, onnistunut nosto ja epäonnistuneet nostot
(väärä salasana, liian suuri nostettava määrä).
Tässä tehtävässä rakennat järjestelmän, joka valvoo rakennusten sähkönkulutusta ja estää sulakkeiden palamisen. Tehtävä koostuu vaiheista.
Vaihe 1: Sähkölaite
Tee luokka Sahkolaite. Laitteilla on kaksi muuttumatonta ominaisuutta: nimi ja
virrankulutus ampeereina.
Lisää attribuutit: private final String NIMI ja private final double VIRTA.
Oletetaan, että virrankulutus on aina positiivinen luku.
Tee konstruktori, joka asettaa nämä arvot.
Lisää private boolean kytketty, joka kertoo, onko laite päällä.
Tee metodit kytke() ja irrota(), jotka muuttavat kytketty-muuttujan tilaa.
Tee myös getVirta()-metodi, joka palauttaa laitteen virrankulutuksen,
vastaavasti getNimi().
Kokeile luokkaasi pääohjelmassa luomalla muutama laite ja kytkemällä niitä päälle ja pois.
Vaihe 2: Keskus ja oliolista
Luo Sahkokeskus-luokka. Tämän luokan tarkoituksena on hallita sähkölaitteita.
Lisää luokkaan attribuutti final double SULAKKEEN_KOKO. Sulakkeen koko voi
olla esimerkiksi 16 ampeeria tai 35 ampeeria. Lisää myös private boolean sulakePaalla, joka kertoo, onko sulake ehjä (true) vai palanut (false). Aluksi
sulake on päällä.
Luo List<Sahkolaite> paallaOlevatLaitteet (käytä ArrayListia).
Tee metodi double laskeNykyinenVirta(), joka käy listan läpi ja laskee laitteiden VIRTA-arvojen summan.
Vaihe 3: Valvova logiikka ja tilan hallinta
Keskuksen on päätettävä, saako laitteen kytkeä päälle.
Tee metodi boolean kytke(Sahkolaite laite). Metodin tulee tarkistaa, onko nykyinen virta + uuden laitteen virta <= sulakekoko. Jos on, laite lisätään listaan ja sille kutsutaan laite.kytke(). Muussa tapauksessa sulake palaa: aseta sulakePaalla = false, sammuta kaikki listan laitteet (irrota()) ja tyhjennä päälläolevien laitteiden lista.
Tee myös metodi void irrota(Sahkolaite laite), joka poistaa laitteen listasta ja kutsuu laite.irrota().
Vaihe 4: Globaali seuranta
Sähköyhtiö haluaa seurata kaikkien keskusten tilannetta.
Lisää Sahkokeskus-luokkaan static double kokonaisKulutusValtakunnassa.
Päivitä tätä muuttujaa aina, kun jokin laite missä tahansa keskuksessa kytketään päälle, irrotetaan sähkökeskuksesta tai kun sulake palaa.
Lisää static-metodi tulostaValtakunnanTilanne(), joka tulostaa kokonaiskulutuksen.
Voit testata ohjelmaasi TIMissä olevalla valmiilla pääohjelmalla, tai voit kirjoittaa oman testiohjelmasi.
Muokkaa esimerkin Rakennus, Tila ja Varaus -luokat sisältävää ohjelmaa
niin, että ohjelma ei anna lisätä samaan tilaan päällekkäisiä varauksia. Jos
tilassa on jo varaus, joka olisi päällekkäin uuden varauksen kanssa, uutta
varausta ei luoda.
Lisää myös tarkistukset, jotka estävät virheellisten varausten luomisen. Varauksen keston täytyy olla vähintään 1 tunti ja alkuajankohdan täytyy olla välillä 0-23.
Virhetilanteet voi tässä tehtävässä käsitellä tulostamalla virheilmoituksen.
Ennen tehtävän aloittamista kannattaa miettiä hetki, mitkä vastuut kuuluvat millekin oliolle.
Voit testata ohjelman toimintaa valmiiksi annetulla pääohjelmalla.
Perintä ja polymorfismi
osaamistavoitteet
- Osaat tehdä luokkahierarkian käyttämällä perintää
- Osaat korvata kantaluokan toiminnallisuuden
- Ymmärrät konkreettisten ja abstraktien luokkien eron
- Ymmärrät, miten polymorfismi mahdollistaa erilaisten luokkien käsittelyn yhtenäisesti
Perintä
osaamistavoitteet
- Ymmärrät perinnän käsitteen olio-ohjelmoinnissa ja osaat periä luokkia Javassa
- Osaat luoda yksinkertaisen luokkahierarkian, jossa luokka perii toisen luokan
- Ymmärrät, että kaikki Javan luokat perivät
Object-luokan
Perintä tarkoittaa mekanismia, jossa luokka sisällyttää itseensä toisen luokan ominaisuudet (attribuutit) ja toiminnallisuudet (metodit). Tämä mahdollistaa koodin uudelleenkäytön ja luokkien välisen hierarkian luomisen.
Käytännössä olioilla on usein yhteisiä piirteitä. Yksi tapa näiden yhteisten piirteiden käsittelemiseen siten, että koodia ei tarvitse toistaa, on perintä.
Opintotietojärjestelmä
Otetaan keksitty esimerkki henkilötietojärjestelmästä: Olli Opiskelija, Maija Opettaja ja Satu Sihteeri voisivat kaikki olla olioita kuvitteellisessa Kisu-opintotietojärjestelmässä. Kaikilla näillä on kaikille käyttäjille tyypillisiä ominaisuuksia, kuten nimi ja käyttäjätunnus. Jokaisen pitäisi myös päästä kirjautumaan sisään järjestelmään ja sieltä ulos.
Kullakin käyttäjällä on kuitenkin myös omia erityispiirteitään: Opiskelijalla voisi olla lista kursseista, joille hän on ilmoittautunut, sekä hänen suorittamansa opintopisteet. Opettajalla on kurssit, joita hän opettaa sekä tehtävänimike, mutta hänellä ei ole opintopisteitä. Sihteeri on vastuussa opintosuoritusten kirjaamisesta ja tutkinnon antamisesta, mutta hänellä ei ole opiskelijanumeroa tai opetettavia kursseja.
Lähdetään kuitenkin aluksi liikkeelle pienesti. Alla on Opiskelija- ja
Opettaja-luokat, joihin olemme tehneet pari attribuuttia ja metodia. Tutki
näitä luokkia.
varoitus
Useista alla olevista esimerkeistä puuttuu dokumentaatiokommentit, tai kommentit voivat olla osittain puutteellisia. Tämä on tietoinen valinta, sillä niiden lisääminen pidentäisi esimerkkikoodia, ja siten hankaloittaisi lukemista. Tässä materiaalissa koodia "dokumentoidaan" ja sitä selitetään ympäröivällä tekstillä, joten tämän materiaalin esitystavassa dokumentaatio ei ole välttämättä tarpeen.
varoitus
Alla oleva esimerkki on tarkoitettu havainnollistamaan perinnän
syntaksia, eikä siitä syystä noudata (vielä) parhaita käytäntöjä. Erityisesti
nimen asettaminen sellaisenaan attribuutin arvoksi käyttämällä julkista
setNimi-metodia rikkoo tiedon piilottamisen periaatetta (ks. Luku
2.3). Korjaamme tämän asian kuitenkin esimerkin
edetessä.
import java.util.ArrayList;
class Opiskelija {
String nimi;
List<String> kaynnissaOlevatKurssit;
public Opiskelija() {
this.kaynnissaOlevatKurssit = new ArrayList<>();
}
void setNimi(String nimi) {
this.nimi = nimi;
}
String getNimi() {
return this.nimi;
}
void naytaOpintoOhjelma() {
String kurssit = String.join(", ", kaynnissaOlevatKurssit);
IO.println(nimi + " opiskelee kursseilla: " + kurssit);
}
void ilmoittauduKurssille(String kurssi) {
IO.println(this.nimi + " ilmoittautui kurssille: " + kurssi);
kaynnissaOlevatKurssit.add(kurssi);
}
}
Huomaat, että kummassakin luokassa on samat attribuutti nimi sekä metodit
getNimi ja setNimi. Näiden luokkien välillä on toki myös eroja, mutta nimen
omaan toisto on ongelmallista, koska:
- jokaisessa luokassa on määriteltävä samat ominaisuudet ja toiminnot uudelleen,
- jos haluamme muuttaa jotain yhteistä ominaisuutta tai toimintoa, meidän täytyy tehdä se kolmessa eri paikassa,
- uuden luokan lisääminen, jolla on samat ominaisuudet, vaatii saman koodin kopioimisen uudelleen taas uuteen paikkaan.
Jos nyt haluaisimme muuttaa esimerkiksi nimi-attribuuttia niin, että etunimi
ja sukunimi tallennetaan erikseen kahteen attribuuttiin, meidän pitäisi tehdä
tämä muutos kaikissa näissä luokissa. Tämä lisää virheiden mahdollisuutta ja
tekee koodin ylläpidosta hyvin hankalaa. Yksi ohjelmistokehityksen periaatteista
onkin älä toista itseäsi (Don't Repeat Yourself, lyh. DRY; ks.
Wikipedia).
Luokkahierarkia
Toistamisen välttämiseksi voimme luoda yliluokan (engl. superclass) nimeltä
Henkilo, joka sisältää kaikki yhteiset ominaisuudet ja toiminnot. Sitten
alaluokat (engl. subclass) Opiskelija ja Opettaja voivat periä
Henkilo-luokan, jolloin ne saavat automaattisesti kaikki sen määrittelemät
ominaisuudet ja metodit. Näin voimme lisätä vain erityispiirteet kuhunkin
aliluokkaan ilman koodin toistamista.
Toteutetaan nyt uusi Henkilo-luokka, ja muutetaan Opiskelija- ja
Opettaja-luokkia niin, että ne perivät Henkilo-luokan. Javassa perintä
toteutetaan käyttämällä extends-avainsanaa. Esimerkiksi class Opiskelija extends Henkilo tarkoittaa, että Opiskelija-luokka perii Henkilo-luokan.
Tehdään tämä muutos koodissamme.
public class Henkilo {
String nimi;
String getNimi() {
return this.nimi;
}
void setNimi(String nimi) {
this.nimi = nimi;
}
}
Huomaa, että Opiskelija- ja Opettaja-luokat eivät enää määrittele nimi-attribuuttia tai getNimi- ja setNimi-metodeja, koska ne perivät nämä Henkilo-luokasta, eikä sitä koodia enää tarvitse uudelleen kirjoittaa. Tämä tekee koodista huomattavasti siistimpää ja helpommin ylläpidettävää. Perinnällä siis määritetään yksi yliluokka (tässä Henkilo) ja aliluokka tai aliluokat (tässä Opiskelija ja Opettaja), jotka laajentavat (engl. extend) Henkilo-luokan lisätiedoilla ja -toiminnallisuuksilla opiskelijasta ja opettajasta.
Toisin sanoen, Opiskelija ja Opettaja saavat itselleen samat (ei-yksityiset) attribuutit ja (ei-yksityiset) metodit kuin Henkilo-luokka ilman sitä, että ne pitää erikseen määritellä aliluokissa.
Kirjallisuudessa käytetään joskus yliluokka- ja aliluokka-termeistä myös nimityksiä kantaluokka (engl. base class) ja johdettu luokka (engl. derived class). Myös muita nimityksiä on käytössä. Näillä termeillä tarkoitetaan samaa asiaa kuin yliluokka- ja aliluokka-termeillä. Käytämme tässä materiaalissa kuitenkin pääasiassa yliluokka- ja aliluokka-termejä.
Periytymistä voidaan kuvata alla olevan tapaisella kuviolla. Tässä Henkilo on yliluokka (superclass) ja Opiskelija ja Opettaja ovat aliluokkia (subclasses), jotka perivät Henkilo-luokan ominaisuudet ja metodit.
Iso C-kirjain tarkoittaa, että kyseessä on luokka. Nuoli ylöspäin tarkoittaa perintää, eli aliluokka (nuolen tyvessä) perii yliluokan (nuolen kärjessä). Yllä oleva kuvio on tehty mukaillen niin sanottua UML-kuvauskieltä (engl. Unified Modelling Language).
Muodostajat ja super-avainsana
Yliluokan muodostajia voidaan kutsua aliluokista käyttäen super-avainsanaa.
Katsotaan esimerkin kautta, missä tällainen kutsu on tarpeen.
Yllä olevassa esimerkissämme on pari ongelmaa. Henkilo-luokassa ei ole
muodostajaa, jolloin nimen alustaminen tapahtuu setNimi-metodin kautta. Tästä
seuraa, että Henkilo-olion muodostamisen jälkeen nimi-attribuutti on aina
null, ennen kuin se erikseen asetetaan. Tämä ei ole hyvä käytäntö kahdestakaan
syystä: Ensinnäkin, on parempi, että olio on käyttökelpoinen heti luomisen
jälkeen ilman, että erillisiä asettamisia tarvitsee tehdä. Toiseksi, nimen
asettaminen julkisen setNimi-metodin kautta ei ole hyvä idea, sillä se rikkoo
tiedon kapseloinnin periaatetta.
Vaikka nimen muuttaminen toki pitäisikin tietyissä tilanteissa olla
opintotietojärjestelmässä mahdollista, sen asettaminen julkisen metodin kautta,
eli niin, että mikä tahansa olio voisi kutsua minkä tahansa Henkilo-olion
metodia nimen muuttamiseksi, ei pitäisi olla sallittua, vaan pitäisi tapahtua
huomattavasti hallitumman prosessin kautta.
Korjataan tilanne. Asetetaan aluksi nimi-attribuutti yksityiseksi
Henkilo-luokassa. Lisätään sitten muodostaja, joka ottaa nimi-parametrin, ja
alustaa attribuutin arvon vastaavasti. Tämän jälkeen voimme poistaa
setNimi-metodin kokonaan, jolloin nimen asettaminen onnistuu vain muodostajan
kautta. Niinpä nimen muuttaminen ei enää onnistu, mutta tämä sopii meille tässä
vaiheessa.
Muutetaan olioiden rakentelu pääohjelmassa vastaamaan tätä uutta muodostajaa.
class Henkilo {
// HIGHLIGHT_GREEN_BEGIN
private String nimi;
public Henkilo(String nimi) {
this.nimi = nimi;
}
// HIGHLIGHT_GREEN_END
// HIGHLIGHT_RED_BEGIN
void setNimi(String nimi) {
this.nimi = nimi;
}
// HIGHLIGHT_RED_END
public String getNimi() {
return this.nimi;
}
}
Nyt koska Henkilo-luokassa on määritelty muodostaja, joka ottaa parametreja,
Java ei enää luo oletusmuodostajaa—siis sellaista, jossa ei ole
parametreja—automaattisesti. Tämä aiheuttaa käännösvirheen—valitettavasti
hieman kryptisen sellaisen.
java: constructor Opiskelija in class Opiskelija cannot be applied to given types;
required: no arguments
found: java.lang.String
reason: actual and formal argument lists differ in length
Virheilmoituksen pointti on, että Opiskelija-olion muodostaja ei vastaa sitä,
miten yritämme luoda olion pääohjelmassa.
Tässä tuleekin tärkeä huomio: Luokat eivät peri muodostajia yliluokiltaan.
Esimerkiksi Opiskelija-luokka ei peri Henkilo-luokan muodostajia, vaan ne
täytyy määritellä erikseen jokaisessa aliluokassa. Tehdään Opiskelija ja
Opettaja-luokkiin muodostajat vastaamaan tätä vaatimusta. Esimerkiksi
Opiskelija-luokassa muodostajan alku näyttäisi tältä.
class Opiskelija extends Henkilo {
public Opiskelija(String nimi) {
// Muodostajan runko tulee tähän
}
}
Toisaalta nyt kun määrittelimme nimi-attribuutin yksityiseksi, emme voi
myöskään asettaa niitä perivästä luokasta käsin, esimerkiksi seuraavasti.
class Opiskelija extends Henkilo {
public Opiskelija(String nimi) {
// HIGHLIGHT_YELLOW_BEGIN
this.nimi = nimi;
// HIGHLIGHT_YELLOW_END
}
}
Opiskelija.java:6:5
java: constructor Henkilo in class Henkilo cannot be applied to given types;
required: java.lang.String
found: no arguments
reason: actual and formal argument lists differ in length
Opiskelija.java:8:13
java: nimi has private access in Henkilo
Ensimmäinen virhe liittyy siihen, että Henkilo-luokassa ei ole ei-parametrista
muodostajaa. Korjaamme tämän hieman myöhemmin. Jälkimmäinen virhe on tämän
hetkinen ongelmamme: nimi-attribuutti on yksityinen, joten emme voi asettaa
sitä suoraan perivästä luokasta käsin.
Koska poistimme setNimi-metodin, niin ainoa tapa asettaa nimen arvo on tehdä
se kutsumalla aliluokasta muodostajasta käsin yliluokan muodostajaa ja
välittämällä tuossa kutsussa tarvittavat parametrit. Tämä kutsuminen toteutetaan
super-avainsanaa. Tehdään tämä muutos kumpaankin aliluokkaan. Muutetaan
samalla myös loputkin attribuutit yksityisiksi.
import java.util.ArrayList;
class Opiskelija extends Henkilo {
// HIGHLIGHT_GREEN_BEGIN
private ArrayList<String> kaynnissaOlevatKurssit;
// HIGHLIGHT_GREEN_END
// HIGHLIGHT_GREEN_BEGIN
public Opiskelija(String nimi) {
super(nimi);
kaynnissaOlevatKurssit = new ArrayList<>();
}
// HIGHLIGHT_GREEN_END
// ...
}
Huomaa, että järjestys on oltava nimen omaan tämä: super-kutsu tulee
ensimmäisenä muodostajan rungossa. Vasta sen jälkeen voidaan tehdä muita
alustuksia.
Tee vastaava muutos myös Opettaja-luokkaan.
Tämän jälkeen ohjelma ei kuitenkaan vielä käänny, koska perivissä luokissa emme
edelleenkään pääse käsiksi yliluokan yksityiseen nimi-attribuuttiin.
class Opiskelija extends Henkilo {
void naytaOpintosuunnitelma() {
String kurssit = String.join(", ", kaynnissaOlevatKurssit);
// HIGHLIGHT_YELLOW_BEGIN
IO.println(this.nimi + " opiskelee kursseilla: " + kurssit);
// HIGHLIGHT_YELLOW_END
// Käännösvirhe: nimi on yksityinen muuttuja
}
}
Ainoa tapa päästä käsiksi nimi-attribuuttiin on kutsua yliluokan
getNimi()-metodia, sillä kyseinen metodi on julkinen. Tehdään tämä muutos
kaikkiin kohtiin, joissa nimi-attribuuttiin viitataan suoraan perivissä
luokissa.
class Henkilo {
private String nimi;
public Henkilo(String nimi) {
this.nimi = nimi;
}
public String getNimi() {
return nimi;
}
}
Ei-parametrista muodostajaa emme tarvitse enää, joten jätämme sen toteuttamatta.
UML-kaavioihin on tapana lisätä perintäsuhteiden lisäksi tietoja luokkien
attribuuteista ja metodeista sekä niiden näkyvyydestä. Attribuutit tyyppeineen
merkitään luokan nimen alle, ja metodit, myös muodostajat, vastaavasti ihan
alimmaiseksi. Perittyjä attribuutteja metodeja, kuten tässä attribuutti nimi
ja metodi getNimi(), ei yleensä merkitä kaavioon, paitsi jos ne
ylikirjoitetaan aliluokassa—tästä lisää Luvussa 3.2.
Vihreä pallo tarkoittaa, että kyseessä on julkinen (public) attribuutti/metodi,
ja punainen neliö, että kyseessä on yksityinen attribuutti/metodi. Tietojen
merkitseminen kaavioon mahdollistaa rakenteiden kuvailemisen ilman, että
tarvitsee sanallisesti kuvailla kaikkia yksityiskohtia.
Luokkahierarkia voi olla enemmänkin kuin kaksi tasoa syvä. Meillä voisi olla
myös Sihteeri, joka voi kirjata opintosuorituksia. Sihteeri peritään
Henkilo-luokasta. Voisimme tehdä myös kahdenlaisia erilaisia opiskelijoita:
Tutkinto-opiskelijoita sekä Avoimen yliopiston opiskelijoita.
Tutkinto-opiskelijalla voisi olla oma tutkinto-ohjelma, kun taas Avoimen
opiskelijalla ei ole tutkinto-ohjelmaa. Toisaalta Avoimen opiskelijan täytyisi
suorittaa maksu ennen kuin hän voi saada opintopisteitä.
Luokkahierarkia näyttäisi nyt seuraavalta. Merkitään tähänkin kuvioon attribuutit ja metodit mukaan. Tekstit menevät jo aika pieneksi, joten saat halutessasi kuvan auki uuteen välilehteen klikkaamalla sitä oikealla (tai Ctrl+klikkaamalla macOS:ssa) ja avaamalla kuvan uuteen välilehteen.
Koska nuoli TutkintoOpiskelija-luokasta osoittaa Opiskelija-luokkaan ja
sieltä edelleen Henkilo-luokkaan, niin TutkintoOpiskelija-luokasta
muodostettu olio perii sekä Opiskelija-luokan ominaisuudet ja metodit, että
Henkilo-luokan ominaisuudet ja metodit. Vastaavasti AvoinOpiskelija-luokasta
tehty olio perii myös molemmat luokat.
Jätämme esimerkin tässä toteuttamatta, mutta voit halutessasi tutkia valmista koodia täällä.
Huomautetaan vielä, että yliluokan muodostajan kutsuminen super-avainsanalla
kutsuu nimen omaan luokan välittömän yliluokan muodostajaa. Luokkarakenteessa
"yli hyppiminen", tyyliin super().super() ei ole mahdollista. Niinpä
TutkintoOpiskelija-luokan muodostajassa voi kutsua vain Opiskelija-luokan
muodostajaa, ei Henkilo-luokan muodostajaa.
Metodit ja super-avainsana
super-avainsanaa voidaan käyttää kutsuttaessa yliluokasta perittyä
metodia, kun halutaan eksplisiittisesti viitata yliluokan metodiin.
class Opiskelija extends Henkilo {
// ...
public void naytaKurssit(){
String kaikkiKurssit = String.join(", ", kaynnissaOlevatKurssit);
// HIGHLIGHT_RED_BEGIN
IO.println(this.getNimi() + " opiskelee kursseilla: " + kaikkiKurssit);
// HIGHLIGHT_RED_END
// HIGHLIGHT_GREEN_BEGIN
IO.println(super.getNimi() + " opiskelee kursseilla: " + kaikkiKurssit);
// HIGHLIGHT_GREEN_END
}
}
Tässä esimerkissä tällä ei ole mitään vaikutusta, koska getNimi() on sama sekä
yliluokassa että aliluokassa. Seuraavassa luvussa 3.2
Polymorfismi käsitellään tilannetta, jossa aliluokassa on
määritelty saman niminen metodi kuin yliluokassa. Tällöin super-avainsanalla
voidaan viitata nimenomaisesti yliluokan metodiin.
Huomautus moniperinnän puuttumisesta
Javassa luokka voi periä vain yhden luokan. Joissain muissa ohjelmointikielissä, kuten C++:ssa, on mahdollista käyttää moniperintää (engl. multiple inheritance), jossa luokka voi periä useamman kuin yhden luokan. Emme tässä mene syvemmälle moniperinnän käsitteeseen, mutta mainittakoon, moniperinnän käyttö voi joissain tilanteissa olla ongelmallista (esim. Timanttiongelma).
Usein kirjallisuudessa mainitaan, että Javassa moniperintää muistuttaa hieman rajapinnan käsite (engl. interface). Kysymys on kuitenkin monin tavoin eri asiasta. Rajapintoja käsitellään osassa 4 (TODO: linkki kuntoon)
Tehtävät
Tee luokka Tuote. Tee sille attribuutit nimi (merkkijono) ja hinta
(desimaaliluku). Aseta attribuuttien arvot konstruktorissa. Tee myös metodi
void tulostaTiedot(), joka tulostaa tuotteen nimen ja hinnan muodossa "Nimi: Hinta
€".
Peri Tuote-luokasta luokat Vaate ja Ruoka. Perivien luokkien
konstruktoreissa ei tarvitse tehdä muuta kuin kutsua yläluokan
konstruktoria oikeilla arvoilla.
Tee nyt ohjelmassasi kaksi erilaista vaatetta ja kaksi erilaista ruokatuotetta
ja tulosta niiden tiedot kutsumalla void tulostaTiedot()-metodia.
Jatketaan edellistä tehtävää. Peri Tuote-luokasta myös luokka Elektroniikka.
Lisää erityispiirteitä kuhunkin aliluokkaan:
Vaate: attribuuttiString koko(esim. "M", "L", jne.), metodivoid sovita(String sovittajanKoko), joka tulostaa, onko vaate sopiva sovittajalle.Elektroniikka: attribuuttiint takuuKuukausina(esim. 24), metodiint takuutaJaljella(int kuukausiaKulunut)palauttaa montako kuukautta takuuta on jäljellä (tai 0, jos takuu on umpeutunut).Ruoka: attribuuttiString parastaEnnen(esim. "31.01.2026"), ja metodivoid syo(), joka tulostaa "Nautit ruoan, jonka viimeinen käyttöpäivä on DD.MM.YYYY." (korvaa DD.MM.YYYYparastaEnnen-arvolla).
Huomaa, että perivien luokkien konstruktoreissa tulee nyt kutsua yläluokan konstruktoria oikeilla arvoilla, sekä asettaa omat attribuutit.
Tehtäväsivulla on valmiiksi annettuna pääohjelma. Käytä sitä luokkiesi testaamiseen. Se ei saa tuottaa käännös- tai ajonaikaisia virheitä. Voit kuitenkin halutessasi lisätä pääohjelmaan omaa koodiasi.
EDIT 30.1.2026: UML-kaavio korjattu vastaamaan tehtävänantoa
EDIT 29.1.2026: UML päivitetty vastaamaan tehtävänantoa
Laajenna aiemmin tekemääsi verkkokaupan luokkahierarkiaa alla olevan UML-kaavion mukaisesti. Saat kuvan suuremmaksi oikeaklikkaamalla (Windows) tai Control-klikkaamalla (macOS) sitä ja valitsemalla "Avaa kuva uudessa välilehdessä".
Tehtäväsivulla on valmiiksi annettuna pääohjelma, jota voit käyttää luokkiesi testaamiseen.
Avaa tästä ohjelman antama esimerkkituloste.
Kutsutaan perittyjä metodeja:
Talvitakki Dulce & Käppänä: 120.0 €
Ruisleipä Reissurähjä: 2.5 €
Tietokone HighPower: 899.0 €
----------------------------
Kutsutaan omia metodeja:
Testi 1: Sovitetaan M-kokoista käyttäjää:
Sovitetaan vaatetta Talvitakki Dulce & Käppänä...
Ei välttämättä sopivin koko. Sinä olet kokoa M, mutta tämä vaate on L.
Testi 2: Sovitetaan L-kokoista käyttäjää:
Sovitetaan vaatetta Talvitakki Dulce & Käppänä...
Mahtavaa! Koko L istuu sinulle täydellisesti!
Syödään Ruisleipä Reissurähjä.
Parasta ennen oli 20.12.2024, toivottavasti on hyvää.
Takuuta jäljellä: 19 kk.
pHone: 999.99 €
Takuuta puhelimessa jäljellä: 19 kk
Käyttöjärjestelmä: Orange
Yhteystyyppi: 4G
Soitetaan käyttöjärjestelmästä Orange(4G) numeroon 0401122330
Hernepussi: 0.99 €
Sulatit pakastetta Hernepussi 10 minuuttia. Säilytyssuositus on -18 astetta C.
Syödään Hernepussi.
Parasta ennen oli 31.5.2026, toivottavasti on hyvää.
Tehtävän kuvaus sanallisessa muodossa
Tässä on kuvaus uusista luokista ja niiden vaadituista ominaisuuksista. Löydät vastaavat tiedot UML-kaaviosta.
-
Puhelin(periiElektroniikka)- Lisää attribuutit:
private String kayttojarjestelma(esim. "Droid" tai "AiOS")private boolean onko5G
- Lisää metodit:
public void soita(String numero). Metodi tulostaa esimerkiksi:Soitetaan käyttöjärjestelmästä Orange(4G) numeroon 0401122330public void tulostaPuhelimenTiedot(int kuukausiaKulunut). Metodin tulee kutsua ensin perittyä metodia (tulostaTiedot()), ja sitten tulostaa puhelimeen liittyvät lisätiedot (jäljellä olevan takuuajan, käyttöjärjestelmän ja 5G-tuki).
- Lisää attribuutit:
-
Pakaste(periiRuoka)- Lisää attribuutti:
private int lampotilaSuositus(esim. -18)
- Lisää metodi:
private void sulata(int minuutit)(huomaa private-määre)- Kun metodia kutsutaan, se tulostaa esimerkiksi:
Sulatat pakastetta 10 minuuttia. Säilytyssuositus: -18 C.
- Lisää metodi:
public void sulataJaNauti(int minuutit)- Metodi kutsuu ensin
sulata(minuutit)-metodia ja sitten perittyäsyo()-metodia.
- Lisää attribuutti:
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.
Abstrakti luokka
osaamistavoitteet
- Osaat tehdä abstraktin luokan ja abstrakteja metodeja Javassa.
- Ymmärrät abstraktin luokan ja abstraktin metodin käsitteet ja niiden hyödyt olio-ohjelmoinnissa.

Suunnitellessamme ali- ja yliluokkasuhteita voi tulla tilanne, että olisi hyödyllistä tehdä yhteistä toiminnallisuutta määrittävä yliluokka, josta itsestään ei kuitenkaan ole mielekästä luoda ilmentymiä eli olioita.
Ajatellaan vaikkapa tuolia. Vaikka sana tuoli varmasti herättää meissä mielikuvan jostain tietynlaisesta tuolista, niin todellisuudessa tuoleja on monenlaisia: on puutuoleja, keinutuoleja, työtuoleja ja niin edelleen. Jokainen näistä tuolityypeistä on omanlainen ja hieman erilainen. Voidaan argumentoida, että tuoli-käsite itsessään on abstraktio. Tuolihan on oikeastaan vain asia, joka mahdollistaa istumisen. Tarvitaan aina jokin erikoistava käsite, kuten työtuoli, joka todella kuvaa millaisesta konkreettisesta tuolista on kysymys, ja jollaisia lopulta voidaan valmistaa tuotantolinjalla.
Otetaan toinen esimerkki, joka on ehkä jo hieman lähempänä oikeaa koodia. Jatketaan edellisessä luvussa esitettyä Muoto-esimerkkiä. Voisimme periaatteessa luoda Muoto-luokan ilmentymän ja kutsua sen laskeAla()-metodia.
Muoto muoto = new Muoto();
double ala = muoto.laskeAla();
IO.println("Muodon ala on " + ala); // 0.0
Tässä ei kuitenkaan ole mitään järkeä. Muoto-olio ei edusta mitään konkreettista muotoa, vaan se on vain yleinen käsite, josta konkreettiset muodot, kuten Ympyrä ja Suorakulmio, periytyvät. Tämä on ilmeistä viimeistään siinä vaiheessa, kun yritämme tulostaa tämän yleisen muodon pinta-alaa. Näin ollen Muoto-luokka on tarkoitettu vain perittäväksi, eikä siitä ole mielekästä luoda ilmentymiä. Muutetaan Muoto-luokka abstraktiksi luokaksi, ja muutetaan myös laskeAla()-metodi abstraktiksi metodiksi.
public abstract class Muoto {
public abstract double laskeAla();
}
Tämän jälkeen Muoto-luokasta ei voi enää luoda ilmentymiä.
Muoto muoto = new Muoto();
java: Muoto is abstract; cannot be instantiated
Abstrakti luokka (engl. abstract class) on luokka, jonka avulla tällainen käsitteen piirre voidaan tehdä selväksi koodin tasolla luokkahierarkiassa. Abstraktista luokasta ei voi luoda suoria ilmentymiä, vaan se toimii ainoastaan pohjana muille luokille, jotka perivät sen. Abstrakti luokka voi sisältää sekä abstrakteja metodeja (ts. joilla ei ole toteutusta), että konkreettisia metodeja (ts. joilla on toteutus). Perivän luokan tulee sitten toteuttaa nuo abstraktit metodit, ellei perivä luokka ole myös abstrakti.
huomautus
Tässä kohtaa voi pysähtyä hetkeksi miettimään tarvitaanko edellisen osion henkilötietojärjestelmässä laisinkaan henkilö-olioita, vai ovatko kaikki henkilöt jotain muutakin kuin henkilöitä, kuten opiskelijoita tai opettajia.
Esimerkki: Älykoti
Älykodissa voisi olla monenlaisia laitteita, kuten valoja, turvakamera sekä tietysti älykahvinkeitin. Sovitaan, että kaikilla laitteilla olisi toiminto vaihdaTilaa(), joka suorittaa laitteen päätoiminnon (esim. valot syttyvät tai sammuvat, kamera aloittaa tai päättää videon tallennuksen, kahvinkeitin aloittaa tai lopettaa kahvin keittämisen). Kukin laite voisi myös raportoida oman tilansa raportoiTila()-metodilla.
Lähdemme aluksi liikkeelle yksinkertaisesta esimerkistä, jossa voi vain vaihtaa laitteen tilaa kahden mahdollisen tilan välillä. Palaamme monimutkaisempiin säätömahdollisuuksiin myöhemmin.
Luokkakaaviomme voisi näyttää seuraavanlaiselta.
public class Main {
public static void main() {
Laite[] laitteet = {
new Valo(),
new Turvakamera(),
new Kahvinkeitin()
};
for (Laite laite : laitteet) {
laite.vaihdaTilaa();
laite.raportoiTila();
}
}
}
Jos katsotaan Laite-luokkaa, huomataan, että sen metodit vaihdaTilaa() ja raportoiTila() eivät tee mitään. Teoriassa voisimme luoda myös Laite-luokasta ilmentymän ja kutsua sen metodeja:
Laite laite = new Laite();
laite.vaihdaTilaa(); // Ei tee mitään
laite.raportoiTila(); // Ei tee mitään
Kuten nähdään, mitään ei tapahdu näitä metodeja kutsuttaessa, ja sikäli Laite-luokasta tehdyt oliot ovat tavallaan hyödyttömiä. Ei ole oikeastaan järkevää, että olisi olemassa jokin "yleinen laite", ilman, että tiedetään tarkemmin, minkä tyyppisestä laitteesta on kyse. Näin ollen Laite-luokka on oikeastaan tarkoitettu vain perittäväksi.
Muutetaan Laite-luokka abstraktiksi luokaksi. Koska myös metodit on tarkoitettu toteutettavaksi perivissä luokissa, määritellään myös metodit abstrakteiksi. Kaikkien perivien luokkein on toteutettava nämä metodit, kuten ne esimerkissämme jo tekevätkin.
public class Main {
public static void main() {
Laite[] laitteet = {
new Valo(),
new Turvakamera(),
new Kahvinkeitin()
};
for (Laite laite : laitteet) {
laite.vaihdaTilaa();
laite.raportoiTila();
}
}
}
Vastaavasti kuin aiemmassa Muoto-esimerkissä, nyt Laite-luokasta ei voi enää luoda ilmentymiä.
Laite laite = new Laite();
java: Laite is abstract; cannot be instantiated
Luokkakaaviona kuvio näyttää samalta kuin ennen, mutta nyt Laite-luokka on merkitty abstraktiksi luokaksi A-kirjaimella, ja sen metodit on merkitty abstrakteiksi metodeiksi. UML-notaatiossa abstrakti luokka ja abstraktit metodit merkitään kursiivilla.
Abstraktin luokan "vastakohtana" voidaan pitää konkreettista luokkaa, josta voi luoda ilmentymiä. Esimerkiksi Valo, Turvakamera ja Kahvinkeitin ovat konkreettisia luokkia, koska niistä voi luoda ilmentymiä.
Miksi abstrakti luokka on hyödyllinen?
Abstrakti luokka ei ole vain kielto tehdä luokasta ilmentymiä. Sen ensisijainen tarkoitus on
- määritellä yhteinen sopimus siitä, mitä metodeja kaikkien aliluokkien pitää tarjota, ja
- tarjota yhteisiä ominaisuuksia ja tarvittaessa myös toteutuksia, jotta aliluokat keskittyvät vain olennaiseen.
Kun Laite on abstrakti, voimme lisätä sille attribuutteja ja metodien valmiita toteutuksia, joita kaikki aliluokat käyttävät.
Lisätään Laite-luokkaan attribuutti nimi, joka kertoo laitteen nimen, sekä attribuutti kytketty, joka kertoo, onko laite päällä vai pois päältä. Sellainen attribuutti on hyödyllinen kaikille laitteille, joten se sopii hyvin abstraktiin luokkaan.
public abstract class Laite {
// HIGHLIGHT_GREEN_BEGIN
private String nimi;
private boolean kytketty;
// HIGHLIGHT_GREEN_END
public abstract void vaihdaTilaa();
public abstract void raportoiTila();
}
Jos kyse olisi verkkolaitteesta, hyödyllisiä tai jopa pakollisia attribuutteja voisivat olla muun muassa MAC-osoite ja IP-osoite. Pidämme kuitenkin tämän esimerkin yksinkertaisena, joten tyydymme tässä vain nimeen ja kytketty-tilan seuraamiseen.
Lisätään myös metodit kytkePaalle() ja kytkePois(), jotka sisältävät yleisen logiikan laitteen käynnistämiseen ja sammuttamiseen, jota kaikki laitteet voivat noudattavat.
public abstract class Laite {
private String nimi;
private boolean kytketty;
// HIGHLIGHT_GREEN_BEGIN
protected Laite(String nimi) {
this.nimi = nimi;
this.kytketty = false; // oletus
}
public void kytkePaalle() {
if (!kytketty) {
kytketty = true;
IO.println(nimi + " käynnistyy.");
}
}
public void kytkePois() {
if (kytketty) {
kytketty = false;
IO.println(nimi + " sammuu.");
}
}
// HIGHLIGHT_GREEN_END
public abstract void vaihdaTilaa();
public abstract void raportoiTila();
}
Huomaa, että koska päätimme, että joka laitteella on oltava nimi, siitä seuraa, että nimi on asetettava muodostajan parametrin kautta. Tämän seurauksena emme voi enää luoda ilmentymiä ei-parametrisen muodostajan avulla.
Valo valo = new Valo();
Valo.java
java: constructor Laite in class Laite cannot be applied to given types;
required: java.lang.String
found: no arguments
reason: actual and formal argument lists differ in length
Muodostajan kutsuminen vaatii nyt nimen välittämisen, esimerkiksi new Valo("PhilipsHue"). Niinpä kussakin aliluokan muodostajassa on kutsuttava yliluokan muodostajaa. Tehdään tämä muutos kaikkiin aliluokkiin.
public class Main {
public static void main() {
Laite[] laitteet = {
new Valo("PhilipsHue"),
new Kahvinkeitin("Moccamaster"),
new Turvakamera("Reolink")
};
for (Laite laite : laitteet) {
laite.kytkePaalle();
laite.vaihdaTilaa();
laite.raportoiTila();
laite.kytkePois();
}
}
}
Aliluokat perivät nyt päälle- ja pois-kytkemislogiikan sellaisenaan, mutta niiden on pakko toteuttaa laitteen omat, oliokohtaiset toiminnallisuudet. Tämä luo tasapainoa joustavuuden ja pakollisen rakenteen välille: Tilan vaihtaminen ja tilan raportointi ovat pakollisia, mutta niiden toteutus on vapaa. Toisaalta laitteen käynnistys- ja sammutuslogiikka on yhteinen kaikille laitteille.
Abstraktin luokan metodien näkyvyys
Abstraktin luokan metodien näkyvyys määritellään samojen periaatteiden mukaan kuin muidenkin metodien. Abstraktit metodit määritellään joko public- tai protected-metodeina, jotta aliluokat voivat toteuttaa ne. Jos metodia kutsuu koodi, joka luo olion, metodin tulee olla public. Jos metodia kutsutaan vain perivästä luokasta, riittää että metodi on protected. On kuitenkin huomattava, että aliluokan toteuttaman metodin näkyvyys ei voi olla rajoittavampi kuin abstraktin metodin näkyvyys. Esimerkiksi public-abstraktia metodia ei voi toteuttaa protected-metodina aliluokassa.
Konkreettiset metodit voivat olla myös private: tällöin kyseessä on vain abstraktin luokan sisäinen apumetodi, jota aliluokat eivät näe.
Abstraktia metodia ei voi määritellä private-määreellä.
Operaatiorunko-malli
Abstraktissa luokassa voi olla myös konkreettinen metodi, jonka toteutuksessa kutsutaan abstraktia metodia. Tällaista toteutusta kutsutaan ohjelmistosuunnittelussa operaatiorunko-suunnittelumalliksi. Abstrakti luokka määrittelee toimenpiteelle "kaavan", mutta delegoi osan vaiheista aliluokkien toteutettavaksi.
Jo aiemmin toteutetut osat on piilotettu koodista. Saat ne esiin klikkaamalla silmä-kuvaketta koodialueen oikeasta yläreunasta.
public abstract class Laite {
private String nimi;
private boolean kytketty;
protected Laite(String nimi) {
this.nimi = nimi;
}
public final void suoritaPaivitys() {
kytkePaalle();
valmistelePaivitys(); // Abstrakti askel, jonka aliluokka toteuttaa
paivitys();
kytkePois();
}
protected abstract void valmistelePaivitys();
private void paivitys() {
IO.println("Haetaan uusin päivitys verkosta...");
IO.println("Laite päivitetään...");
}
public void kytkePaalle() {
if (!kytketty) {
kytketty = true;
IO.println(nimi + " käynnistyy.");
}
}
public void kytkePois() {
if (kytketty) {
kytketty = false;
IO.println(nimi + " sammuu.");
}
}
public abstract void vaihdaTilaa();
public abstract void raportoiTila();
}
suoritaPaivitys() on nyt ikään kuin valmis resepti, jota aliluokat eivät voi muuttaa (final). Sen sijaan ne täydentävät reseptin tarvitsemansa tavoilla toteuttamalla abstraktit metodit.
🤔 Pohdittavaksi: Missä tilanteissa haluaisit estää aliluokkaa ylikirjoittamasta tiettyä metodia?
Tehtävät
Tee abstrakti luokka Viesti, jolla on attribuutti String viesti, joka
asetetaan konstruktorissa. Aseta viesti-attribuutin näkyvyys mahdollisimman
rajoitetuksi. Luokalla on myös abstrakti metodi void laheta().
Peri Viesti-luokasta luokat Sahkoposti ja Tekstiviesti. Molemmissa luokissa on
konstruktori, joka kutsuu yliluokan konstruktoria. Toteuta molempiin luokkiin
laheta()-metodit. Sahkoposti-luokan laheta()-metodi tulostaa muodossa
"Lähetetään sähköposti: <viesti>" ja Tekstiviesti-luokan laheta()-metodi
tulostaa muodossa `"Lähetetään tekstiviesti: <viesti>"
Muuta Tehtävän 3.6 Ajoneuvo-luokka ja sen liiku()-metodi abstrakteiksi. Jätä
toString()-metodi edelleen tavalliseksi (ei-abstraktiksi) metodiksi.
-
Tee abstrakti luokka
Viestikanava. Sillä on attribuuttiString vastaanottaja, joka asetetaan konstruktorissa. Lisää abstrakti metodilahetaSisaisesti(String viesti), joka ei palauta mitään. -
Tee myös metodi
String getVastaanottaja(), joka palauttaa vastaanottajan. -
Tee konkreettinen metodi
laheta(String viesti), joka aluksi lopettaa metodin (return), jos viesti on tyhjä tainull. Muuten metodi kutsuu abstraktia metodialahetaSisaisesti(String viesti). -
Peri
Viestikanava-luokastaSahkopostijaTekstiviesti. Molemmissa luokissa ylikirjoita abstrakti metodilahetaSisaisesti(String viesti), joka tulostaa konsoliin viestin muodossa "Lähetetään <kanava> <osoite/numero>: <viesti>", esim. "Lähetetään sähköposti osoitteeseen antti-jussi@lakanen.com: Hei, mikä on homma?" tai "Lähetetään tekstiviesti numeroon 0401234567: Tervetuloa kurssille!".
Tee Viestipalvelu-luokka, jolle voi lisätä erilaisia Viestikanava-olioita
lisaaKanava(Viestikanava kanava)-metodilla. Lisää myös metodi
lahetaKaikille(String viesti), joka lähettää viestejä kaikilla kanavilla
kerralla.
Pari valinnaista lisähaastetta (ei pisteitä; nämä ovat kuitenkin mallivastauksessa mukana):
- Muuta
Viestikanava-luokkaa siten, että se ottaa listan vastaanottajia, ei vain yhtä. Tämän seurauksena pitää muuttaa myöslahetaSisaisesti-metodeja. - Laita
Tekstiviesti-luokkaan merkkiraja (esim. 80 merkkiä). Jos viesti on tätä pidempi, niin viesti tulee pilkkoa merkkirajan mukaisiin pätkiin.
Osan kaikki tehtävät
huomautus
Jos palautat tehtävät ennen osion takarajaa (ma 2.2.2026 klo 11:59 (keskipäivä)), voit saada DL-BONUS-pisteitä harjoitustehtäviin. Lue lisää suorittaminen-sivulta.
Tee luokka Tuote. Tee sille attribuutit nimi (merkkijono) ja hinta
(desimaaliluku). Aseta attribuuttien arvot konstruktorissa. Tee myös metodi
void tulostaTiedot(), joka tulostaa tuotteen nimen ja hinnan muodossa "Nimi: Hinta
€".
Peri Tuote-luokasta luokat Vaate ja Ruoka. Perivien luokkien
konstruktoreissa ei tarvitse tehdä muuta kuin kutsua yläluokan
konstruktoria oikeilla arvoilla.
Tee nyt ohjelmassasi kaksi erilaista vaatetta ja kaksi erilaista ruokatuotetta
ja tulosta niiden tiedot kutsumalla void tulostaTiedot()-metodia.
Jatketaan edellistä tehtävää. Peri Tuote-luokasta myös luokka Elektroniikka.
Lisää erityispiirteitä kuhunkin aliluokkaan:
Vaate: attribuuttiString koko(esim. "M", "L", jne.), metodivoid sovita(String sovittajanKoko), joka tulostaa, onko vaate sopiva sovittajalle.Elektroniikka: attribuuttiint takuuKuukausina(esim. 24), metodiint takuutaJaljella(int kuukausiaKulunut)palauttaa montako kuukautta takuuta on jäljellä (tai 0, jos takuu on umpeutunut).Ruoka: attribuuttiString parastaEnnen(esim. "31.01.2026"), ja metodivoid syo(), joka tulostaa "Nautit ruoan, jonka viimeinen käyttöpäivä on DD.MM.YYYY." (korvaa DD.MM.YYYYparastaEnnen-arvolla).
Huomaa, että perivien luokkien konstruktoreissa tulee nyt kutsua yläluokan konstruktoria oikeilla arvoilla, sekä asettaa omat attribuutit.
Tehtäväsivulla on valmiiksi annettuna pääohjelma. Käytä sitä luokkiesi testaamiseen. Se ei saa tuottaa käännös- tai ajonaikaisia virheitä. Voit kuitenkin halutessasi lisätä pääohjelmaan omaa koodiasi.
EDIT 30.1.2026: UML-kaavio korjattu vastaamaan tehtävänantoa
EDIT 29.1.2026: UML päivitetty vastaamaan tehtävänantoa
Laajenna aiemmin tekemääsi verkkokaupan luokkahierarkiaa alla olevan UML-kaavion mukaisesti. Saat kuvan suuremmaksi oikeaklikkaamalla (Windows) tai Control-klikkaamalla (macOS) sitä ja valitsemalla "Avaa kuva uudessa välilehdessä".
Tehtäväsivulla on valmiiksi annettuna pääohjelma, jota voit käyttää luokkiesi testaamiseen.
Avaa tästä ohjelman antama esimerkkituloste.
Kutsutaan perittyjä metodeja:
Talvitakki Dulce & Käppänä: 120.0 €
Ruisleipä Reissurähjä: 2.5 €
Tietokone HighPower: 899.0 €
----------------------------
Kutsutaan omia metodeja:
Testi 1: Sovitetaan M-kokoista käyttäjää:
Sovitetaan vaatetta Talvitakki Dulce & Käppänä...
Ei välttämättä sopivin koko. Sinä olet kokoa M, mutta tämä vaate on L.
Testi 2: Sovitetaan L-kokoista käyttäjää:
Sovitetaan vaatetta Talvitakki Dulce & Käppänä...
Mahtavaa! Koko L istuu sinulle täydellisesti!
Syödään Ruisleipä Reissurähjä.
Parasta ennen oli 20.12.2024, toivottavasti on hyvää.
Takuuta jäljellä: 19 kk.
pHone: 999.99 €
Takuuta puhelimessa jäljellä: 19 kk
Käyttöjärjestelmä: Orange
Yhteystyyppi: 4G
Soitetaan käyttöjärjestelmästä Orange(4G) numeroon 0401122330
Hernepussi: 0.99 €
Sulatit pakastetta Hernepussi 10 minuuttia. Säilytyssuositus on -18 astetta C.
Syödään Hernepussi.
Parasta ennen oli 31.5.2026, toivottavasti on hyvää.
Tehtävän kuvaus sanallisessa muodossa
Tässä on kuvaus uusista luokista ja niiden vaadituista ominaisuuksista. Löydät vastaavat tiedot UML-kaaviosta.
-
Puhelin(periiElektroniikka)- Lisää attribuutit:
private String kayttojarjestelma(esim. "Droid" tai "AiOS")private boolean onko5G
- Lisää metodit:
public void soita(String numero). Metodi tulostaa esimerkiksi:Soitetaan käyttöjärjestelmästä Orange(4G) numeroon 0401122330public void tulostaPuhelimenTiedot(int kuukausiaKulunut). Metodin tulee kutsua ensin perittyä metodia (tulostaTiedot()), ja sitten tulostaa puhelimeen liittyvät lisätiedot (jäljellä olevan takuuajan, käyttöjärjestelmän ja 5G-tuki).
- Lisää attribuutit:
-
Pakaste(periiRuoka)- Lisää attribuutti:
private int lampotilaSuositus(esim. -18)
- Lisää metodi:
private void sulata(int minuutit)(huomaa private-määre)- Kun metodia kutsutaan, se tulostaa esimerkiksi:
Sulatat pakastetta 10 minuuttia. Säilytyssuositus: -18 C.
- Lisää metodi:
public void sulataJaNauti(int minuutit)- Metodi kutsuu ensin
sulata(minuutit)-metodia ja sitten perittyäsyo()-metodia.
- Lisää attribuutti:
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.
Tee abstrakti luokka Viesti, jolla on attribuutti String viesti, joka
asetetaan konstruktorissa. Aseta viesti-attribuutin näkyvyys mahdollisimman
rajoitetuksi. Luokalla on myös abstrakti metodi void laheta().
Peri Viesti-luokasta luokat Sahkoposti ja Tekstiviesti. Molemmissa luokissa on
konstruktori, joka kutsuu yliluokan konstruktoria. Toteuta molempiin luokkiin
laheta()-metodit. Sahkoposti-luokan laheta()-metodi tulostaa muodossa
"Lähetetään sähköposti: <viesti>" ja Tekstiviesti-luokan laheta()-metodi
tulostaa muodossa `"Lähetetään tekstiviesti: <viesti>"
Muuta Tehtävän 3.6 Ajoneuvo-luokka ja sen liiku()-metodi abstrakteiksi. Jätä
toString()-metodi edelleen tavalliseksi (ei-abstraktiksi) metodiksi.
-
Tee abstrakti luokka
Viestikanava. Sillä on attribuuttiString vastaanottaja, joka asetetaan konstruktorissa. Lisää abstrakti metodilahetaSisaisesti(String viesti), joka ei palauta mitään. -
Tee myös metodi
String getVastaanottaja(), joka palauttaa vastaanottajan. -
Tee konkreettinen metodi
laheta(String viesti), joka aluksi lopettaa metodin (return), jos viesti on tyhjä tainull. Muuten metodi kutsuu abstraktia metodialahetaSisaisesti(String viesti). -
Peri
Viestikanava-luokastaSahkopostijaTekstiviesti. Molemmissa luokissa ylikirjoita abstrakti metodilahetaSisaisesti(String viesti), joka tulostaa konsoliin viestin muodossa "Lähetetään <kanava> <osoite/numero>: <viesti>", esim. "Lähetetään sähköposti osoitteeseen antti-jussi@lakanen.com: Hei, mikä on homma?" tai "Lähetetään tekstiviesti numeroon 0401234567: Tervetuloa kurssille!".
Tee Viestipalvelu-luokka, jolle voi lisätä erilaisia Viestikanava-olioita
lisaaKanava(Viestikanava kanava)-metodilla. Lisää myös metodi
lahetaKaikille(String viesti), joka lähettää viestejä kaikilla kanavilla
kerralla.
Pari valinnaista lisähaastetta (ei pisteitä; nämä ovat kuitenkin mallivastauksessa mukana):
- Muuta
Viestikanava-luokkaa siten, että se ottaa listan vastaanottajia, ei vain yhtä. Tämän seurauksena pitää muuttaa myöslahetaSisaisesti-metodeja. - Laita
Tekstiviesti-luokkaan merkkiraja (esim. 80 merkkiä). Jos viesti on tätä pidempi, niin viesti tulee pilkkoa merkkirajan mukaisiin pätkiin.
Rajapintojen ja geneeristen tyyppien perusteet
osaamistavoitteet
- Osaat käyttää rajapintoja määrittääksesi luokan toimintaa määrittävän sopimuksen
- Osaat toteuttaa
Comparable-rajapintaan, joka mahdollistaa olioiden järjestämisen, - Ymmärrät geneeristen luokkien ja tyyppiparametrien käsitteet
- Osaat toteuttaa yleiskäyttöisiä luokkia ja metodeja geneeristen tyyppien avulla
Rajapinta
osaamistavoitteet
- Ymmärrät, mitä rajapinta (interface) tarkoittaa olio-ohjelmoinnissa.
- Osaat määritellä ja käyttää rajapintoja Javassa.
- Osaat käyttää rajapintaa aliohjelman parametrina ja muuttujan tyyppinä.
- Ymmärrät, milloin kannattaa käyttää rajapintaa perinnän sijaan.
- Ymmärrät, että luokka voi toteuttaa monta rajapintaa, mutta periä vain yhden luokan
Rajapinta toimii sitovana sopimuksena: Se määrittelee, mitä metodeja luokan on tarjottava, ottamatta kantaa siihen, miten ne on teknisesti toteutettu. Toisin kuin abstrakti luokka, joka luo pohjan luokan metodeille ja attribuuteille, rajapinta keskittyy kuvailemaan olion kyvykkyyksiä. Rajapinta mahdollistaa yhtenevän kyvykkyyksien määrittelyn, vaikka luokat olisivat täysin erilaisia tai periytyisivät eri paikoista luokkahierarkiassa. Kun ohjelmoija sitten käsittelee oliota rajapinnan kautta, hän voi luottaa siihen, että olio tarjoaa sovitun kyvykkyyden riippumatta siitä, mitä luokkaa olio edustaa.
Älykoti: säädettävät laitteet
Jatketaan Osassa 3
aloittamaamme
älykoti-esimerkkiä. Jotkin älykotimme laitteet voisivat olla säädettäviä, eli
niihin voisi asettaa suoraan arvon, kuten kirkkauden, lämpötilan tai
äänenvoimakkuuden. Näinhän periaatteessa toimimmekin jo esimerkkimme
Valo-luokassa, jossa kirkkaus vaihtelee kolmen arvon välillä. Olion käyttäjän
kannalta olisi kuitenkin kätevämpää, jos voisi asettaa kirkkauden suoraan
haluttuun arvoon (esim. 33%), sen sijaan, että pitäisi kutsua
vaihdaTilaa()-metodia useita kertoja ja toivoa, että arvo osuu kohdalleen.
Loppukäyttäjän kannalta tätä voisi verrata tilanteeseen, jossa käyttäjä voisi
asettaa vaikkapa mobiilisovelluksesta suoraan haluamansa kirkkauden sen sijaan,
että pitäisi klikkailla Lisää kirkkautta- tai Vähennä kirkkautta
-painikkeita useita kertoja.
Määritellään rajapinta Saadettava, jossa on metodi asetaArvo(int arvo).
Tiedosto tallennetaan nimellä Saadettava.java, eli samaan tapaan kuin luokat.
/**
* Laite, jonka voi säätää suoraan haluttuun arvoon.
*/
public interface Saadettava {
void asetaArvo(int arvo);
}
Tämän voi lukea seuraavasti: Jokaisella Saadettava-rajapinnan toteuttavalla
luokalla tulee olla asetaArvo-metodi.
Nyt voimme muokata Valo-luokkaa toteuttamaan Saadettava-rajapinnan:
Lisätään Valo-luokkaan rajapinnan toteutus (klikkaa Valo.java-tiedostoa).
Jätämme Kahvinkeitin- ja Turvakamera-luokat tässä vaiheessa esimerkistä
pois, koska päätämme yksinkertaisuuden vuoksi, että ne eivät ole säädettäviä
laitteita.
public class Main {
public static void main() {
Valo valo = new Valo("PhilipsHue");
valo.asetaArvo(33);
valo.raportoiTila();
valo.vaihdaTilaa();
valo.raportoiTila();
}
}
Luokkakaaviona esimerkkimme näyttäisi tältä. I-kirjain ilmaisee, että kyseessä on rajapinta. Abstraktin luokan tapaan rajapinta on merkitty kursiivilla. Rajapinnan toteuttaminen esitetään katkoviivalla, jossa on avoin nuoli kohti rajapintaa.
Usean rajapinnan toteuttaminen
Luokka voi toteuttaa useita rajapintoja. Esimerkiksi Javan sisäänrakennettu
ArrayList-luokka toteuttaa rajapintoja: List, RandomAccess, Cloneable ja
Serializable (ks. ArrayList-luokan
dokumentaatio).
List-rajapinta määrittelee listan perustoiminnot, kuten elementtien lisäämisen, poistamisen ja hakemisen.RandomAccess-rajapinta määrittelee, että listan alkioihin tulee päästä käsiksi nopeasti indeksien avulla.Cloneable-rajapinta sallii olion kloonauksen eli kopioinnin.Serializable-rajapinta sallii olion tallentamisen tiedostoon tai lähettämiseen verkon yli.
Toisaalta myös Javan
Date-luokka
toteuttaa muun muassa Cloneable-rajapinnan, joka mahdollistaa päivämääräolion
kloonaamisen. Huomaa, että Date-luokka ei liity mitenkään
ArrayList-luokkaan, mutta molemmat toteuttavat saman rajapinnan.
Luodaan nyt itse kaksi rajapintaa ja luokkia, jotka toteuttavat molemmat rajapinnat.
Otetaan esimerkki käyttöliittymäkomponenteista, joita voi piirtää näytölle ja
joita voi klikata hiirellä. Määritellään kaksi rajapintaa: Piirrettava ja
Klikattava. Näiden rajapintojen avulla voitaisiin määritellä, millaisia
komponentteja käyttöliittymässä on. Sovitaan niin, että piirrettävä komponentti
osaa piirtää itsensä, ja klikattava komponentti osaa käsitellä klikkauksia ja
korostaa itsensä, kun hiiri on sen päällä.
/**
* Käyttöliittymään piirrettävä komponentti.
*/
public interface Piirrettava {
public void piirra();
}
Huomaa, että emme tiedä emmekä välitä siitä, miten nämä metodit aikanaan
toteutetaan. Piirto voi tapahtua graafisella käyttöliittymällä,
tekstipohjaisella käyttöliittymällä tai vaikkapa tulostamalla tiedostoon. Meille
riittää, että tiedämme, että jokaisella Pirrettava-rajapinnan toteuttavalla
luokalla on piirra()-metodi, ja jokaisella Klikattava-rajapinnan
toteuttavalla luokalla on klikattu()- ja asetaKorostus(boolean korostus)-metodit.
Mennään eteenpäin. Toteutetaan Teksti, joka on pelkkää tekstiä näyttävä
käyttöliittymäkomponentti.
/**
* Pelkkää tekstiä esittävä piirrettävä komponentti.
*/
public class Teksti implements Piirrettava {
private String sisalto;
public Teksti(String sisalto)
{
this.sisalto = sisalto;
}
@Override
public void piirra() {
// Piirretään vain pelkkä tekstisisältö ilman kehyksiä
IO.println(sisalto);
}
}
Rajapintojen hyöty ei vielä kokonaisuudessaan välity, osittain siksi, että
piirra()-metodi on ainoa metodi, jota Piirrettava-rajapinta tarjoaa. Nyt
kuitenkin voimme luoda toisen komponentin, Painike, joka on laatikon näköinen
klikkattava painike, jossa on tekstiä. Painike-luokka toteuttaa molemmat
rajapinnat: Pirrettava ja Klikattava.
/**
* Laatikon näköinen klikkattava painike,
* jossa on tekstiä.
*/
public class Painike implements Piirrettava, Klikattava {
private String sisalto;
private boolean korostettu;
public Painike(String sisalto)
{
this.sisalto = sisalto;
this.korostettu = false;
}
@Override
public void piirra() {
// Piirretään suorakulmio ja teksti
if (!korostettu) {
IO.println("[ " + sisalto + " ]");
} else {
IO.println("[*" + sisalto + "*]");
}
}
/**
* Käsitellään klikkaustapahtuma
*/
@Override
public void klikattu() {
IO.println("(Klikattiin painiketta, jossa lukee \"" + sisalto + "\")");
}
/**
* Asetetaan korostustila. Jos tila muuttuu, piirretään komponentti uudestaan.
*/
@Override
public void asetaKorostus(boolean korostus) {
if (this.korostettu == korostus) {
return;
}
this.korostettu = korostus;
this.piirra();
}
}
Nyt meillä on kaksi erilaista käyttöliittymäkomponenttia, jotka molemmat voidaan
piirtää näytölle. Painike-komponentti on lisäksi klikattava. Käytetään näitä
komponentteja pääohjelmassa.
/**
* Käyttöliittymään piirrettävä komponentti.
*/
public interface Piirrettava {
public void piirra();
}
Jos haluat testata tätä koodia omalla koneellasi, voit ladata tämänkin esimerkin GitHubista.
Valinnaista lisätietoa: Piirtämisvastuun siirtäminen pois komponenteista
Yllä oleva esimerkkimme on siinä mielessä aavistuksen epätodellinen, että käyttöliittymäkomponentit eivät yleensä huolehdi itse itsensä piirtämisestä, vaan piirtämisvastuu on usein erotettu muuhun osaan järjestelmää. Tällöin komponentit vain tarjoavat tiedot, jotka tarvitaan piirtämiseen, ja joku muu osa järjestelmää huolehtii siitä, että komponentit piirretään oikein näytölle (tai muuhun esitystapaan).
Muokataan esimerkkiämme tämän ajatuksen mukaisesti. Tehdään Naytto-luokka,
joka pitää kirjaa kaikista näytöllä näkyvistä käyttöliittymäkomponenteista.
/**
* Naytto-luokka hallinnoi piirrettäviä komponentteja.
*/
public class Naytto {
private ArrayList<Piirrettava> komponentit = new ArrayList<>();
public void lisaaKomponentti(Piirrettava p) {
komponentit.add(p);
}
public void poistaKomponentti(Piirrettava p) {
komponentit.remove(p);
}
}
Tehdään myös Piirturi-luokka, joka toimii välikerroksena Naytto-luokan ja
käyttöliittymäkomponenttien välillä. Piirturi-luokka huolehtii siitä, että
komponentit piirretään oikein näytölle. Tässä esimerkissä ne tulostetaan
konsolille, mutta oikeassa käyttöliittymässä ne piirrettäisiin graafiselle
näytölle.
/**
* Piirturi-luokka vastaa piirtoalueen piirtämisestä.
*/
public class Piirturi {
public void piirraPainike(String teksti, boolean korostettu) {
if (!korostettu) {
IO.println("[ " + teksti + " ]");
} else {
IO.println("[*" + teksti + "*]");
}
}
public void piirraTeksti(String teksti) {
IO.println(teksti);
}
public void tyhjaa() {
IO.println("Tyhjennetään piirtoalue");
// Jätetään tässä toteuttamatta
}
}
Nyt Naytto-luokka voi käyttää Piirturi-luokkaa piirtämään ne tarvittaessa.
Lisätään Naytto-luokkaan metodi paivita(), joka käy läpi kaikki näytöllä
olevat komponentit ja pyytää niitä piirtämään itsensä Piirturi-olion avulla.
import java.util.ArrayList;
/**
* Naytto-luokka hallinnoi piirrettäviä komponentteja.
*/
public class Naytto {
private ArrayList<Piirrettava> komponentit = new ArrayList<>();
// HIGHLIGHT_GREEN_BEGIN
private Piirturi piirturi = new Piirturi();
// HIGHLIGHT_GREEN_END
public void lisaaKomponentti(Piirrettava p) {
komponentit.add(p);
}
public void poistaKomponentti(Piirrettava p) {
komponentit.remove(p);
}
// HIGHLIGHT_GREEN_BEGIN
public void paivita() {
piirturi.tyhjaa();
for (Piirrettava p : komponentit) {
p.piirra(piirturi);
}
}
// HIGHLIGHT_GREEN_END
}
Huomaa, että Piirrettava-rajapinnan piirra()-metodin tulee nyt ottaa
parametrina Piirturi-olio. Tämän avulla komponentit voivat käyttää
Piirturi-oliota piirtämiseen.
public interface Piirrettava {
// HIGHLIGHT_GREEN_BEGIN
public void piirra(Piirturi piirturi);
// HIGHLIGHT_GREEN_END
}
Ja nyt se oleellinen kohta: Tämän seurauksena Teksti- ja Painike-luokkien
piirra()-metodit eivät enää itse tulosta mitään, vaan ne kutsuvat
Piirturi-olion metodeja.
/**
* Pelkkää tekstiä esittävä piirrettävä komponentti.
*/
public class Teksti implements Piirrettava {
private String sisalto;
public Teksti(String sisalto)
{
this.sisalto = sisalto;
}
/**
* Piirrä komponentti
* @param piirturi Piirturi
*/
@Override
// HIGHLIGHT_GREEN_BEGIN
public void piirra(Piirturi piirturi) {
piirturi.piirraTeksti(sisalto);
}
// HIGHLIGHT_GREEN_END
}
Vastaava muutos tulee tehdä Painike-luokkaan.
Tässä meidän yksinkertaisessa esimerkissämme kaikki tietysti tapahtuu konsolille
tulostamalla, mutta oikeassa graafisessa käyttöliittymässä Piirturi-luokka
voisi käyttää jotain graafista kirjastoa, kuten JavaFX:ää tai Swingiä.
Esimerkki on pitkähkö, ja jos haluat ajaa sen omalla tietokoneellasi, lataa se GitHubista.
Rajapinnan periminen
Rajapinta voi myös laajentaa (periä) toista rajapintaa. Syntaktisesti tämä
tapahtuu käyttämällä extends-avainsanaa, kuten luokkien perinnässä. Luokkien
perinnästä poiketen rajapinta voi periä useita rajapintoja. Alirajapinta saa
kaikki ylirajapinnan metodit. Alla synteettinen esimerkki.
Esimerkit
Löydät kaikki tällä sivulla esitellyt esimerkit GitHubista (E34-alkuiset kansiot).
Huomautuksia
Valinnaista lisätietoa: Javan versiosta 8 alkaen rajapinnat voivat sisältää myös metodien oletustoteutuksia. Ominaisuus saattaa olla hyödyllinen esimerkiksi tilanteissa, jossa halutaan lisätä uusi metodi olemassa olevaan rajapintaan rikkomatta vanhoja toteutuksia. Lue aiheesta lisää Javan dokumentaatiosta.
Tehtävät
-
Luo rajapinta nimeltään
Muunnin. Määrittele rajapintaan yksi metodi:String muunna(String syote). Muista, että rajapinnassa metodilla ei ole runkoa (ei aaltosulkeita{}). -
Tee luokat
PienetKirjaimet,IsotKirjaimetjaIsoAlkukirjain, jotka toteuttavatMuunnin-rajapinnan.
PienetKirjaimet-luokanmuunna-metodi muuntaa annetun merkkijonon pieniksi kirjaimiksi.muunna("Hei Maa")-->"hei maa".IsotKirjaimet-luokanmuunna-metodi muuntaa annetun merkkijonon suuraakkosiksi.muunna("Hei Maa")-->"HEI MAA".IsoAlkukirjain-luokanmuunna-metodi muuntaa annetun merkkijonon siten, että vain ensimmäinen kirjain on suuraakkonen ja muut pieniä.muunna("HEI MAA")-->"Hei maa".
- Testaa ohjelmaasi valmiiksi annetulla pääohjelmalla.
Vakoojat lähettävät viestejä toisilleen, mutta salausmenetelmä vaihtuu päivittäin, jotta vihollinen ei pääse perille logiikasta. Tarvitsemme rajapinnan, jonka avulla voimme vaihtaa salausalgoritmia lennosta.
- Luo rajapinta
Salaaja. Määrittele rajapintaan kaksi metodia
String salaa(String viesti);
String pura(String salattuViesti);
- Toteuta kolme erilaista luokkaa:
Kaantaja,HakkerijaSeuraavaKirjain, jotka toteuttavatSalaaja-rajapinnan seuraavilla logiikoilla:
-
Kaantaja(Peilikuvakirjoitus). Kääntää sanan väärinpäin. Esimerkki: "Agentti" → "ittnegA". Vihje: Voit käyttääStringBuilder-luokanreverse()-komentoa tai silmukkaa, joka käy sanan läpi lopusta alkuun. -
Hakkeri("Leet-speak"). Korvaa tietyt kirjaimet numeroilla tai merkeillä. Esimerkki: "Agentti" -> "@g3ntt!"
Korvaa 'a' -> '@'
Korvaa 'e' -> '3'
Korvaa 'i' -> '!'
Korvaa 'o' -> '0'
SeuraavaKirjain(Caesar-siirros). Jokaista kirjainta siirretään aakkosissa yksi eteenpäin. Esimerkki: abc -> bcd. Vihje: Javassacharon luku. Voit tehdämerkki + 1.
'a' -> 'b'
'b' -> 'c'
'k' -> 'l'
jne.
Tässä harjoituksessa ei tarvitse huolehtia ö-kirjaimen pyörähtämisestä ympäri,
ellei halua. Tehtävässä ei myöskään tarvitse huolehtia siitä, että salauksen ja
purkamisen jälkeen saatu viesti ei välttämättä ole samanlainen kuin alkuperäinen
viesti. Esimerkiksi jos Hakkeri-muuntajaa käytettäessä alkuperäisessä
viestissä on oikeasti merkki @, pura-metodi antaa tulokseksi tuohon paikalle
merkin a. Tämä ei haittaa tässä, mutta tietenkin oikeassa salauksessa pitäisi
varmistaa, ettei tietoa katoa tai muutu vahingossa.
Saat TIMissä valmiina pääohjelman, jonka avulla voit testata luokkarakennettasi.
Comparable-rajapinta ja luonnollinen järjestys
Rajapinta
Comparable
määrittelee metodin compareTo, jonka avulla luokan olioille voi määrittää oman
luonnollisen järjestyksensä suhteessa toiseen olioon.
Rajapinnan ainoa metodi compareTo palauttaa kokonaisluvun, joka ilmaisee olion
järjestyksen suhteessa toiseen olioon:
| Tapaus | Merkitys | Tulkinta |
|---|---|---|
olioA.compareTo(olioB) < 0 | olioA < olioB | olioA on pienempi kuin olioB |
olioA.compareTo(olioB) == 0 | olioA == olioB | olioA on yhtä suuri kuin olioB |
olioA.compareTo(olioB) > 0 | olioA > olioB | olioA on suurempi kuin olioB |
Esimerkiksi Integer-tyyppi toteuttaa Comparable-rajapinnan
Integer-olioille, eli kaksi kokonaislukuoliota voidaan vertailla
keskenään compareTo-metodilla.
void main() {
Integer luku1 = 5;
Integer luku2 = 18;
int tulos = luku1.compareTo(luku2);
// Tulostaa negatiivisen arvon (< 0), koska 5 < 18
IO.println("luku1.compareTo(luku2): " + tulos);
}
Luonnollisella järjestyksellä tarkoitetaan ihmisjärjen mukaisesti olion tyypille ominaista ja intuitiivista järjestystä. Esimerkiksi merkkijonoille on Javassa luonnollinen järjestys määritelty aakkosjärjestyksenä:
// Apufunktio, joka tulostaa kahden merkkijonon välisen järjestyksen
void kerroJarjestys(String sana1, String sana2) {
int tulos = sana1.compareTo(sana2);
if (tulos < 0) {
IO.println("Merkkijono '" + sana1 + "' on järjestyksessä ennen merkkijonoa '" + sana2 + "'");
} else if (tulos > 0) {
IO.println("Merkkijono '" + sana1 + "' on järjestyksessä merkkijonon '" + sana2 + "' jälkeen");
} else {
IO.println("'" + sana1 + " on yhtä suuri kuin '" + sana2 + "'");
}
}
void main() {
String sana1 = "omena";
String sana2 = "appelsiini";
String sana3 = "banaani";
kerroJarjestys(sana1, sana2);
kerroJarjestys(sana1, sana3);
kerroJarjestys(sana2, sana3);
}
Vastaavasti Integer-luokalla on toteutus Comparable-rajapinnalle, joka
kertoo kokonaislukujen luonnollisen järjestyksen, joka on suuruusjärjestys.
int kerroJarjestys(Integer luku1, Integer luku2) {
int tulos = luku1.compareTo(luku1);
if (tulos < 0) {
IO.println(luku1 + " on pienempi kuin " + luku2);
} else if (tulos > 0) {
IO.println(luku1 + " on suurempi kuin " + luku2);
} else {
IO.println(luku1 + " on yhtä suuri kuin " + luku2);
}
return tulos;
}
void main() {
Integer luku1 = 5;
Integer luku2 = 18;
Integer luku3 = 5;
kerroJarjestys(luku1, luku2);
kerroJarjestys(luku1, luku3);
kerroJarjestys(luku2, luku3);
}
Olennainen Comparable-rajapinnan hyöty on, että voimme kirjoittaa
ohjelmia ja käyttää vertailuja vaativia algoritmeja, jotka toimivat
yleisesti kaikille olioille, jotka toteuttavat Comparable-rajapinnan.
Esimerkiksi voimme käyttää Javan valmiita
kokoelmien järjestämistoteutuksia, kuten esimerkiksi
Collections.sort-metodia.
Näin saamme järjestettyä kokoelmia helposti ilman, että meidän tarvitsee itse
kirjoittaa järjestämisalgoritmeja jokaiselle luokalle erikseen.
void main() {
List<Integer> numerot = Arrays.asList(18, 5, 42);
List<String> hedelmat = Arrays.asList("omena", "päärynä", "appelsiini");
IO.println(numerot); // [18, 5, 42]
IO.println(hedelmat); // [omena, päärynä, appelsiini]
// Järjestetään listat alkioiden luontaisen järjestyksen mukaan
Collections.sort(hedelmat);
Collections.sort(numerot);
IO.println(numerot); // [5, 18, 42]
IO.println(hedelmat); // [appelsiini, omena, päärynä]
}
Ekstra: Collections-luokka
Collections
on Javan valmis luokka, joka tarjoaa yllämainitun sort-metodin lisäksi monia
yleishyödyllisiä metodeja Javan kokoelmille.
Kokoelma on Javassa käytetty yleistys alkioita sisältäville tietorakenteille,
kuten taulukoille, listoille ja sanakirjoille.
Käsittelemme kokoelmia tarkemmin osassa 5.
Voit kuitenkin halutessasi tutkia jo Collections-luokkaa, joka
sisältää yleispäteviä metodeja kokoelmien käsittelyyn.
Mikäli katsot linkin, varaudu, että se sisältää paljon vasta myöhemmin
käsiteltävää syntaksia.
Monet Collections-luokan metodit perustuvat siihen, että kokoelman alkiot toteuttavat erilaisia
rajapintoja, kuten edellä mainittu Comparable. Yhdistämällä Javan
kokoelmille ja alkioille tarkoitettuja rajapintoja
onkin mahdollista kirjoittaa hyvin yleisiä algoritmeja, jotka toimivat
riippumatta siitä, onko parametrina lista numeroita tai vaikkapa
taulukko opiskelijoita.
Tutki Javan dokumentaatiota. Vastaa kysymyksiin Comparable-rajapinnasta.
Oma toteutus Comparable-rajapinnalle
Kokeillaan Comparable-rajapinnan toteuttamista omassa luokassamme.
Otetaan esimerkiksi luokka Kerailykortti, joka mallintaa eräässä
keräilypelissä käytettäviä kortteja. Meidän keräilykortti sisältää alkuun vain
keräilykortin nimen ja yksilöivän, ykkösestä alkavan tunnistenumeron:
class Kerailykortti {
private String nimi;
private int tunnistenumero;
public Kerailykortti(String nimi, int tunnistenumero) {
this.nimi = nimi;
this.tunnistenumero = tunnistenumero;
}
@Override
public String toString() {
return "Kortti: " + nimi + " (#" + tunnistenumero + ")";
}
}
Mikäli nyt yritämme järjestää Kerailykortti-olioita
Collections.sort()-metodilla, saamme käännöksenaikaisen virheen,
koska se ei toteuta Comparable-rajapintaa:
void main() {
List<Kerailykortti> kortit = Arrays.asList(
new Kerailykortti("Loistava Lohikäärme", 3),
new Kerailykortti("Aloittelijan Ameeba", 1),
new Kerailykortti("Mieletön Merihevonen", 2)
);
IO.println("Ennen järjestämistä:");
for (Kerailykortti kortti : kortit) {
IO.println(kortti);
}
Collections.sort(kortit);
IO.println();
IO.println("Jälkeen järjestämisen:");
for (Kerailykortti kortti : kortit) {
IO.println(kortti);
}
}
main.java:11: error: no suitable method found for sort(List<Kerailykortti>)
Collections.sort(kortit);
Virheilmoitus on vähintäänkin kryptinen. Yksinkertaistetusti virhe johtuu
perimmäisesti siitä, että Collections.sort() ei voi meidän puolestamme arvata,
mikä on Kerailykortti-olioiden luonnollinen järjestys. Onko se kenties kortin
nimen aakkosjärjestys vai kenties numerotunnisteen mukainen nouseva järjestys?
Vastataksemme tähän kysymykseen meidän täytyy toteuttaa Comparable-rajapinta
Kerailykortti-luokalle.
Kun lähdemme toteuttamaan Comparable-rajapintaa keräilykortille, joudumme heti
pohtimaan, mikä on luonnollinen järjestys keräilykorteillemme. Esimerkiksi
aakkosjärjestys nimen mukaan voi olla hyödyllinen. Toisaalta koska korteilla on
numeeriset ykkösestä alkavat numerotunnisteet, numerojärjestys tunnisteen mukaan
voidaan myös mieltää luonnollisemman tuntuiseksi ja yhtälailla tarpeelliseksi.
Luonnollista järjestystä valittaessa on lisäksi syytä pohtia kohdealueen ja
sovelluksen tarpeen — mitä luokkaa käyttäjät muut ohjelmoijat tai sovelluksen
lopulliset käyttäjät kaipaavat tai olettavat keräilykorttien
oletusjärjestyksestä?
Päättäkäämme tämän esimerkin puiteissa, että järjestys yksilöllisen tunnisteen
mukaan on tässä tapauksessa järkevin luonnollinen järjestys.
Toteutetaan tällä pohjustuksella Comparable-rajapinta siten, että kortit
järjestetään numerotunnisteen mukaan. Tätä varten tarvitsemme rajapinnan
toteutuksen luokan määrittelyyn sekä toteutuksen edellä mainitulle
compareTo-metodille.
Käytämme toteutuksessa luvun alussa olevaa palautustaulukkoa:
// HIGHLIGHT_GREEN_BEGIN
class Kerailykortti implements Comparable<Kerailykortti> {
// HIGHLIGHT_GREEN_END
private String nimi;
private int tunnistenumero;
public Kerailykortti(String nimi, int tunnistenumero) {
this.nimi = nimi;
this.tunnistenumero = tunnistenumero;
}
// HIGHLIGHT_GREEN_BEGIN
@Override
public int compareTo(Kerailykortti other) {
if (tunnistenumero > other.tunnistenumero) {
return 1;
}
if (tunnistenumero < other.tunnistenumero) {
return -1;
}
return 0;
}
// HIGHLIGHT_GREEN_END
@Override
public String toString() {
return "Kortti: " + nimi + " (#" + tunnistenumero + ")";
}
}
Comparable on niin sanottu geneerinen rajapinta, eli se ei itsessään kerro
minkä tyyppisiin olioihin vertailu kohdistuu.
Käsittelemme geneeristä ohjelmointia tarkemmin osassa
4.4.
Tästä syystä Comparable-rajapinnan toteuttamisessa meidän täytyy kertoa minkä
tyypin olioille luonnollinen järjestys määritellään. Tässä tapauksessa toteutamme
järjestyksen keräilykorteille, joten määrittelemme implements Comparable<Kerailykortti>.
Valmiiden vertailumetodien käyttö
Yllä olevassa tapauksessa toteutimme compareTo-metodin käyttäen suoraan
Comparable-rajapinnan määritelmää.
Kuitenkin Javan valmiit tyypit useimmiten tarjoavat jo
valmiita vertailumetodeja, joita voi hyödyntää Comparable-rajapinnan toteuttamiseksi.
Esimerkiksi int-kokonaisluvuille Java tarjoaa valmiin Integer.compare-metodin
(JavaDoc), jolla Kerailykortti-luokan compareTo-metodin toteutus
voidaan yksinkertaistaa yhden rivin funktioksi:
class Kerailykortti implements Comparable<Kerailykortti> {
private String nimi;
private int tunnistenumero;
public Kerailykortti(String nimi, int tunnistenumero) {
this.nimi = nimi;
this.tunnistenumero = tunnistenumero;
}
@Override
public int compareTo(Kerailykortti other) {
// HIGHLIGHT_GREEN_BEGIN
return Integer.compare(tunnistenumero, other.tunnistenumero);
// HIGHLIGHT_GREEN_END
}
@Override
public String toString() {
return "Kortti: " + nimi + " (#" + tunnistenumero + ")";
}
}
void main() {
List<Kerailykortti> kortit = Arrays.asList(
new Kerailykortti("Loistava Lohikäärme", 3),
new Kerailykortti("Aloittelijan Ameeba", 1),
new Kerailykortti("Mieletön Merihevonen", 2)
);
IO.println("Ennen järjestämistä:");
IO.println(kortit);
Collections.sort(kortit);
IO.println("Jälkeen järjestämisen:");
IO.println(kortit);
}
Toteuttaessa Comparable-rajapintaa itse tehdyille luokille onkin syytä suosia
valmiita vertailumetodeja ja niiden yhdistämistä.
Esimerkiksi Integer.compare osaa käsitellä kaikkia erikoistapauksia,
kuten lukualueen ylivuotoja. Vastaavasti Double.compare osaa
käsitellä kaikkia liukulukutyyppien erikoisarvoja, kuten äärettömyyttä tai
"Not a Number" -arvoja.
Useamman attribuutin vertailu
Monesti luonnollinen järjestys voi määräytyä useamman luokan attribuutin mukaan.
Mitä jos kohdealueen kannalta nyt olisikin järkevämpi, että kortit
järjestetäänkin ensin aakkosjärjestyksen ja sitten vasta numerotunnisteen
mukaan?
Tätä varten meidän
täytyy muuttaa compareTo-metodia siten, että ensin verrataan nimi
aakkosjärjestyksen mukaan käyttäen String-luokan omaa compareTo-metodia.
Jos merkkijonot ovat samat (eli compareTo palauttaa 0), tehdään vertailu
tunnistenumero-attribuutille:
class Kerailykortti implements Comparable<Kerailykortti> {
private String nimi;
private int tunnistenumero;
public Kerailykortti(String nimi, int tunnistenumero) {
this.nimi = nimi;
this.tunnistenumero = tunnistenumero;
}
@Override
public int compareTo(Kerailykortti other) {
// HIGHLIGHT_GREEN_BEGIN
int nimiVertailu = this.nimi.compareTo(other.nimi);
if (nimiVertailu != 0) {
return nimiVertailu;
}
return Integer.compare(this.tunnistenumero, other.tunnistenumero);
// HIGHLIGHT_GREEN_END
}
@Override
public String toString() {
return "Kortti: " + nimi + " (#" + tunnistenumero + ")";
}
}
Tehtävät
Tehtävässä on pohjana Henkilo-luokka omassa tiedostossaan sekä
jarjestaHenkilot-metodi main.java-tiedostossa. Kyseinen metodi ei kuitenkaan
toimi, sillä se käyttää Javan valmista Collections.sort-metodia ja
Henkilo-luokasta puuttuu sille tuki.
Muokkaa Henkilo-luokkaa niin, että List<Henkilo>-tyyppiset listat voidaan järjestää Collections.sort-metodilla henkilön nimen mukaan aakkosjärjestykseen.
Esimerkiksi listan
List<Henkilo> henkilot = Arrays.asList(
new Henkilo("Joukahainen"),
new Henkilo("Ilmatar"),
new Henkilo("Kyllikki"),
new Henkilo("Kokko")
);
pitäisi olla Collections.sort(henkilot);-kutsun jälkeen järjestyksessä:
- Ilmatar
- Joukahainen
- Kokko
- Kyllikki
Jatkoa edelliselle tehtävälle. Nyt Henkilo-luokassa henkilöiden nimet on
jaettu erikseen sukunimeen ja etunimiin.
Muokkaa uudistettua Henkilo-luokkaa niin, että List<Henkilo>-tyyppiset
listat voidaan järjestää Collections.sort-metodilla henkilön sukunimen ja
etunimien mukaan aakkosjärjestykseen, niin että järjestys tapahtuu ensin
sukunimen mukaan.
Esimerkiksi listan
List<Henkilo> henkilot = Arrays.asList(
new Henkilo("Pacius", "Fredrik"),
new Henkilo("Mozart", "Wolfgang Amadeus"),
new Henkilo("Mozart", "Leopold"),
new Henkilo("Chopin", "Frédéric")
);
pitäisi olla Collections.sort(henkilot);-kutsun jälkeen järjestyksessä:
- Chopin Frédéric
- Mozart Leopold
- Mozart Wolfgang Amadeus
- Pacius Fredrik
Perintä ja rajapinnat olioiden yhteistyössä
osaamistavoitteet
- Osaat hyödyntää rajapintoja ja abstrakteja luokkia luokkien välisen riippuvuuden välttämiseksi
- Tunnistaa milloin perintää kannattaa käyttää, ja milloin koostaminen on parempi vaihtoehto. ("Composition over inheritance")

Perintä ja rajapinnat voivat toimia, ja usein toimivatkin yhdessä. Perintä määrittelee luokkien välisen hierarkian ja jakaa yhteistä toiminnallisuutta, kun taas rajapinnat määrittelevät kyvykkyyksiä, joita eri luokat voivat toteuttaa riippumatta niiden sijainnista luokkahierarkiassa.
Itse asiassa käytimme jo
Älykoti-esimerkissämme sekä perintää
(Laite abstraktina luokkana) että rajapintaa (Saadettava-rajapinta).
Laajennetaan kuitenkin perinnän ja rajapintojen yhteistyötä hieman eteenpäin.
Tarkastellaan tilannetta, jossa meillä on ohjelmassamme luokkia, jotka eivät jaa
yhteistä yliluokkaa, mutta kuitenkin jakavat yhteisen kyvykkyyden.
Pistorasia ja sähkölaitteet
Tehdään pieni ajatusharjoitus. Kuvittele kotisi seinässä olevaa pistorasiaa. Pistorasia tarjoaa sähkövirtaa, mutta se ei anna sitä mihin tahansa. Se vaatii, että laitteessa on sopiva pistotulppa, joka sopii pistorasiaan.
Tässä analogiassa rajapinta on se standardi eli sopimus, jonka laitteen täytyy täyttää, jotta se voi käyttää pistorasiaa. Asiaa voidaan tarkastella myös niin päin, että jos laitteessa on pistorasiaan sopiva pistotulppa, niin sillä täytyy olla kyky toimia siinä tilanteessa, että se kytketään pistorasiaan.
Pistorasiaa ei kiinnosta, kytketkö siihen leivänpaahtimen vai sirkkelin.
Laitteet ovat itse asiassa täysin erilaisia. Toisella voi tehdä ruokaa, toinen
on työkalu. Niillä ei ole yhteistä "esi-isää" laitehierarkiassa samalla tavalla,
kuin vaikkapa Auto ja Moottoripyora voisivat periä Ajoneuvo-luokan. Ainoa
leivänpaahdinta ja sirkkeliä yhdistävä tekijä on kyky kytkeytyä verkkovirtaan.
Jos yrittäisimme mallintaa tämän perinnällä, joutuisimme ongelmiin heti, kun
haluaisimme käyttää leivänpaahdinta. Onko leivänpaahdin Sahkolaite,
Keittiolaite, vai kenties molempia? Javassa luokka ei kuitenkaan voi periä
kahta yliluokkaa.
Rajapinta ratkaisee tämän ongelman tyylikkäästi:
LeivanpaahdinonKeittiolaite(perintä), mutta se myös toteuttaaVerkkovirtalaite-rajapinnan.- Samoin
Sirkkelivoisi olla vaikkapaTyokalu(perintä), joka myöskin toteuttaa samanVerkkovirtalaite-rajapinnan.
Näin pistorasia voi hyväksyä kumman tahansa laitteen, koska molemmat täyttävät sopimuksen eli toteuttavat rajapinnan vaatiman kytkennän.
Yksinkertaisimmillaan Verkkovirtalaite-rajapinnan sisältö olisi määritelmä
siitä, että laitteen on pystyttävä reagoimaan siihen, kun se kytketään
pistorasiaan ja virta alkaa kulkea johdossa.
public interface Verkkovirtalaite {
// Tämä metodi on "pistotulppa".
// Kun pistorasia aktivoi tämän, laite saa sähköä.
void kytkeVirta();
}
Nyt Leivanpaahdin ja Sirkkeli voivat toteuttaa tämän rajapinnan.
public class Leivanpaahdin implements Verkkovirtalaite {
@Override
public void kytkeVirta() {
// Leivänpaahtimen oma tapa reagoida virtaan:
IO.println("Leivänpaahdin: Vastukset alkavat hehkua punaisena.");
}
}
public class Sirkkeli implements Verkkovirtalaite {
@Override
public void kytkeVirta() {
// Sirkkelin oma tapa reagoida virtaan:
IO.println("Sirkkeli: Moottori alkaa pyörittää terää 4000 rpm.");
}
}
Nämä luokat voivat olla aivan eri puolella luokkahierarkiaa. Toinen on
keittiölaite, toinen työkalu. Molemmat kuitenkin reagoivat sähkövirran
kytkemiseen -- joskin omalla tavallaan. Tehdään vielä abstraktit Keittiolaite-
ja Tyokalu-yliluokat, joista Leivanpaahdin ja Sirkkeli periytyvät. Jotta
esimerkki olisi hieman mielekkäämpi, lisätään näihin yliluokkiin joitain
ominaisuuksia ja metodeja.
public abstract class Keittiolaite {
/**
* Sisältääkö laite lämmitysvastuksia.
*/
boolean lammittava;
/**
* Kaikki keittiölaitteet pitää voida pestä.
*/
public abstract void puhdista();
}
public abstract class Tyokalu {
/**
* Laitteen käyttötunnit
*/
private int kayttotunnit = 0;
/**
* Käytä laitetta
* @param tunnit Montako tuntia laitetta käytetään.
*/
public void kayta(int tunnit)
{
this.kayttotunnit = tunnit;
}
/**
* Huolla laitetta
* @return Onnistuiko huolto
*/
public abstract boolean huolla();
}
Toteutetaan nyt nuo ominaisuudet ja metodit perivissä luokissa.
// Sirkkeli on Työkalu, joka toimii verkkovirralla
public class Sirkkeli extends Tyokalu implements Verkkovirtalaite {
@Override
public void kytkeVirta() {
// Sirkkelin oma tapa reagoida virtaan:
IO.println("Sirkkeli: Moottori alkaa pyörittää terää 4000 rpm.");
// Kutsutaan tässä myös yliluokan kayta()-metodia, jolloin
// käyttötunnit lisääntyvät.
super.kayta(1);
}
/**
* Huolletaan sirkkeli.
* @return Onnistuiko huolto.
*/
@Override
public boolean huolla() {
IO.println("Huolletaan sirkkeliä..."
+ "Teroitetaan terää ja säädetään kierrosnopeutta.");
return true;
}
}
// Leivänpaahdin on Keittiölaite, joka toimii verkkovirralla
public class Leivanpaahdin extends Keittiolaite
implements Verkkovirtalaite {
@Override
public void kytkeVirta() {
// Leivänpaahtimen oma tapa reagoida virtaan:
IO.println("Leivänpaahdin: "
+ "Vastukset alkavat hehkua punaisena.");
}
@Override
public void puhdista() {
IO.println("Leivänpaahdin: Poistetaan murut "
+ "ja pyyhitään kevyesti kostealla rätillä.");
}
}
Luokkahierarkia näyttäisi seuraavanlaiselta.
Tämä on tärkein kohta ymmärryksen kannalta: Pistorasia on luokka, joka käyttää rajapintaa.
public class Pistorasia {
// Pistorasiaan voi kytkeä MINKÄ TAHANSA verkkovirtalaitteen.
// Pistorasiaa ei kiinnosta, onko se sirkkeli vai paahdin.
public void kytkeLaite(Verkkovirtalaite laite) {
IO.println("--- Pistorasia antaa sähköä ---");
// Pistorasia kutsuu sopimuksen mukaista metodia.
// Tässä toteutuu polymorfismi:
// laite reagoi oikealla, sille ominaisella tavalla.
laite.kytkeVirta();
}
}
Huomaamme, että aliohjelman parametrin tyyppinä on Verkkovirtalaite-rajapinta!
Parametrin ei tarvitse olla Leivanpaahdin, Sirkkeli tai mikään muukaan
konkreettinen luokka. Riittää, että se toteuttaa Verkkovirtalaite-rajapinnan.
Tässä kytkeLaite()-metodi ottaa parametrinaan Verkkovirtalaite-rajapinnan
mukaisen tyypin. Tämä tarkoittaa, että metodi voi hyväksyä minkä tahansa olion,
joka toteuttaa tämän rajapinnan, riippumatta siitä, mihin luokkahierarkiaan
kyseinen olio kuuluu.
Rajapinta muuttujan tyyppinä
Jotta Pistorasia-luokka pääsisi tositoimiin, tarvitsemme vielä pääohjelman,
jossa luomme Pistorasia-olion ja kytkemme siihen erilaisia laitteita. Luodaan
nyt pääohjelma, jossa kytketään ensin Leivanpaahdin pistorasiaan.
Esimerkki sisältää jo aika monta tiedostoa, joten lue esimerkki huolellisesti läpi. Voit vaihtoehtoisesti selata esimerkin tiedostoja GitHubissa.
- main.java
- Verkkovirtalaite.java
- Leivanpaahdin.java
- Sirkkeli.java
- Keittiolaite.java
- Tyokalu.java
- Pistorasia.java
public class KodinSahkot {
public static void main(String[] args) {
// 1. Luodaan infrastruktuuri: Pistorasia
// Tässä kohtaa Pistorasia-olio syntyy tietokoneen muistiin.
Pistorasia keittionPistoke = new Pistorasia();
// 2. Luodaan laitteet
Leivanpaahdin paahdin = new Leivanpaahdin();
Sirkkeli sirkkeli = new Sirkkeli();
// 3. Käytetään laitteita pistorasian kautta
IO.println("--- Aamu keittiössä ---");
// Kytketään paahdin seinään
keittionPistoke.kytkeLaite(paahdin);
IO.println("\n--- Remontti alkaa ---");
// Kytketään sirkkeli SAMAAN pistorasiaan
// Koska yhdessä pistorasiassa voi olla yksi laite kerrallaan,
// paahdin irrotetaan, vaikka sitä ei erikseen
// tässä esitetäkään.
keittionPistoke.kytkeLaite(sirkkeli);
}
}
Kuten Luvussa 3.2 Polymorfismi opimme, meidän
ei olisi pääohjelmassa pakko määritellä paahdin- ja sirkkeli-muuttujia
konkreettisten tyyppien (Leivanpaahdin ja Sirkkeli) avulla, vaan voisimme
määritellä molemmat Verkkovirtalaite-tyyppisiksi. Tässähän nimittäin meitä
kiinnostaa vain se, että laitteet pystytään kytkemään pistorasiaan.
public class KodinSahkot {
public static void main(String[] args) {
// 1. Luodaan infrastruktuuri: Pistorasia
// Tässä kohtaa Pistorasia-olio syntyy tietokoneen muistiin.
Pistorasia keittionPistoke = new Pistorasia();
// 2. Luodaan laitteet
// HIGHLIGHT_GREEN_BEGIN
Verkkovirtalaite paahdin = new Leivanpaahdin();
Verkkovirtalaite sirkkeli = new Sirkkeli();
// HIGHLIGHT_GREEN_END
// 3. Käytetään laitteita pistorasian kautta
IO.println("--- Aamu keittiössä ---");
// Kytketään paahdin seinään
keittionPistoke.kytkeLaite(paahdin);
IO.println("\n--- Remontti alkaa ---");
// Kytketään sirkkeli SAMAAN pistorasiaan
// Koska yhdessä pistorasiassa voi olla yksi laite kerrallaan,
// paahdin irrotetaan, vaikka sitä ei erikseen
// tässä esitetäkään.
keittionPistoke.kytkeLaite(sirkkeli);
}
}
Ylätyyppiä vasten ohjelmointi
Yllä kuvattu tapa, jossa aliluokan oliota käsitellään ylätyypin, eli yliluokan tai rajapinnan tyyppisenä, on olio-ohjelmoinnissa hyvin yleinen ja suositeltava käytäntö. Tätä tapaa kutsutaan usein nimellä rajapintaa vasten ohjelmointi (program to an interface) tai ylätyyppiä vasten ohjelmointi (program to a supertype). Näin ohjelman eri osat kytkeytyvät toisiinsa löyhemmin, ja yksittäisiä toteutuksia voidaan vaihtaa ilman, että muuta koodia tarvitsee muuttaa.
Miksi ylätyyppiä vasten ohjelmointi on hyödyllistä? Yksi syy on se, että voimme
nyt käsitellä hyvin eri tyyppisiä olioita yhtenäisenä joukkona; näinhän tehtiin
jo Tehtävässä 3.4. Otetaan vaikkapa
Verkkovirtalaite-esimerkkimme: voimme esimerkiksi luoda listan erilaisista
verkkovirtalaitteista ja kytkeä ne kaikki pistorasiaan silmukassa.
List<Verkkovirtalaite> laitteet = List.of(
new Leivanpaahdin(),
new Sirkkeli(),
new Imuri()
);
Pistorasia pistorasia = new Pistorasia();
for (Verkkovirtalaite v : laitteet) {
pistorasia.kytkeLaite(v);
}
Jos jokainen laite olisi määritelty omaksi tyypikseen, meidän täytyisi
kirjoittaa seuraavasti (oletetaan jälleen, että Keittiolaite ja Tyokalu ovat
olemassa olevia yliluokkia).
List<Keittiolaite> keittionLaitteet = ...;
List<Tyokalu> tyokalut = ...;
Pistorasia pistorasia = new Pistorasia();
for (Keittiolaite k : keittionLaitteet) {
pistorasia.kytkeLaite(k);
}
for (Tyokalu t : tyokalut) {
pistorasia.kytkeLaite(t);
}
Toinen syy on helppo vaihdettavuus, josta käytetään englanninkielistä termiä loose coupling. Kun koodi käyttää rajapintaa muuttujan tyyppinä, se ei ole sidottu tiettyyn toteutukseen. Tämä tarkoittaa, että voimme helposti vaihtaa yhden toteutuksen toiseen ilman, että meidän tarvitsee muuttaa koodia, joka käyttää kyseistä rajapintaa.
Kuvitellaan, että teemme ohjelmaa, joka testaa sähkölaitteita.
Leivanpaahdin testattavaLaite = new Leivanpaahdin();
// .. suoritetaan laitteen testaus ..
// Vaihdetaan testattava laite toiseen toteutukseen
testattavaLaite = new Sirkkeli(); // Ei onnistu, koska tyypit eivät täsmää
Kun muuttuja määritellään rajapintana, voimme helposti luoda erilaisia testilaitteita, jotka toteuttavat saman rajapinnan, ja voimme vaihtaa konkreettisen toteutuksen vapaasti.
Verkkovirtalaite testattavaLaite = new Leivanpaahdin();
// .. suoritetaan laitteen testaus ..
// Vaihdetaan testattava laite toiseen toteutukseen
testattavaLaite = new Sirkkeli();
// Tämä onnistuu, koska molemmat toteuttavat
// Verkkovirtalaite-rajapinnan
Kolmas syy liittyy ohjelmiston suunnitteluun ja käytännön kirjoittamiseen. Kun
määrittelet muuttujan tyypiksi Verkkovirtalaite, kääntäjä estää sinua
kutsumasta metodeja, jotka ovat spesifejä vain leivänpaahtimille (kuten
saadaKuumuus()) tai sirkkelille (kuten asetaTeranKorkeus()). Vaikka
tällainen itsensä rajoittaminen saattaa tuntua oudolta, se auttaa pitämään
koodin selkeänä ja estää virheitä, joissa yritetään käyttää laitetta tavalla,
joka ei ole yhteensopiva sen rajapinnan kanssa.
Liskovin korvausperiaate
Yllä mainittuun loose coupling-periaatteeseen liittyy läheisesti myös Liskovin korvausperiaate (engl. Liskov Substitution Principle, LSP). LSP on olio-ohjelmoinnin periaate, jonka mukaan, että olion tulee olla korvattavissa sellaisella oliolla, joka toteuttaa saman rajapinnan tai sovitun sopimuksen ilman, että ohjelman käyttäytyminen muuttuu. Niinpä esimerkiksi aliluokan tulee noudattaa yliluokan määrittelemiä sopimuksia ja käyttäytymismalleja, tai vastaavasti rajapinnan toteuttavan luokan tulee noudattaa rajapinnan määrittelemiä sopimuksia.
Palataan hetkeksi Luvussa 3.2 alustettuun
soitin-esimerkkiin. Oletetaan, että meillä on Soitin-rajapinta, joka
määrittelee yleisölle musiikkia metodin soita().
public interface Soitin {
/**
* Esittää kappaleen yleisölle.
*/
void soita();
}
Kaikki soittimet, kuten Kitara, Piano ja Rumpusetti, toteuttavat tämän
rajapinnan. Konsertin järjestäjä haluaa varmistaa, että kaikki soittimet voivat
soittaa huolimatta siitä, minkä tyyppisiä soittimia ne ovat. Tehdään
Konsertti-luokka, jonka soitaKaikkiaSoittimia()-metodi laittaa kaikki
soittimet soimaan.
public class Konsertti {
public void soitaKaikkiaSoittimia(Soitin[] soittimet) {
for (Soitin soitin : soittimet) {
soitin.soita();
}
}
}
Tehdään nyt uusi soitin, HarjoitusPiano, jolla voi soittaa vain kuulokkeilla,
jolloin yleisö ei kuule mitään. Tämäkin soitin toteuttaa Soitin-rajapinnan,
mutta sen käyttäytyminen poikkeaa muista soittimista.
class HarjoitusPiano implements Soitin {
@Override
public void soita() {
// Toteutus, joka tekee sinänsä jotain "järkevää", mutta rikkoo sopimuksen.
IO.println("Harjoitellaan kuulokkeilla. Yleisö ei kuule mitään.");
}
}
Kootaan nyt "konsertti" pääohjelmaan.
void main() {
Soitin[] soittimet = {
new Kitara(),
new Piano(),
new HarjoitusPiano()
};
Konsertti konsertti = new Konsertti();
konsertti.soitaKaikkiaSoittimia(soittimet);
}
Koodi kyllä sinänsä toimii teknisesti. Silti HarjoitusPiano rikkoo
Soitin-rajapinnan sopimusta: sen soita() ei "esitä kappaletta yleisölle",
vaan vain soittajalle itselleen. Aliluokan toiminta voisi olla järkevää jossain
toisessa kontekstissa (tässä, harjoittelutilanteessa). Hyvin suunnitellussa
luokkahierarkiassa kuitenkin jokainen olio noudattaa yliluokan tai rajapinnan
lupaamaa sopimusta. Tällöin polymorfismia voidaan käyttää luotettavasti ilman,
että ohjelman käyttäjän tarvitsee tuntea kaikkia konkreettisia aliluokkia
erikseen.
Myös kodin sähkölaitteet -esimerkkimme liittyy samaan aiheeseen. Muistetaan,
että koska Esimerkissä 3.8 paahdin ja sirkkeli määriteltiin
Verkkovirtalaite-tyyppisiksi, emme voi kutsua niille metodeja, jotka eivät ole
määritelty kyseisessä rajapinnassa. Esimerkiksi emme voi kutsua
paahdin.puhdista() tai sirkkeli.huolla(), koska nämä metodit eivät kuulu
Verkkovirtalaite-rajapintaan. Jos todella tarvitsisimme pääsyn näihin
metodeihin, meidän tulisi pysähtyä miettimään, miksi käsittelemme oliota
ylipäätään pelkkänä verkkovirtalaitteena. Ohjelmistosuunnittelun näkökulmasta
tilanne vihjaa siihen, että yritämme ehkä ratkaista kahta eri ongelmaa samassa
paikassa.
Hyvässä suunnittelussa koodi, joka käsittelee Verkkovirtalaite-tyyppisiä
olioita (kuten sähkömittari tai sulakekaappi), on kiinnostunut vain ja
ainoastaan sähköön liittyvistä asioista. Sitä ei pitäisikään kiinnostaa, onko
laite leivänpaahdin vai sirkkeli, eikä sen kuulu yrittää puhdistaa tai huoltaa
niitä.
Jos huomaamme tarvitsevamme puhdista()-metodia, olemme todennäköisesti
"väärässä huoneessa":
-
Väärä abstraktiotaso: Jos olemme rakentamassa sovelluslogiikkaa keittiön siivousta varten, meidän ei pitäisi säilyttää laitteita
List<Verkkovirtalaite>-listassa, vaanList<Keittiolaite>-listassa. Tällöin kaikilla listan olioilla on luonnostaanpuhdista()-metodi käytettävissä ilman kikkailua. -
Vastuun jako (Single Responsibility): Jos samassa aliohjelmassa yritetään sekä mitata sähkönkulutusta (rajapinta) että pestä laite (abstrakti luokka), aliohjelma tekee liikaa asioita. Parempi ratkaisu on jakaa ohjelma osiin: yksi osa hallinnoi sähköverkkoa (
Verkkovirtalaite-rajapinnan kautta) ja toinen osa huolehtii ylläpidosta (Keittiolaite- taiTyokalu-tyyppien kautta).
Tiivistetysti: Sen sijaan, että yrittäisimme pakottaa yleisen rajapinnan kautta esiin erityisominaisuuksia, meidän tulisi valita muuttujan tyyppi sen mukaan, mitä olemme sillä hetkellä tekemässä. Sähkömies näkee sirkkelin verkkovirtalaitteena, puuseppä näkee sen työkaluna – ja koodin tulisi heijastaa tätä roolijakoa."
Abstrakti luokka vai rajapinta?
Alla on lyhyt yhteenvetotaulukko, joka tiivistää abstraktin luokan ja rajapinnan keskeiset erot syntaktin ja käyttötarkoituksen osalta.
| Kysymys | Abstrakti luokka | Rajapinta |
|---|---|---|
| Voiko sisältää attribuutteja? | Kyllä | Ei |
| Voiko sisältää metodien toteutuksia? | Kyllä | Ei (Java v8 alkaen mahdollisuus ns. default-metodeihin) |
| Kuinka monta voi periä/toteuttaa? | Luokka voi periä vain yhden abstraktin luokan | Luokka voi toteuttaa useita rajapintoja |
| Käyttötarkoitus | Yhteinen runko ja osittainen toteutus | Yhteinen sopimus käyttäytymisestä |
Tehtävät

Toteutetaan yksinkertainen tekstiseikkailupeli (tai oikeammin pieni palanen pelistä), jossa pelaaja voi yrittää poimia esineitä maasta, sekä syödä saadakseen energiaa. Saat valmiina kaksi rajapintaa: Syotava ja Poimittava. Lisäksi saat osittain toteutetut luokat: Omena ja IsoKivi, jotka toteuttavat nämä rajapinnat. Edelleen, saat osittain toteutetun pääohjelman, jossa pelaajan energiaa ja repun tilaa seurataan.
Täydennä kaikki TODO-sanalla merkityt osat, jotta ohjelma toimii ohjeiden mukaisesti.
Tee Robotti, joka osaa suorittaa erilaisia kotitöitä, kuten imurointia ja
kukkien kastelua.
Toteuta tehtävä oheisen UML-kaavion mukaisesti. Katkoviiva, jossa on musta
nuoli, tarkoittaa, että Robotti-luokka käyttää KayttoEsine-rajapintaa:
Robotti-luokka sisältää attribuutin, joka on tyyppiä KayttoEsine.
Kuvaus sanallisessa muodossa
Tässä on kuvaus luokista ja niiden vaadituista ominaisuuksista (vastaavat kuin UML-kaaviossa):
Robotilla on seuraavat metodit:
void vaihdaKayttoEsine(KayttoEsine esine): Vaihtaa robotin käyttämän esineen (esim. imuri tai kastelukannu).void teeTyota(String kohde): Suorittaa kotityön. Joskohdeon sillä listalla, jotka kyseiseltä käyttöesineeltä on kielletty (esim.Kastelukannu-oliolla ei saa kastella"Tietokone"-kohdetta), robotin tulee tulostaa virheilmoitus. Kielletyt käyttökohteet määritellään käyttöesineen attribuuttina merkkijonolistana.Kastelukannu-olio ei kastele jos vettä ei ole riittävästi. Sen voi täyttäätaytaVesi()-metodilla. Kastelukannun vesimäärä on aluksi 50 yksikköä. Voit halutessasi tehdä uuden muodostajan, joka asettaa vesimäärän alkutilan toiseksi.Imuri-olio ei imuroi jos roskasäiliö on täynnä. Sen voi tyhjentäätyhjennaSailio()-metodilla. Roskasäiliön kapasiteetti on 100 yksikköä. Voit halutessasi tehdä uuden muodostajan, joka asettaa roskasäiliön alkutilan toiseksi.- Molemmat käyttöesineet palauttavat
kayta(String kohde)-metodin avulla totuusarvon, joka kertoo onnistuiko työ.
Tyyppiparametrit ja geneerisyys
osaamistavoitteet
- Osaat hyödyntää tyyppiparametreja toteuttaaksesi yleiskäyttöisiä eli geneerisiä luokkia ja metodeja
Olet oppinut aiemmissa ohjelmointiopinnoissasi, että parametrit mahdollistavat toiston vähentämisen yleistämällä ohjelman toimintaa erilaisille arvoille. Parametrien idea on erottaa laskennan logiikka itse arvoista. Kun kirjoitamme metodin, laskenta määritellään vain kerran. Esimerkiksi, jos haluaisimme ilman parametreja selvittää, missä indeksissä etsimämme luku ilmaantuu ensimmäistä kertaa, voisimme kirjoittaa koodin näin.
void main() {
int[] taulukko1 = {2, 3, 4};
// Etsitään luku 3
for (int i = 0; i < taulukko1.length; i++) {
if (taulukko1[i] == 3) {
IO.println("Luku 3 on ekan kerran indeksissä " + i);
break;
}
}
int[] taulukko2 = {-20, 10, 2, 1};
// Etsitään luku 2
for (int i = 0; i < taulukko2.length; i++) {
if (taulukko2[i] == 2) {
IO.println("Luku 2 on ekan kerran indeksissä " + i);
break;
}
}
}
Tämä toimii, mutta koodia on kopioitu turhaan. Tehdään nyt funktio, joka ottaa taulukon parametrina.
int etsiIndeksi(int[] taulukko, int etsittava) {
for (int i = 0; i < taulukko.length; i++)
if (taulukko[i] == etsittava) return i;
return -1;
}
void main() {
IO.println("Luku 3 on indeksissä " + etsiIndeksi(new int[] {2, 3, 4}, 3));
IO.println("Luku 2 on indeksissä " + etsiIndeksi(new int[] {-20, 10, 2, 1}, 2));
}
Tätä voi ajatella parametrien käyttönä arvojen tasolla: metodi on yleinen, mutta sille annettavat arvot vaihtelevat.
Huomaamme kuitenkin nopeasti, että sama etsiIndeksi-funktio ei kuitenkaan
toimi muille lukutyypeille, kuten long tai float, eikä myöskään kokonaan
muunlaisille tyypeille, kuten String. Näitä varten täytyisi tehdä erilliset
funktiot.
int etsiIndeksiLong(long[] taulukko, long etsittava) {
for (int i = 0; i < taulukko.length; i++)
if (taulukko[i] == etsittava) return i;
return -1;
}
int etsiIndeksiFloat(float[] taulukko, float etsittava) {
for (int i = 0; i < taulukko.length; i++)
if (taulukko[i] == etsittava) return i;
return -1;
}
int etsiIndeksiString(String[] taulukko, String etsittava) {
for (int i = 0; i < taulukko.length; i++)
if (taulukko[i].equals(etsittava)) return i;
return -1;
}
void main() {
long[] longTaulukko = {-10L, 5L, 1L};
float[] floatTaulukko = {-10.0f, 2.0f};
String[] stringTaulukko = {"Koira", "Kissa", "Lintu"};
IO.println("Luku -10 on indeksissä " + etsiIndeksiLong(longTaulukko, -10L));
IO.println("Luku 2.0 on indeksissä " + etsiIndeksiFloat(floatTaulukko, 2.0f));
IO.println("Merkkijono \"Kissa\" on indeksissä " + etsiIndeksiString(stringTaulukko, "Kissa"));
}
Koska Java on staattisesti tyypitetty kieli, emme voi kirjoittaa yhtä ja samaa
metodia, joka toimisi automaattisesti kaikille näille. Ilman tällaista ratkaisua
päätyisimme helposti tilanteeseen, jossa meillä on joukko lähes identtisiä
metodeja: etsiIndeksiInt, etsiIndeksiDouble, etsiIndeksiString ja niin
edelleen. Koodi on käytännössä sama, vain tyypit vaihtuvat.
Oikeastaan etsiIndeksi-funktion perusajatus on aina sama:
int etsiIndeksi(TYYPPI[] taulukko, TYYPPI etsittava) {
for (int i = 0; i < taulukko.length; i++)
if (taulukko[i] == etsittava) return i;
return -1;
}
Javassa tämän tapaisen koodin kirjoittaminen on mahdollista tyyppiparametrien avulla.
Tyyppiparametrit
Tyyppiparametri on parametri, jonka arvona on tietotyyppi. Tyyppiparametrin tarkoitus on vähentää toistoa tapauksissa, jossa sama koodi toimii eri tyyppisille arvoille luopumatta staattisen tyypityksen antamista hyödyistä. Lisäksi tyyppiparametrit mahdollistavat ylimääräisten tyyppimuunnosten välttämistä jossain tapauksissa.
Tyypiparametri tai -parametrit voidaan määrittää metodille tavallisten parametrien lisäksi. Erikoisuutena on, että tyyppiparametreja voidaan myös määrittää luokille. Yhdessä metodien ja luokkien tyyppiparametrit mahdollistavat geneeristä ohjelmointia, eli tyypistä riippumattomien algoritmien ja tietorakenteiden ohjelmointia.
Geneerinen metodi
Metodia, joka määrittelee tyyppiparametrin, kutsutaan geneeriseksi metodiksi.
Geneerisessä metodissa tietotyyppiä ei ole lukittu metodia määriteltäessä
tiettyyn tyyppiin etukäteen, vaan tyyppi ilmaistaan symbolilla, joka
täsmennetään vasta metodia kutsuttaessa. Metodin tyyppiparametri laitetaan
kulmasulkeiden väliin ennen metodin palautustyyppiä. Yleinen käytäntö on käyttää
yksikirjaimisia, isoja kirjaimia. Tavallisin näistä on T, joka tulee sanasta
Type.
// aliohjelma "tulosta", jolla on yksi tyyppiparametri T
// ja yksi tavallinen parametri "arvo"
<T> void tulosta(T arvo) {
IO.println("Moikka, olen '" + arvo + "' ja olen luokan '" + arvo.getClass() + "' olio!");
}
void main() {
tulosta(1.0);
tulosta(1);
tulosta("kissa");
}
Geneerinen metodi voi olla staattinen, ei-staattinen tai konstruktori. Tyyppiparametri voi esiintyä metodin palautustyypissä, parametreissa tai molemmissa.
Tyyppiparametreja voi olla yksi tai useampia. Ne määritellään pilkulla
eroteltuina kulmasulkeiden sisällä, ja jokainen niistä voi edustaa toisistaan
riippumatonta tyyppiä. Esimerkiksi alla olevassa metodissa on kaksi
tyyppiparametria, T1 ja T2, jotka voivat edustaa mitä tahansa kahta
erilaista tietotyyppiä.
<T1, T2> String yhdista(T1 arvo1, T2 arvo2) {
return arvo1.toString() + ", " + arvo2.toString();
}
void main() {
IO.println(yhdista(1, 2)); // T1 = Integer, T2 = Integer
IO.println(yhdista(true, 1.0)); // T1 = Boolean, T2 = Double
}
Tyyppiparametrien nimeämisen osalta yleistynyt käytäntö tällä hetkellä lienee,
että nimi on yleensä yksi suuraakkonen, joka on johdettu tyyppiparametrin
merkityksestä, kuten T (Type), E (Element), K (Key), N
(Number), V (Value). Jossain tapauksissa tyyppiparametrien nimeen
lisätään myös numero, kuten T1, T2, T3, jne.
Huomaa, että yllä olevissa esimerkeissä tyyppiparametri määritellään, mutta
tyyppiparametreille ei anneta arvoa kutsuttaessa. Tämä voitaisiin kyllä tehdä;
esimerkiksi yllä oleva tulosta-aliohjelman kutsulle voidaan määrittää
tarkasti tyyppiparametrin tyyppi.
<T> void tulosta(T arvo) {
IO.println("Moikka, olen '" + arvo + "' ja olen luokan '" + arvo.getClass() + "' olio!");
}
void main() {
this.<Double>tulosta(1.0); // sama kuin tulosta(1.0)
this.<String>tulosta("kissa"); // tulosta("String")
}
Tyyppiparametrin arvoa ei yleensä määritetä kutsussa, sillä kääntäjä osaa
yleensä päätellä tyyppiparametrin arvon automaattisesti. Esimerkiksi
tulosta(1.0)-kutsussa lausekkeen 1.0 tyyppi on double, joten kääntäjä
päättelee tyyppiparametrin T olevan (Double). On kuitenkin hyvä pitää
mielessä, että tyypiparametrille kyllä annetaan taustalla arvo, vaikka sitä ei
itse kirjoittaisikaan näkyville.
Katsotaan, miten aiempi etsimisongelma ratkeaa geneerisen metodin avulla.
<T> int etsiIndeksi(T[] taulukko, T etsittava) {
for (int i = 0; i < taulukko.length; i++)
if (taulukko[i].equals(etsittava)) return i;
return -1;
}
void main() {
Integer[] kokonaisluvut = {2, 3, 4};
Double[] liukuluvut = {-10.0, 2.0, 0.0, 5.5};
String[] elaimet = {"koira", "kissa", "gepardi", "kissa", "gepardi"};
IO.println(etsiIndeksi(kokonaisluvut, 3));
IO.println(etsiIndeksi(liukuluvut, 5.4));
IO.println(etsiIndeksi(elaimet, "gepardi"));
}
Huomaa, että jouduimme tekemään erityisesti pari muutosta:
Ensinnäkin, vertailu tapahtuu nyt kirjoittamalla
taulukko[i].equals(etsittava). Tämä johtuu siitä, että tyyppiparametri T voi
edustaa mitä tahansa viitetietotyyppiä, ja viitetietotyyppisten arvojen
vertailua ei voi tehdä ==-operaattorilla.
Toiseksi, main-pääohjelmassa tulee käyttää perustietotyyppien int, double
ja long sijaan käärijäluokkia Integer, Double ja Long. Tämä johtuu
siitä, että Javassa vain viitetietotyyppejä voidaan käyttää tyyppiparametreina.
Rajoite puolestaan johtuu Javan tavasta toteuttaa viitetietotyyppejä.
Mainittakoon, että Java-kieltä kehitetään jatkuvasti, ja on hyvin mahdollista,
että lähitulevaisuudessa tämä rajoite jää pois.
Valinnaista lisätietoa: Miksi tyyppiparametrit eivät voi olla perustietotyyppejä?
Java käyttää mekanismia nimeltä type erasure, jonka voisi vapaasti suomentaa
"tyyppien poistamiseksi". Tämä tarkoittaa, että käännettäessä Java-koodi
tavukoodiksi tyyppiparametrit poistetaan ja korvataan niiden ylärajalla.
Ylärajalla tarkoitetaan sitä tyyppiä, jota geneerinen parametri varmasti
edustaa. Jos tyyppiparametrille on asetettu rajoitus, kuten <T extends Number>, yläraja on tällöin Number. Käännöksen jälkeen kaikki T:hen
viittaava koodi käsitellään ikään kuin tyyppi olisi Number. Jos taas
tyyppiparametrille ei ole asetettu rajoitusta, sen yläraja on automaattisesti
Object. Esimerkiksi tyyppiparametri T käsitellään käännöksen jälkeen ikään
kuin se olisi Object.
Käytännössä tämä tarkoittaa, että geneerisyys ei ole Javan ajonaikainen ominaisuus, vaan käännösaikainen tarkistusmekanismi. Tyyppitiedot poistetaan, jotta geneerinen koodi olisi yhteensopivaa vanhemman, ei-geneerisen Java-koodin kanssa.
Koska primitiivityypit eivät peri Object-luokkaa, ne eivät voi toimia
tyyppiparametreina. Siksi geneerisissä rakenteissa on aina käytettävä
käärijäluokkia (Integer, Double, Boolean).
Sama rajoitus näkyy myös taulukoiden kanssa: Java ei salli geneeristen
taulukoiden luomista. Esimerkiksi lause new T[10] ei ole sallittu, koska
tyyppiparametri ei ole ajonaikana tiedossa type erasure -mekanismin vuoksi.
Käytännössä tämä tarkoittaa, että geneerisen koodin yhteydessä käytetään lähes
aina kokoelmia (kuten ArrayList
Geneerinen luokka ja geneerinen rajapinta
Geneerisyys ei rajoitu vain metodeihin. Tyyppiparametrien todellinen hyöty
tapana tuottaa yleistyvää koodia tulee esiin erityisesti silloin, kun
tyyppiparametreja määritellään luokille tai rajapinnoille. Olemmekin jo
käyttäneet kurssilla tyyppiparametreja valmiissa luokissa, kuten ArrayList<T>.
lista itsessään on yleinen, mutta sen sisältämä tyyppi täsmennetään.
todo
DZ: Joku yksinkertainen esimerkki? Vaikkapa Osassa 1 oleva salasanatehtävä,
mutta se palauttaisi Tulos(boolean oikein, String virhe). Se refaktoroidaan
luokaksi Pari<T, U>.
Geneerinen luokka on erityisen perusteltu silloin, kun luokka säilyttää jonkin
tyyppisiä arvoja ja useat metodit liittyvät samaan tyyppiparametriin.
Esimerkiksi Pari<T, U> voisi olla tällainen: luokan tarkoitus on säilyttää
kahta arvoa, ja on olennaista, että niiden tyypit säilyvät koko elinkaaren
ajan.
public class Pari<T, U> {
private T eka;
private U toka;
public Pari(T eka, U toka) {
this.eka = eka;
this.toka = toka;
}
public T getEka() {
return eka;
}
public U getToka() {
return toka;
}
public void setEka(T eka) {
this.eka = eka;
}
public void setToka(U toka) {
this.toka = toka;
}
}
Tämän luokan avulla voimme luoda ilmentymiä, joiden arvot voivat olla mitä tahansa tyyppejä, ilman, että meidän tarvitsee kirjoittaa erillisiä luokkia jokaista käyttötarkoitusta varten. Alla esimerkki
public class Pari<T, U> {
private T eka;
private U toka;
public Pari(T eka, U toka) {
this.eka = eka;
this.toka = toka;
}
public T getEka() {
return eka;
}
public U getToka() {
return toka;
}
public void setEka(T eka) {
this.eka = eka;
}
public void setToka(U toka) {
this.toka = toka;
}
}
void main() {
Pari<String, Integer> nimiJaIka = new Pari<>("Matti", 30);
IO.println("Nimi: " + nimiJaIka.getEka() + ", Ikä: " + nimiJaIka.getToka());
Pari<Double, Double> koordinaatit = new Pari<>(60.192059, 24.945831);
IO.println("Leveysaste: " + koordinaatit.getEka() + ", Pituusaste: " + koordinaatit.getToka());
}
Jos saman toteuttaisi Object-tyyppisillä attribuuteilla ja yrittäisi "paikata"
sen geneerisillä metodeilla, tyyppiturvallisuus katoaa helposti ja mukaan tulee
pakollisia tyyppimuunnoksia, mistä taas seuraa mahdollisia ajonaikaisia
virheitä.
public class Pari {
private final Object eka;
private final Object toka;
public Pari(Object eka, Object toka) {
this.eka = eka;
this.toka = toka;
}
public <T> T getEka() {
return (T) eka; // tyyppimuunnos, ei käännösaikaista varmistusta
}
}
Yllä olevassa esimerkissä mukamas geneerinen metodi ei oikeasti tee luokasta
tyyppiturvallista, koska luokan tila on edelleen Object-tasolla ja
tyyppimuunnos tapahtuu vasta ajon aikana. Geneerisen luokan idea on nimenomaan
se, että tyyppi kiinnittyy luokan kenttiin ja niiden käyttöön käännösaikaisesti.
On tärkeää huomata, että geneerisen metodin ja geneerisen luokan valinta ei riipu siitä, onko metodi staattinen, vaan siitä, kuuluuko tyyppi luokan pysyvään rakenteeseen vai vain yksittäiseen toimintaan. Metodi luokan sisällä voi edelleen olla geneerinen, kunhan se käyttää omaa, eri nimistä tyyppiparametria eikä sekoitu luokan tyyppiparametriin.
Valinnaista lisätietoa: Java ei voi kaikissa tilanteissa päätellä tyyppiä yksikäsitteisesti
Edellä mainittiin, että Java pystyy usein päättelemään geneerisen metodin tyyppiparametrin automaattisesti. Tätä ominaisuutta kutsutaan nimellä type inference. Käytännössä kääntäjä tarkastelee metodikutsun argumentteja ja niiden tyyppejä ja päättelee niiden perusteella, mikä tyyppiparametri täyttää metodin määrittelyn vaatimukset.
Esimerkiksi kutsussa etsiIndeksi(kokonaisluvut, 3) kääntäjä näkee, että taulukon
tyyppi on Integer[] ja etsittävä arvo on Integer. Näiden perusteella se
päättelee, että tyyppiparametrin T on oltava Integer, eikä kutsussa tarvitse
kirjoittaa sitä erikseen.
Java sallii myös eksplisiittisen geneerisen metodikutsun, jossa tyyppiparametri annetaan itse:
Etsija.<Integer>etsiIndeksi(kokonaisluvut, 3);
Vaikka useimmissa käytännön tilanteissa kääntäjän automaattinen päättely on kuitenkin riittävä, voi olla tilanteita, joissa kääntäjä ei pysty päättelemään tyyppiä yksiselitteisesti tai kun halutaan tehdä tyyppi eksplisiittiseksi luettavuuden tai virheiden paikantamisen vuoksi.
Yksi tällainen tapaus syntyy, kun argumenteilla on eri, mutta yhteensopivia tyyppejä, eikä ole selvää, mikä niistä pitäisi valita tyyppiparametriksi.
static <T> T valitse(T a, T b) {
return a;
}
// valitse(1, 1.0); // KÄÄNNÖSVIRHE: tyyppiä T ei voida päätellä
Number n = <Number>valitse(1, 1.0); // OK: tyyppi annetaan eksplisiittisesti
Tässä tapauksessa argumentit ovat eri tyyppiä (Integer ja Double). Molemmat
perivät Number-luokan, mutta kääntäjä ei voi itse päättää, mikä näistä (tai
niiden yhteinen yläluokka) olisi oikea valinta tyyppiparametrille. Antamalla
tyyppiparametrin eksplisiittisesti kerromme kääntäjälle, että haluamme käyttää
metodia Number-tyyppisenä.
Geneerisyys ja polymorfismi
Geneerisyys ja polymorfismi (tarkemmin alityyppipolymorfismi) ovat kaksi eri mekanismia, jotka täydentävät toisiaan. Vaikka molemmat lisäävät koodin joustavuutta, ne ratkaisevat eri ongelmia ja toimivat eri vaiheissa ohjelman suoritusta.
- Polymorfismi (alityypitys): Ajonaikainen mekanismi, johon tutustuimme Luvussa 3. Sen tehtävä on mahdollistaa olioiden käsittely niiden yliluokan tai rajapinnan kautta, jolloin oikea toiminnallisuus (metodin toteutus) valitaan vasta ohjelman ajon aikana.
- Geneerisyys (parametrinen polymorfismi): Käännösaikainen mekanismi. Sen tehtävä on varmistaa tyyppiturvallisuus ja vähentää toistoa sallimalla saman koodin toimia eri tyypeillä ilman että tyyppitieto katoaa.
- Pelkkä polymorfismi (ei tyyppiturvaa) Ennen geneerisyyttä (Java 1.4 ja
aiemmat) kokoelmat perustuivat pelkkään polymorfismiin ja
Object-luokkaan.
// "Raaka" lista (raw type) - ei suositella enää
List lista = new ArrayList();
lista.add("teksti");
lista.add(123); // Sallittu, koska Integer on Object
for (Object o : lista) {
// toString() kutsuu kunkin olion omaa toteutusta
IO.println(o.toString());
}
Tässä polymorfismi sinänsä toimii, mutta koodi ei ole tyyppiturvallista. Kääntäjä ei voi estää meitä lisäämästä listaan vääriä tyyppejä, mikä johtaa virheisiin usein vasta, kun yritämme muuntaa (cast) oliota takaisin alkuperäiseen tyyppiinsä.
Geneerisyys tuo koodiin rajoitteet, jotka kääntäjä tarkistaa.
List<String> sanat = new ArrayList<>();
sanat.add("kissa");
sanat.add("koira");
// sanat.add(123); // KÄÄNNÖSVIRHE!
Tässä geneerisyys estää virheellisen käytön jo ennen kuin ohjelmaa edes ajetaan. Tässä esimerkissä emme varsinaisesti hyödynnä polymorfismia omien luokkien suhteen, vaan luotamme kääntäjän tiukkaan valvontaan siitä, että lista sisältää vain merkkijonoja.
Tehokkainta on yhdistää molemmat: geneerisyys rajaa sallitut tyypit tiettyyn
perheeseen (esim. Number), ja polymorfismi hoitaa kyseisen perheen jäsenten
yksilöllisen toiminnan.
// Listalle kelpaa mikä tahansa luku (Integer, Double, Long...)
List<Number> luvut = new ArrayList<>();
luvut.add(1); // Integer on Number
luvut.add(2.5); // Double on Number
for (Number n : luvut) {
// Geneerisyys takaa, että 'n' on vähintään Number.
// Polymorfismi (Number-luokan toteutus) hoitaa arvot.
IO.println(n.doubleValue());
}
Tyyppirajoitukset
Tyyppiparametreille voidaan asettaa rajoituksia, jotka määrittelevät, millainen
tyyppi parametrina voidaan antaa. Tämä tehdään käyttämällä extends-avainsanaa
tyyppiparametrin määrittelyn yhteydessä. Rajoitukset voivat olla luokkia tai
rajapintoja, ja ne määrittelevät ylärajan tyypille, jota tyyppiparametri voi
edustaa. Huomaa, että extends-avainsanaa käytetään tässä yhteydessä sekä
luokista että rajapinnoista, vaikka rajapinnat eivät perikään luokkia.
// Tyyppiparametri T voi olla vain Number-luokan alityyppi
<T extends Number> void tulostaLuku(T luku) {
IO.println("Numero: " + luku.doubleValue());
}
void main() {
tulostaLuku(10); // OK: Integer on Number
tulostaLuku(3.14); // OK: Double on Number
// tulostaLuku("kissa"); // KÄÄNNÖSVIRHE: String ei ole Number
}
Rajoituksia voidaan asettaa useita käyttämällä &-operaattoria, jolloin tyyppiparametrin
on täytettävä useita ehtoja. Alla on esimerkki metodista, jossa tyyppiparametrin tulee olla
sekä Number-luokan että Comparable-rajapinnan alityyyppi.
// Tyyppiparametri T voi olla vain luokka, joka on sekä Number että Comparable
<T extends Number & Comparable<T>> void vertaile(T a, T b) {
if (a.compareTo(b) < 0) {
IO.println(a + " on pienempi kuin " + b);
} else if (a.compareTo(b) > 0) {
IO.println(a + " on suurempi kuin " + b);
} else {
IO.println(a + " on yhtä suuri kuin " + b);
}
}
void main() {
vertaile(10, 20); // OK: Integer on Number ja Comparable
vertaile(3.14, 2.71); // OK: Double on Number ja Comparable
// vertaile("kissa", "koira"); // KÄÄNNÖSVIRHE: String ei ole Number
}
Tyyppirajoituksia voidaan tehdä myös käyttäen niin sanottuja jokerimerkkiä
(?), joka edustaa tuntematonta tyyppiä. Jokerimerkin avulla on mahdollista
muun muassa määritellä niin sanottuja ala- ja ylärajoituksia geneerisille
tyypeille. Jokerimerkkiä käytetään usein geneerisissä kokoelmissa, kun halutaan
ilmaista, että kokoelmasta voi lukea tai siihen voi kirjoittaa tietyn tyyppisiä
alkioita, mutta tarkkaa tyyppiä ei tiedetä etukäteen. Alla esimerkkejä.
// Metodi, joka ottaa listan, joka voi sisältää mitä tahansa Number-luokan alityyppejä.
// Listasta voi lukea Number-tyyppisiä arvoja, mutta ei voi lisätä mitään, koska
// emme tiedä tarkkaa tyyppiä.
void tulostaLuvut(List<? extends Number> luvut) {
for (Number n : luvut) {
IO.println(n); // Huomaa, että emme tiedä tarkkaa tyyppiä, mutta tiedämme että se on Number
}
// luvut.add(10); // KÄÄNNÖSVIRHE: emme voi lisätä, koska emme tiedä tarkkaa tyyppiä
}
/* Ottaa listan, jonka alkioiden tyyppi on Number tai jokin sen ylityyppi
* (esim. Object), joten listaan on turvallista lisätä Number-arvoja, ja siten myös Integer, Double jne.
*/
void lisaaLukuja(List<? super Number> lista) {
lista.add(10); // OK: Integer on Number
lista.add(3.14); // OK: Double on Number
// lista.add("kissa"); // KÄÄNNÖSVIRHE: String ei ole Number
// Integer eka = lista.getFirst(); // KÄÄNNÖSVIRHE: emme tiedä tarkkaa tyyppiä, joten emme voi olettaa että se on Integer
}
Emme käsittele jokerimerkkiä tässä osiossa tarkemmin, mutta voit tutustua niihin omatoimisesti Javan dokumentaatiosta.
Geneeristen tyyppien invarianssi
Vaikka Integer on Number-luokan alityyppi, List<Integer> ei ole
List<Number>-luokan alityyppi. Ne ovat täysin erillisiä tyyppejä, eikä niillä
ole perintäsuhdetta. Tätä kutsutaan invarianssiksi, eli muuttumattomuudeksi
tyyppisuhteissa. Geneeriset tyypit ovat oletuksena invariantteja
turvallisuussyistä. Alla on lyhyt esimerkki, joka havainnollistaa tätä
periaatetta.
List<Integer> kokonaisluvut = new ArrayList<>();
kokonaisluvut.add(1);
// Jos geneerisyys EI olisi invarianttia, voisimme tehdä näin:
List<Number> luvut = kokonaisluvut; // (Tämä on se kohta, minkä Java estää)
// Nyt luvut ja kokonaisluvut viittaavat samaan listaan muistissa.
// Koska luvut on tyyppiä List<Number>, voimme lisätä sinne liukuluvun:
luvut.add(3.14);
// MUTTA 'kokonaisluvut' luulee edelleen sisältävänsä vain Integer-lukuja!
Integer i = kokonaisluvut.get(1); // PAM! Ajonaikainen virhe (ClassCastException)
Jos voisimme kohdella kokonaislukulistaa yleisenä numerolistana, voisimme
vahingossa ujuttaa sinne desimaalilukuja. Sitten kun alkuperäinen koodi yrittää
lukea listaa kokonaislukuina, ohjelma kaatuisi. Tämä on erityisen hämmentävää
siksi, että Javan taulukot toimivat eri tavalla. Taulukot ovat kovariantteja,
eli Integer[]-taulukkoa voidaan käsitellä Number[]-taulukkona, mutta tällöin
tyyppiturvallisuus tarkistetaan vasta ajonaikaisesti: jos taulukkoon yritetään
tallentaa väärän tyyppinen alkio (esim. Double), Java heittää
ArrayStoreException-poikkeuksen.
// Tämä on sallittua Javassa:
Integer[] kokonaisluvut = {1, 2};
Number[] luvut = kokonaisluvut; // OK taulukoilla!
// Mutta tämä aiheuttaa virheen vasta ohjelmaa ajettaessa:
luvut[0] = 3.14; // ArrayStoreException!
Taulukoiden kanssa Java hyväksyy riskin ja heittää virheen vasta, kun ohjelma on käynnissä. Geneerisyyden (listat yms.) yksi tärkeimmistä tavoitteista oli korjata tämä ongelma ja siirtää virhe käännösaikaan.
Jos haluamme hyödyntää polymorfismia geneeristen kokoelmien välillä, meidän on
käytettävä jokerimerkkejä. Esimerkiksi, jos haluamme
käsitelläList<Integer>-listaa kuten List<Number>-listaa, voimme käyttää
List<? extends Number>-tyyppiä.
// Nyt tämä on sallittua, mutta lista on "read-only" turvallisuussyistä
List<? extends Number> luvut = kokonaisluvut;
for (Number n : luvut) {
IO.println(n); // Toimii
}
Tee geneerinen metodi etsiSuurin, joka etsii listan suurimman alkion. Parametrina tulevan
listan tulee toteuttaa Comparable-rajapinta, muutoin lista voi olla minkä
tyyppinen tahansa. Älä käytä valmiita Collections-luokan metodeja.
Tee luokka Kontti, joka hyödyntää geneerisyyttä ja toimii yksinkertaisena
säiliönä yhdelle minkä tahansa tyypin oliolle.
Lisää luokkaan attribuutti sisalto, joka voi sisältää minkä tahansa
tyyppisen olion. Lisää myös merkkijono omistaja. Tee luokkaan muodostaja, joka
ottaa nämä arvot vastaan parametreina.
Lisää lisäksi saantimetodit getOmistaja, getSisalto ja getTyyppi, joista
viimeinen palauttaa kontin sisällön tyypin merkkijonona. Tee myös override
toString-metodille, joka palauttaa nämä tiedot yhdessä merkkijonossa.
Tehtävässä on valmiiksi pääohjelma, jolla voit kokeilla luokan toimintaa.
Vinkki
Olion tyypin saa merkkijonona metodilla olio.getClass().getSimpleName().
Luo luokka IsoKontti, joka toimii säiliönä usealle minkä tahansa tyypin
oliolle. Konttiin pakataan esineitä niin, että viimeisimpänä lisätty
otetaan aina ensimmäiseksi pois.
Lisää luokkaan attribuutiksi lista, johon oliot tallennetaan.
Lisää myös seuraavat metodit:
-
lisaalisää parametrina annetun olion listan loppuun. -
otapalauttaa viimeisimmän olion ja ottaa sen pois listasta. -
katsopalauttaa viimeisimmän olion, mutta ei ota sitä pois listasta. -
sisaltaaottaa parametrina olion ja palauttaatrue, jos olio löytyy kontista. Muussa tapauksessa se palauttaafalse. -
tulostatulostaa kontin sisällön. Voit itse päättää, missä muodossa sisältö tulostetaan.
Tehtävässä on valmiiksi pääohjelma, jolla voit kokeilla luokan toimintaa.
Lisää edellisen tehtävän IsoKontti-luokkaan kaksi metodia.
-
Metodi
summaaNumerotottaa parametrina vastaan kontin, joka sisältää numeroita eliNumber-luokan tai sen alityyppien olioita ja palauttaa näiden summan. -
Metodi
siirraKaikkiottaa parametrina toisen kontin ja siirtää nykyisen kontin sisällön sinne. Toisen kontin täytyy olla tyypiltään sellainen, että se voi sisältää tämän kontin tyypin olioita.
Tehtävässä on valmiiksi pääohjelma, jolla voit kokeilla luokan toimintaa.
Vinkki
Tarvitset tässä tehtävässä tyyppirajoituksia.
-
Kaikilla
Number-luokan olioilla ondoubleValue()-metodi, joka palauttaa sen arvondouble-muodossa. -
Huomaa, että konttien tyyppien ei tarvitse olla täysin samat;
Number-kontti voi sisältääInteger-olioita, silläIntegeron sen alityyppi.
Tee geneerinen funktio, joka kopioi yhdestä listasta kaikki tyyppiä T vastaavat alkiot toiseen listaan, jonka tyyppi voi olla T tai sen ylityyppi.
Osan kaikki tehtävät
huomautus
Jos palautat tehtävät ennen osion takarajaa (ma 9.2.2026 klo 11:59 (keskipäivä)), voit saada DL-BONUS-pisteitä harjoitustehtäviin. Lue lisää suorittaminen-sivulta.
-
Luo rajapinta nimeltään
Muunnin. Määrittele rajapintaan yksi metodi:String muunna(String syote). Muista, että rajapinnassa metodilla ei ole runkoa (ei aaltosulkeita{}). -
Tee luokat
PienetKirjaimet,IsotKirjaimetjaIsoAlkukirjain, jotka toteuttavatMuunnin-rajapinnan.
PienetKirjaimet-luokanmuunna-metodi muuntaa annetun merkkijonon pieniksi kirjaimiksi.muunna("Hei Maa")-->"hei maa".IsotKirjaimet-luokanmuunna-metodi muuntaa annetun merkkijonon suuraakkosiksi.muunna("Hei Maa")-->"HEI MAA".IsoAlkukirjain-luokanmuunna-metodi muuntaa annetun merkkijonon siten, että vain ensimmäinen kirjain on suuraakkonen ja muut pieniä.muunna("HEI MAA")-->"Hei maa".
- Testaa ohjelmaasi valmiiksi annetulla pääohjelmalla.
Vakoojat lähettävät viestejä toisilleen, mutta salausmenetelmä vaihtuu päivittäin, jotta vihollinen ei pääse perille logiikasta. Tarvitsemme rajapinnan, jonka avulla voimme vaihtaa salausalgoritmia lennosta.
- Luo rajapinta
Salaaja. Määrittele rajapintaan kaksi metodia
String salaa(String viesti);
String pura(String salattuViesti);
- Toteuta kolme erilaista luokkaa:
Kaantaja,HakkerijaSeuraavaKirjain, jotka toteuttavatSalaaja-rajapinnan seuraavilla logiikoilla:
-
Kaantaja(Peilikuvakirjoitus). Kääntää sanan väärinpäin. Esimerkki: "Agentti" → "ittnegA". Vihje: Voit käyttääStringBuilder-luokanreverse()-komentoa tai silmukkaa, joka käy sanan läpi lopusta alkuun. -
Hakkeri("Leet-speak"). Korvaa tietyt kirjaimet numeroilla tai merkeillä. Esimerkki: "Agentti" -> "@g3ntt!"
Korvaa 'a' -> '@'
Korvaa 'e' -> '3'
Korvaa 'i' -> '!'
Korvaa 'o' -> '0'
SeuraavaKirjain(Caesar-siirros). Jokaista kirjainta siirretään aakkosissa yksi eteenpäin. Esimerkki: abc -> bcd. Vihje: Javassacharon luku. Voit tehdämerkki + 1.
'a' -> 'b'
'b' -> 'c'
'k' -> 'l'
jne.
Tässä harjoituksessa ei tarvitse huolehtia ö-kirjaimen pyörähtämisestä ympäri,
ellei halua. Tehtävässä ei myöskään tarvitse huolehtia siitä, että salauksen ja
purkamisen jälkeen saatu viesti ei välttämättä ole samanlainen kuin alkuperäinen
viesti. Esimerkiksi jos Hakkeri-muuntajaa käytettäessä alkuperäisessä
viestissä on oikeasti merkki @, pura-metodi antaa tulokseksi tuohon paikalle
merkin a. Tämä ei haittaa tässä, mutta tietenkin oikeassa salauksessa pitäisi
varmistaa, ettei tietoa katoa tai muutu vahingossa.
Saat TIMissä valmiina pääohjelman, jonka avulla voit testata luokkarakennettasi.

Toteutetaan yksinkertainen tekstiseikkailupeli (tai oikeammin pieni palanen pelistä), jossa pelaaja voi yrittää poimia esineitä maasta, sekä syödä saadakseen energiaa. Saat valmiina kaksi rajapintaa: Syotava ja Poimittava. Lisäksi saat osittain toteutetut luokat: Omena ja IsoKivi, jotka toteuttavat nämä rajapinnat. Edelleen, saat osittain toteutetun pääohjelman, jossa pelaajan energiaa ja repun tilaa seurataan.
Täydennä kaikki TODO-sanalla merkityt osat, jotta ohjelma toimii ohjeiden mukaisesti.
Tee Robotti, joka osaa suorittaa erilaisia kotitöitä, kuten imurointia ja
kukkien kastelua.
Toteuta tehtävä oheisen UML-kaavion mukaisesti. Katkoviiva, jossa on musta
nuoli, tarkoittaa, että Robotti-luokka käyttää KayttoEsine-rajapintaa:
Robotti-luokka sisältää attribuutin, joka on tyyppiä KayttoEsine.
Kuvaus sanallisessa muodossa
Tässä on kuvaus luokista ja niiden vaadituista ominaisuuksista (vastaavat kuin UML-kaaviossa):
Robotilla on seuraavat metodit:
void vaihdaKayttoEsine(KayttoEsine esine): Vaihtaa robotin käyttämän esineen (esim. imuri tai kastelukannu).void teeTyota(String kohde): Suorittaa kotityön. Joskohdeon sillä listalla, jotka kyseiseltä käyttöesineeltä on kielletty (esim.Kastelukannu-oliolla ei saa kastella"Tietokone"-kohdetta), robotin tulee tulostaa virheilmoitus. Kielletyt käyttökohteet määritellään käyttöesineen attribuuttina merkkijonolistana.Kastelukannu-olio ei kastele jos vettä ei ole riittävästi. Sen voi täyttäätaytaVesi()-metodilla. Kastelukannun vesimäärä on aluksi 50 yksikköä. Voit halutessasi tehdä uuden muodostajan, joka asettaa vesimäärän alkutilan toiseksi.Imuri-olio ei imuroi jos roskasäiliö on täynnä. Sen voi tyhjentäätyhjennaSailio()-metodilla. Roskasäiliön kapasiteetti on 100 yksikköä. Voit halutessasi tehdä uuden muodostajan, joka asettaa roskasäiliön alkutilan toiseksi.- Molemmat käyttöesineet palauttavat
kayta(String kohde)-metodin avulla totuusarvon, joka kertoo onnistuiko työ.
Tutki Javan dokumentaatiota. Vastaa kysymyksiin Comparable-rajapinnasta.
Tehtävässä on pohjana Henkilo-luokka omassa tiedostossaan sekä
jarjestaHenkilot-metodi main.java-tiedostossa. Kyseinen metodi ei kuitenkaan
toimi, sillä se käyttää Javan valmista Collections.sort-metodia ja
Henkilo-luokasta puuttuu sille tuki.
Muokkaa Henkilo-luokkaa niin, että List<Henkilo>-tyyppiset listat voidaan järjestää Collections.sort-metodilla henkilön nimen mukaan aakkosjärjestykseen.
Esimerkiksi listan
List<Henkilo> henkilot = Arrays.asList(
new Henkilo("Joukahainen"),
new Henkilo("Ilmatar"),
new Henkilo("Kyllikki"),
new Henkilo("Kokko")
);
pitäisi olla Collections.sort(henkilot);-kutsun jälkeen järjestyksessä:
- Ilmatar
- Joukahainen
- Kokko
- Kyllikki
Jatkoa edelliselle tehtävälle. Nyt Henkilo-luokassa henkilöiden nimet on
jaettu erikseen sukunimeen ja etunimiin.
Muokkaa uudistettua Henkilo-luokkaa niin, että List<Henkilo>-tyyppiset
listat voidaan järjestää Collections.sort-metodilla henkilön sukunimen ja
etunimien mukaan aakkosjärjestykseen, niin että järjestys tapahtuu ensin
sukunimen mukaan.
Esimerkiksi listan
List<Henkilo> henkilot = Arrays.asList(
new Henkilo("Pacius", "Fredrik"),
new Henkilo("Mozart", "Wolfgang Amadeus"),
new Henkilo("Mozart", "Leopold"),
new Henkilo("Chopin", "Frédéric")
);
pitäisi olla Collections.sort(henkilot);-kutsun jälkeen järjestyksessä:
- Chopin Frédéric
- Mozart Leopold
- Mozart Wolfgang Amadeus
- Pacius Fredrik
Tee geneerinen metodi etsiSuurin, joka etsii listan suurimman alkion. Parametrina tulevan
listan tulee toteuttaa Comparable-rajapinta, muutoin lista voi olla minkä
tyyppinen tahansa. Älä käytä valmiita Collections-luokan metodeja.
Tee luokka Kontti, joka hyödyntää geneerisyyttä ja toimii yksinkertaisena
säiliönä yhdelle minkä tahansa tyypin oliolle.
Lisää luokkaan attribuutti sisalto, joka voi sisältää minkä tahansa
tyyppisen olion. Lisää myös merkkijono omistaja. Tee luokkaan muodostaja, joka
ottaa nämä arvot vastaan parametreina.
Lisää lisäksi saantimetodit getOmistaja, getSisalto ja getTyyppi, joista
viimeinen palauttaa kontin sisällön tyypin merkkijonona. Tee myös override
toString-metodille, joka palauttaa nämä tiedot yhdessä merkkijonossa.
Tehtävässä on valmiiksi pääohjelma, jolla voit kokeilla luokan toimintaa.
Vinkki
Olion tyypin saa merkkijonona metodilla olio.getClass().getSimpleName().
Luo luokka IsoKontti, joka toimii säiliönä usealle minkä tahansa tyypin
oliolle. Konttiin pakataan esineitä niin, että viimeisimpänä lisätty
otetaan aina ensimmäiseksi pois.
Lisää luokkaan attribuutiksi lista, johon oliot tallennetaan.
Lisää myös seuraavat metodit:
-
lisaalisää parametrina annetun olion listan loppuun. -
otapalauttaa viimeisimmän olion ja ottaa sen pois listasta. -
katsopalauttaa viimeisimmän olion, mutta ei ota sitä pois listasta. -
sisaltaaottaa parametrina olion ja palauttaatrue, jos olio löytyy kontista. Muussa tapauksessa se palauttaafalse. -
tulostatulostaa kontin sisällön. Voit itse päättää, missä muodossa sisältö tulostetaan.
Tehtävässä on valmiiksi pääohjelma, jolla voit kokeilla luokan toimintaa.
Lisää edellisen tehtävän IsoKontti-luokkaan kaksi metodia.
-
Metodi
summaaNumerotottaa parametrina vastaan kontin, joka sisältää numeroita eliNumber-luokan tai sen alityyppien olioita ja palauttaa näiden summan. -
Metodi
siirraKaikkiottaa parametrina toisen kontin ja siirtää nykyisen kontin sisällön sinne. Toisen kontin täytyy olla tyypiltään sellainen, että se voi sisältää tämän kontin tyypin olioita.
Tehtävässä on valmiiksi pääohjelma, jolla voit kokeilla luokan toimintaa.
Vinkki
Tarvitset tässä tehtävässä tyyppirajoituksia.
-
Kaikilla
Number-luokan olioilla ondoubleValue()-metodi, joka palauttaa sen arvondouble-muodossa. -
Huomaa, että konttien tyyppien ei tarvitse olla täysin samat;
Number-kontti voi sisältääInteger-olioita, silläIntegeron sen alityyppi.
Tee geneerinen funktio, joka kopioi yhdestä listasta kaikki tyyppiä T vastaavat alkiot toiseen listaan, jonka tyyppi voi olla T tai sen ylityyppi.
Tietorakenteita ja algoritmeja
varoitus
Tämä osio julkaistaan 9. helmikuuta 2026.
Voit tutkia osion sisältöä etukäteen, mutta huomioi, että sisältö todennäköisesti vielä muuttuu ennen julkaisua. Et voi myöskään suorittaa osion tehtäviä ennen julkaisupäivää.
osaamistavoitteet
Kokoelmarajapinnat
varoitus
Tämä osio julkaistaan 9. helmikuuta 2026.
Voit tutkia osion sisältöä etukäteen, mutta huomioi, että sisältö todennäköisesti vielä muuttuu ennen julkaisua. Et voi myöskään suorittaa osion tehtäviä ennen julkaisupäivää.
osaamistavoitteet
- List, Set, Map. Oikean kokoelman valinta käyttötarkoituksen mukaan.
- Tunnet Java-kielen kokoelmarajapinnat ja niitä toteuttavia tietorakenteita: List, Set, Map
- Collections-luokka ja Collection-rajapinta
Valmiit kokoelmat Javassa
varoitus
Tämä osio julkaistaan 9. helmikuuta 2026.
Voit tutkia osion sisältöä etukäteen, mutta huomioi, että sisältö todennäköisesti vielä muuttuu ennen julkaisua. Et voi myöskään suorittaa osion tehtäviä ennen julkaisupäivää.
osaamistavoitteet
- Tunnet Java-kielen yleisimmät valmiit tietorakenteet ArrayList, HashMap, LinkedList, Stack, Queue
- Rajapintaa vasten ohjelmointi: List, Set, Map
- Lyhyesti käydään läpi vaikutukset suorituskykyyn: ArrayList, HashMap, List, Set
- Ymmärrät ym. tietorakenteiden keskeisimmät operaatiot ja niiden aikakompleksisuudet
- Ymmärrät, miksi
hashCodetarvitaan (HashMap tapauksessa ainakin)
Esimerkki: oma ArrayList-toteutus
varoitus
Tämä osio julkaistaan 9. helmikuuta 2026.
Voit tutkia osion sisältöä etukäteen, mutta huomioi, että sisältö todennäköisesti vielä muuttuu ennen julkaisua. Et voi myöskään suorittaa osion tehtäviä ennen julkaisupäivää.
osaamistavoitteet
TODO: Pitäisikö olla sen sijaan ohjattu tehtävä? TAI: Voisi tehdä Full Stack Moocin tavoin ohjatusti ja sitten tehtävänä on tehdä LinkedList tai HashMap. Vrt. myös HY
Rekursio
varoitus
Tämä osio julkaistaan 9. helmikuuta 2026.
Voit tutkia osion sisältöä etukäteen, mutta huomioi, että sisältö todennäköisesti vielä muuttuu ennen julkaisua. Et voi myöskään suorittaa osion tehtäviä ennen julkaisupäivää.
osaamistavoitteet
- Ymmärrät miten rekursio toimii
- Ymmärrät, miten rekursio voi mallintaa pinon avulla
- Rekursio, perus- ja induktiotapaukset, rekursiivinen tietorakenne (?). Hajota ja hallitse -periaate. Pinon käyttö rekursiossa.
- Mahdollisesti jotakin dynaamisesta ohjelmoinnista (?)
Osan kaikki tehtävät
varoitus
Tämä osio julkaistaan 9. helmikuuta 2026.
Voit tutkia osion sisältöä etukäteen, mutta huomioi, että sisältö todennäköisesti vielä muuttuu ennen julkaisua. Et voi myöskään suorittaa osion tehtäviä ennen julkaisupäivää.
huomautus
Jos palautat tehtävät ennen osion takarajaa (ma 16.2.2026 klo 11:59 (keskipäivä)), voit saada DL-BONUS-pisteitä harjoitustehtäviin. Lue lisää suorittaminen-sivulta.
Hyödyllisiä menetelmiä Javassa
varoitus
Tämä osio julkaistaan 16. helmikuuta 2026.
Voit tutkia osion sisältöä etukäteen, mutta huomioi, että sisältö todennäköisesti vielä muuttuu ennen julkaisua. Et voi myöskään suorittaa osion tehtäviä ennen julkaisupäivää.
osaamistavoitteet
Funktiorajapinnat ja lambda-lausekkeet
varoitus
Tämä osio julkaistaan 16. helmikuuta 2026.
Voit tutkia osion sisältöä etukäteen, mutta huomioi, että sisältö todennäköisesti vielä muuttuu ennen julkaisua. Et voi myöskään suorittaa osion tehtäviä ennen julkaisupäivää.
osaamistavoitteet
- Funktionaalinen ohjelmointi
- Funktionaalinen rajapinta ja Javan
Function,BiFunction - lambda-lausekkeet
Esimerkki: Comparator-rajapinta
wip
Tämä osio on kirjoitettava hieman uusiksi lambdoja ja funktioviitteitä
käyttäen.
Comparator-rajapintaa käytetään Javassa nykyään aikalailla vain funktiorajapintana.
Kuten totesimme ylempänä, toisinaan voi olla vaikeaa valita yksittäinen järkevä järjestys. Yleisestikin, luonnollisen järjestyksen lisäksi voi olla järkevää pystyä määrittämään vaihtoehtoisia järjestystapoja samalle luokalle.
Esimerkiksi, vaikka kokonaislukujen suuruusjärjestys on järkevä luonnolliseksi järjestykselle, joskus lukuja saatetaan haluta järjestää niiden suuruusluokan mukaan tai vaikkapa sen mukaan, kuinka lähellä luvut ovat jotakin toista tiettyä lukua. Vastaavasti, vaikka yllä oleville keräilykorteille voisi olla järkevää määrätä järjestys tunnisteen mukaan, voi olla mielekästä pystyä järjestämään niitä kortin nimen mukaan.
Javan Comparator-rajapinta
JavaDoc
tarjoaa tavan määrittää vaihtoehtoisia järjestystapoja tyypeille.
Lisäksi rajapinta tarjoaa mahdollisuuden määrittää järjestystapoja
ilman, että alkuperäisen luokan tarvitsisi toteuttaa Comparable-rajapintaa.
Rajapinta sisältää ainoastaan yhden pakollisen metodin compare, joka
ottaa parametriksi kaksi samantyyppistä oliota ja palauttaa
vertailuluvun samoilla säännöillä kuin Comparable-rajapinnan compareTo:
| Tapaus | Merkitys | Tulkinta |
|---|---|---|
cmp.compareTo(olioA, olioB) < 0 | olioA < olioB | olioA on pienempi kuin olioB |
cmp.compareTo(olioA, olioB) == 0 | olioA == olioB | olioA on yhtä suuri kuin olioB |
cmp.compareTo(olioA, olioB) > 0 | olioA > olioB | olioA on suurempi kuin olioB |
Laajenetaan hieman Kerailykortti-luokkaa lisäämällä attribuutti
sarja, joka kuvaa korttisarjaa (esim. eläimet, ajoneuvot, jne.):
class Kerailykortti implements Comparable<Kerailykortti> {
private String nimi;
// HIGHLIGHT_GREEN_BEGIN
private String sarja;
// HIGHLIGHT_GREEN_END
private int tunnistenumero;
// HIGHLIGHT_GREEN_BEGIN
public Kerailykortti(String nimi, String sarja, int tunnistenumero) {
// HIGHLIGHT_GREEN_END
this.nimi = nimi;
// HIGHLIGHT_GREEN_BEGIN
this.sarja = sarja;
// HIGHLIGHT_GREEN_END
this.tunnistenumero = tunnistenumero;
}
@Override
public int compareTo(Kerailykortti other) {
int sarjaVertailu = this.sarja.compareTo(other.sarja);
if (sarjaVertailu != 0) {
return sarjaVertailu;
}
return Integer.compare(this.tunnistenumero, other.tunnistenumero);
}
public String getNimi() {
return nimi;
}
public String getSarja() {
return sarja;
}
@Override
public String toString() {
return "Kortti: " + nimi + " (Sarja: " + sarja + ", #" + tunnistenumero + ")";
}
}
void main() {
List<Kerailykortti> kortit = Arrays.asList(
new Kerailykortti("Loistava Lohikäärme", "Eläimet", 3),
new Kerailykortti("Vauhdikas Vespajetti", "Ajoneuvot", 1),
new Kerailykortti("Aloittelijan Ameeba", "Eläimet", 1),
new Kerailykortti("Mieletön Merihevonen", "Eläimet", 2),
new Kerailykortti("Nopea Nopsa", "Ajoneuvot", 2)
);
IO.println("Ennen järjestämistä:");
for (Kerailykortti kortti : kortit) {
IO.println(kortti);
}
Collections.sort(kortit);
IO.println();
IO.println("Jälkeen järjestämisen:");
for (Kerailykortti kortti : kortit) {
IO.println(kortti);
}
}
Tällä hetkellä keräilykorteille on määritelty luonnollinen järjestys siten, että
ensin keräilykortit järjestetään nimen ja sitten tunnisteen mukaan.
Haluaisimme kuitenkin tarjota vaihtoehtoisen tavan järjestää keräilykortteja
sarjan nimen mukaan.
Tätä varten voimme luoda uuden vertailuluokan, joka toteuttaa
Comparator-rajapinnan:
import java.util.Comparator;
class KerailykorttiSarjaVertailija implements Comparator<Kerailykortti> {
@Override
public int compare(Kerailykortti kortti1, Kerailykortti kortti2) {
return kortti1.getSarja().compareTo(kortti2.getSarja());
}
}
Huomaa erityisesti, että:
- Vaihtoehtoinen vertailu on nyt toteutettu omaan luokkaan
KerailykorttiSarjaVertailija. Tämä on tarkoituksellista ja se mahdollistaa, että vertailijoita voi tehdä myös sellaisille luokille, joiden koodia ei voi suoraan muokata (esim. Javan sisäänrakennetut luokat). - Koska
KerailykorttiSarjaVertailijaon oma luokkansa, määritimmeKerailykortti-luokkaan saantimetodingetSarja(). - Jotta vertailijaa voi käyttää, siitä tulee alustaa olio. Alustuksen jälkeen
vertailijaolio voidaan käyttää
Collections.sort-metodin ylikuormituksen kanssa, joka joka ottaaComparator-olion toisena parametrina.
varoitus
Yllä olevassa esimerkissä toteutimme Comparator-rajapinnan luokassa, jotta esimerkki
voidaan pitää yksinkertaisena.
Modernissa Javassa on kuitenkin yleistä, että vertailuluokkia ei luoda käsin.
Comparator on nimittäin ns. funktiorajapinta, jonka ansiosta mikä tahansa
luokkametodi, jonka määrittely vastaa compare-metodia, voidaan sijoittaa
suoraan Comparator-tyyppiseen muuttujaan tekemättä luokkaa:
void main() {
List<Integer> luvut = new ArrayList<>(List.of(5, 4, 2, 1, 3));
Comparator<Integer> vertailija = Integer::compare;
Collections.sort(luvut, vertailija);
IO.println(luvut);
}
Tutustumme funktiorajapintoihin ja palaamme taas Comparator-tyyppiin tarkemmin
osassa 6.
Comparator-rajapinta tarjoaa lisäksi muutaman hyödyllisen metodin, jotka
auttavat algoritmien suunnittelussa.
Comparator.naturalOrder() palauttaa Comparator-tyyppisen vertailuolion,
joka järjestää oliot niiden luonnollisen järjestyksen mukaan.
Toisin sanoen, tämä mahdollistaa ns. eristää Comparable-rajapintaa
toteuttavan olion compareTo-metodin toteutuksen vertailuolioksi.
Esimerkiksi merkkijonojen aakkosjärjestystä vastaavan vertailuolion saa tällä
tavoin:
void main() {
List<String> jonoja = new ArrayList<>(List.of("Denis", "Antti-Jussi", "Karri", "Rauli", "Sami"));
Comparator<String> aakkosjarjestys = Comparator.naturalOrder();
Collections.sort(jonoja, aakkosjarjestys);
IO.println(jonoja);
}
Comparator.reversed() luo uuden vertailuolion, joka kääntää
vertailujärjestyksen.
Tämän avulla esimerkiksi pystyy helposti järjestämään merkkijonot
käänteiseen aakkosjärjestykseen:
void main() {
List<String> jonoja = new ArrayList<>(List.of("Denis", "Antti-Jussi", "Karri", "Rauli", "Sami"));
Comparator<String> aakkosjarjestys = Comparator.naturalOrder();
// HIGHLIGHT_GREEN_BEGIN
Comparator<String> kaanteinenAakkosjarjestys = aakkosjarjestys.reversed();
// HIGHLIGHT_GREEN_END
Collections.sort(jonoja, kaanteinenAakkosjarjestys);
IO.println(jonoja);
}
Kun olioita vertailee käyttäen luonnollista tai vaihtoehtoista järjestystä,
ei voi olla varma siitä, että null-viite on käsitelty järkevästi
tai ollenkaan.
Esimerkiksi jopa Javassa määritelty String-merkkijonojen luonnollinen
järjestys ei käsittele tapausta, jos jompikumpi verrattavista merkkijonoista
on null:
void main() {
String[] jono = {"Ohjelmointi 1", null, "Ohjelmointi 2"};
Arrays.sort(jono);
IO.println(Arrays.toString(jono));
}
java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" because "a[runHi]" is null
Tätä varten on olemassa Comparator.nullsFirst() ja Comparator.nullsLast():
ne ottavat parametriksi vertailuolion ja palauttavat uuden vertailijan,
joka osaa käsitellä null-viitteitä. Nimensä mukaan nullsFirst()
asettaa null-viitteet pienemmäksi kuin muut arvot (ja siten järjestyksessä
ensimmäiseksi), kun taas nullsLast asettaa null-viitteet suuremmaksi kuin
muut arvot (eli järjestyksessä viimeiseksi):
void main() {
String[] jono = {"Ohjelmointi 1", null, "Ohjelmointi 2"};
Comparator<String> aakkosjarjestys = Comparator.naturalOrder();
Comparator<String> nullitEnsimmaiseksi = Comparator.nullsFirst(aakkosjarjestys);
Arrays.sort(jono, nullitEnsimmaiseksi);
IO.println(Arrays.toString(jono));
Comparator<String> nullitViimeiseksi = Comparator.nullsLast(aakkosjarjestys);
Arrays.sort(jono, nullitViimeiseksi);
IO.println(Arrays.toString(jono));
}
Kokoelmien käsittely: Stream API
varoitus
Tämä osio julkaistaan 16. helmikuuta 2026.
Voit tutkia osion sisältöä etukäteen, mutta huomioi, että sisältö todennäköisesti vielä muuttuu ennen julkaisua. Et voi myöskään suorittaa osion tehtäviä ennen julkaisupäivää.
osaamistavoitteet
- Ainakin map, filter, reduce
- lambda-lausekkeiden käyttö Stream API:ssa
Stream,IntStream, ero iteraattoreihin
Poikkeusten hallinta
varoitus
Tämä osio julkaistaan 16. helmikuuta 2026.
Voit tutkia osion sisältöä etukäteen, mutta huomioi, että sisältö todennäköisesti vielä muuttuu ennen julkaisua. Et voi myöskään suorittaa osion tehtäviä ennen julkaisupäivää.
osaamistavoitteet
- Poikkeukset (checked, unchecked), try-catch, finally, heittäminen (throw, throws).
- Optional-luokka: isPresent, ifPresent, orElse, map, flatMap
Ulkoiset kirjastot ja Java-projektien hallintatyökalut
varoitus
Tämä osio julkaistaan 16. helmikuuta 2026.
Voit tutkia osion sisältöä etukäteen, mutta huomioi, että sisältö todennäköisesti vielä muuttuu ennen julkaisua. Et voi myöskään suorittaa osion tehtäviä ennen julkaisupäivää.
osaamistavoitteet
- Build-työkalut (Gradle/Maven)
- Kolmannen osapuolen riippuvuuksia (miten etsitään ja lisätään kirjasto)
- Pakkaukset Javassa
Tiedostojen käsittely
varoitus
Tämä osio julkaistaan 16. helmikuuta 2026.
Voit tutkia osion sisältöä etukäteen, mutta huomioi, että sisältö todennäköisesti vielä muuttuu ennen julkaisua. Et voi myöskään suorittaa osion tehtäviä ennen julkaisupäivää.
osaamistavoitteet
- Osaat käsitellä tiedostoja Javan valmiiden rajapintojen kautta (Tiedostomuotojen käsittely "käsin" (CSV) ja kirjastolla (JSON))
- Files API
- Tietovirrat (Stream) ja sen oheisluokat (BufferedReader/Writer, Scanner)
- Yksinkertaisen tiedoston lukeminen (CSV-tyylinen)
- Jokin JSON-kirjasto ja JSON-tiedoston lukeminen: Gson, Jackson, org.json???
Osan kaikki tehtävät
varoitus
Tämä osio julkaistaan 16. helmikuuta 2026.
Voit tutkia osion sisältöä etukäteen, mutta huomioi, että sisältö todennäköisesti vielä muuttuu ennen julkaisua. Et voi myöskään suorittaa osion tehtäviä ennen julkaisupäivää.
huomautus
Jos palautat tehtävät ennen osion takarajaa (ma 23.2.2026 klo 11:59 (keskipäivä)), voit saada DL-BONUS-pisteitä harjoitustehtäviin. Lue lisää suorittaminen-sivulta.
Osa 7
varoitus
Tämä osio julkaistaan 23. helmikuuta 2026.
Voit tutkia osion sisältöä etukäteen, mutta huomioi, että sisältö todennäköisesti vielä muuttuu ennen julkaisua. Et voi myöskään suorittaa osion tehtäviä ennen julkaisupäivää.
Osa 8
varoitus
Tämä osio julkaistaan 2. maaliskuuta 2026.
Voit tutkia osion sisältöä etukäteen, mutta huomioi, että sisältö todennäköisesti vielä muuttuu ennen julkaisua. Et voi myöskään suorittaa osion tehtäviä ennen julkaisupäivää.
Osa 9
varoitus
Tämä osio julkaistaan 9. maaliskuuta 2026.
Voit tutkia osion sisältöä etukäteen, mutta huomioi, että sisältö todennäköisesti vielä muuttuu ennen julkaisua. Et voi myöskään suorittaa osion tehtäviä ennen julkaisupäivää.
Osa 10
varoitus
Tämä osio julkaistaan 16. maaliskuuta 2026.
Voit tutkia osion sisältöä etukäteen, mutta huomioi, että sisältö todennäköisesti vielä muuttuu ennen julkaisua. Et voi myöskään suorittaa osion tehtäviä ennen julkaisupäivää.
Osa 11
varoitus
Tämä osio julkaistaan 23. maaliskuuta 2026.
Voit tutkia osion sisältöä etukäteen, mutta huomioi, että sisältö todennäköisesti vielä muuttuu ennen julkaisua. Et voi myöskään suorittaa osion tehtäviä ennen julkaisupäivää.
Osa 12
varoitus
Tämä osio julkaistaan 30. maaliskuuta 2026.
Voit tutkia osion sisältöä etukäteen, mutta huomioi, että sisältö todennäköisesti vielä muuttuu ennen julkaisua. Et voi myöskään suorittaa osion tehtäviä ennen julkaisupäivää.





