Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

TukikanavaAikaPaikka/Linkki
Lähiohjauske 8-18, to 8-18, pe 8-14Agoralla luokat Ag B212.1 Finland ja Ag B211.1 Sovjet
Etäohjauske 8-18, to 8-18, pe 8-14Ohjelmointi 2 Teams-kanava
Vastuuopettajien ja tuntiopettajien sähköpostiosoiteJatkuvaohj2-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)
  1. Kirjaudu Sisuun

  2. Jos olet jo ilmoittautunut kurssille, klikkaa ylhäällä välilehteä Opintokalenteri tai klikkaa sitä hampurilaisvalikosta

  3. Selaa oikealla oikea kurssi näkyville, eli tässä tapauksessa Ohjelmointi 2

  4. Klikkaa oikealla olevaa oikealle osoittavaa väkästä Ohjelmointi 2 -kurssin kohdalla

  5. Skrollaa alaspäin, kunnes tulee alaotsikko Pääteohjaus

  6. Jos ei vielä näy, niin skrollaa alaspäin, kunnes näkyy Muiden ryhmien tiedot ja klikkaa sitä

  7. Nyt voit skrollaamalla alaspäin haluamiesi pääteohjauksien kohdalta klikata nappulaa Näytä tapahtumat kalenterissa.

    Näytä Sisu-tapahtumat kalenterissa

  8. 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)

  1. 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 muoto student.jyu.fi ei käy. Tunnuksen toimiminen vaatii, että olet hyväksynyt Office 365 -palvelut OMA-palvelussa (https://sso.jyu.fi).

  2. 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ä.

  3. Teams-sovelluksessa klikkaa Teams Join or create team Join a team with a code

  4. Syötä koodi nnobn49

  5. 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:

  1. 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)
  2. 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)
  3. Aloita kokous New meeting toiminnolla
  4. Testaa Audio Test speaker & mikrofone toiminnolla että äänet pelittää
  5. Ota kokouslinkki talteen Participants Copy invite link
  6. Avaa ohjauspyyntölomake: https://forms.gle/5QULUPBHjjqS4ndf6
  7. Täytä omat tietosi ja HUOM Pasteta lisätietokenttään kohdassa 5 kopioimasi linkki
  8. Odota, että ohjaaja tulee huoneeseesi. Saatat joutua hyväksymään hänen sisäänpääsyn (riippuu kokoushuoneesi asetuksista)

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:

  1. 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ää.

  2. 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.

  3. 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):

  1. Keräät jokaisesta osasta vähintään 50% siitä pistemäärästä mitä harjoitustehtävien perustehtävistä voi saada1
  2. Teet harjoitustyön aikataulussa
  3. 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äänHarjoitustehtä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:

OsaTakaraja DL-BONUS-pisteille
1ma 19.1.2026 klo 11:59 (keskipäivä)
2ma 26.1.2026 klo 11:59 (keskipäivä)
3ma 2.2.2026 klo 11:59 (keskipäivä)
4ma 9.2.2026 klo 11:59 (keskipäivä)
5ma 16.2.2026 klo 11:59 (keskipäivä)
6ma 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.

Esim1Esim2Esim3
Osuus harjoitustehtävistä (sis DL-BONUS)70%90%70%
Harjoitustehtävien arvosana353
Tentin arvosana145
Painotettu keskiarvo1.84.44.6
Pyöristetty arvosana245

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

  1. Keräät vähintään 100% pistettä harjoitustehtävistä (kaikista osista yhteensä) JA keräät kaikki DL-BONUS-pisteet (6 * 0.5 = 3 pistettä)
  2. Teet harjoitustyön aikataulussa
  3. Osallistut suulliseen kuulusteluun, jossa ohjaaja arvioi harjoitustyösi

Arvosanasi on tällöin 1, jota voit vapaaehtoisesti korottaa tentillä.

Suoritustapa 3

Harjoitustyö ja loppukoe.

  1. Teet harjoitustyön
  2. 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:

Valitse käyttöjärjestelmäsi yllä olevista vaihtoehdoista.

Esivalmistelut

Valitse käyttöjärjestelmäsi yllä olevista vaihtoehdoista.

Git

Valitse käyttöjärjestelmäsi yllä olevista vaihtoehdoista.

IntelliJ IDEA

Valitse käyttöjärjestelmäsi yllä olevista vaihtoehdoista.

Java Development Kit (JDK)

  1. Avaa IntelliJ IDEA ja odota, kunnes pääset Welcome to IntelliJ IDEA -näkymään.

  2. Klikkaa ikkunan keskellä tai ylädassa olevaa New Project -painiketta:

  3. Avautuneesta ikkunasta klikkaa JDK-alasvetolaatikkoa ja valitse Download JDK... -painike:

  4. Aseta avautuneessa ikkunassa asetukset seuraavasti:

    • Version: 25
    • Vendor: Oracle OpenJDK Älä muuta Location-kohdassa olevaa polkua! Paina lopuksi Select-painiketta.
  5. 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ää.

  6. Kun projekti on latautunut eikä virheitä näy, kokeile ajaa projekti painamalla oikeassa ylälaidassa olevaa Play-painiketta:

  7. 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 = 5
    
  8. Voit nyt sulkea IntelliJ IDEA:n.

