Tämä materiaali on lisensoitu Creative Commons BY-NC-SA-lisenssillä, joten voit käyttää ja levittää sitä vapaasti, kunhan alkuperäisten tekijöiden nimiä ei poisteta. Jos teet muutoksia materiaaliin ja haluat levittää muunneltua versiota, se täytyy lisensoida samanlaisella vapaalla lisenssillä. Materiaalien käyttö kaupalliseen tarkoitukseen on ilman erillistä lupaa kielletty.
Tekijät: Arto Vihavainen ja Matti Luukkainen
Tämä on suoraa jatkoa Ohjelmoinnin perusteet -kurssin materiaaliin.
Tämä materiaali on tarkoitettu Helsingin Yliopiston Tietojenkäsittelytieteen laitoksen syksyn 2012 Ohjelmoinnin perusteet- ja jatkokurssille. Materiaali pohjautuu keväiden 2012, 2011 ja 2010 kurssimateriaaleihin, joiden sisältöön ovat vaikuttaneet Matti Paksula, Antti Laaksonen, Pekka Mikkola, Juhana Laurinharju, Martin Pärtel, Joel Kaasinen ja Mikael Nousiainen
Lue materiaalia siten, että teet samalla itse kaikki lukemasi esimerkit. Esimerkkeihin kannattaa tehdä pieniä muutoksia ja tarkkailla, miten muutokset vaikuttavat ohjelman toimintaan. Äkkiseltään voisi luulla, että esimerkkien tekeminen myös itse ja niiden muokkaaminen hidastaa opiskelua. Tämä ei kuitenkaan pidä ollenkaan paikkansa. Ohjelmoimaan ei ole vielä tietääksemme kukaan ihminen oppinut lukemalla (tai esim. luentoa kuuntelemalla). Oppiminen perustuu oleellisesti aktiiviseen tekemiseen ja rutiinin kasvattamiseen. Esimerkkien ja erityisesti erilaisten omien kokeilujen tekeminen on parhaita tapoja "sisäistää" luettua tekstiä.
Pyri tekemään tai ainakin yrittämään tehtäviä sitä mukaa kuin luet tekstiä. Jos et osaa heti tehdä jotain tehtävää, älä masennu, sillä saat ohjausta tehtävän tekemiseen pajassa.
Tekstiä ei ole tarkoitettu vain kertaalleen luettavaksi. Joudut varmasti myöhemmin palaamaan jo aiemmin lukemiisi kohtiin tai aiemmin tekemiisi tehtäviin. Tämä teksti ei sisällä kaikkea oleellista ohjelmointiin liittyvää. Itse asiassa ei ole olemassa mitään kirjaa josta löytyisi kaikki oleellinen. Eli joudut joka tapauksessa ohjelmoijan urallasi etsimään tietoa myös omatoimisesti. Kurssin harjoitukset sisältävät jo jonkun verran ohjeita, mistä suunnista ja miten hyödyllistä tietoa on mahdollista löytää.
Muutamiin kohtiin olemme myös liittäneet screencasteja joita katsomalla voi pelkän valmiin koodin lukemisen sijaan seurata miten ohjelma muodostuu.
Kurssi alkaa siitä mihin ohjelmoinnin perusteet loppui ja oikeastaan kaikki ohjelmoinnin perusteet -kurssilla opitut asiat oletetaan nyt osattavan. Kannattaa käydä aina silloin tällöin kertaamassa Ohjelmoinnin perusteet -kurssin materiaalia.
Huom! Käytämme tällä kurssilla NetBeans-nimistä ohjelmointiympäristöä. Ohjeet NetBeansin ja kurssilla käytettävän tehtäväautomaatin käyttöön löydät täältätäältä.
Tässä luvussa kerrataan pikaisesti muutamia ohjelmoinnin perusteissa tutuksi tulleita asioita. Ohjelmoinnin perusteet -kurssin materiaaliin pääsee tutustumaan tarkemmin tästä.
Ruostunutta ohjelmointitaitoa voi myös verestää täältä löytyvän tehtäväsarjan avulla.
Tässä luvussa kerrataan pikaisesti muutamia aiempina viikkoina tutuksi tulleita asioita.
Tietokoneohjelma koostuu joukosta käskyjä, joita tietokone suorittaa yksi kerrallaan, ylhäältä alaspäin. Käskyillä on aina määrätty rakenne ja semantiikka. Javassa, eli kurssilla käyttämässämme ohjelmointikielessä, käskyjä luetaan ylhäältä alas vasemmalta oikealle. Ohjelmointikurssit aloitetaan usein esittelemällä ohjelma, joka tulostaa merkkijonon Hei maailma!
. Alla on Java-kielellä kirjoitettu käsky Hei maailma!
-merkkijonon tulostamiseksi.
System.out.println("Hei maailma!");
Käskyssä kutsutaan System
-luokkaan liittyvän muuttujan println
-metodia, joka tulostaa ensin parametrina annetun merkkijonon ja sen jälkeen rivinvaihdon. Metodille annetaan parametrina merkkijono Hei maailma!
, jolloin tulostus on Hei maailma!
jota seuraa rivinvaihto.
Ohjelmaan voi liittyä muuttujia joita voi käyttää osana ohjelman toimintaa. Alla on ohjelma, joka ensin esittelee kokonaislukutyyppisen muuttujan pituus
johon asetetaan seuraavalla rivillä arvo 179. Tämän jälkeen tulostetaan muuttujan pituus
arvo eli 179
.
int pituus; pituus = 179; System.out.println(pituus);
Yllä ohjelman suoritus tapahtuisi rivi kerrallaan. Ensin suoritetaan rivi int pituus;
, jossa esitellään muuttuja nimeltä pituus
. Seuraavaksi suoritetaan rivi pituus = 179;
, jossa asetetaan edellisellä rivillä esiteltyyn muuttujaan arvo 179
. Tämän jälkeen suoritetaan rivi System.out.println(pituus);
, jossa kutsutaan aiemmin näkemäämme tulostusmetodia, jolle annetaan parametrina muuttuja pituus. Metodi tulostaa muuttujan pituus
sisällön eli arvon 179
.
Yllä olevassa ohjelmassa ei oikeastaan ole tarvetta esitellä muuttujaa pituus
ja asettaa siihen arvoa erillisillä riveillä. Muuttujan esittelyn ja siihen liittyvän arvon asetuksen voi tehdä myös samalla rivillä.
int pituus = 179;
Yllä olevaa riviä suoritettaessa esitellään ensin muuttuja pituus
, johon asetetaan esittelyn yhteydessä arvo 179
.
Kaikki tieto esiintyy oikeasti tietokoneen sisällä jonona bittejä, eli lukuja nolla ja yksi. Muuttujat ovat ohjelmointikielten tarjoama abstraktio, jolla voidaan käsitellä erilaisia arvoja helpommin. Muuttujia käytetään arvojen säilyttämiseen ja ohjelman tilan ylläpitoon. Javassa käytössämme on muun muassa alkeisarvoiset muuttujatyypit int
(kokonaisluku), double
(liukuluku), boolean
(totuusarvo), char
(merkki) sekä viitetyyppiset muuttujatyypit String
(merkkijono), ArrayList
(taulukko) ja kaikki luokat. Palaamme alkeistyyppisiin ja viitetyyppisiin muuttujiin ja niiden eroihin tarkemmin myöhemmin.
Ohjelmien toiminnallisuus rakennetaan kontrollirakenteiden avulla. Kontrollirakenteet mahdollistavat erilaiset toiminnot ohjelman muuttujien arvoista riippuen. Alla esimerkki if-else if-else
-kontrollirakenteesta, jossa tehdään erilainen toiminto vertailun tuloksesta riippuen. Esimerkissä tulostetaan merkkijono Kaasua
jos muuttujan nopeus
arvo on pienempi kuin 110, merkkijono Jarrua
jos muuttujan nopeus
arvo on suurempi kuin 120 ja merkkijono Kruisaillaan
muissa tapauksissa.
int nopeus = 105; if(nopeus < 110) { System.out.println("Kaasua"); } else if (nopeus > 120) { System.out.println("Jarrua"); } else { System.out.println("Kruisaillaan"); }
Koska yllä olevassa esimerkissä muuttujan nopeus
arvo on 105, tulostaa ohjelma aina merkkijonon Kaasua
. Muistathan että merkkijonojen yhtäsuuruusvertailu tapahtuu merkkijonoon liittyvällä equals
-metodilla. Alla on esimerkki jossa käytetään Javan Scanner-luokasta luotua olioita käyttäjän kirjoittaman syötteen lukemiseen. Ohjelma tarkistaa ovatko käyttäjän syöttämät merkkijonot samat.
Scanner lukija = new Scanner(System.in); System.out.print("Syötä ensimmäinen merkkijono: "); String ensimmainen = lukija.nextLine(); System.out.print("Syötä toinen merkkijono: "); String toinen = lukija.nextLine(); System.out.println(); if (ensimmainen.equals(toinen)) { System.out.println("Kirjoittamasi merkkijonot ovat samat!"); } else { System.out.println("Kirjoittamasi merkkijonot eivät olleet samat!"); }
Ohjelman toiminta riippuu käyttäjän syötteestä. Alla esimerkki, punaisella värillä tarkoitetaan käyttäjän kirjoittamaa syötettä.
Syötä ensimmäinen merkkijono: porkkana Syötä toinen merkkijono: salaatti Kirjoittamasi merkkijonot eivät olleet samat!
Ohjelmissa tarvitaan usein toistoa. Tehdään ensin ns. while-true-break -toistolause, jota jatketaan niin pitkään kunnes käyttäjä syöttää merkkijonon salasana
. Lause while(true)
aloittaa toistolauseen, jota jatketaan kunnes kohdataan avainsana break
.
Scanner lukija = new Scanner(System.in); while(true) { System.out.print("Syötä salasana: "); String salasana = lukija.nextLine(); if(salasana.equals("salasana")) { break; } } System.out.println("Kiitos!");
Syötä salasana: porkkana Syötä salasana: salasana Kiitos
While-toistolausekkeeseen voi asettaa totuusarvon true
sijaan myös vertailun. Alla tulostetaan käyttäjän syöttämä merkkijono siten, että sillä on ylä- ja alapuolella tähtiä.
Scanner lukija = new Scanner(System.in); System.out.print("Syötä merkkijono: "); String merkkijono = lukija.nextLine(); int tahdenNumero = 0; while(tahdenNumero < merkkijono.length()) { System.out.print("*"); tahdenNumero = tahdenNumero + 1; } System.out.println(); System.out.println(merkkijono); tahdenNumero = 0; while(tahdenNumero < merkkijono.length()) { System.out.print("*"); tahdenNumero = tahdenNumero + 1; } System.out.println();
Syötä merkkijono: porkkana ******** porkkana ********
Yllä olevan esimerkin pitäisi nostattaa kylmiä väreitä selkäpiissäsi. Kylmät väreet toivottavasti johtuvat siitä, että huomaat esimerkin rikkovan ohjelmoinnin perusteissa opittuja käytänteitä. Esimerkissä on turhaa toistoa joka tulee poistaa metodien avulla.
While-toistolauseen lisäksi käytössämme on myös for-toistolauseen kaksi versiota. Uudempaa for-lauseketta käytetään listojen läpikäyntiin.
ArrayList<String> tervehdykset = new ArrayList<String>(); tervehdykset.add("Hei"); tervehdykset.add("Hallo"); tervehdykset.add("Hi"); for(String tervehdys: tervehdykset) { System.out.println(tervehdys); }
Hei Hallo Hi
Perinteisempää for-lausetta käytetään samankaltaisissa tilanteissa kuin while
-toistolausetta. Sitä voidaan käyttää esimerkiksi taulukoiden läpikäyntiin. Seuraavassa esimerkissä kerrotaan jokaisen taulukon luvut
alkion sisältö kahdella ja lopuksi tulostetaan luvut uudempaa for-lausetta käyttäen.
int[] luvut = new int[] {1, 2, 3, 4, 5, 6}; for (int i = 0; i < luvut.length; i++) { luvut[i] = luvut[i] * 2; } for (int luku : luvut) { System.out.println(luku); }
2 4 6 8 10 12
Perinteinen for-lauseke on erittäin hyödyllinen tapauksissa joissa käymme indeksejä yksitellen läpi. Alla oleva toistolauseke käy merkkijonon merkit yksitellen läpi, ja tulostaa merkkijono Hip!
aina kun törmäämme merkkiin a
.
String merkkijono = "saippuakauppias"; for (int i = 0; i < merkkijono.length(); i++) { if (merkkijono.charAt(i) == 'a') { System.out.println("Hip!"); } }
Hip! Hip! Hip! Hip!
Metodit ovat tapa pilkkoa ohjelman toiminnallisuutta pienempiin osakokonaisuuksiin. Kaikkien Java-ohjelmien suoritus alkaa pääohjelmametodista, joka määritellään lauseella public static void main(String[] args)
. Lause määrittelee staattisen, eli luokkaan liittyvän metodin, joka saa parametrina merkkijonotaulukon.
Ohjelmoija määrittelee metodeja ohjelman sisältävien toiminnallisuuksien abstrahoimiseksi. Ohjelmoidessa tulee tavoitella tilannetta, jossa ohjelmaa voidaan katsoa korkeammalta tasolta, jolloin pääohjelma koostuu joukosta itse määriteltyjen, hyvin nimettyjen metodien kutsuja. Metodit taas tarkentavat ohjelman toiminnallisuutta ja ehkä toteuttavat oman toiminnallisuutensa kutsumalla joitain muita metodeja.
Metodit joiden määrittelyssä on sana static
, liittyvät metodin sisältävään luokkaan ja toimivat ns. apumetodeina. Metodit joiden määrittelyssä ei ole sanaa static
liittyvät luokasta tehtyihin ilmentymiin, eli olioihin, ja voivat muokata yksittäisten olioiden sisäistä tilaa.
Metodilla on aina näkyvyysmääre (public, näkyy kaikille, private, näkyy vain luokan sisällä), paluutyyppi (void, ei palauta arvoa) sekä metodin nimi. Seuraavassa luodaan luokkaan liittyvä metodi public static void tulosta(String merkkijono, int kertaa)
, joka tulostaa merkkijonon halutun määrän kertoja. Käytetään tällä kertaa metodia System.out.print
, joka toimii muuten samoin kuin System.out.println
, mutta ei tulosta rivinvaihtoa.
public static void tulosta(String merkkijono, int kertaa) { for (int i = 0; i < kertaa; i++) { System.out.print(merkkijono); } }
Yllä oleva metodi tulostaa parametrina annetun merkkijonon niin monta kertaa kuin parametrina annettu kokonaisluku kertoo.
Toistolausekkeisiin liittyvässä luvussa huomasimme että koodi sisälsi tähtien tulostamisen osalta ikävästi "copypastea". Metodien avulla voimme siirtää tähtien tulostamisen erillisen metodiin. Luodaan metodi public static void tulostaTahtia(int kertaa)
, joka tulostaa parametrina annetun määrän tähtiä. Metodi käyttää for
-toistolausetta while
:n sijaan.
public static void tulostaTahtia(int kertaa) { for(int i = 0; i < kertaa; i++) { System.out.print("*"); } System.out.println(); }
Metodia hyödyntäessä aiemmin kauhistusta aihettanut esimerkkimme näyttää seuraavalta.
Scanner lukija = new Scanner(System.in); System.out.print("Syötä merkkijono: "); String merkkijono = lukija.nextLine(); tulostaTahtia(merkkijono.length()); System.out.println(merkkijono); tulostaTahtia(merkkijono.length());
Metodit toimivat ohjelman abstrahoimisessa tiettyyn pisteeseen asti, mutta ohjelmien kasvaessa suuremmiksi, ohjelmia halutaan pilkkoa vielä pienempiin ja loogisempiin kokonaisuuksiin. Luokkien avulla voimme määritellä ohjelmaan korkeamman tason käsitteitä ja käsitteisiin liittyviä toiminnallisuuksia. Jokainen Java-ohjelma vaatii toimiakseen luokan, eli yllä rakennettu Hei maailma!
-esimerkki ei toimisi ilman luokkamäärittelyä. Luokka määritellään avainsanoilla public class LuokanNimi
.
public class HeiMaailma { public static void main(String[] args) { System.out.println("Hei maailma!"); } }
Luokkia siis käytetään ohjelmassa esiintyvien käsitteiden ja niihin liittyvien toiminnallisuuksien määrittelyyn. Luokista voidaan luoda olioita, jotka ovat luokan ilmentymiä. Jokaisella tiettyyn luokkaan liittyvällä oliolla on sama rakenne, mutta oliohin liittyvien muuttujien arvot voivat olla erilaiset. Olioiden metodit käsittelevät olioiden tilaa, eli sisäisiä muuttujia.
Tutkitaan alla olevaa luokkaa Kirja
, jolla on oliomuuttujat nimi (merkkijono) ja julkaisuvuosi (kokonaisluku).
public class Kirja { private String nimi; private int julkaisuvuosi; public Kirja(String nimi, int julkaisuvuosi) { this.nimi = nimi; this.julkaisuvuosi = julkaisuvuosi; } public String getNimi() { return this.nimi; } public int getJulkaisuvuosi() { return this.julkaisuvuosi; } }
Alussa oleva määritelmä public class Kirja
kertoo luokan nimen. Tätä seuraa oliomuuttujien määrittely. Oliomuuttujat ovat muuttujia, joista jokaisella luokan oliolla on oma toisista riippumaton kopionsa. On yleensä tarkoituksenmukaista piilottaa oliomuuttujat luokan käyttäjiltä, eli määritellä niille näkyvyysmääre private. Jos näkyvyysmääreeksi asetetaan public pääsee olion käyttäjä suoraan käsiksi oliomuuttujiin.
Luokasta luodaan olioita konstruktorilla. Konstruktori on metodi, joka alustaa olion (eli luo oliolle siihen liittyvät muuttujat) ja suorittaa konstruktorin sisällä olevat käskyt. Konstruktori on aina samanniminen kuin konstruktorin sisältävä luokka. Konstruktorissa public Kirja(String nimi, int julkaisuvuosi)
luodaan uusi olio luokasta Kirja
ja asetetaan siihen liittyviin muuttujiin parametrina annetut arvot.
Ylläolevalle luokalle on määritelty myös kaksi olioiden tietoja käsittelevää metodia. Metodi public String getNimi()
palauttaa käsiteltävän olion nimen. Metodi public int getJulkaisuvuosi()
palauttaa käsiteltävän olion julkaisuvuoden.
Olioita luodaan luokkaan määritellyn konstruktorin avulla. Ohjelmakoodissa konstruktoria kutsutaan new
-käskyllä, joka palauttaa viitteen uuteen olioon. Oliot ovat luokista tehtyjä ilmentymiä. Tutkitaan ohjelmaa, joka luo kaksi eri kirjaa, jonka jälkeen tulostetaan olioihin liittyvien metodien getNimi
palauttamat arvot.
Kirja jarkiJaTunteet = new Kirja("Järki ja tunteet", 1811); Kirja ylpeysJaEnnakkoluulo = new Kirja("Ylpeys ja ennakkoluulo", 1813); System.out.println(jarkiJaTunteet.getNimi()); System.out.println(ylpeysJaEnnakkoluulo.getNimi());
Järki ja tunteet Ylpeys ja ennakkoluulo
Jokaisella oliolla on siis oma sisäinen tila. Tila muodostuu olioon liittyvistä oliomuuttujista. Oliomuuttujat voivat olla sekä alkeistyyppisiä muuttujia että viitetyyppisiä muuttujia. Jos olioon liittyy viitetyyppisiä muuttujia, voi olla että muutkin oliot viittaavat samoihin olioihin! Visualisoidaan tämä pankkiesimerkillä, jossa on tilejä ja henkilöitä.
public class Tili { private String tilitunnus; private int saldoSentteina; public Tili(String tilitunnus) { this.tilitunnus = tilitunnus; this.saldoSentteina = 0; } public void pane(int summa) { this.saldoSentteina += summa; } public int getSaldoSentteina() { return this.saldoSentteina; } // .. muita tiliin liittyviä metodeja }
import java.util.ArrayList; public class Henkilo { private String nimi; private ArrayList<Tili> tilit; public Henkilo(String nimi) { this.nimi = nimi; this.tilit = new ArrayList<Tili>(); } public void lisaaTili(Tili tili) { this.tilit.add(tili); } public int rahaaYhteensa() { int yhteensa = 0; for (Tili tili: this.tilit) { yhteensa += tili.getSaldoSentteina(); } return yhteensa; } // ... muita henkilöön liittyviä metodeja }
Jokaisella Henkilo
-luokasta tehdyllä oliolla on oma nimi sekä oma lista tileistä. Luodaan seuraavaksi kaksi henkilöä ja kaksi tiliä. Toinen tileistä on vain yhden henkilön oma, toinen yhteinen.
Henkilo matti = new Henkilo("Matti"); Henkilo maija = new Henkilo("Maija"); Tili palkkatili = new Tili("NORD-LOL"); Tili kotitaloustili = new Tili("SAM-LOL"); matti.lisaaTili(palkkatili); matti.lisaaTili(kotitaloustili); maija.lisaaTili(kotitaloustili); System.out.println("Matin tileillä rahaa: " + matti.rahaaYhteensa()); System.out.println("Maijan tileillä rahaa: " + maija.rahaaYhteensa()); System.out.println(); palkkatili.pane(150000); System.out.println("Matin tileillä rahaa: " + matti.rahaaYhteensa()); System.out.println("Maijan tileillä rahaa: " + maija.rahaaYhteensa()); System.out.println(); kotitaloustili.pane(10000); System.out.println("Matin tileillä rahaa: " + matti.rahaaYhteensa()); System.out.println("Maijan tileillä rahaa: " + maija.rahaaYhteensa()); System.out.println();
Matin tileillä rahaa: 0 Maijan tileillä rahaa: 0 Matin tileillä rahaa: 150000 Maijan tileillä rahaa: 0 Matin tileillä rahaa: 160000 Maijan tileillä rahaa: 10000
Ensin kummankin henkilön tilit ovat tyhjiä. Kun palkkatilille, johon oliolla matti
on viite, lisätään rahaa, kasvaa Matin tileillä oleva rahamäärä. Kun kotitaloustilille lisätään rahaa, kasvaa kummankin henkilön rahamäärä. Tämä johtuu siitä että sekä Matilla että Maijalla on "oikeus" kotitaloustilille, eli kummakin omassa oliomuuttujassa tilit
on viite kotitaloustiliin. Palaamme alkeis- ja viitetyyppisten muuttujien eroon tarkemmin myöhemmin.
Ohjelmista tulee olla selkeitä niin ohjelmoijalle kuin muille ohjelmaa tarkasteleville. Selkeys saadaan aikaan sopivalla luokkarakenteella ja hyvien nimeämiskäytänteiden seuraamisella. Jokaisella luokalla tulee olla selkeä vastuu, johon liittyviä tehtäviä luokka hoitaa. Metodeja käytetään toiston vähentämiseen ja luokkien sisäisten toimintojen jäsentämiseen. Myös metodilla tulee olla selkeä vastuu eli metodien ei tule olla liian pitkiä ja liian montaa asiaa tekeviä. Liian montaa asiaa tekevät monimutkaiset metodit tuleekin pilkkoa useiksi pienemmiksi apumetodeiksi joita alkuperäinen metodi kutsuu. Hyvät ohjelmoijat ohjelmoivat koodia, jota he ja heidän työkaverinsa ymmärtävät myös viikkoja koodin kirjoittamisen jälkeenkin.
Ymmärrettävään koodiin liittyy niin kuvaava muuttujien, metodien ja luokkien nimeäminen kuin ohjelmakoodin ilmavuus ja johdonmukainen sisentäminen. Tutkitaan seuraavaa esimerkkiä käyttöliittymästä, jossa käyttäjä voi ostaa ja myydä esineitä. Vaikka esimerkissä ainoat ostettavat ja myytävät asiat ovat porkkanoita, eikä niistä pidetä kirjaa, voisi käyttöliittymää laajentaa esimerkiksi siten että sille annettaisiin myytävien tavaroiden varasto konstruktorin parametrina.
public class Kayttoliittyma { private Scanner lukija; public Kayttoliittyma(Scanner lukija) { this.lukija = lukija; } public void kaynnista() { while (true) { String komento = lukija.nextLine(); if (komento.equals("lopeta")) { break; } else if (komento.equals("osta")) { String luettu = null; while(true) { System.out.print("Mitä ostetaan: "); luettu = lukija.nextLine(); if(luettu.equals("porkkana") { break; } else { System.out.println("Ei löydy!"); } } System.out.println("Ostettu!"); } else if (komento.equals("myy")) { String luettu = null; while(true) { System.out.print("Mitä myydään: "); luettu = lukija.nextLine(); if(luettu.equals("porkkana") { break; } else { System.out.println("Ei löydy!"); } } System.out.println("Myyty!"); } } } }
Huomaamme esimerkissä heti monta ongelmakohtaa. Ensimmäinen pulma liittyy kaynnista
-metodin sisällön suuruuteen. Huomaamme että metodia voi pienentää siirtämällä muiden komentojen kuin lopeta
-komennon käsittelyn erilliseen metodiin.
public class Kayttoliittyma { private Scanner lukija; public Kayttoliittyma(Scanner lukija) { this.lukija = lukija; } public void kaynnista() { while (true) { String komento = lukija.nextLine(); if (komento.equals("lopeta")) { break; } else { hoidaKomento(komento); } } } public void hoidaKomento(String komento) { if (komento.equals("osta")) { String luettu = null; while(true) { System.out.print("Mitä ostetaan: "); luettu = lukija.nextLine(); if(luettu.equals("porkkana") { break; } else { System.out.println("Ei löydy!"); } } System.out.println("Ostettu!"); } else if (komento.equals("myy")) { String luettu = null; while(true) { System.out.print("Mitä myydään: "); luettu = lukija.nextLine(); if(luettu.equals("porkkana") { break; } else { System.out.println("Ei löydy!"); } } System.out.println("Myyty!"); } } }
Metodissa hoidaKomento
on vielä toisteisuutta syötteen lukemiseen liittyen. Huomaamme että lukemisessa toistuu aina muutama merkkijono. Kun ostetaan kysytään "Mitä ostetaan: ", kun myydään kysytään "Mitä myydään: ". Molemmat lukemiskohdat odottavat merkkijonoa "porkkana" ja tulostavat "Ei löydy!" jos käyttäjä ei syötä merkkijonoa "Porkkana". Luodaan tätä varten erillinen metodi public String lueKayttajalta(String kysymys)
, joka kysyy käyttäjältä haluttua merkkijonoa. Huomaa että jos käyttöliittymässämme olisi käytössä jonkinlainen varastonhallintaolio, vertaisimme sen sisältämiä esineitä käyttäjän syötteeseen porkkanan sijasta.
public class Kayttoliittyma { private Scanner lukija; public Kayttoliittyma(Scanner lukija) { this.lukija = lukija; } public void kaynnista() { while (true) { String komento = lukija.nextLine(); if (komento.equals("lopeta")) { break; } else { hoidaKomento(komento); } } } public void hoidaKomento(String komento) { if (komento.equals("osta")) { String luettu = lueKayttajalta("Mitä ostetaan: "); System.out.println("Ostettu!"); } else if (komento.equals("myy")) { String luettu = lueKayttajalta("Mitä myydään: "); System.out.println("Myyty!"); } } public String lueKayttajalta(String kysymys) { while(true) { System.out.print(kysymys); String luettu = lukija.nextLine(); if(luettu.equals("porkkana") { return luettu; } else { System.out.println("Ei löydy!"); } } } }
Ohjelma on nyt pilkottu sopiviksi osiksi. Huomaamme kuitenkin että kaynnista
-metodin yhteydessä on turha else
-haara. Jos ohjelman suoritus päätyy if
-haaraan, suoritetaan komento break
ja poistutaan toistolauseesta. Voimme siis poistaa else
-haaran jolloin hoidaKomento
-metodin suorittaminen on omalla rivillään. Samanlainen tilanne on myös metodissa lueKayttajalta
. Siistitään se myös samalla.
public class Kayttoliittyma { private Scanner lukija; public Kayttoliittyma(Scanner lukija) { this.lukija = lukija; } public void kaynnista() { while (true) { String komento = lukija.nextLine(); if (komento.equals("lopeta")) { break; } hoidaKomento(komento); } } public void hoidaKomento(String komento) { if (komento.equals("osta")) { String luettu = lueKayttajalta("Mitä ostetaan: "); System.out.println("Ostettu!"); } else if (komento.equals("myy")) { String luettu = lueKayttajalta("Mitä myydään: "); System.out.println("Myyty!"); } } public String lueKayttajalta(String kysymys) { while(true) { System.out.print(kysymys); String luettu = lukija.nextLine(); if(luettu.equals("porkkana") { return luettu; } System.out.println("Ei löydy!"); } } }
Yllä kuvaavaamme ohjelman pilkkomista pienempiin osiin kutsutaan refaktoroinniksi. Refaktoroinnissa ohjelman toiminta pysyy samana, mutta sisäinen rakenne muuttuu selkeämmäksi ja ylläpidettävämmäksi. Nykyinen versiomme on huomattavasti selkeämpi alkuperäiseen ohjelmaan verrattuna. Ohjelmassa on toki vieläkin parannettavaa. Esimerkiksi metodin hoidaKomento
voi pilkkoa ostamis- ja myymistoiminnallisuutta hoitaviin metodeihin.
public class Kayttoliittyma { private Scanner lukija; public Kayttoliittyma(Scanner lukija) { this.lukija = lukija; } public void kaynnista() { while (true) { String komento = lukija.nextLine(); if (komento.equals("lopeta")) { break; } hoidaKomento(komento); } } public void hoidaKomento(String komento) { if (komento.equals("osta")) { komentoOsta(); } else if (komento.equals("myy")) { komentoMyy(); } } public void komentoOsta() { String luettu = lueKayttajalta("Mitä ostetaan: "); System.out.println("Ostettu!"); } public void komentoMyy() { String luettu = lueKayttajalta("Mitä myydään: "); System.out.println("Myyty!"); } public String lueKayttajalta(String kysymys) { while(true) { System.out.print(kysymys); String luettu = lukija.nextLine(); if(luettu.equals("porkkana") { return luettu; } System.out.println("Ei löydy!"); } } }
Nyt ohjelma on rakenteeltaan riittävän selkeä, metodit ovat kuvaavasti nimettyjä ja jokaisella metodilla on oma pieni tehtävä, josta metodi huolehtii. Huomaa että emme lisänneet refaktoroidessa ohjelmaan uutta toiminnallisuutta, muokkasimme vain rakennetta selkeämmäksi.
Tietääksemme kukaan ei ole oppinut ohjelmoimaan luentoja kuuntelemalla. Ohjelmointitaidon kehittymisen kannalta harjoittelu ja kertaaminen on tärkeää. Ohjelmointitaitoa on verrattu niin kielten puhumiseen kuin instrumentin soittamiseen, kummassakin kehittyy vain harjoittelemalla. Oikeastaan, esimerkiksi hyvät viulistit eivät ole hyviä vain sen takia, että he ovat harjoitelleet paljon. Harjoittelua motivoi myös se, että se on hauskaa. Samaa voi sanoa ohjelmoijista.
Linus Torvaldsin sanoin "Most good programmers do programming not because they expect to get paid or get adulation by the public, but because it is fun to program.".
Tohtori Luukkainen on kirjoittanut listan jota kannattaa seurata ohjelmoidessa ja siinä kehittyessä. Seuraa listan neuvoja kunnes osaat ne unissasikin.
Olemme tähän mennessä käyttäneet kahta erilaista näkyvyyteen liittyvää avainsanaa metodien ja oliomuuttujien määrittelyssä. Avainsana public
asettaa metodit ja muuttujat kaikille näkyviksi. Esimerkiksi luokan metodit ja konstruktorit merkitään usein määreellä public, jolloin niitä voi kutsua luokan ulkopuolelta.
Avainsana private
taas piilottaa metodit ja muuttujat luokan sisälle. Metodia, jolla on määre private
, ei pysty kutsumaan luokan ulkopuolelta.
public class Kirja { private String nimi; private String sisalto; public Kirja(String nimi, String sisalto) { this.nimi = nimi; this.sisalto = sisalto; } public String getNimi() { return this.nimi; } public String getSisalto() { return this.sisalto; } // ... }
Yllä olevasta Kirja-luokasta luotujen olioiden tietoihin pääsee käsiksi vain kirjan julkisten metodien kautta. Private-määreellä merkityt oliomuuttujat ovat näkyvillä ja käsiteltävissä vain luokan sisäisessä koodissa. Jos kirjalla olisi private-määreellinen metodi, ei sitäkään voisi käyttää muualta kuin Kirja-luokan sisältä.
Sitten itse asiaan, eli harjoitteluun!
Laadi tehtavapohjan mukana tulevalle luokalle Hymiot
apumetodi private static void tulostaHymioityna(String merkkijono)
. Metodin tulee tulostaa annettu merkkijono hymiöillä ympyröitynä. Käytä hymiönä merkkijonoa :)
.
tulostaHymioityna("\\:D/");
:):):):):) :) \:D/ :) :):):):):)
Huomaa, että merkkijonoon on kirjoitettava \\ jotta saadaan tulostumaan merkki \.
Huom! Jos merkkijonon pituus on pariton, tulee ylimääräinen välilyönti lisätä annetun merkkijonon oikealle puolelle.
tulostaHymioityna("\\:D/"); tulostaHymioityna("87.");
:):):):):) :) \:D/ :) :):):):):) :):):):):) :) 87. :) :):):):):)
Kannattaa ensin miettiä montako hymiötä minkäkin pituiselle merkkijonolle tulee tulostaa. Merkkijonon pituuden saa selville siihen liittyvällä length
-metodilla. Ala- ja ylärivin hymiöiden tulostamiseen auttaa toistolause, keskimmäisellä rivillä selviät normaalilla tulostuskomennolla. Pituuden parittomuuden voit tarkistaa jakojäännöksen avulla merkkijono.length() % 2 == 1
.
Tässä tehtävässä luodaan merkkijonomuuntaja, joka koostuu kahdesta luokasta. Luokka Muunnos muuttaa yksittäiset merkit toiseksi, Muuntaja sisältää joukon Muunnoksia ja muuttaa merkkijonoja sisältämiensä Muunnos-olioiden avulla.
Luo luokka Muunnos
, jolla on seuraavat toiminnot:
public Muunnos(char muunnettava, char muunnettu)
luo olion joka tekee muunnoksia merkiltä muunnettava
merkille muunnettu
public String muunna(String merkkijono)
palauttaa muunnetun version annetusta merkkijonostaLuokkaa käytetään seuraavalla tavalla:
String sana = "porkkana"; Muunnos muunnos1 = new Muunnos('a', 'b'); sana = muunnos1.muunna(sana); System.out.println(sana); Muunnos muunnos2 = new Muunnos('k', 'x'); sana = muunnos2.muunna(sana); System.out.println(sana);
Yllä oleva esimerkki tulostaisi:
porkkbnb porxxbnb
Vihje: voit tehdä merkkien korvaamisen kahdella tavalla, joko suoraan hyödyntäen luokan String
metodia (etsi metodi itse!) tai käymällä merkkijonon läpi merkki merkiltä ja samalla muodostaen muunnetun merkkijonon.
Jos et käytä Stringin valmista metodia, niin kannattaa muistaa, että vaikka merkkijonoja vertaillaankin komennolla equals
yksittäisiä merkkejä vertaillaan operaattorin == avulla:
String sana = "porkkana"; String korvattuAat = ""; for ( int i=0; i < sana.length(); i++) { char merkki = sana.charAt(i); if ( merkki == 'a' ) { korvattuAat += '*' } else { korvattuAat += merkki; } } System.out.println(korvattuAat); // tulostu porkk*n*
Luo luokka Muuntaja
, jolla on seuraavat toiminnot:
public Muuntaja()
luo uuden muuntajanpublic void lisaaMuunnos(Muunnos muunnos)
lisää uuden muunnoksen muuntajaanpublic String muunna(String merkkijono)
suorittaa merkkijonolle kaikki lisätyt muunnokset niiden lisäysjärjestyksessä ja palauttaa muunnetun merkkijononLuokkaa käytetään seuraavalla tavalla:
Muuntaja skanditPois = new Muuntaja(); skanditPois.lisaaMuunnos(new Muunnos('ä', 'a')); skanditPois.lisaaMuunnos(new Muunnos('ö', 'o')); System.out.println(skanditPois.muunna("ääliö älä lyö, ööliä läikkyy"));
Yllä oleva esimerkki tulostaisi:
aalio ala lyo, oolia laikkyy
Vihje: Muunnokset kannattaa tallettaa Muuntajan oliomuuttujana olevalle listalle (samaan tyyliin kuin peruskurssilla talletettiin esim. pelaajat joukkueeseen, puhelinnumerot puhelinluetteloon tai kirjat kirjastoon). Muunnos suoritetaan siten, että muunnettavalle merkkijonolle suoritetaan muunnokset yksi kerrallaan seuraavan esimerkin tapaan:
ArrayList<Muunnos> muunnokset = new ArrayList<Muunnos>(); muunnokset.add( new Muunnos('a', 'b') ); muunnokset.add( new Muunnos('k', 'x') ); muunnokset.add( new Muunnos('o', 'å') ); String sana = "porkkana"; for (Muunnos muunnos : muunnokset) { sana = muunnos.muunna(sana); } System.out.println(sana); // tulostuu pårxxbnb
MUISTUTUS kun lisäät ohjelmaasi ArrayList:in, Scanner:in tai Random:in ei Java tunnista luokkaa ellet "importoi" sitä lisäämällä ohjelmatiedoston alkuun:
import java.util.ArrayList; // importoi ArrayListin import java.util.*; // importoi kaikki java.util:sissa olevat työkalut, mm. ArrayListin, Scannerin ja Randomin
Teemme tässä tehtävässä samantyylisen yksinkertaisen laskimen, joka oli jo ohjelmoinnin perusteiden viikon 1 materiaalissa. Tällä kertaa kiinnitämme kuitenkin huomiota ohjelman rakenteeseen. Erityisesti teemme main-metodista eli pääohjelmasta hyvin kevyen. Pääohjelmametodi ei tee oikeastaan mitään muuta kun käynistää ohjelman:
public class Paaohjelma { public static void main(String[] args) { Laskin laskin = new Laskin(); laskin.kaynnista(); } }
Pääohjelma siis ainoastaan luo varsinaisen sovelluslogiikan toteuttavan olion ja käynnistää sen. Tämä on oikea tyyli tehdä ohjelmia ja tulemme jatkossa usein pyrkimään tähän rakenteeseen.
Kommunikoidakseen käyttäjän kanssa laskin tarvitsee Scanner-olion. Kuten olemme huomanneet, on kokonaislukujen lukeminen scannerilla hieman työlästä. Teemme nyt erillisen luokan Lukija
joka kapseloi sisälleen Scanner-olion.
Toteuta luokka Lukija
ja lisää sille metodit
public String lueMerkkijono()
public int lueKokonaisluku()
Lukijan sisällä tulee olla oliomuuttujana Scanner-olio jota metodit käyttävät ohjelmoinnin perusteista tuttuun tyyliin. Muistathan että kokonaislukujen lukemisessa kannattaa ensin lukea koko rivi, jonka jälkeen rivi tulee muuttaa kokonaisluvuksi. Tässä on hyödyksi Integer
-luokan metodi parseInt
.
Laskin toimii seuraavan esimerkin mukaan:
komento: summa luku1: 4 luku2: 6 lukujen summa 10 komento: tulo luku1: 3 luku2: 2 lukujen tulo 6 komento: lopetus
Tee ohjelmaasi sovelluslogiikasta huolehtiva luokka Laskin
ja sille metodi public void kaynnista()
jonka sisältö on täsmälleen seuraava:
public void kaynnista() { while (true) { System.out.print("komento: "); String komento = lukija.lueMerkkijono(); if (komento.equals("lopetus")) { break; } if (komento.equals("summa")) { summa(); } else if (komento.equals("erotus")) { erotus(); } else if (komento.equals("tulo")) { tulo(); } } statistiikka(); }
Laskimellamme on operaatiot summa, erotus, tulo
.
Tee valmiiksi rungot metodeille summa
, erotus
, tulo
ja statistiikka
. Kaikkien tulee olla tyyppiä private void
, eli metodit ovat vain laskimen sisäisessä käytössä.
Lisää laskimelle oliomuuttuja jonka tyyppi on Lukija
, ja luo lukija konstruktorissa. Laskimessa ei saa olla erikseen Scanner-tyyppistä muuttujaa!
Toteuta nyt metodit summa
, erotus
ja tulo
siten, että ne toimivat yllä olevan esimerkin mukaan. Esimerkissä kysytään aina ensin komento, jonka jälkeen kysytään 2 lukua käyttäjältä, suoritetaan haluttu operaatio, ja tulostetaan operaation arvo. Huomaa, että lukujen kysyminen tapahtuu metodiensumma
, erotus
ja tulo
sisällä! Metodit käyttävät lukujen kysymiseen lukija-olioa. Metodien runko on siis seuraavanlainen:
private void summa() { System.out.print("luku1: "); int luku1 = // luetaan luku käyttäen lukija-olioa System.out.print("luku2: "); int luku2 = // luetaan luku käyttäen lukija-olioa // tulostetaan tulos ylläolevan esimerkin tapaan }
Metodissa kaynnista
olevan while
-silmukan jälkeen kutsutaan metodia statistiikka
. Metodin on tarkoitus tulostaa kuinka montaa kertaa laskin-oliolla suoritettiin joku laskutoimenpide. Esim:
komento: summa luku1: 4 luku2: 6 lukujen summa 10 komento: tulo luku1: 3 luku2: 2 lukujen tulo 6 komento: lopetus Laskuja laskettiin 2
Toteuta metodi private void statistiikka()
, ja tee statistiikan keräämiseen tarvittavat muutokset muualle Laskin-luokan koodiin.
Huom: jos ohjelmalle annetaan virheellinen komento (eli joku muu kuin summa, erotus, tulo, tai lopetus), ei laskin reagoi komentoon millään tavalla vaan jatkaa kysymällä seuraavaa komentoa. Statistiikka ei saa laskea virheellistä komentoa laskutoimenpiteeksi.
komento: integraali komento: erotus luku1: 3 luku2: 2 lukujen erotus 1 komento: lopetus Laskuja laskettiin 1
Bonustehtävä (ei testata): Syötteen lukeminen toistuu samanlaisena kaikissa kolmessa laskuoperaation suorittavassa metodissa. Posta koodistasi toisteisuus sopivan apumetodin avulla. Metodi voi palauttaa käyttäjältä luetut 2 lukua esim. taulukossa.
Java on vahvasti tyypitetty kieli, eli kaikilla sen muuttujilla on tyyppi. Muuttujien tyypit voidaan jakaa kahteen kategoriaan: alkeis- ja viitetyyppisiin muuttujiin. Kummankin kategorian tyypisillä muuttujilla on oma "lokero", joka sisältää niihin liittyvän tiedon. Alkeistyyppisillä muuttujilla lokeroon talletetaan muuttujien konkreettinen arvo, kun taas viitetyyppisten muuttujien lokero sisältää viitteen muuttujaan liittyvään konkreettiseen olioon.
Alkeistyyppisen muuttujan arvo tallennetaan muuttujaa varten luotuun lokeroon. Jokaisella alkeistyyppisellä muuttujalla on oma lokero ja oma arvo. Muuttujalle luodaan uusi lokero silloin kun se esitellään (esim. int numero;
). Lokeroon asetetaan arvo sijoitusoperaatiolla =
. Alla on esimerkki alkeistyyppisen int (kokonaisluku) -muuttujan esittelemisestä ja arvon asettamisesta samassa lausekkeessa.
int numero = 42;
Alkeistyyppisiä muuttujia ovat muun muassa int
, double
, char
, boolean
sekä harvemmin käyttämämme short
, float
, byte
ja long
. Myös void
on alkeistyyppi, mutta sillä ei ole omaa lokeroa tai arvoa. Void-tyyppiä käytetään silloin kun halutaan ilmaista että metodi ei palauta mitään arvoa.
Esitellään seuraavaksi kaksi alkeistyyppistä muuttujaa ja asetetaan niihin arvot.
int vitonen = 5; int kutonen = 6;
Yllä esiteltyjen alkeistyyppisten muuttujien nimet ovat vitonen
ja kutonen
. Muuttujaa vitonen
luodessa sitä varten luotuun lokeroon asetetaan arvo 5 (int vitonen = 5;
). Muuttujaa kutonen
luodessa sitä varten luotuun lokeroon asetetaan arvo 6 (int kutonen = 6;
). Muuttujat vitonen
ja kutonen
ovat kumpikin int
-tyyppisiä, eli kokonaislukuja.
Alkeistyyppiset muuttujat voi visualisoida laatikkoina joiden sisälle kuhunkin muuttujaan liittyvä arvo on tallennettu:
Tarkastellaan seuraavaksi alkeistyyppisten muuttujien arvojen kopioitumista.
int vitonen = 5; int kutonen = 6; vitonen = kutonen; // muuttuja vitonen sisältää nyt arvon 6, eli arvon joka oli muuttujassa kutonen kutonen = 64; // muuttuja kutonen sisältää nyt arvon 64 // muuttuja vitonen sisältää vieläkin arvon 6
Yllä esitellään muuttujat vitonen
ja kutonen
ja asetetaan niihin arvot. Tämän jälkeen muuttujan vitonen
lokeroon kopioidaan muuttujan kutonen
lokeron sisältämä arvo (vitonen = kutonen;
). Tässä vaiheessa muuttujan vitonen
lokeroon kopioituu muuttujan kutonen
sisältämä arvo. Jos muuttujan kutonen
arvoa muutetaan tämän jälkeen, ei muuttujan vitonen
sisältämä arvo muutu: muuttujan vitonen
arvo on sen omassa lokerossa eikä liity muuttujan kutonen
lokerossa olevaan arvoon millään tavalla. Lopputilanne kuvana.
Kun alkeistyyppinen muuttuja annetaan metodille parametrina, asetetaan metodin parametrille kopio annetun muuttujan lokerossa olevasta. Käytännössä myös metodin parametreillä on omat lokerot, joihin arvo kopioidaan kuten asetuslauseessa. Katsotaan seuraavaa metodia lisaaLukuun(int luku, int paljonko)
.
public int lisaaLukuun(int luku, int paljonko) { return luku + paljonko; }
Metodi lisaaLukuun
saa kaksi parametria, kokonaisluvut luku
ja paljonko
. Metodi palauttaa uuden luvun, joka on annettujen parametrien summa. Tutkitaan vielä metodin kutsumista.
int omaLuku = 10; omaLuku = lisaaLukuun(omaLuku, 15); // muuttuja omaLuku sisältää nyt arvon 25
Esimerkissä kutsutaan lisaaLukuun
-metodia muuttujalla omaLuku
ja arvolla 15
. Metodin muuttujiin luku
ja paljonko
kopioituvat arvot 10, eli muuttujan omaLuku
sisältö, ja 15. Metodi palauttaa muuttujien luku
ja paljonko
summan, eli 10 + 15 = 25
.
Huom! Edellisessä esimerkissä muuttujan omaLuku
arvo muuttuu ainoastaan koska metodin lisaaLukuun
palauttama arvo asetetaan siihen sijoituslausekkeella (omaLuku = lisaaLukuun(omaLuku, 15);
). Jos metodin lisaaLukuun
kutsu olisi seuraavanlainen, ei muuttujan omaLuku
arvo muutu.
int omaLuku = 10; lisaaLukuun(omaLuku, 15); // muuttuja omaLuku sisältää vieläkin arvon 10
Eri tietotyypeillä on omat minimi- ja maksimiarvonsa, eli arvot joita pienempiä tai suurempia ne eivät voi olla. Tämä johtuu Javan (ja useimpien ohjelmointikielten) sisäisestä tiedon esitysmuodosta, jossa tietotyyppien koot on ennalta määrätty.
Alla muutama Javan alkeistyyppi ja niiden minimi- ja maksimiarvot
Muuttujatyyppi | Selitys | Minimiarvo | Maksimiarvo |
---|---|---|---|
int | Kokonaisluku | -2 147 483 648 (Integer.MIN_VALUE ) |
2 147 483 647 (Integer.MAX_VALUE ) |
long | Iso kokonaisluku | -9 223 372 036 854 775 808 (Long.MIN_VALUE ) |
9 223 372 036 854 775 807 (Long.MAX_VALUE ) |
boolean | Totuusarvo |
true tai false
|
|
double | Liukuluku | Double.MIN_VALUE |
Double.MAX_VALUE |
Pyöristysvirheet
Liukulukuja käyttäessä kannattaa muistaa että liukuluvun arvo on aina arvio oikeasta arvosta. Koska liukuluvun, kuten kaikkien muidenkin alkeistyyppien sisältämä tietomäärä on rajoitettu, voidaan huomata yllättäviäkin pyöristysvirheitä. Esimerkiksi seuraava tilanne.
double eka = 0.39; double toka = 0.35; System.out.println(eka - toka);
Esimerkki tulostaa arvon 0.040000000000000036
. Ohjelmointikielet tarjoavat usein työkalut liukulukujen tarkempaa käsittelyä varten. Esimerkiksi Javassa on luokka BigDecimal, johon voi asettaa äärettömän pitkiä liukulukuja.
Liukulukuja vertaillessa pyöristysvirheisiin varaudutaan usein vertaamalla arvojen etäisyyttä toisistaan. Esimerkiksi edellisen esimerkin muuttujia käytettäessä vertailu eka - toka == 0.04
ei tuota toivottua tulosta pyöristysvirheen takia.
double eka = 0.39; double toka = 0.35; if((eka - toka) == 0.04) { System.out.println("Vertailu onnistui!"); } else { System.out.println("Vertailu epäonnistui!"); }
Vertailu epäonnistui!
Arvon etäisyyttä jostain luvusta voi tarkastella esimerkiksi seuraavasti. Apufunktio Math.abs
palauttaa sille annetun luvun itseisarvon.
double eka = 0.39; double toka = 0.35; double etaisyys = 0.04 - (eka - toka); if(Math.abs(etaisyys) < 0.0001) { System.out.println("Vertailu onnistui!"); } else { System.out.println("Vertailu epäonnistui!"); }
Viitetyyppiset muuttujat tallentavat niihin liittyvän tiedon viitteen taakse eli "langan päähän". Viitetyyppisten muuttujien lokerossa on viite tiedon sisältävään paikkaan. Toisin kuin alkeistyyppisillä muuttujilla, viitetyyppisillä muuttujilla ei ole rajoitettua arvoaluetta, koska niiden oikea arvo tai tieto on viitteen takana. Oleellinen ero alkeistyyppisiin muuttujiin on se, että usea viitetyyppinen muuttuja voi viitata samaan olioon.
Tutkitaan kahden viitetyyppisen muuttujan luontia. Esimerkeissä käytetään seuraavaa luokkaa Laskuri:
public class Laskuri { private int arvo; public Laskuri(int alkuarvo) { // Konstruktori this.arvo = alkuarvo; } public void kasvataArvoa() { this.arvo = this.arvo + 1; } public int annaArvo() { return arvo; } }
Pääohjelma:
Laskuri bonusLaskuri = new Laskuri(5); Laskuri axeLaskuri = new Laskuri(6);
Esimerkissä luodaan ensin viitetyyppinen muuttuja bonusLaskuri
. Komentoa new
kutsuessa varataan tila muuttujan tietoa varten, suoritetaan new
-kutsua seuraavan konstruktorin koodi, ja palautetaan viite juuri luotuun olioon. . Palautettu viite asetetaan sijoitusoperaattorilla =
muuttujaan bonusLaskuri
. Sama tapahtuu muuttujalle nimeltä axeLaskuri
. Kuvana viitetyyppi kannattaa ajatella siten, että muuttuja sisältää "langan" tai "nuolen", jonka päässä on olio itse. Muuttuja ei siis sisällä oliota, vaan viitteen olioon liittyviin tietoihin.
Tutkitaan seuraavaksi viitetyyppisen muuttujan kopioitumista.
Laskuri bonusLaskuri = new Laskuri(5); Laskuri axeLaskuri = new Laskuri(6); bonusLaskuri = axeLaskuri; // muuttujaan bonusLaskuri kopioidaan muuttujan axeLaskuri sisältämä viite, // eli viite Laskuri-tyyppiseen olioon joka on saanut konstruktorissaan arvon 6
Viitetyyppistä muuttujaa kopioitaessa (yllä bonusLaskuri = axeLaskuri;
) muuttujan viite kopioituu. Yllä muuttujan bonusLaskuri
lokeroon kopioituu muuttujan axeLaskuri
lokerossa oleva viite. Nyt kummatkin oliot viittaavat samaan paikkaan!
Jatketaan yllä olevaa esimerkkiä ja asetetaan muuttujaan axeLaskuri
uusi viite, joka osoittaa kutsulla new Laskuri(10)
luotuun olioon.
Laskuri bonusLaskuri = new Laskuri(5); Laskuri axeLaskuri = new Laskuri(6); bonusLaskuri = axeLaskuri; // muuttujaan bonusLaskuri kopioidaan muuttujan axeLaskuri sisältämä viite, // eli viite Laskuri-tyyppiseen olioon joka on saanut konstruktorissaan arvon 6 axeLaskuri = new Laskuri(10); // muuttujaan axeLaskuri asetetaan uusi viite, joka osoittaa // new Laskuri(10) - kutsulla luotuun Laskuri-olioon // muuttuja bonusLaskuri sisältää vieläkin viitteen Laskuri-olioon, joka sai konstruktorissaan arvon 6
Esimerkissä tehdään käytännössä samat operaatiot kuin alkeistyyppi-kappaleessa olevassa asetusesimerkissä. Äskeisessä esimerkissä kopioimme viitetyyppisten muuttujien viitteitä, kun taas alkeistyyppisiin muuttujiin liittyvässä esimerkissä kopioimme alkeistyyppien arvoja. Kummassakin tapauksessa siis lokeron sisältö kopioidaan, alkeistyyppisten muuttujien lokero sisältää arvon, viitetyyppisten muuttujien lokero sisältää viitteen.
Edellisen esimerkin lopussa kukaan ei viittaa Laskuriolioon, joka sai arvokseen 5 sen konstruktorissa. Javassa oleva roskienkeruumekanismi käy ajallaan poistamassa tällaiset turhat oliot. Lopputilanne kuvana:
Tarkastellaan vielä kolmatta esimerkkiä, joka näyttää viite- ja alkeistyyppisten muuttujien oleellisen eron.
Laskuri bonusLaskuri = new Laskuri(5); Laskuri axeLaskuri = new Laskuri(6); bonusLaskuri = axeLaskuri; // muuttujaan bonusLaskuri kopioidaan muuttujan axeLaskuri sisältämä viite, // eli viite Laskuri-tyyppiseen olioon joka on saanut konstruktorissaan arvon 6 axeLaskuri.kasvataArvoa(); // kasvatetaan axeLaskuri-viitteen takana olevan olion arvoa yhdellä System.out.println(bonusLaskuri.annaArvo()); System.out.println(axeLaskuri.annaArvo());
7 7
Koska asetuksen bonusLaskuri = axeLaskuri;
jälkeen bonusLaskuri
-muuttuja viittaa samaan olioon kuin axeLaskuri
-muuttuja, on kummankin laskurin arvo 7 vaikka kasvatuksia on tehty vain yksi. Tämä johtuu siitä että kummatkin laskurit viittaavat samaan olioon.
Kuvana tilanne on ehkä selkeämpi. Kutsu axeLaskuri.kasvataArvoa()
kasvattaa muuttujan axeLaskuri
viittaaman olion sisältämää muuttujan arvo
arvoa yhdellä. Koska muuttuja bonusLaskuri
viittaa samaan olioon, palauttaa kutsu bonusLaskuri.annaArvo()
saman muuttujan arvon, jota aiempi kutsu axeLaskuri.kasvataArvoa()
kasvatti.
Seuraavassa esimerkissä on kolme viitetyyppistä muuttujaa, jotka kaikki osoittavat samaan Laskuri
-olioon.
Laskuri bonus = new Laskuri(5); Laskuri ihq = bonus; Laskuri lennon = bonus;
Esimerkissä luodaan vain yksi Laskuri
-olio, mutta kaikki kolme Laskuri
-tyyppistä muuttujaa osoittavat lopussa siihen. Tällöin kaikki metodikutsut viitteille bonus
, ihq
ja lennon
muokkaavat samaa oliota. Vielä kerran: viitetyyppisiä muuttujia kopioitaessa viitteet kopioituvat. Kuvana:
Katsotaan kopioitumista vielä esimerkillä.
Laskuri bonus = new Laskuri(5); Laskuri ihq = bonus; Laskuri lennon = bonus; lennon = new Laskuri(3);
Kun muuttujan lennon
sisältö, eli viite muuttuu, se ei vaikuta muuttujien bonus
tai ihq
sisältämiin viitteisiin. Muuttujan arvoa asetettaessa muutetaan aina vain muuttujan oman lokeron sisältöä. Kuvana:
Kun viitetyyppinen muuttuja annetaan parametrina metodille, luodaan metodin parametrille kopio annetusta muuttujan viitteestä. Parametrilla on siis oma lokero, johon viite kopioidaan. Alkeistyyppisistä muuttujista poiketen kopioimme viitteen, emmekä arvoa, eli voimme muokata viitteen takana olevaa oliota myös metodin sisällä. Oletetaan että metodimme on alla esitelty public void lisaaLaskuriin(Laskuri laskuri, int paljonko)
.
public void lisaaLaskuriin(Laskuri laskuri, int paljonko) { for (int i = 0; i < paljonko; i++) { laskuri.kasvataArvoa(); } }
Metodille lisaaLaskuriin
annetaan kaksi parametria, viitetyyppinen muuttuja ja alkeistyyppinen muuttuja. Kumpaankin muuttujaan liittyvän lokeron sisältö kopioidaan metodin parametrien omiin lokeroihin. Viitetyyppiselle parametrille laskuri
kopioituu viite ja alkeistyyppiselle parametrille paljonko
kopioituu arvo. Metodi kutsuu Laskuri
-tyyppisen parametrin metodia kasvataArvoa()
paljonko
-muuttujan sisältämän arvon määrän. Tutkitaan vielä metodin kutsumista.
int kertoja = 10; Laskuri bonus = new Laskuri(10); lisaaLaskuriin(bonus, kertoja); // muuttujan bonus sisäinen arvo on nyt 20
Esimerkissä kutsutaan lisaaLaskuriin()
-metodia muuttujilla bonus
ja kertoja
. Metodin parametriin laskuri
ja paljonko
kopioituvat siis viitetyyppisen muuttujan bonus
viite, ja alkeistyyppisen muuttujan kertoja
arvo 10
. Metodi suorittaa metodissa olevalle muuttujalle laskuri
paljonko
muuttujan määrittelemän määrän kasvataArvoa()
-metodikutsuja. Tämä kuvana:
Metodissa on siis pääohjelmasta täysin erilliset muuttujat!
Viitetyyppisestä muuttujasta kopioituu metodin sisäiseen muuttujaan viite, eli metodin sisäinen muuttuja viittaa vieläkin samaan olioon. Alkeistyyppisestä muuttujasta kopioituu arvo, eli metodin sisäisellä muuttujalla on täysin oma arvonsa.
Metodi näkee saman laskurin johon muuttuja bonus
viittaa, eli metodin tekemä muutos vaikuttaa suoraan olioon. Alkeistyyppien suhteen tilanne on toinen, eli metodille tulee ainoastaan kopio muuttujan kertoja
arvosta. Metodista käsin ei siis voi muuttaa alkeistyyppisten muuttujien arvoja.
Kun metodi palauttaa viitetyyppisen muuttujan, palauttaa se viitteen muualla sijaitsevaan olioon. Metodin palauttaman viitetyyppisen muuttujan voi asettaa muuttujalle samalla tavalla kuin normaalikin asetus tapahtuu, eli yhtäsuuruusmerkin avulla. Katsotaan metodia public Laskuri luoLaskuri(int alkuarvo)
, joka luo uuden viitetyyppisen muuttujan.
public Laskuri luoLaskuri(int alkuarvo) { return new Laskuri(alkuarvo); }
Metodi luoLaskuri palauttaa metodissa luotuun olioon viittaavan viitteen uusiLaskuri
. Uusi olio luodaan aina metodia kutsuttaessa, seuraavassa esimerkissä luomme kaksi erillistä Laskuri
-tyyppistä oliota.
Laskuri bonus = luoLaskuri(10); Laskuri lennon = luoLaskuri(10);
Metodi luoLaskuri
luo aina uuden Laskuri
-tyyppisen olion. Ensimmäisessä kutsussa, eli kutsussa Laskuri bonus = luoLaskuri(10);
asetetaan metodin palauttama viite viitetyyppiseen muuttujaan bonus
. Toisessa metodikutsussa luodaan uusi viite, joka asetetaan muuttujaan lennon
. Muuttujat bonus
ja lennon
eivät sisällä samaa viitettä, sillä metodi luo aina uuden olion ja palauttaa viitteen luotuun olioon.
Kerrataan ja täsmennetään Ohjelmoinnin perusteiden luvussa 30 käsiteltyä asiaa. Staattisuudella ja ei-staattisisuudella erotetaan se, mihin muuttuja tai metodi liittyy. Staattiset metodit liittyvät aina luokkaan, kun taas ei-staattiset metodit voivat muokata olion omia muuttujia.
Static-määreen saavat metodit eivät liity olioihin vaan luokkiin. On mahdollista määritellä myös luokkakohtaisia muuttujia lisäämällä muuttujan eteen määre static
. Esimerkiksi Integer.MAX_VALUE
, Long.MIN_VALUE
ja Double.MAX_VALUE
ovat kaikki staattisia muuttujia. Staattisia muuttujia ja metodeja käytetään luokan nimen kautta, esimerkiksi LuokanNimi.muuttuja
tai LuokanNimi.metodi()
.
Luokkakirjastoksi kutsutaan luokkaa, jossa on yleiskäyttöisiä metodeja ja muuttujia. Esimerkiksi Javan Math
-luokka on luokkakirjasto. Se tarjoaa muun muassa Math.PI
-muuttujan. Omien luokkakirjastojen toteuttaminen on usein hyödyllistä. Esimerkiksi Helsingin Seudun Liikenne (HSL) voisi pitää lippujensa hintoja luokkakirjastossa, josta ne löytyisi tarvittaessa.
public class HslHinnasto { public static final double KERTALIPPU_AIKUINEN = 2.50; public static final double RAITIOVAUNULIPPU_AIKUINEN = 2.50; }
Avainsana final
muuttujan määrittelyssä kertoo ettei muuttujaan voi asettaa uutta arvoa kun se on kerran asetettu. Final-tyyppiset muuttujat ovat vakioita, ja niiden tulee sisältää aina arvo. Esimerkiksi suurimman kokonaisluvun kertova luokkamuuttuja Integer.MAX_VALUE
on vakiotyyppinen luokkamuuttuja.
Jos käytössämme on yllä esitelty luokka HslHinnasto
, voivat kaikki ohjelmat, jotka tarvitsevat kerta- tai raitiovaunulipun hintaa, päästä niihin käsiksi HslHinnasto
-luokan kautta. Seuraavassa esimerkissä esitellään luokka Ihminen
, jolla on metodi onkoRahaaKertalippuun()
, joka käyttää HslHinnasto
-luokasta löytyvää lipun hintaa.
public class Ihminen { private String nimi; private double rahat; // muut oliomuuttujat // konstruktori public boolean onkoRahaaKertalippuun() { if(this.rahat >= HslHinnasto.KERTALIPPU_AIKUINEN) { return true; } return false; } // muut luokkaan Ihminen liittyvät metodit }
Metodi public boolean onkoRahaaKertalippuun()
vertaa luokan Ihminen
oliomuuttujaa rahat
HslHinnasto
-luokan staattiseen muuttujaan KERTALIPPU_AIKUINEN
. Metodia onkoRahaaKertalippuun()
voi kutsua vain olioviitteen kautta. Esimerkiksi:
Ihminen matti = new Ihminen(); if (matti.onkoRahaaKertalippuun()) { System.out.println("Ostetaan kertalippu."); } else { System.out.println("Mennään pummilla."); }
Huomaa nimeämiskäytäntö! Kaikki vakiot eli final-määreellä varustetut muuttujat kirjoitetaan ISOLLA_JA_ALAVIIVOILLA.
Staattiset metodit toimivat vastaavasti. Esimerkiksi Luokka HslHinnasto
saattaisi kapseloida muuttujat ja antaa vain aksessorit niihin. Aksessoriksi kutsutaan metodia, jolla voi joko lukea muuttujan arvon tai sijoittaa muuttujalle uuden arvon.
public class HslHinnasto { private static final double KERTALIPPU_AIKUINEN = 2.50; private static final double RAITIOVAUNULIPPU_AIKUINEN = 2.50; public static double annaKertalipunHinta() { // Aksessori return KERTALIPPU_AIKUINEN; } public static double annaRaitiovaunulipunHinta() { // Aksessori return RAITIOVAUNULIPPU_AIKUINEN; } }
Tällöin Ihminen
-luokan toteutuksessa tulee kutsua metodia annaKertalipunHinta()
sen sijaan että kutsuttaisiin muuttujaa suoraan.
public class Ihminen { private String nimi; private double rahat; // muut oliomuuttujat // konstruktori public boolean onkoRahaaKertalippuun() { if(this.rahat >= HslHinnasto.annaKertalipunHinta()) { return true; } return false; } // muut luokkaan Ihminen liittyvät metodit }
Vaikka Java periaatteessa mahdollistaa staattisten muuttujien käytön, ei niille useinkaan ole tarvetta. Usein staattisten muuttujien käyttö aiheuttaa ongelmia ohjelman rakenteelle, sillä staattiset muuttujat toimivat pahamaineisten globaalien muuttujien tapaan. Tällä kurssilla käytämme ainoastaan vakioarvoisia eli final määreen omaavia staattisia muuttujia!
Ei-staattiset metodit ja muuttujat liittyvät olioihin. Oliomuuttujat, eli attribuutit määritellään luokan alussa. Kun olio luodaan new
-kutsulla, kaikille oliomuuttujille varataan tila olioon liittyvän viitteen päähän. Muuttujien arvot ovat oliokohtaisia, eli jokaisella oliolla on omat muuttujien arvot. Tutkitaan taas luokkaa Ihminen
, jolla on oliomuuttujat nimi
ja rahat
.
public class Ihminen { private String nimi; private double rahat; // muut tiedot }
Kun luokasta Ihminen luodaan uusi ilmentymä, alustetaan myös siihen liittyvät muuttujat. Jos viitetyyppistä muuttujaa nimi
ei alusteta, saa se arvokseen null-viitteen. Lisätään luokan Ihminen toteutukseen vielä konstruktori ja muutama metodi.
public class Ihminen { private String nimi; private double rahat; // konstruktori public Ihminen(String nimi, double rahat) { this.nimi = nimi; this.rahat = rahat; } public String getNimi() { return this.nimi; } public double getRahat() { return this.rahat; } public void lisaaRahaa(double summa) { if(summa > 0) { this.rahat += summa; } } public boolean onkoRahaaKertalippuun() { if(this.rahat >= HslHinnasto.annaKertalipunHinta()) { return true; } return false; } }
Konstruktori Ihminen(String nimi, double rahat)
luo uuden ihmisolion ja palauttaa viitteen siihen. Metodi getNimi()
palauttaa viitteen nimi
-olioon, ja getRahat()
-metodi palauttaa alkeistyyppisen muuttujan rahat
. Metodi lisaaRahaa(double summa)
lisää oliomuuttujaan rahat
parametrina annetun summan jos parametrin arvo on suurempi kuin 0.
Oliometodeja kutsutaan olion viitteen kautta. Seuraava koodiesimerkki luo uuden Ihmis-olion, lisää sille rahaa, ja lopuksi tulostaa sen nimen. Huomaa että metodikutsut ovat muotoa olionNimi.metodinNimi()
Ihminen matti = new Ihminen("Matti", 5.0); matti.lisaaRahaa(5); if (matti.onkoRahaaKertalippuun()) { System.out.println("Ostetaan kertalippu."); } else { System.out.println("Mennään pummilla."); }
Esimerkki tulostaa "Ostetaan kertalippu
".
Luokan sisäisiä ei-staattisia metodeja voi kutsua myös ilman olio-etuliitettä metodiin liittyvissä luokissa. Esimerkiksi seuraava toString()
-metodi Ihminen
luokalle, joka kutsuu olioon liittyvää metodia getNimi()
.
public class Ihminen { // aiemmin toteutetun luokan sisältö public String toString() { return this.getNimi(); } }
Metodi toString()
kutsuu siis luokan sisäistä juuri käsiteltävään olioon liittyvää getNimi()
-metodia. Etuliite this
korostaa kutsun liittyvän juuri tähän olioon.
Ei-staattiset metodit voivat kutsua myös staattisia, eli luokkakohtaisia metodeja. Toisaalta, luokkakohtaiset metodit eivät voi kutsua oliokohtaisia metodeja ilman viitettä itse olioon, sillä ilman viitettä ei päästä käsiksi olioon liittyviin tietoihin.
Metodien sisällä määriteltävät muuttujat ovat metodien suorituksessa käytettäviä apumuuttujia, eikä niitä tule sekoittaa oliomuuttujiin. Alla esimerkki metodista, jossa luodaan metodiin paikallinen muuttuja. Muuttuja indeksi
on olemassa ja käytössä vain metodin suorituksen ajan.
public class ... { ... public static void tulostaTaulukko(String[] taulukko) { int indeksi = 0; while(indeksi < taulukko.length) { System.out.println(taulukko[indeksi]); indeksi++; } } }
Metodissa tulostaTaulukko()
luodaan apumuuttuja indeksi
jota käytetään taulukon läpikäynnissä. Muuttuja indeksi
on olemassa vain metodin suorituksen ajan.
Tässä tehtäväsarjassa tehdään luokat Tavara
, Matkalaukku
ja Lastiruuma
, joiden avulla harjoitellaan olioita, jotka sisältävät toisia olioita.
Tee luokka Tavara
, josta muodostetut oliot vastaavat erilaisia tavaroita. Tallennettavat tiedot ovat tavaran nimi ja paino (kg).
Lisää luokkaan seuraavat metodit:
public String getNimi()
, joka palauttaa tavaran nimenpublic int getPaino()
, joka palauttaa tavaran painonpublic String toString()
, joka palauttaa merkkijonon muotoa "nimi (paino kg)"Seuraavassa on luokan käyttöesimerkki:
public class Main { public static void main(String[] args) { Tavara kirja = new Tavara("Aapiskukko", 2); Tavara puhelin = new Tavara("Nokia 3210", 1); System.out.println("Kirjan nimi: " + kirja.getNimi()); System.out.println("Kirjan paino: " + kirja.getPaino()); System.out.println("Kirja: " + kirja); System.out.println("Puhelin: " + puhelin); } }
Ohjelman tulostuksen tulisi olla seuraava:
Kirjan nimi: Aapiskukko Kirjan paino: 2 Kirja: Aapiskukko (2 kg) Puhelin: Nokia 3210 (1 kg)
Tee luokka Matkalaukku
. Matkalaukkuun liittyy tavaroita ja maksimipaino, joka määrittelee tavaroiden suurimman mahdollisen yhteispainon.
Lisää luokkaan seuraavat metodit:
public void lisaaTavara(Tavara tavara)
, joka lisää parametrina annettavan tavaran matkalaukkuun. Metodi ei palauta mitään arvoa.public String toString()
, joka palauttaa merkkijonon muotoa "x tavaraa (y kg)"Tavarat kannattaa tallentaa ArrayList
-olioon:
ArrayList<Tavara> tavarat = new ArrayList<Tavara>();
Luokan Matkalaukku
tulee valvoa, että sen sisältämien tavaroiden yhteispaino ei ylitä maksimipainoa. Jos maksimipaino ylittyisi lisättävän tavaran vuoksi, metodi lisaaTavara
ei saa lisätä uutta tavaraa laukkuun.
Seuraavassa on luokan käyttöesimerkki:
public class Main { public static void main(String[] args) { Tavara kirja = new Tavara("Aapiskukko", 2); Tavara puhelin = new Tavara("Nokia 3210", 1); Tavara tiiliskivi = new Tavara("tiiliskivi", 4); Matkalaukku matkalaukku = new Matkalaukku(5); System.out.println(matkalaukku); matkalaukku.lisaaTavara(kirja); System.out.println(matkalaukku); matkalaukku.lisaaTavara(puhelin); System.out.println(matkalaukku); matkalaukku.lisaaTavara(tiiliskivi); System.out.println(matkalaukku); } }
Ohjelman tulostuksen tulisi olla seuraava:
0 tavaraa (0 kg) 1 tavaraa (2 kg) 2 tavaraa (3 kg) 2 tavaraa (3 kg)
Ilmoitukset "0 tavaraa" ja "1 tavaraa" eivät ole kovin hyvää suomea – paremmat muodot olisivat "ei tavaroita" ja "1 tavara". Tee tämä muutos luokkaan Matkalaukku
.
Nyt edellisen ohjelman tulostuksen tulisi olla seuraava:
ei tavaroita (0 kg) 1 tavara (2 kg) 2 tavaraa (3 kg) 2 tavaraa (3 kg)
Lisää luokkaan Matkalaukku
seuraavat metodit:
tulostaTavarat
, joka tulostaa kaikki matkalaukussa olevat tavaratyhteispaino
, joka palauttaa tavaroiden yhteispainonSeuraavassa on luokan käyttöesimerkki:
public class Main { public static void main(String[] args) { Tavara kirja = new Tavara("Aapiskukko", 2); Tavara puhelin = new Tavara("Nokia 3210", 1); Tavara tiiliskivi = new Tavara("tiiliskivi", 4); Matkalaukku matkalaukku = new Matkalaukku(10); matkalaukku.lisaaTavara(kirja); matkalaukku.lisaaTavara(puhelin); matkalaukku.lisaaTavara(tiiliskivi); System.out.println("Matkalaukussa on seuraavat tavarat:"); matkalaukku.tulostaTavarat(); System.out.println("Yhteispaino: " + matkalaukku.yhteispaino() + " kg"); } }
Ohjelman tulostuksen tulisi olla seuraava:
Matkalaukussa on seuraavat tavarat: Aapiskukko (2 kg) Nokia 3210 (1 kg) Tiiliskivi (4 kg) Yhteispaino: 7 kg
Muokkaa myös luokkaasi siten, että käytät vain kahta oliomuuttujaa. Toinen sisältää maksimipainon, toinen on lista laukussa olevista tavaroista.
Lisää vielä luokkaan Matkalaukku
metodi raskainTavara
, joka palauttaa painoltaan suurimman tavaran. Jos yhtä raskaita tavaroita on useita, metodi voi palauttaa minkä tahansa niistä. Metodin tulee palauttaa olioviite. Jos laukku on tyhjä, palauta arvo null.
Seuraavassa on luokan käyttöesimerkki:
public class Main { public static void main(String[] args) { Tavara kirja = new Tavara("Aapiskukko", 2); Tavara puhelin = new Tavara("Nokia 3210", 1); Tavara tiiliskivi = new Tavara("Tiiliskivi", 4); Matkalaukku matkalaukku = new Matkalaukku(10); matkalaukku.lisaaTavara(kirja); matkalaukku.lisaaTavara(puhelin); matkalaukku.lisaaTavara(tiiliskivi); Tavara raskain = matkalaukku.raskainTavara(); System.out.println("Raskain tavara: " + raskain); } }
Ohjelman tulostuksen tulisi olla seuraava:
Raskain tavara: Tiiliskivi (4 kg)
Tee luokka Lastiruuma
, johon liittyvät seuraavat metodit:
public void lisaaMatkalaukku(Matkalaukku laukku)
, joka lisää parametrina annetun matkalaukun lastiruumaanpublic String toString()
, joka palauttaa merkkijonon muotoa "x matkalaukkua (y kg)"Tallenna matkalaukut sopivaan ArrayList
-rakenteeseen.
Luokan Lastiruuma
tulee valvoa, että sen sisältämien matkalaukkujen yhteispaino ei ylitä maksimipainoa. Jos maksimipaino ylittyisi uuden matkalaukun vuoksi, metodi lisaaMatkalaukku
ei saa lisätä uutta matkalaukkua.
Seuraavassa on luokan käyttöesimerkki:
public class Main { public static void main(String[] args) { Tavara kirja = new Tavara("Aapiskukko", 2); Tavara puhelin = new Tavara("Nokia 3210", 1); Tavara tiiliskivi = new Tavara("tiiliskivi", 4); Matkalaukku matinLaukku = new Matkalaukku(10); matinLaukku.lisaaTavara(kirja); matinLaukku.lisaaTavara(puhelin); Matkalaukku pekanLaukku = new Matkalaukku(10); pekanLaukku.lisaaTavara(tiiliskivi); Lastiruuma lastiruuma = new Lastiruuma(1000); lastiruuma.lisaaMatkalaukku(matinLaukku); lastiruuma.lisaaMatkalaukku(pekanLaukku); System.out.println(lastiruuma); } }
Ohjelman tulostuksen tulisi olla seuraava:
2 matkalaukkua (7 kg)
Lisää luokkaan Lastiruuma
metodi public void tulostaTavarat()
, joka tulostaa kaikki lastiruuman matkalaukuissa olevat tavarat.
Seuraavassa on luokan käyttöesimerkki:
public class Main { public static void main(String[] args) { Tavara kirja = new Tavara("Aapiskukko", 2); Tavara puhelin = new Tavara("Nokia 3210", 1); Tavara tiiliskivi = new Tavara("tiiliskivi", 4); Matkalaukku matinLaukku = new Matkalaukku(10); matinLaukku.lisaaTavara(kirja); matinLaukku.lisaaTavara(puhelin); Matkalaukku pekanLaukku = new Matkalaukku(10); pekanLaukku.lisaaTavara(tiiliskivi); Lastiruuma lastiruuma = new Lastiruuma(1000); lastiruuma.lisaaMatkalaukku(matinLaukku); lastiruuma.lisaaMatkalaukku(pekanLaukku); System.out.println("Ruuman matkalaukuissa on seuraavat tavarat:"); lastiruuma.tulostaTavarat(); } }
Ohjelman tulostuksen tulisi olla seuraava:
Ruuman matkalaukuissa on seuraavat tavarat: Aapiskukko (2 kg) Nokia 3210 (1 kg) tiiliskivi (4 kg)
Testataan vielä, että lastiruuman toiminta on oikea eikä maksimipaino pääse ylittymään. Tee Main-luokkaan metodi public static void lisaaMatkalaukutTiiliskivilla(Lastiruuma lastiruuma)
, joka lisää parametrina annettuun lastiruumaan 100 matkalaukkua, joissa jokaisessa on yksi tiiliskivi. Tiiliskivien painot ovat 1, 2, 3, ..., 100 kg.
Ohjelman runko on seuraava:
public class Main { public static void main(String[] args) { Lastiruuma lastiruuma = new Lastiruuma(1000); lisaaMatkalaukutTiiliskivilla(lastiruuma); System.out.println(lastiruuma); } public static void lisaaMatkalaukutTiiliskivilla(Lastiruuma lastiruuma) { // 100 matkalaukun lisääminen, jokaiseen tulee tiiliskivi } }
Ohjelman tulostus on seuraava:
44 matkalaukkua (990 kg)
Hajautustaulu on yksi Javan yleishyödyllisistä tietorakenteista. Hajautustaulun ideana on laskea olioon liittyvälle avaimelle, eli yksilöivälle arvolle (esimerkiksi henkilötunnus, opiskelijanumero, puhelinnumero), indeksi hajautustaulun sisältämästä taulukosta. Indeksin avulla Avaimen muuttamista indeksiksi kutsutaan hajautukseksi, joka tarkoittaa indeksin laskemista. Hajautus tapahtuu aina tietyn hajautusfunktion avulla, joka takaa että tietyllä avaimella saadaan aina sama indeksi.
Avaimen perusteella lisääminen ja hakeminen mahdollistaa erittäin nopean hakemisen. Sen sijaan että tutkisimme taulukon alkiot järjestyksessä (pahimmassa tapauksessa joudumme käymään kaikki alkiot läpi), tai etsisimme arvoa binäärihaulla (pahimmassa tapauksessa käymme taulukon kokoon liittyvän logaritmisen määrän alkoita läpi), voimme periaatteessa katsoa tasan yhtä taulukon indeksiä ja tarkistaa onko indeksiin tallennettu arvoa vai ei.
Hajautustaulu käyttää avaimen arvon laskemiseen Object
-luokassa määriteltyä hashCode()
-metodia, jonka jokainen toteutettu luokka perii. Emme kuitenkaan tutustu hajautustaulun toteutukseen tarkemmin tällä kurssilla. Perintään tutustumme muutaman viikon kuluttua.
Javan luokka HashMap
kapseloi eli piilottaa hajautustaulun toteutuksen, ja tarjoaa valmiit metodit sen käyttöön.
Hajautustaulua luodessa tarvitaan kaksi tyyppiparametria, avainmuuttujan tyyppi ja tallennettavan olion tyyppi. Seuraava esimerkki käyttää avaimena String
-tyyppistä oliota, ja tallennettavana oliona String
-tyyppistä oliota.
HashMap<String, String> numerot = new HashMap<String, String>(); numerot.put("Yksi", "Uno"); numerot.put("Kaksi", "Dos"); String kaannos = numerot.get("Yksi"); System.out.println(kaannos); System.out.println(numerot.get("Kaksi")); System.out.println(numerot.get("Kolme")); System.out.println(numerot.get("Uno"));
Uno Dos null null
Esimerkissä luodaan hajatustaulu, jonka avaimena ja tallennettavana oliona merkkijono. Hajautustauluun lisätään tietoa put()
-metodilla, joka saa parametreikseen viitteet avaimeen ja tallennettavaan olioon. Metodi get()
-palauttaa parametrina annettuun avaimeen liittyvän viitteen tai arvon null
jos avaimella ei löydy viitettä.
Hajautustaulussa tiettyä avainta vastaa aina yksi arvo. Jos jo olemassaolevalla avaimella tallennetaan uusi arvo, katoaa vanha arvo hajautustaulusta.
HashMap<String, String> numerot = new HashMap<String, String>(); numerot.put("Yksi", "Uno"); numerot.put("Kaksi", "Dos"); numerot.put("Yksi", "Ein"); String kaannos = numerot.get("Yksi"); System.out.println(kaannos); System.out.println(numerot.get("Kaksi")); System.out.println(numerot.get("Kolme")); System.out.println(numerot.get("Uno"));
Koska avain "Yksi
" asetetaan uudestaan, on esimerkin tulostus nyt seuraavanlainen.
Ein Dos null null
Luo main
-metodissa
uusi HashMap<String,String>
-olio. Tallenna tähän
HashMappiin seuraavien henkilöiden nimet ja lempinimet niin, että
nimi on avain ja lempinimi on arvo. Käytä pelkkiä pieniä kirjaimia.
Tämän jälkeen hae HashMapistä mikaelin lempinimi ja tulosta se.
Testit edellyttävät että kirjoitat nimet pienellä alkukirjaimella.
Tutkitaan hajautustaulun toimintaa seuraavaksi kirjastoesimerkin avulla. Kirjastosta voi hakea kirjoja kirjan nimen perusteella, nimi toimii siis kirjan avaimena. Jos annetulle nimelle löytyy kirja, saadaan siihen liittyvä viite ja samalla kirjan tiedot. Luodaan ensin esimerkkiluokka Kirja
, jolla on oliomuuttujina nimi ja kirjaan liittyvä sisältö.
public class Kirja { private String nimi; private String sisalto; private int julkaisuvuosi; public Kirja(String nimi, int julkaisuvuosi, String sisalto) { this.nimi = nimi; this.julkaisuvuosi = julkaisuvuosi; this.sisalto = sisalto; } public String getNimi() { return this.nimi; } public void setNimi(String nimi) { this.nimi = nimi; } public int getJulkaisuvuosi() { return this.julkaisuvuosi; } public void setJulkaisuvuosi(int julkaisuvuosi) { this.julkaisuvuosi = julkaisuvuosi; } public String getSisalto() { return this.sisalto; } public void setSisalto(String sisalto) { this.sisalto = sisalto; } public String toString() { return "Nimi: " + this.nimi + " (" + this.julkaisuvuosi + ")\n" + "Sisältö: " + this.sisalto; } }
Luodaan seuraavaksi hajautustaulu, joka käyttää avaimena kirjan nimeä eli String-tyyppistä oliota, ja tallentaa viitteitä Kirja
-olioihin.
HashMap<String, Kirja> kirjahakemisto = new HashMap<String, Kirja>();
Yllä oleva hajautustaulu käyttää avaimena String
-oliota. Laajennetaan esimerkkiä siten, että kirjahakemistoon lisätään kaksi kirjaa, "Järki ja tunteet"
ja "Ylpeys ja ennakkoluulo"
.
Kirja jarkiJaTunteet = new Kirja("Järki ja tunteet", 1811, "..."); Kirja ylpeysJaEnnakkoluulo = new Kirja("Ylpeys ja ennakkoluulo", 1813, "...."); HashMap<String, Kirja> kirjahakemisto = new HashMap<String, Kirja>(); kirjahakemisto.put(jarkiJaTunteet.getNimi(), jarkiJaTunteet); kirjahakemisto.put(ylpeysJaEnnakkoluulo.getNimi(), ylpeysJaEnnakkoluulo);
Kirjahakemistosta voi hakea kirjoja kirjan nimellä. Haku kirjalla "Viisasteleva sydän"
ei tuota osumaa, jolloin hajautustaulu palauttaa null
-viitteen. Kirja "Ylpeys ja ennakkoluulo" kuitenkin löytyy.
Kirja kirja = kirjahakemisto.get("Viisasteleva sydän"); System.out.println(kirja); System.out.println(); kirja = kirjahakemisto.get("Ylpeys ja ennakkoluulo"); System.out.println(kirja);
null Nimi: Ylpeys ja ennakkoluulo (1813) Sisältö: ...
Hajautustaulu on hyödyllinen silloin kun tiedetään avain minkä perusteella halutaan hakea. Avaimet ovat aina yksilöllisiä, joten saman avaimen taakse ei voi tallettaa montaa eri oliota. Tallennettava olio voi toki olla lista tai toinen hajautustaulukko!
Yllä olevan kirjahakemiston ongelmana on se, että kirjoja haettaessa täytyy muistaa kirjan nimi merkki merkiltä oikein. Javan valmis String
-luokka tarjoaa meille välineet tähänkin. Metodi toLowerCase()
muuttaa merkkijonon kirjaimet pieniksi, ja metodi trim()
poistaa merkkijonon alusta ja lopusta tyhjät merkit (esimerkiksi välilyönnit). Tietokoneen käyttäjät usein kirjoittavat tekstin alkuun tai loppuun vahingossa välilyöntejä.
String teksti = "Ylpeys ja ennakkoluulo "; teksti = teksti.toLowerCase(); // teksti nyt "ylpeys ja ennakkoluulo " teksti = teksti.trim() // teksti nyt "ylpeys ja ennakkoluulo"
Luodaan luokka Kirjasto
, joka kapseloi kirjat sisältävän hajautustaulun ja mahdollistaa kirjoitusasusta riippumattoman kirjojen haun. Lisätään Kirjasto
-luokalle metodit lisaaKirja(Kirja kirja)
ja poistaKirja(String kirjanNimi)
. Huomaamme jo nyt että merkkijonon siistimistä tarvitsisi useammassa metodissa, joten tehdään siitä erillinen metodi private String siistiMerkkijono(String merkkijono)
.
public class Kirjasto { private HashMap<String, Kirja> hakemisto; public Kirjasto() { this.hakemisto = new HashMap<String, Kirja>(); } public void lisaaKirja(Kirja kirja) { String nimi = siistiMerkkijono(kirja.getNimi()); if(this.hakemisto.containsKey(nimi)) { System.out.println("Kirja on jo kirjastossa!"); } else { hakemisto.put(nimi, kirja); } } public void poistaKirja(String kirjanNimi) { kirjanNimi = siistiMerkkijono(kirjanNimi); if(this.hakemisto.containsKey(kirjanNimi)) { this.hakemisto.remove(kirjanNimi); } else { System.out.println("Kirjaa ei löydy, ei voida poistaa!"); } } private String siistiMerkkijono(String merkkijono) { if (merkkijono == null) { return ""; } merkkijono = merkkijono.toLowerCase(); return merkkijono.trim(); } }
Toteutetaan kirjan hakutoiminnallisuus siten, että kirjaa haetaan hajautusrakenteesta sen nimellä.
public Kirja haeKirja(String kirjanNimi) { kirjanNimi = siistiMerkkijono(kirjanNimi); return this.hakemisto.get(kirjanNimi); }
Yllä oleva metodi palauttaa haetun kirjan jos sellainen löytyy, muulloin null
-arvon. Voimme myös käydä kaikki hakemiston avaimet läpi yksitellen, etsien esimerkiksi alkuosaa kirjan nimestä. Tällä tavalla etsiessä menetämme kuitenkin hajautustaulun nopeusedun, sillä huonoimmassa tapauksessa joudumme käymään kaikkien kirjojen nimet läpi. Hakeminen alkuosan perusteella onnistuisi hajautustaulun keySet()
-metodin avulla. Metodi keySet()
palauttaa avaimet joukossa, jonka voi käydä läpi for-each
-toistolauseella.
public Kirja haeKirjaNimenAlkuosalla(String kirjanAlkuosa) { kirjanAlkuosa = siistiMerkkijono(kirjanAlkuosa); for (String avain: this.hakemisto.keySet()) { if (avain.startsWith(kirjanAlkuosa)) { return this.hakemisto.get(avain); } } return null; }
Jätämme yllä olevan metodin kuitenkin pois kirjastostamme. Kirjastosta puuttuu oleellisista toiminnoista vielä kirjojen listaaminen. Luodaan metodi public ArrayList<Kirja> kirjalista()
, joka palauttaa listan kirjaston kirjoista. Metodi kirjalista
hyödyntää hajautustaulun tarjoamaa values()
-metodia. Metodi values()
palauttaa kokoelman kirjaston kirjoista, jonka voi antaa parametrina ArrayList
-luokan konstruktorille.
public class Kirjasto { private HashMap<String, Kirja> hakemisto; public Kirjasto() { this.hakemisto = new HashMap<String, Kirja>(); } public Kirja haeKirja(String kirjanNimi) { kirjanNimi = siistiMerkkijono(kirjanNimi); return this.hakemisto.get(kirjanNimi); } public void lisaaKirja(Kirja kirja) { String nimi = siistiMerkkijono(kirja.getNimi()); if(this.hakemisto.containsKey(nimi)) { System.out.println("Kirja on jo kirjastossa!"); } else { this.hakemisto.put(nimi, kirja); } } public void poistaKirja(String kirjanNimi) { kirjanNimi = siistiMerkkijono(kirjanNimi); if(this.hakemisto.containsKey(kirjanNimi)) { this.hakemisto.remove(kirjanNimi); } else { System.out.println("Kirjaa ei löydy, ei voida poistaa!"); } } public ArrayList<Kirja> kirjalista() { return new ArrayList<Kirja>(this.hakemisto.values()); } private String siistiMerkkijono(String merkkijono) { if (merkkijono == null) { return ""; } merkkijono = merkkijono.toLowerCase(); return merkkijono.trim(); } }
Yksi ohjelmoinnin periaatteista on ns. DRY-periaate (Don't Repeat Yourself), jolla pyritään välttämään saman koodin olemista useassa paikassa. Merkkijonon pieneksi muuttaminen ja trimmaus, eli tyhjien merkkien poisto alusta ja lopusta, olisi toistunut useasti kirjastoluokassamme ilman metodia siistiMerkkijono
. Toistuvaa koodia ei usein huomaa ennen kuin sitä on jo kirjoittanut, jolloin sitä päätyy koodiin lähes pakosti. Tässä ei kuitenkaan ole mitään pahaa. Tärkeintä on että siistit koodiasi sitä mukaa kun huomaat siistimistä vaativia tilanteita.
Huomaa että hajautustaulun avain ja tallennettava olio ovat aina viitetyyppisiä. Jos haluat käyttää alkeistyyppisiä muuttujia avaimena tai tallennettavana arvona, on niille olemassa myös viitetyyppiset vastineet. Alla on esitelty muutama.
Alkeistyyppi | Viitetyyppinen vastine |
---|---|
int | Integer |
double | Double |
char | Character |
Java oikeastaan kapseloi alkeistyyppiset muuttujat automaattisesti viitetyyppisiksi muuttujiksi tarvittaessa. Vaikka numero 1
on alkeistyyppinen muuttuja, voit käyttää sitä suoraan Integer
-tyyppisenä avaimena seuraavasti.
HashMap<Integer, String> taulu = new HashMap<Integer, String>(); taulu.put(1, "Ole!");
Alkeistyyppisten muuttujien automaattista muunnosta viitetyyppisiksi kutsutaan Javassa auto-boxingiksi, eli automaattiseksi "laatikkoon" asettamiseksi. Vastaava onnistuu myös toisinpäin. Voimme luoda metodin, joka palauttaa hajautustaulun sisältämän kokonaisluvun. Seuraavassa esimerkissä olevassa metodissa lisaaBongaus
tapahtuu automaattinen tyyppimuunnos.
public class Rekisteribongaus { private HashMap<String, Integer> bongatut; public Rekisteribongaus() { this.bongatut = new HashMap<String, Integer>(); } public void lisaaBongaus(String nimi, int numero) { this.bongatut.put(nimi, numero); } public int viimeisinBongaus(String nimi) { this.bongatut.get(nimi); } }
Vaikka hajautustaulu sisältää Integer-tyyppisiä olioita, osaa Java myös muuntaa tietyt viitetyyppiset muuttujat myös niiden alkeistyyppisiksi vastineiksi. Esimerkiksi Integer
-oliot muuttuvat tarpeen vaatiessa int
-tyyppisiksi muuttujiksi. Tässä piilee kuitenkin vaara! Jos yritämme muuttaa null-viitettä numeroksi, näemme virheen java.lang.reflect.InvocationTargetException. Kun teemme automaattista muunnosta, tulee varmistaa että muunnettava arvo ei ole null. Yllä olevassa ohjelmassa oleva viimeisinBongaus
-metodi tulee korjata esimerkiksi seuraavasti.
public int viimeisinBongaus(String nimi) { if(this.bongatut.containsKey(nimi) { return this.bongatut.get(nimi); } return 0; }
Luo luokka Velkakirja
, jolla on seuraavat toiminnot:
public Velkakirja()
luo uuden velkakirjanpublic void asetaLaina(String kenelle, double maara)
tallettaa velkakirjaan merkinnän lainasta tietylle henkilölle.public double paljonkoVelkaa(String kuka)
palauttaa velan määrän annetun henkilön nimen perusteellaLuokkaa käytetään seuraavalla tavalla:
Velkakirja matinVelkakirja = new Velkakirja(); matinVelkakirja.asetaLaina("Arto", 51.5); matinVelkakirja.asetaLaina("Mikael", 30); System.out.println(matinVelkakirja.paljonkoVelkaa("Arto")); System.out.println(matinVelkakirja.paljonkoVelkaa("Joel"));
Yllä oleva esimerkki tulostaisi:
51.5 0
Ole tarkkana tilanteessa, jossa kysytään velattoman ihmisen velkaa. Kertaa luvun 4.3 lopun esimerkki!
Huom! Velkakirjan ei tarvitse huomioida vanhoja lainoja. Kun asetat uuden velan henkilölle jolla on vanha velka, vanha velka unohtuu.
Velkakirja matinVelkakirja = new Velkakirja(); matinVelkakirja.asetaLaina("Arto", 51.5); matinVelkakirja.asetaLaina("Arto", 10.5); System.out.println(matinVelkakirja.paljonkoVelkaa("Arto"));
10.5
Tässä tehtäväsarjassa toteutetaan sanakirja, josta voi hakea suomen kielen sanoille englanninkielisiä käännöksiä. Sanakirjan tekemisessä käytetään HashMap
-tietorakennetta.
Toteuta luokka nimeltä Sanakirja
. Luokalla on aluksi seuraavat metodit:
public String kaanna(String sana)
metodi palauttaa parametrinsa käännöksen. Jos sanaa ei tunneta, palautetaan null.public void lisaa(String sana, String kaannos)
metodi lisää sanakirjaan uuden käännöksenToteuta luokka Sanakirja siten, että sen ainoa oliomuuttuja on HashMap
-tietorakenne.
Testaa sanakirjasi toimintaa:
Sanakirja sanakirja = new Sanakirja(); sanakirja.lisaa("apina", "monkey"); sanakirja.lisaa("banaani", "banana"); sanakirja.lisaa("cembalo", "harpsichord"); System.out.println(sanakirja.kaanna("apina")); System.out.println(sanakirja.kaanna("porkkana"));
monkey null
Lisää sanakirjaan metodi public int sanojenLukumaara()
, joka palauttaa sanakirjassa olevien sanojen lukumäärän.
Sanakirja sanakirja = new Sanakirja(); sanakirja.lisaa("apina", "monkey"); sanakirja.lisaa("banaani", "banana"); System.out.println(sanakirja.sanojenLukumaara()); sanakirja.lisaa("cembalo", "harpsichord"); System.out.println(sanakirja.sanojenLukumaara());
2 3
Lisää sanakirjaan metodi public ArrayList<String> kaannoksetListana()
joka palauttaa sanakirjan sisällön listana avain = arvo muotoisia merkkijonoja.
Sanakirja sanakirja = new Sanakirja(); sanakirja.lisaa("apina", "monkey"); sanakirja.lisaa("banaani", "banana"); sanakirja.lisaa("cembalo", "harpsichord"); ArrayList<String> kaannokset = sanakirja.kaannoksetListana(); for(String kaannos: kaannokset) { System.out.println(kaannos); }
banaani = banana apina = monkey cembalo = harpsichord
Vihje: saat käytyä kaikki HashMapissa olevat avaimet läpi metodin keySet
avulla seuraavasti:
HashMap<String, String> sanaparit = new HashMap<String, String>(); sanaparit.put("apina", "eläin"); sanaparit.put("etelä", "ilmansuunta"); sanaparit.put("sauerkraut", "ruoka"); for ( String avain : sanaparit.keySet() ) { System.out.print( avain + " " ); } // tulostuu apina etelä sauerkraut
Harjoitellaan tässäkin tehtävässä erillisen tekstikäyttöliittymän tekemistä. Luo luokka Tekstikayttoliittyma
, jolla on seuraavat metodit
public Tekstikayttoliittyma(Scanner lukija, Sanakirja sanakirja)
public void kaynnista()
, joka käynnistää tekstikäyttöliittymän.Tekstikäyttöliittymä tallettaa konstruktorin parametrina saamansa lukijan ja sanakirjan oliomuuttujiin. Muita oliomuuttujia ei tarvita. Käyttäjän syötteen lukeminen tulee hoitaa konstruktorin parametrina saatua lukija-olioa käyttäen! Myös kaikki käännökset on talletettava konstruktorin parametrina saatuun sanakirja-olioon. Tekstikäyttöliittymä ei saa luoda olioita itse!
HUOM: vielä uudelleen edellinen, eli Tekstikäyttöliittymä ei saa luoda itse skanneria vaan sen on käytettävä parametrina saamaansa skanneria syötteiden lukemiseen!
Tekstikäyttöliittymässä tulee aluksi olla vain komento lopeta
, joka poistuu tekstikäyttöliittymästä. Jos käyttäjä syöttää jotain muuta, käyttäjälle sanotaan "Tuntematon komento".
Scanner lukija = new Scanner(System.in); Sanakirja sanakirja = new Sanakirja(); Tekstikayttoliittyma kayttoliittyma = new Tekstikayttoliittyma(lukija, sanakirja); kayttoliittyma.kaynnista();
Komennot: lopeta - poistuu käyttöliittymästä Komento: apua Tuntematon komento. Komento: lopeta Hei hei!
Lisää tekstikäyttöliittymälle komennot lisaa
ja kaanna
. Komento lisaa
lisää kysyy käyttäjältä sanaparin ja lisää sen sanakirjaan. Komento kaanna
kysyy käyttäjältä sanaa ja tulostaa sen käännöksen.
Scanner lukija = new Scanner(System.in); Sanakirja sanakirja = new Sanakirja(); Tekstikayttoliittyma kayttoliittyma = new Tekstikayttoliittyma(lukija, sanakirja); kayttoliittyma.kaynnista();
Komennot: lisaa - lisää sanaparin sanakirjaan kaanna - kysyy sanan ja tulostaa sen käännöksen lopeta - poistuu käyttöliittymästä Komento: lisaa Suomeksi: porkkana Käännös: carrot Komento: kaanna Anna sana: porkkana Käännös: carrot Komento: lopeta Hei hei!
Ohjelman testaaminen käsin on toivottoman työlästä. Syötteen antaminen on kuitenkin mahdollista automatisoida esimerkiksi syöttämällä Scanner-oliolle luettava merkkijono. Alla on annettu esimerkki siitä, miten yllä olevassa tehtävässä luotua ohjelmaa voi testata automaattisesti.
String syote = "kaanna\n" + "apina\n" + "kaanna\n" + "juusto\n" + "lisaa\n" + "juusto\n" + "cheese\n" + "kaanna\n" + "juusto\n" + "lopeta\n"; Scanner lukija = new Scanner(syote); Sanakirja sanakirja = new Sanakirja(); Tekstikayttoliittyma kayttoliittyma = new Tekstikayttoliittyma(lukija, sanakirja); kayttoliittyma.kaynnista();
Ohjelma tulostus näyttää vain ohjelman antaman tulostuksen, ei käyttäjän tekemiä komentoja.
Komennot: lisaa - lisää sanaparin sanakirjaan kaanna - kysyy sanan ja tulostaa sen käännöksen lopeta - poistuu käyttöliittymästä Komento: Anna sana: Tuntematon sana! Komento: Anna sana: Tuntematon sana! Komento: Suomeksi: Käännös: Komento: Anna sana: Käännös: cheese Komento: Hei hei!
Merkkijonon antaminen Scanner-luokalle korvaa näppäimistöltä luettavan syötteen merkkijonolla. Merkkijonomuuttujan syote
sisältö siis "simuloi" käyttäjän antamaa syötettä. Rivinvaihto syötteeseen merkitään \n
:llä. Jokainen yksittäinen rivinvaihtomerkkiin loppuva osa syote
-merkkijonossa siis vastaa käyttäjän yhteen nextLine()-komentoon antamaa syötettä.
Testityötettä on helppo muuttaa, esim. seuraavassa syötetään lisää uusia sanoja sanakirjaan:
String syote = "lisaa\n" + "juusto\n" + "cheese\n" + "lisaa\n" + "olut\n" + "beer\n" + "lisaa\n" + "kirja\n" + "book\n" + "lisaa\n" + "tietokone\n" + "computer\n" + "lisaa\n" + "auto\n" + "car\n" + "lopeta\n";
Kun haluat testata ohjelmasi toimintaa jälleen käsin, vaihda Scanner-olion konstruktorin parametriksi System.in
, eli järjestelmän syötevirta.
Ohjelman toiminnan oikeellisuus pitää edelleen tarkastaa itse ruudulta. Tulostus voi olla aluksi hieman hämmentävää, sillä automatisoitu syöte ei näy ruudulla ollenkaan.
Lopullinen tavoite on automatisoida myös ohjelman tulostuksen oikeellisuden tarkastaminen niin hyvin, että ohjelman testaus ja testituloksen analysointi onnistuu "nappia painamalla". Palaamme aiheeseen myöhemmin kurssin aikana.
Kurssilla käyttämämme Java-ohjelmointikieli koostuu kolmesta osasta. Ensimmäinen osa on ohjelmointikielen syntaksi ja semantiikka: muuttujien määrittelytapa, kontrollirakenteiden muoto, ja muuttujien ja luokkien rakenne ja niiden toimintatapa. Toinen osa on JVM, eli Java Virtual Machine, jota käytetään ohjelmien suorittamiseen. Java-ohjelmat käännetään tavukoodiksi, jota voidaan suorittaa missä tahansa koneessa olevan JVM:n avulla. Emme oikeastaan ole törmänneet ohjelmien kääntämiseen, sillä ohjelmointiympäristöt tekevät sen ohjelmoijien puolesta. Silloin tällöin ohjelmointiympäristö ei toimi odotetulla tavalla, ja saatamme joutua valitsemaan clean & build, joka poistaa vanhat lähdekoodit ja kääntää ohjelman uudestaan. Kolmantena osana on API (Application Programming Interface), eli ohjelmointirajapinta tai standardikirjasto.
API on ohjelmointikielen tarjoama joukko valmiita luokkia, joita ohjelmoija voi käyttää omissa projekteissaan. Esimerkiksi luokat ArrayList
, Arrays
, Collections
, ja String
ovat kaikki osa Javan valmista APIa. Javan version 7 API-kuvaus löytyy osoitteesta http://docs.oracle.com/javase/7/docs/api/. Osoitteessa olevan sivuston vasemmassa laidassa on Javan valmiille luokille luokkakuvaus. Etsiessäsi sivulta luokkaa ArrayList
, löydät sivun http://docs.oracle.com/javase/7/docs/api/java/util/ArrayList.html, joka kuvaa ArrayList
-luokan rakenteen, konstruktorit, ja metodit.
NetBeans osaa näyttää luokkaan liittyvän APIn tarvittaessa. Kun kirjoitat luokan nimen ja lisäät siihen liittyvän import-lauseen, voit klikata luokan nimeä oikealla hiirennapilla ja valita Show Javadoc. Tämä avaa luokkaan liittyvän API-kuvauksen selaimessa.
Jokaisella viikolla on yksi laajempi tehtävä, jossa pääset vapaasti suunnittelemaan ohjelman rakenteen, käyttöliittymän ulkomuoto ja vaaditut komennot on määritelty ennalta. Ohjelmoinnin jatkokurssin ensimmäinen vapaasti suunniteltava tehtävä on Lentokenttä.
Huom: jotta testit toimisivat, ohjelmasi saa luoda vain yhden Scanner-olion. Älä myöskään käytä staattisia muuttujia, testit suorittavat ohjelman useita kertoja joten staattisiin muuttujiin edellisillä suorituskerroilla jääneet arvot todennäköisesti häiritsevät testien toimintaa!
Lentokenttä-tehtävässä toteutetaan lentokentän hallintasovellus. Lentokentän hallintasovelluksessa hallinnoidaan lentokoneita ja lentoja. Lentokoneista tiedetään aina tunnus ja kapasiteetti. Lennoista tiedetään lennon lentokone, lähtöpaikan tunnus (esim. HEL) ja kohdepaikan tunnus (esim. BAL).
Sekä lentokoneita että lentoja voi olla useita. Sama lentokone voi myös lentää useaa eri lentoa (useaa eri reittiä). Sovelluksen tulee toimia kahdessa vaiheessa. Ensin lentokentän työntekijä syöttää lentokoneiden ja lentojen tietoja hallintakäyttöliittymässä.
Kun käyttäjä poistuu hallintakäyttöliittymässä, avautuu käyttäjälle mahdollisuus lentopalvelun käyttöön. Lentopalvelussa on kolme toimintoa; lentokoneiden tulostaminen, lentojen tulostaminen, ja lentokoneen tietojen tulostaminen. Tämän lisäksi käyttäjä voi poistua ohjelmasta valitsemalla vaihtoehdon x
. Jos käyttäjä syöttää epäkelvon komennon, kysytään komentoa uudestaan.
Lentokentän hallinta -------------------- Valitse toiminto: [1] Lisää lentokone [2] Lisää lento [x] Poistu hallintamoodista > 1 Anna lentokoneen tunnus: HA-LOL Anna lentokoneen kapasiteetti: 42 Valitse toiminto: [1] Lisää lentokone [2] Lisää lento [x] Poistu hallintamoodista > 1 Anna lentokoneen tunnus: G-OWAC Anna lentokoneen kapasiteetti: 101 Valitse toiminto: [1] Lisää lentokone [2] Lisää lento [x] Poistu hallintamoodista > 2 Anna lentokoneen tunnus: HA-LOL Anna lähtöpaikan tunnus: HEL Anna kohdepaikan tunnus: BAL Valitse toiminto: [1] Lisää lentokone [2] Lisää lento [x] Poistu hallintamoodista > 2 Anna lentokoneen tunnus: G-OWAC Anna lähtöpaikan tunnus: JFK Anna kohdepaikan tunnus: BAL Valitse toiminto: [1] Lisää lentokone [2] Lisää lento [x] Poistu hallintamoodista > 2 Anna lentokoneen tunnus: HA-LOL Anna lähtöpaikan tunnus: BAL Anna kohdepaikan tunnus: HEL Valitse toiminto: [1] Lisää lentokone [2] Lisää lento [x] Poistu hallintamoodista > x Lentopalvelu ------------ Valitse toiminto: [1] Tulosta lentokoneet [2] Tulosta lennot [3] Tulosta lentokoneen tiedot [x] Lopeta > 1 G-OWAC (101 henkilöä) HA-LOL (42 henkilöä) Valitse toiminto: [1] Tulosta lentokoneet [2] Tulosta lennot [3] Tulosta lentokoneen tiedot [x] Lopeta > 2 HA-LOL (42 henkilöä) (HEL-BAL) HA-LOL (42 henkilöä) (BAL-HEL) G-OWAC (101 henkilöä) (JFK-BAL) Valitse toiminto: [1] Tulosta lentokoneet [2] Tulosta lennot [3] Tulosta lentokoneen tiedot [x] Lopeta > 3 Mikä kone: G-OWAC G-OWAC (101 henkilöä) Valitse toiminto: [1] Tulosta lentokoneet [2] Tulosta lennot [3] Tulosta lentokoneen tiedot [x] Lopeta > x
Huom1: Testien kannalta on oleellista että käyttöliittymä toimii täsmälleen kuten yllä kuvattu. Ohjelman tulostamat menut kannattaneekin copypasteta tästä ohjelmakoodiin. Testit eivät oleta, että ohjelmasi on varautunut epäkelpoihin syötteisiin. Tämä tehtävä on kolmen yksittäisen tehtäväpisteen arvoinen.
Huom2: älä käytä luokkein nimissä skandeja, ne saattavat aiheuttaa ongelmia testeihin!
Ohjelman tulee käynnistyä kun tehtäväpohjassa oleva main-metodi suoritetaan.
Vielä uudelleen varoitus: jotta testit toimisivat, ohjelmasi saa luoda vain yhden Scanner-olion. Älä myöskään käytä staattisia muuttujia, testit suorittavat ohjelman useita kertoja joten staattisiin muuttujiin edellisillä suorituskerroilla jääneet arvot todennäköisesti häiritsevät testien toimintaa!