ComTest

  1. 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.

  2. Klikkaa ikkunan vasemmassa alalaidassa oleva Configure (Ratas-ikoni) Settings.

  3. Valitse vasemmalla puolella olevista asetusnäkymistä Plugins

  4. Valitse Marketplace-välilehti ja hae hakusanalla ComTest

  5. Valitse Comtest Runner -pluginin kohdalta Install

  6. Paina Save

  7. 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:

  1. 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.

  2. Klikkaa ikkunan vasemmassa alalaidassa oleva Configure (Ratas-ikoni) Settings.

  3. Valitse vasemmalla puolella olevista asetusnäkymistä Editor File Encodings

  4. Aseta Create UTF-8 files -asetuksen arvoksi with no BOM.

  5. Paina Save.

  6. 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ä.

  1. Sulje IntelliJ IDEA kokonaan.
  2. Avaa Rider.
  3. Sulje Rider.
  4. 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

TenttiPäivämääräAikaPaikkaIlmoittautumislinkki
Kevät 1pp.kk.2025klo xx-xxAgora / ZoomIlmoittaudu
Kevät 2pp.kk.2025klo xx-xxAgora / ZoomIlmoittaudu
Kevät 3pp.kk.2025klo xx-xxAgora / ZoomIlmoittaudu
Kesä 1pp.kk.2025klo xx-xxAgora / ZoomIlmoittaudu
Kesä 2pp.kk.2025klo xx-xxAgora / ZoomIlmoittaudu
Syksy 1pp.kk.2025klo xx-xxAgora / ZoomIlmoittaudu
Syksy 2pp.kk.2025klo xx-xxAgora / ZoomIlmoittaudu
Syksy 3pp.kk.2025klo xx-xxAgora / ZoomIlmoittaudu

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ä.

LuentoPäivämääräSijaintiStriimi ja nauhoiteMateriaalit
Luento 1: Java-kielen perusteetma 12.1.2026 klo 10.15Ag Auditorio 3YouTube, MoniviestinKalvot
Luento 2: Olio-ohjelmoinnin perusteetma 19.1.2026 klo 14.15Ag Auditorio 3YouTube, MoniviestinKalvot, Koodit
Luento 3: Perintä, polymorfismima 26.1.2026 klo 14.15Ag Auditorio 3YouTube, MoniviestinKalvot, Koodit
Luento 4: Rajapinta, geneeriset luokatma 2.2.2026 klo 14.15Ag Auditorio 3YouTube, MoniviestinKalvot, Koodit
Luento 5: Tietorakenteita ja algoritmejama 9.2.2026 klo 14.15Ag Auditorio 3YouTube, Moniviestin
Luento 6: Hyödyllisiä menetelmiä Javassama 16.2.2026 klo 14.15Ag Auditorio 3YouTube, Moniviestin
Luento 7ma 23.2.2026 klo 14.15Ag Auditorio 3YouTube, Moniviestin
Luento 8ma 2.3.2026 klo 14.15Ag Auditorio 3YouTube, Moniviestin
Luento 9ma 9.3.2026 klo 14.15Ag Auditorio 3YouTube, Moniviestin
Luento 10ma 16.3.2026 klo 14.15Ag Auditorio 3YouTube, Moniviestin
Luento 11ma 23.3.2026 klo 14.15Ag Auditorio 3YouTube, 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ä:

  1. Java-ohjelman suoritus alkaa main-nimisestä aliohjelmasta. void tarkoittaa, 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 {.

  2. Tekstin tulostaminen komentorivi-ikkunaan onnistuu IO.println-metodilla. Javassa lause loppuu yleensä puolipisteeseen ;, kuten tässäkin.

  3. Aliohjelman runko lopetetaan aaltosululla }. Ohjelman suoritus päättyy automaattisesti, kun main-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, kuten if-, for-, while- ja do-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:

  1. 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.

  2. 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):

  3. Paina Create. Tämän jälkeen IDEA luo uuden projektin valitsemaasi kansioon.

    Käydään pikaisesti läpi IDEAn olennaisimmat osat:

    1. Koodialue: projektissa olevien tiedostojen sisällöt näkyvät tässä, kun ne avataan. Kukin avattu tiedosto avautuu omaan välilehteen.
    2. Projektiselain: projektissa olevat kansiot ja tiedostot näkyvät tässä. Selaimen kautta voidaan lisätä, poistaa, siirtää tai uudelleennimetä tiedostoja ja kansioita.
    3. Projektin ajaminen ja debuggaus: tässä näkyy ajettavan Java-ohjelman nimi, ohjelman ajopainike () ja debuggauspainike ().
    4. 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:

  1. Klikkaa hiiren toissijaisella painikkeella projektin nimeä projektinäkymässä (HelloWorld) ja valitse New Module.

  2. 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.

  1. Klikkaa projektiselaimessa olevaa HelloProgram-moduulin alasvetopainiketta. Sen jälkeen klikkaa toissijaisella hiiren painikkeella src-kansiosta ja valitse New Java Compact File.

  2. Anna lähdekooditiedoston nimeksi Ohjelma ja 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:

  1. Poista kaikki koodi Ohjelma.java-tiedostosta.

    IDEA lisää yleensä valmista pohjakoodia uusiin lähdekooditiedostoihin. Tätä harjoitusta varten kirjoitamme kuitenkin koodia itse.

  2. 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 kirjoittamalla main. 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ä kursori main-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:

  1. 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ä.

  2. 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 .idea workspace.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 sulkea Ohjelma.java ja 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:

Valitse käyttöjärjestelmäsi yllä olevista vaihtoehdoista.

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.iml on projektin asetustiedosto, jolla IDEA tunnistaa kansion olevan Java-projekti
  • HelloProgram on lähdekoodikansio, jossa kaikki lähdekooditiedostot sijaitsevat
  • out on 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.iml on moduulin asetustiedosto, jolla IDEA tunnistaa kansion olevan Java-moduuli
  • src on 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ä:

AliohjelmaEsimerkkiSelitys
printlnIO.println("Moi!"); Tulostaa parametrina annetun arvon ja lisää loppuun rivinvaihdon
IO.println(); Tulostaa rivin rivinvaihdolla
printIO.print("Samalla rivillä!"); Tulostaa parametrina annetun arvon ilman rivinvaihtoa
readlnIO.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.

TyyppiKoko (tavua /bittiä)Lukualue (suuntaa antava)
byte1 tavu (8 bittiä)-128 ... 127
short2 tavua (16 bittiä)-32 768 ... 32 767
int4 tavua (32 bittiä)n. -2 miljardia ... 2 miljardia
long8 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.

TyyppiKoko (tavua)Tarkkuus
float4 tavua (32 bittiä)n. 7 merkitsevää numeroa
double8 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, esimerkiksi 42, -7 ja 0. long-luvun literaali päättyy isoon tai pieneen kirjaimeen L tai l, esimerkiksi 12345678901L.
  • Liukuluvut (float, double): Kirjoitetaan desimaalipisteellä erotettuna, esimerkiksi 3.14, -0.001 ja 2.0. Voidaan käyttää myös tieteellistä muotoa: 1.5e3 (eli 1.5 × 10³ = 1500) ja 2.0E-4 (eli 2.0 × 10⁻⁴ = 0.0002). Oletuksena desimaaliluvut ovat double-tyyppiä. Jos haluat luoda float-luvun, literaalin tulee päättyä isoon tai pieneen kirjaimeen F tai f, esimerkiksi 3.14f.
  • Totuusarvot (boolean): Kirjoitetaan avainsanoina true ja false.
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.

AlkeistietotyyppiKäärijäluokka
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

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ä.

MetodiSelitys
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.

MetodiSelitys
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:

[0][1][2]taulukko2D[0][0]taulukko2D[0][1]taulukko2D[0][2]123taulukko2D[0]taulukko2D[1][0]taulukko2D[1][1]taulukko2D[1][2]taulukko2D[1][3]4567taulukko2D[1]taulukko2D[2][0]taulukko2D[2][1]taulukko2D[2][2]890taulukko2D[2]

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.

MetodiSelitys
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.

ToimintoJavaC#Python
Lukeminen tietystä paikastalist.get(i)list[i]list[i]
Listan kokolist.size()list.Countlen(list)
Poistaminenlist.remove(i)list.RemoveAt(i)list.pop(i)
Onko lista tyhjä?list.isEmpty() tai list.size() == 0list.Count == 0if 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 kokonaisluku int).

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.

OperaattoriMerkitysEsimerkki (kun x=5, y=3)Tulos
==Yhtä suuri kuinx == yfalse
!=Eri suuri kuinx != ytrue
>Suurempi kuinx > ytrue
<Pienempi kuinx < 4false
>=Suurempi tai yhtä suurix >= 5true
<=Pienempi tai yhtä suuriy <= 3true

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ä koodia
  • continue: 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.length antaa taulukossa olevien taulukoiden lukumäärän, eli ns. "rivien" lukumäärän;
  • taulu2D[riviNro] antaa tietyssä indeksissä olevan int[]-taulukon, joka sisältää kaikki rivillä olevat alkiot;
  • taulu2D[riviNro][sarakeNro] antaa taulu2D[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 on void.
  • (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:

Juuri tehdystä dokumentaatiosta kuva, joka voi näyttää tutulta jos on käynyt tutkimassa Javan omaa dokumentaatiota

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.

Tehtävä 1.1: Oma ohjelma Javalla 1 p.

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 tehtävä TIMissä
Tehtävä 1.2: Sanojen määrä 1 p.

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 tehtävä TIMissä
Tehtävä 1.3: Kokonaislukujen lukeminen käyttäjältä 1 p.

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 tehtävä TIMissä
Tehtävä 1.4: Merkkijonon pakkaaminen 1 p.

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:

0123aabbijono.lengthi11
Tee tehtävä TIMissä
Tehtävä 1.5: Salasanan vahvuuden tarkistaminen 1 p.

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 tehtävä TIMissä
Tehtävä 1.6: Matriisin tulostus 1 p.

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 tehtävä TIMissä
Bonus: Tehtävä 1.7: Numerolaskuri 1 p.

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 tehtävä TIMissä
Bonus: Tehtävä 1.8: Puuttuva luku 1 p.

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 }) palauttaa 2
  • puuttuvaLuku(new int[] { 8, 2, 4, 1, 3, 5, 6 }) palauttaa 7
  • puuttuvaLuku(new int[] { }) palauttaa 1
  • puuttuvaLuku(new int[] { 2 }) palauttaa 1

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);
Tee tehtävä TIMissä
Bonus: Tehtävä 1.9: Alkuluvut 1 p.

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.

Tee tehtävä TIMissä

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.

Rakennus.java
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ä.

Rakennus.java
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.

Rakennus.java
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.

Rakennus.java
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.

Rakennus.java
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.

Rakennus.java
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.

Rakennus.java
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.

Rakennus.java
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);
    }
}
Tehtävä 2.1: Kello1 p.

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 tehtävä TIMissä
Tehtävä 2.2: Ajastin1 p.

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.

Tee tehtävä TIMissä
Tehtävä 2.3: Oma luokka1 p.

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.

Tee tehtävä TIMissä

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ä.

Henkilo.java
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.

Henkilo.java
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.

Tehtävä 2.4: Static1 p.

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 tehtävä TIMissä

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.

Henkilo.java
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.

main.java
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.

main.java
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

Tehtävä 2.5: Puhelin1 p.

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.

Tee tehtävä TIMissä
Tehtävä 2.6: Kirjasto1 p.

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

Tee tehtävä TIMissä

Kapselointi

osaamistavoitteet

  • Tiedät, mitä näkyvyysmääreet kuten public ja private tarkoittavat.
  • 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.

Autoa ajetaan, vaikka emme tiedä miten moottori toimii

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.

LuokkaPakkausAliluokkaMuu maailma
publicKylläKylläKylläKyllä
protectedKylläKylläKylläEi
oletusKylläKylläEiEi
privateKylläEiEiEi

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.

Henkilo.java
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.

Auto.java
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.

Moottori.java
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.

Moottori.java
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.

Auto.java
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ä.

Auto.java
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.

Rakennus.java
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.

Rakennus.java
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.

Rakennus.java
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

Tehtävä 2.7: Ovi1 p.

Toteuta luokka Ovi, joka mallintaa ovea, joka voi olla joko lukossa tai auki.

Attribuutit:

  • private boolean lukossa
  • private 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. Palauttaa true, jos ovi avattiin, muuten false.
  • boolean lukitse(): lukitsee oven vain, jos se on auki. Palauttaa true, jos lukitseminen onnistui, muuten false.
  • 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. Palauttaa true, jos vaihto onnistui, muuten false.
  • 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

Tee tehtävä TIMissä
Tehtävä 2.8: Säästölipas1 p.

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ä).

Tee tehtävä TIMissä
Bonus: Tehtävä 2.9: Sähköverkko1 p.

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.

Tee tehtävä TIMissä
Bonus: Tehtävä 2.10: Varaukset1 p.

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.

Tee tehtävä TIMissä

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.

Tehtävä 2.1: Kello1 p.

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 tehtävä TIMissä
Tehtävä 2.2: Ajastin1 p.

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.

Tee tehtävä TIMissä
Tehtävä 2.3: Oma luokka1 p.

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.

Tee tehtävä TIMissä
Tehtävä 2.4: Static1 p.

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 tehtävä TIMissä
Tehtävä 2.5: Puhelin1 p.

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.

Tee tehtävä TIMissä
Tehtävä 2.6: Kirjasto1 p.

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

Tee tehtävä TIMissä
Tehtävä 2.7: Ovi1 p.

Toteuta luokka Ovi, joka mallintaa ovea, joka voi olla joko lukossa tai auki.

Attribuutit:

  • private boolean lukossa
  • private 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. Palauttaa true, jos ovi avattiin, muuten false.
  • boolean lukitse(): lukitsee oven vain, jos se on auki. Palauttaa true, jos lukitseminen onnistui, muuten false.
  • 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. Palauttaa true, jos vaihto onnistui, muuten false.
  • 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

Tee tehtävä TIMissä
Tehtävä 2.8: Säästölipas1 p.

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ä).

Tee tehtävä TIMissä
Tehtävä 2.9: Sähköverkko1 p.

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.

Tee tehtävä TIMissä
Bonus: Tehtävä 2.10: Varaukset1 p.

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.

Tee tehtävä TIMissä

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ä.

Opiskelija.java
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.

Henkilo.java
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.

Henkilo.java
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.

Henkilo.java
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

Tehtävä 3.1: Luokkahierarkia, osa 1. 1 p.

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.

Tee tehtävä TIMissä
Tehtävä 3.2: Luokkahierarkia, osa 2. 1 p.

Jatketaan edellistä tehtävää. Peri Tuote-luokasta myös luokka Elektroniikka.

Lisää erityispiirteitä kuhunkin aliluokkaan:

  • Vaate: attribuutti String koko (esim. "M", "L", jne.), metodi void sovita(String sovittajanKoko), joka tulostaa, onko vaate sopiva sovittajalle.
  • Elektroniikka: attribuutti int takuuKuukausina (esim. 24), metodi int takuutaJaljella(int kuukausiaKulunut) palauttaa montako kuukautta takuuta on jäljellä (tai 0, jos takuu on umpeutunut).
  • Ruoka: attribuutti String parastaEnnen (esim. "31.01.2026"), ja metodi void syo(), joka tulostaa "Nautit ruoan, jonka viimeinen käyttöpäivä on DD.MM.YYYY." (korvaa DD.MM.YYYY parastaEnnen-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.

Tee tehtävä TIMissä
Tehtävä 3.3: Luokkahierarkia, osa 3. 1 p.

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.

  1. Puhelin (perii Elektroniikka)

    • 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 0401122330
      • public 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).
  2. Pakaste (perii Ruoka)

    • 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.

Tee tehtävä TIMissä

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()

Bändi

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:

main.java
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.

main.java
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.

Suorakulmio.java
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.

main.java
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() rajapintaan Viesti.
  • 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.

main.java
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.

main.java
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

Bonus: Tehtävä 3.4: Luokkahierarkia, osa 4. 1 p.

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)
  • metodit
    • lataa(), joka heikentää akun kuntoa 0.1%:lla jokaisella latauskerralla.
    • toString(), joka kutsuu ensin yliluokan metodia toSTring(), jonka jälkeen tulostaa akun kunnon prosentteina ja sitten toimintasäteen kilometreinä, jonka laskemiseen hyödynnetään kaavaa: (akunkunto / 100 * TOIMINTASADE_MAX).

Tee tehtävä TIMissä
Tehtävä 3.5: Korvaaminen, osa 1. 1 p.

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.

Tee tehtävä TIMissä
Tehtävä 3.6: Korvaaminen, osa 2. 1 p.

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 tehtävä TIMissä

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.

Abstraktin luokan ajatusta voidaan havainnollistaa tuoleilla: Puutuoli, keinutuoli ja työtuoli ovat kaikki tuoleja, jotka erikoistavat "istuin"-käsitettä.

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.

main.java
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.

main.java
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.

main.java
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.

Laite.java
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 resep­ti, 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

Tehtävä 3.7: Viestit. 1 p.

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>"

Tee tehtävä TIMissä
Tehtävä 3.8: Abstrakti ajoneuvo. 1 p.

Muuta Tehtävän 3.6 Ajoneuvo-luokka ja sen liiku()-metodi abstrakteiksi. Jätä toString()-metodi edelleen tavalliseksi (ei-abstraktiksi) metodiksi.

Tee tehtävä TIMissä
Tehtävä 3.9: Viestikanavat. 1 p.
  1. Tee abstrakti luokka Viestikanava. Sillä on attribuutti String vastaanottaja, joka asetetaan konstruktorissa. Lisää abstrakti metodi lahetaSisaisesti(String viesti), joka ei palauta mitään.

  2. Tee myös metodi String getVastaanottaja(), joka palauttaa vastaanottajan.

  3. Tee konkreettinen metodi laheta(String viesti), joka aluksi lopettaa metodin (return), jos viesti on tyhjä tai null. Muuten metodi kutsuu abstraktia metodia lahetaSisaisesti(String viesti).

  4. Peri Viestikanava-luokasta Sahkoposti ja Tekstiviesti. Molemmissa luokissa ylikirjoita abstrakti metodi lahetaSisaisesti(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 tehtävä TIMissä
Tehtävä 3.10: Viestipalvelu. 1 p.

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):

  1. Muuta Viestikanava-luokkaa siten, että se ottaa listan vastaanottajia, ei vain yhtä. Tämän seurauksena pitää muuttaa myös lahetaSisaisesti-metodeja.
  2. Laita Tekstiviesti-luokkaan merkkiraja (esim. 80 merkkiä). Jos viesti on tätä pidempi, niin viesti tulee pilkkoa merkkirajan mukaisiin pätkiin.
Tee tehtävä TIMissä

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.

Tehtävä 3.1: Luokkahierarkia, osa 1. 1 p.

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.

Tee tehtävä TIMissä
Tehtävä 3.2: Luokkahierarkia, osa 2. 1 p.

Jatketaan edellistä tehtävää. Peri Tuote-luokasta myös luokka Elektroniikka.

Lisää erityispiirteitä kuhunkin aliluokkaan:

  • Vaate: attribuutti String koko (esim. "M", "L", jne.), metodi void sovita(String sovittajanKoko), joka tulostaa, onko vaate sopiva sovittajalle.
  • Elektroniikka: attribuutti int takuuKuukausina (esim. 24), metodi int takuutaJaljella(int kuukausiaKulunut) palauttaa montako kuukautta takuuta on jäljellä (tai 0, jos takuu on umpeutunut).
  • Ruoka: attribuutti String parastaEnnen (esim. "31.01.2026"), ja metodi void syo(), joka tulostaa "Nautit ruoan, jonka viimeinen käyttöpäivä on DD.MM.YYYY." (korvaa DD.MM.YYYY parastaEnnen-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.

Tee tehtävä TIMissä
Tehtävä 3.3: Luokkahierarkia, osa 3. 1 p.

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.

  1. Puhelin (perii Elektroniikka)

    • 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 0401122330
      • public 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).
  2. Pakaste (perii Ruoka)

    • 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.

Tee tehtävä TIMissä
Bonus: Tehtävä 3.4: Luokkahierarkia, osa 4. 1 p.

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)
  • metodit
    • lataa(), joka heikentää akun kuntoa 0.1%:lla jokaisella latauskerralla.
    • toString(), joka kutsuu ensin yliluokan metodia toSTring(), jonka jälkeen tulostaa akun kunnon prosentteina ja sitten toimintasäteen kilometreinä, jonka laskemiseen hyödynnetään kaavaa: (akunkunto / 100 * TOIMINTASADE_MAX).

Tee tehtävä TIMissä
Tehtävä 3.5: Korvaaminen, osa 1. 1 p.

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.

Tee tehtävä TIMissä
Tehtävä 3.6: Korvaaminen, osa 2. 1 p.

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 tehtävä TIMissä
Tehtävä 3.7: Viestit. 1 p.

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>"

Tee tehtävä TIMissä
Tehtävä 3.8: Abstrakti ajoneuvo. 1 p.

Muuta Tehtävän 3.6 Ajoneuvo-luokka ja sen liiku()-metodi abstrakteiksi. Jätä toString()-metodi edelleen tavalliseksi (ei-abstraktiksi) metodiksi.

Tee tehtävä TIMissä
Tehtävä 3.9: Viestikanavat. 1 p.
  1. Tee abstrakti luokka Viestikanava. Sillä on attribuutti String vastaanottaja, joka asetetaan konstruktorissa. Lisää abstrakti metodi lahetaSisaisesti(String viesti), joka ei palauta mitään.

  2. Tee myös metodi String getVastaanottaja(), joka palauttaa vastaanottajan.

  3. Tee konkreettinen metodi laheta(String viesti), joka aluksi lopettaa metodin (return), jos viesti on tyhjä tai null. Muuten metodi kutsuu abstraktia metodia lahetaSisaisesti(String viesti).

  4. Peri Viestikanava-luokasta Sahkoposti ja Tekstiviesti. Molemmissa luokissa ylikirjoita abstrakti metodi lahetaSisaisesti(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 tehtävä TIMissä
Tehtävä 3.10: Viestipalvelu. 1 p.

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):

  1. Muuta Viestikanava-luokkaa siten, että se ottaa listan vastaanottajia, ei vain yhtä. Tämän seurauksena pitää muuttaa myös lahetaSisaisesti-metodeja.
  2. Laita Tekstiviesti-luokkaan merkkiraja (esim. 80 merkkiä). Jos viesti on tätä pidempi, niin viesti tulee pilkkoa merkkirajan mukaisiin pätkiin.
Tee tehtävä TIMissä

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.

main.java
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ä.

Piirrettava.java
/**
 * 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.

Piirrettava.java
/**
 * 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.

A.java
public interface A {
    void metodiA();
}

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

Tehtävä 4.1: Muunnin. 1 p.
  1. Luo rajapinta nimeltään Muunnin. Määrittele rajapintaan yksi metodi: String muunna(String syote). Muista, että rajapinnassa metodilla ei ole runkoa (ei aaltosulkeita {}).

  2. Tee luokat PienetKirjaimet, IsotKirjaimet ja IsoAlkukirjain, jotka toteuttavat Muunnin-rajapinnan.

  • PienetKirjaimet-luokan muunna-metodi muuntaa annetun merkkijonon pieniksi kirjaimiksi. muunna("Hei Maa") --> "hei maa".
  • IsotKirjaimet-luokan muunna-metodi muuntaa annetun merkkijonon suuraakkosiksi. muunna("Hei Maa") --> "HEI MAA".
  • IsoAlkukirjain-luokan muunna-metodi muuntaa annetun merkkijonon siten, että vain ensimmäinen kirjain on suuraakkonen ja muut pieniä. muunna("HEI MAA") --> "Hei maa".
  1. Testaa ohjelmaasi valmiiksi annetulla pääohjelmalla.
Tee tehtävä TIMissä
Tehtävä 4.2: Vakoojien viestijärjestelmä.1 p.

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.

  1. Luo rajapinta Salaaja. Määrittele rajapintaan kaksi metodia
String salaa(String viesti);
String pura(String salattuViesti);
  1. Toteuta kolme erilaista luokkaa: Kaantaja, Hakkeri ja SeuraavaKirjain, jotka toteuttavat Salaaja-rajapinnan seuraavilla logiikoilla:
  • Kaantaja (Peilikuvakirjoitus). Kääntää sanan väärinpäin. Esimerkki: "Agentti" → "ittnegA". Vihje: Voit käyttää StringBuilder-luokan reverse()-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: Javassa char on 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.

Tee tehtävä TIMissä

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:

TapausMerkitysTulkinta
olioA.compareTo(olioB) < 0olioA < olioBolioA on pienempi kuin olioB
olioA.compareTo(olioB) == 0olioA == olioBolioA on yhtä suuri kuin olioB
olioA.compareTo(olioB) > 0olioA > olioBolioA 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.

Tehtävä 4.5: Miksi Comparable. 1 p.

Tutki Javan dokumentaatiota. Vastaa kysymyksiin Comparable-rajapinnasta.

Tee tehtävä TIMissä

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:

Kerailykortti.java
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:

main.java
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:

Kerailykortti.java
// 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.

Tehtävä 4.x: Henkilöt järjestykseen, osa 1 0,5 p. Tee tehtävä TIMissa

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:

Kerailykortti.java
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ä 4.6: Henkilöt järjestykseen, osa 1. 1 p.

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ä:

  1. Ilmatar
  2. Joukahainen
  3. Kokko
  4. Kyllikki
Tee tehtävä TIMissä
Tehtävä 4.7: Henkilöt järjestykseen, osa 2. 1 p.

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ä:

  1. Chopin Frédéric
  2. Mozart Leopold
  3. Mozart Wolfgang Amadeus
  4. Pacius Fredrik
Tee tehtävä TIMissä

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")

alt text

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:

  • Leivanpaahdin on Keittiolaite (perintä), mutta se myös toteuttaa Verkkovirtalaite-rajapinnan.
  • Samoin Sirkkeli voisi olla vaikkapa Tyokalu (perintä), joka myöskin toteuttaa saman Verkkovirtalaite-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
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, vaan List<Keittiolaite>-listassa. Tällöin kaikilla listan olioilla on luonnostaan puhdista()-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- tai Tyokalu-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.

KysymysAbstrakti luokkaRajapinta
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 luokanLuokka voi toteuttaa useita rajapintoja
KäyttötarkoitusYhteinen runko ja osittainen toteutusYhteinen sopimus käyttäytymisestä

Tehtävät

Tehtävä 4.3: Seikkailupeli. 1 p.

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 tehtävä TIMissä
Bonus: Tehtävä 4.4: Kotityörobotti. 1 p.

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. Jos kohde on 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ö.
Tee tehtävä TIMissä

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) taulukoiden sijaan.

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.

  1. 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.
  2. 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.
  3. 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
}
Tehtävä 4.8: Etsi suurin. 1 p.

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 tehtävä TIMissä
Tehtävä 4.9: Kontti. 1 p.

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().

Tee tehtävä TIMissä
Bonus: Tehtävä 4.10: Iso kontti. 1 p.

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:

  • lisaa lisää parametrina annetun olion listan loppuun.

  • ota palauttaa viimeisimmän olion ja ottaa sen pois listasta.

  • katso palauttaa viimeisimmän olion, mutta ei ota sitä pois listasta.

  • sisaltaa ottaa parametrina olion ja palauttaa true, jos olio löytyy kontista. Muussa tapauksessa se palauttaa false.

  • tulosta tulostaa 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.

Tee tehtävä TIMissä
Bonus: Tehtävä 4.11: Tyyppirajoitukset, osa 1. 1 p.

Lisää edellisen tehtävän IsoKontti-luokkaan kaksi metodia.

  1. Metodi summaaNumerot ottaa parametrina vastaan kontin, joka sisältää numeroita eli Number-luokan tai sen alityyppien olioita ja palauttaa näiden summan.

  2. Metodi siirraKaikki ottaa 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.

  1. Kaikilla Number-luokan olioilla on doubleValue()-metodi, joka palauttaa sen arvon double-muodossa.

  2. Huomaa, että konttien tyyppien ei tarvitse olla täysin samat; Number-kontti voi sisältää Integer-olioita, sillä Integer on sen alityyppi.

Tee tehtävä TIMissä
Bonus: Tehtävä 4.12: Tyyppirajoitukset, osa 2. 1 p.

Tee geneerinen funktio, joka kopioi yhdestä listasta kaikki tyyppiä T vastaavat alkiot toiseen listaan, jonka tyyppi voi olla T tai sen ylityyppi.

Tee tehtävä TIMissä

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.

Tehtävä 4.1: Muunnin. 1 p.
  1. Luo rajapinta nimeltään Muunnin. Määrittele rajapintaan yksi metodi: String muunna(String syote). Muista, että rajapinnassa metodilla ei ole runkoa (ei aaltosulkeita {}).

  2. Tee luokat PienetKirjaimet, IsotKirjaimet ja IsoAlkukirjain, jotka toteuttavat Muunnin-rajapinnan.

  • PienetKirjaimet-luokan muunna-metodi muuntaa annetun merkkijonon pieniksi kirjaimiksi. muunna("Hei Maa") --> "hei maa".
  • IsotKirjaimet-luokan muunna-metodi muuntaa annetun merkkijonon suuraakkosiksi. muunna("Hei Maa") --> "HEI MAA".
  • IsoAlkukirjain-luokan muunna-metodi muuntaa annetun merkkijonon siten, että vain ensimmäinen kirjain on suuraakkonen ja muut pieniä. muunna("HEI MAA") --> "Hei maa".
  1. Testaa ohjelmaasi valmiiksi annetulla pääohjelmalla.
Tee tehtävä TIMissä
Tehtävä 4.2: Vakoojien viestijärjestelmä.1 p.

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.

  1. Luo rajapinta Salaaja. Määrittele rajapintaan kaksi metodia
String salaa(String viesti);
String pura(String salattuViesti);
  1. Toteuta kolme erilaista luokkaa: Kaantaja, Hakkeri ja SeuraavaKirjain, jotka toteuttavat Salaaja-rajapinnan seuraavilla logiikoilla:
  • Kaantaja (Peilikuvakirjoitus). Kääntää sanan väärinpäin. Esimerkki: "Agentti" → "ittnegA". Vihje: Voit käyttää StringBuilder-luokan reverse()-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: Javassa char on 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.

Tee tehtävä TIMissä
Tehtävä 4.3: Seikkailupeli. 1 p.

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 tehtävä TIMissä
Bonus: Tehtävä 4.4: Kotityörobotti. 1 p.

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. Jos kohde on 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ö.
Tee tehtävä TIMissä
Tehtävä 4.5: Miksi Comparable. 1 p.

Tutki Javan dokumentaatiota. Vastaa kysymyksiin Comparable-rajapinnasta.

Tee tehtävä TIMissä
Tehtävä 4.6: Henkilöt järjestykseen, osa 1. 1 p.

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ä:

  1. Ilmatar
  2. Joukahainen
  3. Kokko
  4. Kyllikki
Tee tehtävä TIMissä
Tehtävä 4.7: Henkilöt järjestykseen, osa 2. 1 p.

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ä:

  1. Chopin Frédéric
  2. Mozart Leopold
  3. Mozart Wolfgang Amadeus
  4. Pacius Fredrik
Tee tehtävä TIMissä
Tehtävä 4.8: Etsi suurin. 1 p.

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 tehtävä TIMissä
Tehtävä 4.9: Kontti. 1 p.

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().

Tee tehtävä TIMissä
Bonus: Tehtävä 4.10: Iso kontti. 1 p.

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:

  • lisaa lisää parametrina annetun olion listan loppuun.

  • ota palauttaa viimeisimmän olion ja ottaa sen pois listasta.

  • katso palauttaa viimeisimmän olion, mutta ei ota sitä pois listasta.

  • sisaltaa ottaa parametrina olion ja palauttaa true, jos olio löytyy kontista. Muussa tapauksessa se palauttaa false.

  • tulosta tulostaa 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.

Tee tehtävä TIMissä
Bonus: Tehtävä 4.11: Tyyppirajoitukset, osa 1. 1 p.

Lisää edellisen tehtävän IsoKontti-luokkaan kaksi metodia.

  1. Metodi summaaNumerot ottaa parametrina vastaan kontin, joka sisältää numeroita eli Number-luokan tai sen alityyppien olioita ja palauttaa näiden summan.

  2. Metodi siirraKaikki ottaa 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.

  1. Kaikilla Number-luokan olioilla on doubleValue()-metodi, joka palauttaa sen arvon double-muodossa.

  2. Huomaa, että konttien tyyppien ei tarvitse olla täysin samat; Number-kontti voi sisältää Integer-olioita, sillä Integer on sen alityyppi.

Tee tehtävä TIMissä
Bonus: Tehtävä 4.12: Tyyppirajoitukset, osa 2. 1 p.

Tee geneerinen funktio, joka kopioi yhdestä listasta kaikki tyyppiä T vastaavat alkiot toiseen listaan, jonka tyyppi voi olla T tai sen ylityyppi.

Tee tehtävä TIMissä

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 hashCode tarvitaan (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:

TapausMerkitysTulkinta
cmp.compareTo(olioA, olioB) < 0olioA < olioBolioA on pienempi kuin olioB
cmp.compareTo(olioA, olioB) == 0olioA == olioBolioA on yhtä suuri kuin olioB
cmp.compareTo(olioA, olioB) > 0olioA > olioBolioA 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:

KerailykorttiSarjaVertailija.java
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 KerailykorttiSarjaVertailija on oma luokkansa, määritimme Kerailykortti-luokkaan saantimetodin getSarja().
  • Jotta vertailijaa voi käyttää, siitä tulee alustaa olio. Alustuksen jälkeen vertailijaolio voidaan käyttää Collections.sort-metodin ylikuormituksen kanssa, joka joka ottaa Comparator-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));
}
Tehtävä 4.4: Kortit harvinaisuuden mukaan. 1 p. Tee tehtävä TIMissä

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ää.