Ohjelmoinnin perusteet

Arto Vihavainen ja Matti Luukkainen

Huomautus lukijalle

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. Myös luennoilla tulet näkemään paljon livekoodausta.

Viikko 1

Ensimmäinen ohjelma

Nykyaikainen ohjelmointi tapahtuu lähes poikkeuksetta ohjelmointiympäristössä. Ohjelmointiympäristö sisältää joukon ohjelmoijaa auttavia aputoimintoja. Ohjelmointiympäristö ei rakenna ohjelmaa ohjelmoijan puolesta, mutta se muunmuassa vinkkaa helpoista virheistä ohjelmakoodissa ja auttaa ohjelmoijaa hahmottamaan ohjelman rakennetta.

Käytämme tällä kurssilla NetBeans-nimistä ohjelmointiympäristöä. Ohjeet NetBeansin ja kurssilla käytettävän tehtävien palautusautomaatin käyttöön löydät täältä.

Ohjelmoinnin aloitus ohjelmointiympäristössä

Ohje ohjelmoinnin aloittamiseen NetBeans-ohjelmointiympäristössä löytyy samasta paikasta kuin edellinen ohje.

Jatkossa julkaisemme pikkuhiljaa lisää ohjeita NetBeansin käyttöön. Vaikka ohjelma tuntuisi nyt sekavalta, älä hätäile. NetBeans on loppujenlopuksi hyvin helppokäyttöinen. Perusteet opit 5 minuutissa, ja kurssin myötä opit koko ajan hieman lisää ja joulukuussa olet jo todellinen NetBeans "poweruser".

Ohjelma ja lähdekoodi

Lähdekoodi

Ohjelma muodostuu lähdekoodista. Tietokone suorittaa lähdekoodissa olevia komentoja pääsääntöisesti ylhäältä alaspäin ja vasemmalta oikealle. Lähdekoodi talletetaan tekstimuodossa ja suoritetaan jollakin tavalla.

Komennot

Varsinaisesti ohjelma muodostuu lähdekoodiin kirjoitetuista komennoista. Tietokone suorittaa eri operaatioita, eli toimintoja, komentojen perusteella. Esimerkiksi merkkijonon, eli tekstin, "Hei maailma"-tulostuksessa tärkein komento on System.out.println.

System.out.println("Hei maailma");

Komento System.out.println tulostaa sille sulkeiden sisällä annetun merkkijonon. Pääte ln on lyhenne sanasta line. Komento siis tulostaa rivin, eli kun annettu merkkijono on tulostettu, tulostaa komento myös rivinvaihdon.

Kääntäjä ja tulkki

Tietokone ei suoraan ymmärrä käyttämäämme ohjelmointikieltä. Tarvitsemme lähdekoodin ja tietokoneen väliin kääntäjän. Ohjelmoidessamme komentoriviä käyttäen, komento javac Hei.java kääntää Hei.java-tiedoston tavukoodiksi, jota voidaan ajaa java-tulkin avulla. Käännetty ohjelma ajetaan komentoriviltä komennolla java Hei, missä Hei on käännetyn java-lähdekooditiedoston nimi.

Käyttäessämme ohjelmointiympäristöä, ohjelmointiympäristö hoitaa lähdekoodin kääntämisen. Valitessamme ohjelman suorittamisen, ohjelmointiympäristö kääntää ja suorittaa ohjelman. Ohjelmointiympäristöt kääntävät ohjelmakoodia ohjelmoijan sitä kirjoittaessa, jolloin yksinkertaiset virheet huomataan ennen ohjelman suoritusta.

Komennon osia

Puolipiste

Puolipisteellä ; erotetaan komennot toisistaan. Kääntäjä ja tulkki ei siis ole kiinnostunut lähdekoodissa olevista riveistä, vaan voimme kirjoittaa koko ohjelman yhdelle riville.

Alla olevassa esimerkissä käytetään komentoa System.out.print, joka on kuten komento System.out.println ilman rivinvaihtoa. System.out.print-komento ei siis tulosta rivinvaihtoa tekstin tulostamisen jälkeen.

Esimerkki puolipisteiden käytöstä

System.out.print("Hei "); System.out.print("maailma");
System.out.print("!");
Hei maailma!

Vaikka kääntäjä ja tulkki eivät tarvitse rivinvaihtoja, on niiden käyttö hyvin tärkeää muita ihmisiä ajatellen. Selkeä lähdekoodin osien erottelu vaatii rivinvaihtojen käyttöä. Tätä ja muita lähdekoodin luettavuuteen liittyviä seikkoja tullaan painottamaan tällä kurssilla.

Komennoille lähdetettävät "tiedot" eli parametrit

Komennon käsittelemä tieto eli komennon parametrit lähetetään komennolle lisäämällä ne komennon nimen perässä olevien sulkujen () sisään. Esimerkiksi System.out.print -komennon parametriksi annetaan teksti hei seuraavasti: System.out.print("hei").

Kommentit

Lähdekoodin kommentit ovat kätevä tapa merkitä asioita itselle ja muille muistiin. Kommentti on mikä tahansa rivi, joka alkaa kahdella vinoviivalla //. Kaikki kommenttimerkkiä seuraava samalla rivillä oleva teksti tulkitaan kommentiksi.

Esimerkki kommenttien käytöstä

// Tulostamme tekstin "Hei maailma"
System.out.print("Hei maailma");

System.out.print(" ja kaikki sen ihmiset."); // Lisäämme samalle riville tekstiä.

// System.out.print("tätä riviä ei suoriteta koska se on kommentoitu ulos");

Esimerkissä alin rivi esittelee erityisen kätevän käyttökohteen kommenteille: kirjoitettua koodia ei tarvitse poistaa jos haluaa tilapäisesti kokeilla jotain.

Lisää tulostamisesta

Kuten aiemmin huomattiin, tulostamiseen on kaksi komentoa:

Tulostettavan tekstin osana voi olla erikoismerkkejä. Tärkein näistä on \n, joka vaihtaa riviä. Erikoismerkkejä on muitakin.

System.out.println("Ensimmäinen\nToinen\nKolmas");

Ylläoleva tulostaa suoritettaessa seuraavaa:

Ensimmäinen
Toinen
Kolmas

Pääohjelmarunko

Ohjelman "Esimerkki" runko on seuraavanlainen.

public class Esimerkki {
  public static void main(String[] args) {
    // ohjelmakoodi
  }
}

Ohjelma sijaitsee samannimisessä .java-päätteisessä tiedostossa. Ohjelman Esimerkki täytyy siis sijaita tiedostossa, jonka nimi on Esimerkki.java.

Ohjelmaa suoritettaessa suoritetaan alue, joka on rungossamme merkitty kommentilla ohjelmakoodi. Ohjelmoimme ensimmäisellä viikolla vain tälle alueelle. Kun puhumme komennoista, esimerkiksi tulostamisesta, tulee komennot kirjoittaa ohjelmarungon sisälle. Esimerkiksi System.out.print("Tulostettava teksti");

public class Esimerkki {
  public static void main(String[] args) {
    System.out.print("Tulostettava teksti");
  }
}

Jatkossa esimerkeissä ei erikseen näytetä pääohjelmarunkoa.

Ja itse asiaan, eli aloitetaan tehtävien tekeminen!

Tehtävät tehdään NetBeans-ohjelmointiympäristössä. Ohje alkuunpääsemiseksi täältä. Ennen kuin tiedät mitä teet, toimi täsmälleen ohjeen kuvaamalla tavalla. Useimmat seuraavista tehtävänannoista näyttävät mitä pyydetyn tehtävän tulisi tulostaa ruudulle toimiakseen oikein. Jos tehtävänanto on epäselvä, pyydä apua! Tehtävänannon epäselvyys on aina opettajien vika.

HUOM: älä tee tehtäviä siten että pelkästään kirjoitat koodia ja paineletet testinappia. Suorita myös koodia normaaliin tapaan (vihreällä nuolella) ja katso mitä ruudulle tulostuu. Erityisesti jos ohjelma ei meinaa mennä testeistä läpi, kannattaa varmistaa normaalisti suorittamalla että ohjelma toimii silmämääräisesti niinkuin sen pitäisi.

Seuraavissa tehtävissä harjoitellaan tuntuman saamista NetBeansiin ja ruudulle tulostamista.

Muista lukea ensin NetBeans-aloittelijan ohje!

Nimi

Tee ohjelma, joka tulostaa nimesi.

Ohjelman tulostus voi olla seuraava:

Oskari Opiskelija

Hei Maailma! (Ja Mualima!)

Tee ohjelma, jonka tulostus on seuraava:

Hei Maailma!
(Ja Mualima!)

Kuusi

Tee ohjelma, jonka tulostus on seuraava:

    *
   ***
  *****
 *******
*********
    *

HUOM: kirjoitit todennäköisesti aika monta kertaa System.out.println("..."). Kokeile kirjoittaa NetBeans:iin (main:in sisään) tyhjälle riville sout ja paina tabulaatoria (näppäin q:n vasemmalla puolella). Mitä tapahtuu? Tämä pieni apuväline säästänee jatkossa runsaasti aikaasi.

Muuttuja ja sijoitus

Muuttujat ja tietotyypit

Ohjelmoinnissa eräs keskeinen käsite on muuttuja. Muuttuja kannattaa ajatella lokerona, johon voi tallettaa tietoa. Muuttujaan talletettavalla tiedolla on aina tyyppi. Tyyppejä ovat esimerkiksi teksti eli merkkijono (String), kokonaisluku (int), liukuluku (double) ja totuusarvo (boolean). Muuttujaan asetetaan arvo yhtäsuuruusmerkillä (=).

int kuukausia = 12;

Yllä olevassa asetuslauseessa asetetaan kokonaisluku-tyyppiä (int) olevaan muuttujaan nimeltä kuukausia arvo 12. Asetuslause luetaan "muuttuja kuukausia saa arvon 12"

Muuttujan arvo voidaan yhdistää merkkijonoon +-merkillä seuraavan esimerkin mukaisesti.

String teksti = "sisältää tekstiä";
int kokonaisluku = 123;
double liukuluku = 3.141592653;
boolean onkoTotta = true;

System.out.println("Tekstimuuttujan arvo on " + teksti);
System.out.println("Kokonaislukumuuttujan arvo on " + kokonaisluku);
System.out.println("Liukulukumuuttujan arvo on " + liukuluku);
System.out.println("Totuusarvomuuttujan arvo on " + onkoTotta);

Tulostus:

Tekstimuuttujan arvo on sisältää tekstiä
Kokonaislukumuuttujan arvo on 123
Liukulukumuuttujan arvo on 3.141592653
Totuusarvomuuttujan arvo on true

Muuttuja säilyttää arvonsa kunnes siihen asetetaan toinen arvo. Huomaa että muuttujan tyyppi kirjoitetaan vain kun muuttuja esitellään ohjelmassa ensimmäistä kertaa.

int kokonaisluku = 123;
System.out.println("Kokonaislukumuuttujan arvo on " + kokonaisluku);

kokonaisluku = 42;
System.out.println("Kokonaislukumuuttujan arvo on " + kokonaisluku);

Tulostuu:

Kokonaislukumuuttujan arvo on 123
Kokonaislukumuuttujan arvo on 42

Muuttujan tyyppi pysyy

Kun muuttujan tyyppi on kertaalleen määritelty, ei se enää muutu. Esimerkiksi merkkijonomuuttuja ei voi muuttua kokonaislukumuuttujaksi, eikä siihen voi asettaa kokonaislukua.

String merkkijono = "tsuppadui!";
merkkijono = 42; // Ei onnistu! :(

Liukulukuun voi asettaa kokonaisluvun, sillä kokonaisluku on myös liukuluku

double liukuluku = 0.42;
liukuluku = 1; // Onnistuu! :)

Muuttuvat muuttujat

Tehtäväpohja sisältää ohjelman, joka tulostaa seuraavaa.

Kanoja:
3
Pekonia (kg):
5.5
Traktori:
Ei ole!

Tässä vielä tiivistelmä:
3
5.5
Ei ole!

Muuta ohjelmaa annetuista kohdista niin että tuloste on:

Kanoja:
9000
Pekonia (kg):
0.1
Traktori:
Zetor

Tässä vielä tiivistelmä:
9000
0.1
Zetor

Sallittu ja kuvaava muuttujan nimi

Muuttujan nimeämistä rajoittavat tietyt ehdot. Vaikka muuttujan nimessä voidaan käyttää ääkkösiä, on parempi olla kayttamatta niita, sillä merkistökoodauksesta saattaa tulla ongelmia.

Muuttujan nimessä ei saa olla tiettyjä erikoismerkkejä, kuten huutomerkkejä (!). Välilyönti ei ole sallittu, sillä se erottaa komentojen osat toisistaan. Välilyönti kannattaa korvata camelCase-tyylillä, jolloin nimi muistuttaneeKamelia. Huom! Muuttujien nimien ensimmäinen kirjain kirjoitetaan aina pienellä:

int camelCaseMuuttuja = 7;

Numeroita voidaan käyttää muuttujan nimessä, kunhan nimi ei ala numerolla. Nimi ei myöskään voi koostua pelkistä numeroista.

int 7muuttuja = 4; // Ei sallittu!
int muuttuja7 = 4; // Sallittu, mutta ei kuvaava muuttujan nimi

Muuttujan nimi ei myöskään saa olla jo entuudestaan käytössä. Tälläisiä nimiä ovat mm. aikaisemmin määritellyt muuttujat ja komennot, kuten System.out.print ja System.out.println.

int camelCase = 2;
int camelCase = 5; // Ei sallittu -- muuttuja camelCase on jo käytössä!

Muuttuja kannattaa nimetä siten, että sen käyttötarkoitus on selvää ilman kommentteja tai miettimistä. Tällä kurssilla muuttujat pitää nimetä kuvaavasti.

Sallittuja muuttujien nimiä

Virheellisiä muuttujien nimiä

Laskentaa

Laskentaoperaatiot ovat varsin suoraviivaisia: +, -, * ja /. Erikoisempana operaationa on %, joka on jakojäännös, eli modulo. Laskentajärjestys on myös varsin suoraviivainen: operaatiot lasketaan vasemmalta oikealle sulut huomioon ottaen.

int eka = 2; // kokonaislukutyyppinen muuttuja eka saa arvon 2
int toka = 4; // kokonaislukutyyppinen muuttuja toka saa arvon 4
int summa = eka + toka;  // kokonaislukutyyppinen muuttuja summa saa arvon eka + toka, eli 2 + 4

System.out.println(summa); // tulostetaan muuttujan summa arvo
int laskuSuluilla = (1 + 1) + 3 * (2 + 5);   // 23
int laskuSuluitta = 1 + 1 + 3 * 2 + 5;       // 13

Yllä olevan sulkuesimerkin voi suorittaa myös askeleittain.

int laskuSuluilla = (1 + 1);
laskuSuluilla = laskuSuluilla + 3 * (2 + 5)   // 32

int laskuSuluitta = 1 + 1;
laskuSuluitta = laskuSuluitta + 3 * 2;
laskuSuluitta = laskuSuluitta + 5;    	// 13

Laskentaoperaatioita voidaan suorittaa lähes missä tahansa kohdassa ohjelmakoodia.

int eka = 2;
int toka = 4;

System.out.println(eka+toka);
System.out.println(2 + toka - eka - toka);

Liukuluvut eli desimaaliluvut

Kokonaislukujen jako ja jakojäännös ovat hieman hankalampia. Liukuluku ja kokonaisluku menevät helposti sekaisin. Jos kaikki laskuoperaatiossa olevat muuttujat ovat kokonaislukuja, on tulos myös kokonaisluku.

int tulos = 3 / 2;      // tulos on 1 (kokonaisluku), sillä 3 ja 2 ovat myös kokonaislukuja
int eka = 3:
int toka = 2;
double tulos = eka / toka;      // nytkin tulos on 1, sillä eka ja toka ovat kokonaislukuja

Jakojäännös-operaation (%) avulla saadaan selville jakojäännös. Esimerkiksi laskun 7 % 2 jakojäännös on 1.

int jakojaannos = 7 % 2; // jakojaannos on 1 (kokonaisluku)

Jos jakolaskun jakaja tai jaettava (tai molemmat!) ovat liukulukuja, tulee tulokseksi myös liukuluku

double kunJaettavaOnLiukuluku = 3.0 / 2;  // tulokseksi: 1.5
double kunJakajaOnLiukuluku = 3 / 2.0;    // tulokseksi: 1.5

Kokonaisluku voidaan tarvittaessa muuttaa liukuluvuksi lisäämällä sen eteen tyyppimuunnosoperaatio (double):

int eka = 3;
int toka = 2;
double tulos1 = (double)eka / toka;  // tulokseksi: 1.5

double tulos2 = eka / (double)toka;  // tulokseksi: 1.5

double tulos3 = (double)(eka / toka);  // tulokseksi: 1

Jälkimmäisessä tulos pyöristyy väärin sillä laskuoperaatio kokonaisluvuilla suoritetaan ennen tyyppimuunnosta.

Jos jakolaskun tulos asetetaan kokonaislukutyyppiseen muuttujaan, on tulos automaattisesti kokonaisluku

int tulosKokonaislukuKoskaTyyppiKokonaisluku = 3.0 / 2;  // tulos automaattisesti kokonaisluku: 1

Seuraava esimerkki tulostaa "1.5", sillä jaettavasta tehdään liukuluku kertomalla se liukuluvulla (1.0 * 3 = 3.0) ennen jakolaskua.

int jaettava = 3;
int jakaja = 2;

double tulos = 1.0 * jaettava / jakaja;
System.out.println(tulos); 

Mitä seuraava tulostaa?

int jaettava = 3;
int jakaja = 2;

double tulos = jaettava / jakaja * 1.0;
System.out.println(tulos);

Huomioi, että nimeät muuttujat nyt ja jatkossakin yllä esiteltyjen hyvien käytäntöjen mukaan.

Sekunnit vuodessa

Tee ohjelma, joka laskee, kuinka monta sekuntia on vuodessa. Voit olettaa, että vuodessa on 365 päivää (eli ei ole karkausvuosi).

Ohjelman tulostus on seuraava:

Vuodessa on X sekuntia.

X:n kohdalle tulee ohjelmasi laskema tulos.

Katenointi eli merkkijonojen yhdistäminen

Tarkastellaan vielä lähemmin merkkijonojen yhdistämistä +-merkinnän avulla.

Jos operaatiota + sovelletaan kahden merkkijonon välille, syntyy uusi merkkijono, jossa kaksi merkkijonoa on yhdistetty. Huomaa nokkela välilyönnin käyttö lauseen "muuttujien" osana!

String tervehdys = "Hei ";
String nimi = "Matti";
String hyvästely = ", ja näkemiin!";

String lause = tervehdys + nimi + hyvästely;

System.out.println(lause);
Hei Matti, ja näkemiin!

Jos toinen operaation + kohteista on merkkijono, syntyy uusi merkkijono, jossa esimerkiksi kokonaisluku 2 on muutettu merkkijonoksi "2" ja tähän yhdistetty haluttu merkkijono.

System.out.println("tuossa on kokonaisluku --> " + 2);
System.out.println( 2 + " <-- tuossa on kokonaisluku");

Edellä esitellyt laskusäännöt pätevät täälläkin:

System.out.println("Neljä: " + (2+2));
System.out.println("Mutta! kaksikymmentäkaksi: " + 2 + 2);
Neljä: 4
Mutta! kaksikymmentäkaksi: 22

Edellisiä tietoja yhdistelemällä pystymme tulostamaan muuttujan arvoja ja tekstiä sekaisin:

int x = 10;

System.out.println("muuttujan x arvo on: " + x);

int y = 5;
int z = 6;

System.out.println("y on " + y + " ja z on " + z);

Tämä ohjelma tulostaa tietenkin:

muuttujan x arvo on: 10
y on 5 ja z on 6

Yhteenlasku

Tee ohjelma, jonka avulla voidaan laskea kahden kokonaisluvun summa. Ohjelman alussa määritellään kaksi muuttujaa, jotka sisältävät summattavat luvut. Voit tarvittaessa käyttää myös muita muuttujia.

Esimerkiksi jos muuttujissa on luvut 5 ja 4, ohjelman tulostus on seuraava:

5 + 4 = 9

Jos taas muuttujissa on luvut 73457 ja 12888, ohjelman tulostus on seuraava:

73457 + 12888 = 86345

Kertolasku

Tee edellistä ohjelmaa vastaava ohjelma, joka laskee kahden kokonaislukumuuttujaan sijoitetun arvon kertolaskun.

Esimerkiksi jos muuttujissa on luvut 2 ja 8, ohjelman tulostus on seuraava:

2 * 8 = 16

Jos taas muuttujissa on luvut 277 ja 111, ohjelman tulostus on seuraava:

277 * 111 = 30747

Kuinka suuren kertolaskun ohjelmasi pystyy laskemaan?

Käyttäjän syötteen lukeminen

Tähän asti ohjelmamme ovat olleet kovin yksipuolisia. Seuraavaksi luemme syötettä käyttäjältä. Käytämme syötteen lukemiseen erityistä Scanner-apuvälinettä.

Lisätään Scanner valmiiseen pääohjelmarunkoomme. Älä hätäile vaikka pääohjelmarunko saattaa näyttää vaikeaselkoiselta, jatkamme koodausta kuten ennenkin, eli kohtaan mikä on merkattu kommentilla ohjelmakoodi.

import java.util.Scanner;

public class OhjelmaRunko {
  
  public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);

    // ohjelmakoodi
  }
}

Merkkijonon lukeminen

Seuraava koodi lukee käyttäjän nimen ja tulostaa tervehdyksen:

System.out.print("Mikä on nimesi? ");
String nimi = lukija.nextLine(); // Luetaan käyttäjältä rivi tekstiä ja asetetaan se muuttujaan nimi

System.out.println("Hei, " + nimi);
Mikä on nimesi? Matti
Hei, Matti

(Seuraavassa on yllä oleva ohjelma pääohjelmarungon kanssa. Ohjelman nimi on Tervehdys. Koska ohjelman nimi on Tervehdys, täytyy sen sijaita tiedostossa Tervehdys.java -- Käyttäessäsi NetBeans-ohjelmointiympäristöä, toimi kuten alussa olleessa ohjeessa tehtiin, mutta muuta ohjelman nimi Heistä (Hei) Tervehdykseksi (Tervehdys)

import java.util.Scanner;

public class Tervehdys {

  public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);

    System.out.print("Kenelle sanotaan hei: ");
    String nimi = lukija.nextLine(); // Luetaan käyttäjältä rivi tekstiä ja asetetaan sen arvo muuttujaan nimi

    System.out.print("Hei " + nimi);
  }
}

Kun yllä oleva ohjelma ajetaan, pääset kirjoittamaan syötteen. NetBeansin tulostusvälilehti (alhaalla) näyttää ajetun ohjelman jälkeen seuraavalta (käyttäjä syöttää nimen "Matti").

run:
Kenelle sanotaan hei: Matti
Hei Matti
BUILD SUCCESSFUL (total time: 6 seconds)

Kokonaisluvun lukeminen

Scanner-apuvälineemme ei ole hyvä kokonaislukujen lukemiseen, joten käytämme toista apuvälinettä merkkijonon kokonaisluvuksi muuttamisessa. Komento Integer.parseInt muuttaa sille annetussa tekstimuuttujassa olevan kokonaisluvun kokonaislukumuuttujaksi. Komennolle annetaan tekstimuuttuja sulkuihin, ja se palauttaa kokonaisluvun joka asetetaan kokonaislukumuuttujaan.

Käytännössä kytkemme kaksi komentoa yhteen. Ensin luemme käyttäjältä rivin, jonka annamme heti komennolle Integer.parseInt.

System.out.print("Anna kokonaisluku: ");
int kokonaisluku = Integer.parseInt(lukija.nextLine());

System.out.println("Annoit " + kokonaisluku);

Kysytään seuraavaksi käyttäjältä nimi, ja sen jälkeen ikä. Tällä kertaa esimerkissä on myös ohjelmarunko mukana.

import java.util.Scanner;

public class NimiJaIkaTervehdys {
  public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);

    System.out.print("Nimesi: ");
    String nimi = lukija.nextLine();   // Luetaan käyttäjältä rivi tekstiä

    System.out.print("Kuinka vanha olet: ");
    int ika = Integer.parseInt(lukija.nextLine()); // luetaan käyttäjältä tekstimuuttuja ja muutetaan se kokonaisluvuksi

    System.out.println("Nimesi on siis: " + nimi + ", ja ikäsi " + ika + ", hauska tutustua.");
  }
}

Yhteenveto

Käyttäjän kanssa keskustelevan ohjelman runko:

import java.util.Scanner;
public class OhjelmanNimi {
  public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);

    // koodi tähän
  }
}

Merkkijonon lukeminen:

String merkkijono = lukija.nextLine();

Kokonaisluvun lukeminen:

int kokonaisluku = Integer.parseInt(lukija.nextLine());

Summaaja

Tee ohjelma, joka kysyy käyttäjältä kaksi kokonaislukua ja tulostaa niiden summan.

Anna ensimmäinen luku: 6
Anna toinen luku: 2

Lukujen summa: 8

Esimerkissä punainen väri tarkoittaa käyttäjän kirjoittamaa tekstiä. Tätä käytäntöä noudatetaan jatkossa esimerkeissä.

Jakaja

Tee ohjelma, joka kysyy käyttäjältä kaksi kokonaislukua ja tulostaa niiden osamäärän. Varmista, että 3 / 2 = 1.5. Jos desimaaliosa katoaa, lue materiaalin kohdasta Liukuluvut eli desimaaliluvut missä vika on.

Anna ensimmäinen luku: 3
Anna toinen luku: 2

Jakolasku: 3 / 2 = 1.5

Ympyrän kehän pituus

Ympyrän kehän pituus lasketaan kaavalla säde on 2 * pii * säde. Tee ohjelma, joka kysyy käyttäjältä ympyrän säteen ja laskee sen perusteella ympyrän kehän pituuden. Javasta löytyy valmis piin arvo, saat sen kirjoittamalla Math.PI laskutoimitukseen.

Anna ympyrän säde: 20

Ympyrän kehä: 125.66370614359172

Suurempi luku

Tee ohjelma, joka kysyy käyttäjältä kaksi kokonaislukua ja tulostaa luvuista suuremman.

Vihje: kun kirjoitat NetBeans:iin Math. (eli Math ja perään piste), näet erinäisiä Javan valmiita matemaattisia laskutoimituksia, esim. Math.cos(10) laskee luvun 10 kosinin. Kokeile, löydätkö Math:sta sopivan työkalun seuraavan ohjelman tekemiseen. Jos et löydä tai osaa tehdä tätä kohtaa, siirry heti eteenpäin. Tutustumme asiaan tarkemmin myöhemmässä vaiheessa.

Anna ensimmäinen luku: 20
Anna toinen luku: 14

Luvuista suurempi: 20

Ikien summa

Tee ohjelma, joka kysyy kahden käyttäjän nimet ja iät. Tämän jälkeen ohjelma tulostaa henkilöiden ikien summan.

Kerro nimi: Matti
Kerro ikä: 14

Kerro nimi: Arto
Kerro ikä: 12

Matti ja Arto ovat yhteensä 26 vuotta vanhoja.

NHL-tilastot, osa1

Tehtäväpohjaasi on liitetty mukaan valmis komponentti NHLStatistics jonka avulla on mahdollista tarkastella NHL:n pelaajien pistetietoja, eli pelattuja otteluita, tehtyjä maaleja, syöttöjä, pisteitä ja jäähymääriä.

Pääohjelma "importoi" eli ottaa komponentin käyttöönsä lisäämällä koodin yläosaan import nhlstats.NHLStatistics;. Seuraavassa esimerkkiohjelma 10 parhaan pisteidentekijän tulostamisesta:

import java.util.Scanner;
import nhlstats.NHLStatistics;

public class Paaohjelma {
    public static void main(String[] args) throws Exception {
        Scanner lukija = new Scanner(System.in);

        System.out.println("10 parasta pistemiestä");
        NHLStatistics.sortByPoints();
        NHLStatistics.top(10);
    }
}

Tulostuu (tämä oli maanantain 9.1. tilanne):

10 parasta pistemiestä
Henrik Sedin           VAN        43 11 + 38= 49  36
Phil Kessel            TOR        41 24 + 24= 48  10
Claude Giroux          PHI        36 18 + 30= 48  16
Joffrey Lupul          TOR        41 19 + 28= 47  36
Daniel Sedin           VAN        42 18 + 29= 47  32
Steven Stamkos         TBL        40 28 + 17= 45  34
Marian Hossa           CHI        41 17 + 27= 44  14
Evgeni Malkin          PIT        33 16 + 28= 44  30
Jordan Eberle          EDM        41 17 + 26= 43   6
Jason Pominville       BUF        41 14 + 29= 43   8

Pelaajasta tulostuvat nimi, seuran lyhenne, ottelut, maalit, syötöt, pisteet ja jäähyt.

Ensimmäinen komento NHLStatistics.sortByPoints() järjestää pelaajat tehtyjen pisteiden mukaiseen järjestykseen. Toinen komento NHLStatistics.top(10); tulostaa 10 ensimmäistä pelaajaa senhetkistä järjestystä noudattaen. Parametriksi voidaan antaa mikä tahansa luku.

Vastaavalla tavalla saadaan tulostettua pelaajat tehtyjen maalien, syöttöjen tai jäähyminuuttien mukaan järjestettynä. Eli ensin kutsutaan pelaajat sopivaan järjestykseen laittavaa komentoa, joita ovat:

NHLStatistics.sortByPoints();     // aseta pelaajat pisteiden mukaiseen järjestykseen
NHLStatistics.sortByGoals();      // aseta pelaajat tehtyjen maalien mukaiseen järjestykseen
NHLStatistics.sortByAssists();    // aseta pelaajat syöttöjen mukaiseen järjestykseen
NHLStatistics.sortByPenalties();  // aseta pelaajat jäähyminuuttien mukaiseen järjestykseen

Ja tämän jälkeen tulostetaan pelaajat komennolla top ja parametrina annetaan tulostettavien pelaajien määrä

.

Komponentti mahdollistaa myös tietyn pelaajan tilastotietojen kyselyn:

NHLStatistics.searchByPlayer("Jaromir Jagr");   // tulostaa Jaromir Jagr:in tilastot
NHLStatistics.searchByPlayer("Koivu");          // tulostaa Mikko ja Saku Koivun tilastot
NHLStatistics.searchByPlayer("Teemu");          // tulostaa kaikkien Teemujen tilastot

Sekä tietyn joukkueen kaikkien pelaajien tilastojen tulostuksen:

NHLStatistics.teamStatistics("NYR");            // New York Rangersin tilastot

Joukkueentilastot tulostuvat siinä järjestyksessä mihin ne ovat edelliseksi kutsutulla sortBy...()-komennolla asetettu.

Joukkueen nimi tulee antaa virallisena kolmekirjaimisena lyhenteenä. Joukkueiden lyhenteet kerrottu esim. täällä. Myös tilastokomponetti kertoo käytössä olevat lyhenteet jos yrität pyytää olemassa olemattoman joukkueen tilastoja.

Tee ohjelmarunkoon seuraavat asiat tekevä ohjelma. Tee asiat juui siinä järjestyksessä kun ne tässä mainitaan. Tee kaikki asiat pääohjelmaan peräkkäin, poistamatta mitään tehtyä osaa.

Huom: kun suoritat ohjelman ensimmäisen kerran, saattaa suoritus kestää jonkun aikaa (internetissä oleva palvelin voi olla lepotilassa ja sen herääminen kestää jonkin aikaa). Tämänjälkeisillä kerroilla suoritus on nopea.

Kun olet lähettänyt tehtävän tarkastettavaksi ja palautusautomaatti hyväksyy sen, voit muutella koodia vapaasti!

Valinta ja totuusarvot

Tähän mennessä tekemämme ohjelmat ovat edenneet suoraviivaisesti käskystä toiseen, toimien jokaisella suorituskerralla samalla tavalla. Jotta ohjelman suoritus voisi haarautua erilaisille suorituspoluille käyttäjän esim. käyttäjän antaman syötteen perusteella, tarvitsemme käyttöömme valintakäskyn.

int luku = 11;

if ( luku > 10 ) {
  System.out.println("Luku oli suurempi kuin 10");
}

Ehto ( luku > 10 ) muuntautuu totuusarvoksi true tai false. Valintakäsky if käsittelee siis lopulta vain ja ainoastaan totuusarvoja. Yllä oleva ehtolause luetaan "jos luku on suurempi kuin 10".

Huomaa, että if -lauseen perään ei tule puolipistettä, sillä lause ei lopu ehto-osan jälkeen.

Ehdon jälkeen avaava aaltosulku { aloittaa lohkon (block), jonka sisältö suoritetaan jos ehto on tosi. Lohko loppuu sulkevaan aaltosulkuun }. Lohko voi olla kuinka pitkä tahansa.

Vertailuoperaattoreita ovat seuraavat:

int luku = 55;

if ( luku != 0 ) {
  System.out.println("Luku oli erisuuri kuin 0");
}

if ( luku >= 1000 ) {
   System.out.println("Luku oli vähintään 1000");  
}

Lohkon sisällä voi olla mitä tahansa koodia, myös toinen valintakäsky.

int x = 45;
int luku = 55;

if ( luku > 0 ) {
   System.out.println("Luku on positiivinen");
   if ( luku>x ) {
     System.out.println(" ja suurempi kuin muuttujan x arvo"); 
     System.out.println("muuttujan x arvohan on " + x);
   }
}

Vertailuoperaattoreita voi käyttää myös ehtojen ulkopuolella. Tällöin ehdon totuusarvo asettuu totuusarvomuuttujaan.

int eka = 1;
int toka = 3;

boolean onkoSuurempi = eka > toka;

Yllä olevassa esimerkissä totuusarvomuuttuja onkoSuurempi sisältää nyt totuusarvon false.

Totuusarvomuuttujaa voidaan käyttää ehtolauseessa ehtona.

int eka = 1;
int toka = 3;

boolean onkoPienempi = eka < toka;

if ( onkoPienempi ) {
  System.out.println("1 on pienempi kuin 3!");
}
1 on pienempi kuin 3!

Koodin sisennys

Huomaa, että if-komennon jälkeisen lohkon, eli {-merkkiä seuraavien rivien komentoja ei kirjoiteta samalle tasolle (eli yhtä "vasemmalle") kuin komentoa if, vaan ne sisennetään hieman oikealle. Sisentäminen tapahtuu tabulaattorimerkillä (q:n vasemmalla puolella oleva merkki). Kun lohko sulkeutuu, eli tulee }-merkki, sisennys loppuu. }-merkki on samalla tasolla kuin if.

Sisennys on oleellinen seikka ohjelmien ymmärrettävyyden kannalta. Tällä kurssilla ja kaikkialla "oikeassa elämässä" edellytetään, että koodi sisennetään asiallisesti. NetBeans auttaa sisennyksessä. Ohjelman saa sisennettyä helposti painamalla yhtä aikaa shift, alt ja f.

else

Jos valinnan ehto on epätotta, eli totuusarvo on false, voidaan suorittaa toinen vaihtoehtoinen lohko koodia, tämä käy komennon else avulla.

int luku = 4;

if ( luku > 5 ) {
  System.out.println("Lukusi on suurempi kuin viisi!");
} else {
  System.out.println("Lukusi on viisi tai alle!");
}
Lukusi on viisi tai alle!

Positiivinen luku

Tee ohjelma, joka kysyy käyttäjältä kokonaisluvun ja kertoo, onko se positiivinen (eli suurempi kuin nolla).

Anna luku: 5

Luku on positiivinen.
Anna luku: -2

Luku ei ole positiivinen.

Onhan koodisi varmasti sisennetty oikein?

Kertaa materiaalista kohta Koodin sisennys. Kokeile mitä tapahtuu NetBeansissa kun painat yhtä aikaa shift, alt ja f! Sama toiminnallisuus eli automaattinen sisennys saadaan aikaan valitsemalla yläpalkista Source ja sen alta Format.

Täysi-ikäisyys

Tee ohjelma, joka kysyy käyttäjän ikää ja kertoo, onko tämä täysi-ikäinen (eli 18-vuotias tai vanhempi).

Kuinka vanha olet? 12

Et ole vielä täysi-ikäinen!
Kuinka vanha olet? 32

Olet jo täysi-ikäinen!

Pariton vai parillinen?

Tee ohjelma, joka kysyy käyttäjältä luvun ja ilmoittaa, onko se parillinen vai pariton.

Anna luku: 2
Luku 2 on parillinen.
Anna luku: 7
Luku 7 on pariton.

Vihje: Luvun jakojäännös 2:lla kertoo, onko luku parillinen vai pariton. Jakojäännos taas saadaan %-operaattorilla, tehtäväpohjassa on lisää ohjeita miten parittomuustarkastus hoituu jakojäännöksen avulla.

else if

Jos valittavissa olevia vaihtoehtoja on enemmän kuin kaksi kannattaa käyttää else if-komentoa, joka on kuten else, mutta lisäehdolla. else if tulee if-ehdon jälkeen. else if ehtoja voi olla useita.

int luku = 3;

if ( luku == 1 ) {
  System.out.println("Luku on yksi");
} else if ( luku == 2 ) {
  System.out.println("Lukuna on kaksi");
} else if ( luku == 3 ) {
  System.out.println("Kolme lienee lukuna!");
} else {
  System.out.println("Aika paljon!");
}
Kolme lienee lukuna!

Luetaan ylläoleva esimerkki: 'Jos luku on yksi, tulosta "Luku on yksi", muuten jos luku on kaksi, tulosta "Lukuna on kaksi", muuten jos lukuna on kolme, tulosta "Kolme lienee lukuna". Muulloin, tulosta "Aika paljon!"'.

Merkkijonojen vertailu

Merkkijonoja, eli tekstejä, ei voi vertailla yhtäsuuri kuin (==) operaatiolla. Merkkijonojen vertailuun käytetään erillistä equals-komentoa, joka liittyy aina verrattavaan merkkijonoon.

String teksti = "kurssi";

if ( teksti.equals("marsipaani") ) {
  System.out.println("Teksti-muuttujassa on teksti marsipaani.");
} else {
  System.out.println("Teksti-muuttujassa ei ole tekstiä marsipaani.");
}

Komento equals liitetään aina siihen verrattavaan tekstimuuttujaan, "tekstimuuttuja piste equals teksti". Tekstimuuttujaa voidaan myös verrata toiseen tekstimuuttujaan.

String teksti = "kurssi";
String toinenTeksti = "pursi";

if ( teksti.equals(toinenTeksti) ) {
  System.out.println("Samat tekstit!");
} else {
  System.out.println("Ei samat tekstit!");
}

Merkkijonoja vertailtaessa on syytä varmistaa että verrattavalla tekstimuuttujalla on arvo. Jos muuttujalla ei ole arvoa, ohjelma tuottaa virheen NullPointerException, joka tarkoittaa ettei muuttujan arvoa ole asetettu tai se on tyhjä (null).

Suurempi luku

Tee ohjelma, joka kysyy käyttäjältä kaksi kokonaislukua ja tulostaa niistä suuremman. Jos luvut ovat yhtä suuret, ohjelma huomaa myös tämän.

Esimerkkitulostuksia:

Anna ensimmäinen luku: 5
Anna toinen luku: 3

Suurempi luku: 5
Anna ensimmäinen luku: 5
Anna toinen luku: 8

Suurempi luku: 8
Anna ensimmäinen luku: 5
Anna toinen luku: 5

Luvut ovat yhtä suuret!

Arvosanat ja pisteet

Tee ohjelma, joka ilmoittaa kurssiarvosanan seuraavan taulukon mukaisesti.

pistemääräarvosana
0–29hylätty
30–341
35–392
40–443
45–494
50–605

Esimerkkitulostuksia:

Anna pisteet [0-60]: 37

Arvosana: 2
Anna pisteet [0-60]: 51

Arvosana: 5

Loogiset operaatiot

Valinnan ehto voi olla myös monimutkaisempi, yksittäisistä loogisten operaatioiden avulla koostettu ehto. Loogisia operaatioita ovat:

int vuosi = 2012;
int kuukausi = 9;

System.out.println("Tällä hetkellä käynnistyy kurssi:");

if ( vuosi == 2012 && kuukausi == 9 ) {
  System.out.println("Ohjelmoinnin perusteet");
}

if ( vuosi == 2012 && kuukausi == 11 ) {
  System.out.println("Ohjelmoinnin jatkokurssi");
}
Tällä hetkellä käynnistyy kurssi:
Ohjelmoinnin perusteet
System.out.println("Onkohan luku väliltä 5-10: ");
int luku = 7;

if ( luku > 4 && luku < 11 ) {
  System.out.println("On! :)");
} else {
  System.out.println("Ei ollut :(")
}
Onkohan luku väliltä 5-10: 
On! :)
System.out.println("Onkohan luku 1, 50 tai 100: ");
int luku = 50;

if ( luku == 1 || luku == 50 || luku == 100 ) {
  System.out.println("On! :)");
} else {
  System.out.println("Ei ollut :(")
}
Onkohan luku 1, 50 tai 100: 
On! :)

Monimutkaisten ehtojen muodostamisessa tarvitaan usein sulkuja:

int luku = 99;

if ( (luku > 0 && luku < 10) || luku > 100 ) {
  System.out.println("luku oli joko yhden ja yhdeksän väliltä tai yli sata"); 
} else {
  System.out.println("luku oli 0 tai pienempi tai väliltä 10-99");
}

Iän tarkistus

Tee ohjelma, joka kysyy käyttäjän iän ja tarkistaa, että se on mahdollinen (ainakin 0 ja korkeintaan 120).

Kuinka vanha olet? 10
OK
Kuinka vanha olet? 55
OK
Kuinka vanha olet? -3
Mahdotonta!
Kuinka vanha olet? 150
Mahdotonta!

Käyttäjätunnukset

Tee ohjelma, joka tunnistaa seuraavat käyttäjät:

tunnussalasana
aleksitappara
elinakissa

Ohjelma näyttää käyttäjälle henkilökohtaisen viestin tai ilmoittaa, jos tunnus tai salasana on väärin.

Anna tunnus: aleksi
Anna salasana: tappara
Olet kirjautunut järjestelmään
Anna tunnus: elina
Anna salasana: kissa
Olet kirjautunut järjestelmään
Anna tunnus: aleksi
Anna salasana: jokerit
Virheellinen tunnus tai salasana!

Karkausvuosi

Vuosi on karkausvuosi, jos se on jaollinen 4:llä. Kuitenkin jos vuosi on jaollinen 100:lla, se on karkausvuosi vain silloin, kun se on jaollinen myös 400:lla.

Tee ohjelma, joka tarkistaa, onko vuosi karkausvuosi.

Anna vuosi: 2011
Vuosi ei ole karkausvuosi.
Anna vuosi: 2012
Vuosi on karkausvuosi.
Anna vuosi: 1800
Vuosi ei ole karkausvuosi.
Anna vuosi: 2000
Vuosi on karkausvuosi.

Toiston alkeet

Valintakomennon avulla saamme ohjelman toimintaan mukaan ehdollisuutta, eli esim. jos käyttäjätunnus ja salasana ovat oikein, päästetään käyttäjä kirjautumaan ohjelmaan ja muuten ei.

Ehdollisuuden lisäksi tarvitsemme toistoa: käyttäjätunnusta ja salasanaa pitää pystyä kysymään uudelleen niin kauan kunnes oikea käyttäjätunnus/salasana-pari on annettu.

Yksinkertaisin toiston muoto on ikuinen toisto. Seuraava ohjelma tulostaa merkkijonoa osaan ohjelmoida! ikuisesti eli "äärettömän monta kertaa":

  while ( true ) {
    System.out.println("osaan ohjelmoida!");
  }

Komento while( true ) saa sen aikaan, että siihen liittyvää lohkoa, eli {}:lla ympäröityjä komentoja suoritetaan äärettömän monta kertaa.

Ikuinen toisto ei yleensä ole se mitä halutaan. Toisto voidaan keskeyttää esim. komennolla break.

  while ( true ) {
    System.out.println("osaan ohjelmoida!");

    System.out.print("jatketaanko (ei lopettaa)? ");
    String komento = lukija.nextLine();
    if ( komento.equals("ei") ) {
      break;
    }
  }
  System.out.println("kiitos ja kuulemiin.");

Nyt toisto etenee siten että ensin tulostuu osaan ohjelmoida! ja tämän jälkeen ohjelma kysyy käyttäjältä jatketaanko vielä. Jos käyttäjä vastaa ei, suoritetaan komento break jonka ansiosta toisto lopetetaan ja suoritetaan komento joka tulostaa kiitos ja kuulemiin.

osaan ohjelmoida!
jatketaanko (ei lopettaa)? joo
osaan ohjelmoida!
jatketaanko (ei lopettaa)? jawohl
osaan ohjelmoida!
jatketaanko (ei lopettaa)? ei
kiitos ja kuulemiin.

Toiston sisällä voi tehdä erilaisia asioita. Seuraavassa yksinkertainen laskin. Laskin kysyy käyttäjältä komentoa. Komennossa lopetus suoritetaan break ja toisto loppuu. Tämän jälkeen kysytään kahta lukua. Jos komento oli summa lasketaan lukujen summa ja tulostetaan se. Jos komento oli erotus toimitaan vastaavasti. Muussa tapauksessa ilmoitetaan että komento on tuntematon.

  System.out.println("tervetuloa käyttämään laskinta");

  while ( true ) {    
    System.out.print("anna komento (summa, erotus, lopetus): ");
    String komento = lukija.nextLine();
    if ( komento.equals("lopetus") ) {
      break;
    }

    System.out.print("anna luvut ");
    int eka = Integer.parseInt(lukija.nextLine());
    int toka = Integer.parseInt(lukija.nextLine());

    if ( komento.equals("summa") ) {     
      int summa = eka+toka;
      System.out.println( "lukujen summa " + summa );
    } else if ( komento.equals("erotus") ) {     
      int erotus = eka-toka;
      System.out.println( "lukujen erotus " + erotus );
    } else {     
      System.out.println( "tuntematon komento" );
    } 

  }
  System.out.println("kiitos ja kuulemiin.");

Screencast joka näyttää miten ohjelma syntyy:

Salasana

Tässä tehtävässä luodaan ohjelma joka kyselee käyttäjältä salasanaa. Jos salasana menee oikein, nähdään salainen viesti.

Anna salasana: nauris
Väärin!
Anna salasana: lanttu
Väärin!
Anna salasana: porkkana
Oikein!

Salaisuus on: znvavbfgv grugl!

Toteutetaan ohjelma kolmessa askeleessa.

Salasanan kysyminen

Testipalvelimelta tulevaan ohjelmarunkoon on määritelty muuttuja String salasana, jolle on asetettu arvoksi porkkana -- älä muuta tätä salasanaa. Toteuta lisätoiminnallisuus, jossa ohjelma kysyy käyttäjältä salasanaa ja vertailee sitä muuttujassa salasana olevaan arvoon. Muista mitä erityistä merkkijonojen vertailussa on!

Anna salasana: nauris
Väärin!
Anna salasana: porkkana
Oikein!
Anna salasana: bataatti
Väärin!

Salasanan kysyminen kunnes käyttäjä vastaa oikein

Muokkaa ohjelmaa siten, että se kysyy salasanaa kunnes käyttäjä syöttää oikean salasanan. Toteuta salasanan jatkuva kysyminen while-true -toistolausekkeen avulla. Toistolausekkeesta pääsee pois, jos ja vain jos käyttäjän syöttämä salasana on sama kuin muuttujassa salasana oleva arvo.

Anna salasana: nauris
Väärin!
Anna salasana: lanttu
Väärin!
Anna salasana: porkkana
Oikein!

Salainen viesti

Lisää ohjelmaan oma salainen viestisi joka näytetään kun käyttäjä kirjoittaa salasanan oikein. Se voi olla mitä tahansa!

Anna salasana: nauris
Väärin!
Anna salasana: lanttu
Väärin!
Anna salasana: porkkana
Oikein!

Salaisuus on: znvavbfgv grugl!

Ylläoleva salaisuus on salattu käyttäen Rot13-algoritmia. Toteutamme myöhemmin tällä kurssilla oman salausohjelman.

Lämpötiloja

Saat tehtäväpohjan mukana komponentin nimeltään Kuvaaja. Kuvaaja piirtää sille annetuista luvuista kuvaajan. Lukuja annetaan kuvaajalle näin:

Kuvaaja.lisaaNumero(13.0);

Teemme ohjelman, joka piirtää kuvaajan sille annetuista päivittäisistä lämpötiloista.

Kysely

Tee ohjelma, joka kysyy käyttäjältä liukulukuja (double) ja lisää ne kuvaajaan. Käytä jälleen while-true-rakennetta.

Huom:double-luku luetaan seuraavasti double luku = Double.parseDouble( lukija.nextLine() );

Huom2: ohjelmasi toimii "ikuisessa" silmukassa eli on tarkoitus että sen suoritus ei pääty koskaan. Saat ohjelmasi pysähtymään painamalla NetBeansin konsoli-ikkunan oikeassa laidassa olevaa punaista neliöä.

Tarkastus

Paranna edellistä ohjelmaasi niin, että lämpötilat jotka ovat alle -30 tai yli 40 jätetään lisäämättä.

NHL-tilastot, osa 2

Jatketaan NHL-komponentin käyttöä ja tehdään ohjelma jonka avulla käyttäjä voi kysellä haluamiaan tilastotietoja.

Ohjelma rakentuu samaan tapaan kuin edellä oleva laskin-esimerkki. Ohjelman runko on seuraava:

    public static void main(String[] args) throws Exception {
        Scanner lukija = new Scanner(System.in);

        System.out.println("NHL-tilastopalvelu");
        while ( true ) {
            System.out.println("");
            System.out.print("komento (pisteet, maalit, syotot, jaahyt, pelaaja, joukkue, lopeta): ");
            String komento = lukija.nextLine();
            
            if ( komento.equals("lopeta")) {
                break;
            }
           
            if ( komento.equals("pisteet")) {
                // tulosta 10 parasta pisteiden tekijää    
            } else if ( komento.equals("maalit")) {
                // tulosta 10 parasta maalintekijää     
            } else if ( komento.equals("syotot")) {
                // tulosta 10 parasta syöttäjää      
            } else if ( komento.equals("jaahyt")) {
                // tulosta 10 eniten jäähyjä saanutta    
            } else if ( komento.equals("pelaaja")) {
                // kysy käyttäjältä kenen tiedot halutaan tulostaa ja tulosta ne     
            } else if ( komento.equals("joukkue")) {
               // kysy käyttäjältä minkä joukkueen tilastot halutaan ja tulosta ne 
               // huom: tulostus tulee tapahtua pisteidentekojärjestyksessä (eli eniten pisteitä tehneet ensin) 
            }
        }
    }

Ohjelma siis pyytää käyttäjältä komentoa ja suorittaa sitten komentoa vastaavan toimenpiteen. Komennot ovat pisteet, maalit, syotot, jaahyt, pelaaja, joukkue, lopeta. Huomaa että komentojen nimissä ä ja ö on korvattu a:lla ja o:lla.

Ohjelmarunkoon on merkattu kommentein ne kohdat jotka sinun tulee täydentää.

Seuraavassa esimerkki miten ohjelman tulisi toimia:

NHL-tilastopalvelu

komento (pisteet, maalit, syotot, jaahyt, pelaaja, joukkue): syotot
Henrik Sedin           VAN        43 11 + 38= 49  36
Erik Karlsson          OTT        43  6 + 35= 41  24
Claude Giroux          PHI        36 18 + 30= 48  16
Pavel Datsyuk          DET        41 13 + 30= 43  10
Brian Campbell         FLA        42  3 + 30= 33   4
Daniel Sedin           VAN        42 18 + 29= 47  32
Jason Pominville       BUF        41 14 + 29= 43   8
Nicklas Backstrom      WSH        38 13 + 29= 42  22
Joffrey Lupul          TOR        41 19 + 28= 47  36
Evgeni Malkin          PIT        33 16 + 28= 44  30

komento (pisteet, maalit, syotot, jaahyt, pelaaja, joukkue): pelaaja
kuka pelaaja: Jokinen
Olli Jokinen           CGY        43 12 + 21= 33  32
Jussi Jokinen          CAR        40  4 + 19= 23  30

komento (pisteet, maalit, syotot, jaahyt, pelaaja, joukkue): joukkue
mikä joukkue: DET
Pavel Datsyuk          DET        41 13 + 30= 43  10
Johan Franzen          DET        41 16 + 20= 36  34
Valtteri Filppula      DET        40 14 + 21= 35  10
Henrik Zetterberg      DET        41  8 + 24= 32  14
// lisää pelaajien nimiö

komento (pisteet, maalit, syotot, jaahyt, pelaaja, joukkue): lopeta

Huom: kun suoritat ohjelman ensimmäisen kerran, saattaa suoritus kestää jonkun aikaa (internetissä oleva palvelin voi olla lepotilassa ja sen herääminen kestää jonkin aikaa). Tämänjälkeisillä kerroilla suoritus on nopea.

Viikko 2

Muuttuvat muuttujat

Olemassaolevan muuttujan arvoa halutaan usein muuttaa. Tämä onnistuu tavallisen sijoituslausekkeen avulla. Seuraavassa muuttujan ika arvoa kasvatetaan yhdellä:

int ika = 1;

System.out.println( ika );    // tulostuu 1
ika = ika + 1;                // ika:n uusi arvo on ika:n vanha arvo plus yksi    
System.out.println( ika );    // tulostuu 2

Komento ika = ika + 1 siis kasvattaa muuttujan ika arvoa yhdellä. Muuttujan arvon kasvattaminen yhdellä onnistuu myös seuraavasti:

int ika = 1;

System.out.println( ika );    // tulostuu 1
ika++;                        // tarkoittaa samaa kuin ika = ika + 1;
System.out.println( ika );    // tulostuu 2

Toinen esimerkki:

int pituus = 100;

System.out.println( pituus );   // tulostuu 100
pituus = pituus - 50;    
System.out.println( pituus );   // tulostuu 50
pituus = pituus * 2;
System.out.println( pituus );   // tulostuu 100 
pituus = pituus / 4;
System.out.println( pituus );   // tulostuu 25
pituus--;                       // sama kuin pituus = pituus-1;
System.out.println( pituus );   // tulostuu 24

Kolmen luvun summa

Tee ohjelma, joka kysyy käyttäjältä kolme lukua ja tulostaa niitten summan. Tee ohjelmastasi seuraavan muotoinen:

Scanner lukija = new Scanner(System.in);
int summa = 0;
int luettu;

// KIRJOITA OHJELMA TÄHÄN
// ÄLÄ KÄYTÄ MUITA MUUTTUJIA KUIN lukija, summa JA luettu!

System.out.println("Summa: "+summa);
Anna ensimmäinen luku: 3
Anna toinen luku: 6
Anna kolmas luku: 12

Summa: 21

Useamman luvun summa

Tee ohjelma, joka lukee käyttäjältä lukuja ja tulostaa niiden summan. Ohjelma lopettaa kyselemisen kun syötetään luku 0. Käytä seuraavaa pohjaa, jonka saat myös palautusautomaatilta:

Scanner lukija = new Scanner(System.in);

int summa = 0;
System.out.print("Anna lukuja, nolla lopettaa: ");
while (true) {
    int luettu = Integer.parseInt(lukija.nextLine());
    if (luettu == 0) {
        break;
    }

    // TEE JOTAIN TÄÄLLÄ

    System.out.println("Summa nyt: "+summa);
}
System.out.println("Summa lopussa: "+summa);

Ohjelman tulee toimia seuraavasti:

Anna lukuja, nolla lopettaa:
3
Summa nyt: 3
2
Summa nyt: 5
1
Summa nyt: 6
1
Summa nyt: 7
0
Summa lopussa: 7

Lisää toistoa

Opimme aiemmin toteuttamaan while(true)-komennon avulla ohjelman, joka toistaa tiettyä komentosarjaa komennon break suorittamiseen asti.

Komento break ei ole ainoa tapa lopettaa toistoa. Toistokomennon yleinen muoto on while (ehto), jossa ehtona voi olla mikä tahansa totuusarvoinen lauseke, eli ehto voi olla täsmälleen samaa muotoa kuin if-komentojen ehdot.

Seuraavassa esimerkissä tulostetaan luvut 1, 2, ..., 10. Kun luku-muuttuja saa arvokseen yli 10, while-ehto ei ole enää voimassa ja toistaminen lopetetaan.

int luku = 1;

while (luku < 11) {
  System.out.println(luku);
  luku++;    // luku++ tarkoittaa samaa kuin luku = luku + 1
}

Lue ylläoleva "niin pitkään kuin luku on pienempi kuin 11, tulosta luku ja kasvata lukua yhdellä".

Ylläolevassa koodissa ehdossa olevaa muuttujaa luku kasvatettiin jokaisella kierroksella yhdellä. Päivitys voi olla mikä tahansa, eli ehdossa olevan muuttujan ei tarvitse suinkaan aina kasvaa yhdellä, esim:

int luku = 1024;

while (luku >= 1) {
  System.out.println(luku);
  luku = luku / 2;
}

Screencast aiheesta:

Muutama NetBeans-vihje

Tee seuraavat tehtävät while-komennon avulla:

Yhdestä sataan

Tee ohjelma, joka tulostaa kokonaisluvut väliltä 1–100.

Ohjelman tulostus on seuraava:

1
2
3
(välissä paljon rivejä)
98
99
100

Sadasta yhteen

Tee ohjelma, joka tulostaa kokonaisluvut väliltä 100–1.

Ohjelman tulostus on seuraava:

100
99
98
(välissä paljon rivejä)
3
2
1

Vihje: aseta toistossa käytettävän apumuuttujan arvoksi aluksi 100 ja vähennä muuttujan arvoa yhdellä toiston sisällä.

Parilliset luvut

Tee ohjelma, joka tulostaa parilliset kokonaisluvut väliltä 2–100.

2
4
6
(välissä paljon rivejä)
96
98
100

Lukuun asti

Kirjoita ohjelma, joka tulostaa kokonaisluvut 1:stä käyttäjän antamaan lukuun asti.

Mihin asti? 3
1
2
3
Mihin asti? 5
1
2
3
4
5

Vihje: käyttäjältä lukemasi luku toimii nyt whilen lopetusehdon ylärajana. Muista että Javassa a <= b tarkoittaa a pienempi tai yhtä suuri kuin b.

Alaraja ja yläraja

Kirjoita ohjelma, joka kysyy käyttäjältä ensimmäisen ja viimeisen luvun ja tulostaa niiden välissä olevat luvut.

Ensimmäinen: 5
Viimeinen: 8
5
6
7
8

Jos ensimmäinen on suurempi kuin viimeinen ei tulostu mitään:

Ensimmäinen: 16
Viimeinen: 12

Huom! muista että ala- ja yläraja voivat olla myös negatiivisia!

Sijoitusoperaatiot

Koska vanhan muuttujan arvon muuttaminen on hyvin yleinen operaatio, on Javassa sitä varten erityiset sijoitusoperaatiot.

int pituus = 100;

pituus += 10;  // sama kuin pituus = pituus + 10;
pituus -= 50;  // sama kuin pituus = pituus - 50;

Olemassaolevan muuttujan arvoa muuttava sijoitusoperaatio merkitään muuttuja muutostyyppi= muutos, esimerkiksi muuttuja += 5. Huomaa, että muuttujan tyyppi pitää aina kertoa ennen kuin sille voidaan asettaa arvo. Muuttuja tulee siis aina esitellä ennen kuin se on käytettävissä. Muuttujan esittely tapahtuu kertomalla muuttujan tyyppi ja nimi.

Seuraava esimerkki ei toimi, sillä muuttujan pituus tyyppiä ei ole kerrottu.

pituus = pituus + 100;   // ei toimi!
pituus += 100;           // ei toimi!

Kun tyyppi on kerrottu, laskutkin toimivat oikein.

int pituus = 0;
pituus = pituus + 100;
pituus += 100;

// muuttujan pituus arvo on 200

Myös muille kuin yhteen- ja vähennyslaskuille on Javassa vastaavat sijoitusoperaatiot.

int pituus = 100;

pituus *= 10; // sama kuin pituus = 10 * pituus; 
pituus /= 100; //sama kuin pituus = pituus / 100;
pituus %= 3;  // sama kuin pituus = pituus % 3;

// muuttujan pituus arvo 1

Usein ohjelmissa esiintyy toisto jonka aikana muuttujaan lasketaan jokin toistosta riippuvainen arvo. Seuraava ohjelma laskee tulon 4*3 hieman kömpelöllä tavalla eli summana 3+3+3+3:

int tulos = 0;

int i = 0;
while ( i < 4 ) {
   tulos = tulos + 3;
   i++;   // tarkoittaa samaa kuin i = i+1;
}

Alussa tulos = 0. Toistossa muuttujan arvo nousee joka kierroksella 3:lla. Ja koska toistoja on 4, on lopulta muuttujan arvona siis 3*4.

Käyttämällä yllä esiteltyä sijoitusoperaattoria, sama saadaan aikaan seuraavasti:

int tulos = 0;

int i = 0;
while ( i < 4 ) {
   tulos += 3;      // tämä on siis sama kuin tulos = tulos + 3;
   i++;             // tarkoittaa samaa kuin i = i+1;
}

Lukusarjan summa

Tee ohjelma, joka laskee summan 1+2+3+...+n, missä n on käyttäjän syöttämä luku.

Esimerkkitulostuksia:

Mihin asti? 3
Summa on 6   

Edellisessä esimerkissä laskettiin 1+2+3 = 6

Mihin asti? 7
Summa on 28   

Ja nyt laskettiin 1+2+3+4+5+6+7 = 28

Vihje: Tee ohjelma while-komennon avulla. Käytä ohjelmassasi apumuuttujaa toistokertojen muistamiseen. Lisää jokaisella toistokerralla toistokerrat muistavan muuttujan arvo apumuuttujaan johon lasket summan arvon.

Rajoitetun lukusarjan summa

Muuta edellistä tehtävää siten, että käyttäjä määrää summan laskemisen aloituskohdan. Voit olettaa, että käyttäjä antaa ensin pienemmän luvun ja sitten suuremman luvun.

Esimerkkitulostuksia:

Ensimmäinen: 3
Viimeinen: 5
Summa on 12
Ensimmäinen: 2
Viimeinen: 8
Summa on 35

Kertoma

Tee ohjelma, joka laskee luvun n kertoman. Kertoma n! lasketaan kaavalla 1*2*3*...*n. Esimerkiksi 4! = 1*2*3*4 = 24. Lisäksi on määritelty, että 0! = 1.

Esimerkkitulostuksia:

Anna luku: 3
Kertoma on 6
Anna luku: 10
Kertoma on 3628800

Potenssien summa

Tee ohjelma, joka laskee summan 20+21+22+...+2n, missä n on käyttäjän syöttämä luku. Merkintä 2i tarkoittaa luvun 2 korottamista potenssiin i, esimerkiksi 24 = 2*2*2*2 = 16. Javassa merkintä ab ei ole käytössä, mutta potenssin voi laskea komennolla Math.pow(luku, potenssi). Huom. komento palauttaa double-tyyppisen eli liukulukutyyppisen luvun. Liukulukutyyppisen luvun voi muuttaa kokonaisluvuksi ( int-tyyppiseksi ) seuraavasti: int tulos = (int) Math.pow(2, 3). Lasku asettaa muuttujaan tulos laskun 2 potenssiin 3 arvon.

Esimerkkitulostuksia:

Anna luku: 3
Tulos on 15
Anna luku: 7
Tulos on 255

Ikuinen silmukka

Yksi suosituimmista ohjelmointivirheistä toistolauseissa on tehdä vahingossa ikuinen silmukka. Seuraavassa yritetään tulostaa ruudulle 10 kertaa "En enää ikinä ohjelmoi ikuista silmukkaa":

int i = 0;

while( i<10 ) {
  System.out.println("En enää ikinä ohjelmoi ikuista silmukkaa");
}

Toistokertojen määrää kontrolloiva muuttuja i on aluksi 0 ja toistoja on tarkoitus tehdä niin kauan kuin i<10. Käy kuitenkin hieman hassusti: muuttujan i arvoa ei muuteta missään, joten toistoehto pysyy ikuisesti totena.

while ja lopetus

Olemme tehneet peräkkäin jo muutamia toistotehtäviä, joissa toistoehto on suunnilleen muotoa:

int i = 1;
while ( i < 10 ) {
  // ...
  i++;
}

Ylläolevassa rungossa muuttuja i muistaa toistokertojen määrän, ja lopetusehto perustuu i:n vertailuun.

Muistellaan taas toiston lopetusta. Toiston lopetuksen ei tarvitse perustua toistokertojen laskemiseen. Katsotaan esimerkkiä jossa kysytään käyttäjän ikää. Jos ikä ei ole välillä 5-85, annetaan huomautus ja kysytään ikä uudelleen. Toistolauseen while-lauseen ehto voi siis olla mitä tahansa totuusarvon tuottavaa.

System.out.println("ikäsi: ");

int ika = Integer.parseInt(lukija.nextLine());

while( ika < 5 || ika  > 85 ) {   // ikä pienempi kuin 5 TAI suurempi kuin 85
  System.out.println("Valehtelet");
  if ( ika < 5 ) {
      System.out.println("Olet niin nuori ettet osaa kirjoittaa");   
  } else if ( ika > 85 ) {
      System.out.println("Olet niin vanha ettet osaa käyttää tietokonetta");   
  } 

  System.out.println("syötä ikäsi uudelleen: ");
  ika = Integer.parseInt(lukija.nextLine();
}

System.out.println("Ikäsi on siis "+ ika);

Ohjelman olisi voinut tehdä myös vanhaa tuttua while(true) -rakennetta käyttäen:

System.out.println("ikäsi: ");

while( true ) {   
  int ika = Integer.parseInt(lukija.nextLine());

  if ( ika >= 5 && ika <= 85 ) {   // ikä 5:n JA 85:n välillä
    break;                              // lopetetaan toisto
  }

  System.out.println("Valehtelet");
  if ( ika < 5 ) {
      System.out.println("Olet niin nuori ettet osaa kirjoittaa");   
  } else {  // ikä siis yli 85
      System.out.println("Olet niin vanha ettet osaa käyttää tietokonetta");   
  } 

  System.out.println("syötä ikäsi uudelleen: ");
}

System.out.println("Ikäsi on siis "+ ika);

Silmukat, lopetus ja muistaminen

Tämä tehtäväsarja muodostaa yhden isomman ohjelman, jonka toiminnallisuus toteutetaan pienissä paloissa. Jos et tee tehtäväsarjaa loppuun asti, voit lähettää sen tarkastettavaksi vajaatekoisenakin. Tämä onnistuu painamalla testausnapin oikealla puolella olevasta "submit"-napista eli pienestä ylöspäinosoittavasta nuolesta. Vaikka palautusautomaatti valittaakin vielä tekemättä olevien tehtävänosien testeistä, kirjautuvat jo tekemiesi osien pisteet.

Huom: nyt (ja jatkossa) jokainen isomman tehtävän "alitehtävä" (kuten 36.1) on saman arvoinen tehtävä kuin alikohdaton tehtävä. Tehtävä 36 siis vastaa viittä normaalia tehtävää.

Lukujen lukeminen

Tee ohjelma, joka kysyy käyttäjältä lukuja (ohjelma tulostaa käyttäjälle aluksi "Syötä luvut:"), kunnes käyttäjä antaa luvun -1. Kun käyttäjä syöttää luvun -1, ohjelma tulostaa "Kiitos ja näkemiin!" ja päättyy.

Syötä luvut:
5
2
4
-1
Kiitos ja näkemiin!

Lukujen summa

Laajenna edellistä ohjelmaa siten, että ohjelma ilmoittaa käyttäjän syöttämien lukujen summan. (Lukua -1 ei lasketa mukaan.)

Syötä luvut:
5
2
4
-1
Kiitos ja näkemiin!
Summa: 11

Lukujen summa ja lukumäärä

Laajenna edellistä ohjelmaa siten, että ohjelma ilmoittaa myös käyttäjien antamien lukujen lukumäärän. (Lukua -1 ei lasketa mukaan.)

Syötä luvut:
5
2
4
-1
Kiitos ja näkemiin!
Summa: 11
Lukuja: 3

Lukujen keskiarvo

Muuta edellistä ohjelmaa siten, ohjelma ilmoittaa lukujen keskiarvon. (Lukua -1 ei lasketa mukaan.)

Syötä luvut:
5
2
4
-1
Kiitos ja näkemiin!
Summa: 11
Lukuja: 3
Keskiarvo: 3.666666666666

Parilliset ja parittomat

Laajenna edellistä ohjelmaa siten, että ohjelma ilmoittaa parillisten ja parittomien lukujen määrän. (Lukua -1 ei lasketa mukaan.)

Syötä luvut:
5
2
4
-1
Kiitos ja näkemiin!
Summa: 11
Lukuja: 3
Keskiarvo: 3.666666666666
Parillisia: 2
Parittomia: 1

Huomio: Ohjelmien tekeminen pienissä paloissa

Edeltävissä tehtävissä tehtiin oikeastaan ainoastaan yksi ohjelma, mutta ohjelman rakentaminen tapahtui hyvin pienissä paloissa. Tämä on ehdottoman suositeltava tapa AINA kun ohjelmoit.

Eli kun teet ohjelmaa, oli se sitten harjoitustehtävä tai oma projektisi, kannattaa edetä hyvin pienissä paloissa. Älä koskaan yritä ratkaista koko ongelmaa kerralla. Aloita jollain helpolla asialla jonka tiedät varmasti osaavasi. Esim. edellisessä tehtäväsarjassa keskityttiin aluksi pelkästään siihen, että osataan pysäyttää ohjelma käyttäjän syöttäessä luvun -1. Kun yksi ohjelman osa on saatu toimimaan, voidaan siirtyä ratkaisemaan jotain seuraavaa varsinaisen ongelman osaongelmaa.

Osa kurssin tehtäviä on edellisten tapaan valmiiksi osiin pilkottuja. Usein osat pitää vielä pilkkoa ohjelmoinnin kannalta vieläkin pienempiin paloihin. Kannattaa tehdä siten, että suoritat ohjelman lähes jokaisen uuden koodirivin jälkeen. Tällöin varmistat, että ratkaisu on etenemässä haluttuun suuntaan.

Lisätietoa: toisto for-lauseen avulla

Edellä esitellyn while-toistolauseen lisäksi Javassa toiston voi hoitaa myös for-toistolauseen avulla. Varsinaisesti rupeamme käyttämään for:ia vasta hieman myöhemmin kurssilla, mutta jo tässä vaiheessa mainittakoon, että toistolause

int i = 0;
while ( i ;lt& 10 ) {
  System.out.println( i );
  i++;
}

tehtäisiin for:in avulla seuraavasti:

for ( i = 0; i < 10 ; i ++ ) {
  System.out.println( i );
}

eli for-komento sisältää samalla rivillä indeksimuuttujan alustuksen, toistoehton ja indeksimuuttujan kasvatuksen puolipistein eroteltuna. Voit unohtaa for:in olemassaolon toistaiseksi jos et halua sekoittaa päätäsi sen olemassaololla.

Metodit

Olemme käyttäneet useita erilaisia komentoja Javassa: sijoitusta, laskutoimituksia, vertailuja, if:iä ja whileä. Ruudulle tulostaminen on tehty "komentoa" System.out.println() käyttäen. Kahden luvun maksimi osataan laskea "komennolla" Math.max(). Tuttuja ovat myös lukija.nextLine() ja sen kanssa usein nähty Integer.parseInt().

Huomaamme, että jälkimmäinen joukko edellä lueteltuja komentoja poikkeaa if:istä ja while:stä ym. siinä, että komennon perässä on sulut ja joskus sulkujen sisällä komennolle annettava syöte. "Sulkuihin päättyvät" eivät oikeastaan olekaan komentoja vaan metodeja.

Teknisesti ottaen metodi tarkoittaa koodinpätkää, jota voi kutsua muualta ohjelmakoodista. Koodirivi System.out.println("olen metodille annettava parametri!") siis tarkoittaa, että kutsutaan metodia, joka suorittaa ruudulle tulostamisen. Metodin suorituksen jälkeen palataan siihen kohtaa missä ennen metodikutsua oltiin menossa. Metodille suluissa annettua syötettä kutsutaan metodin parametriksi.

Parametrin lisäksi metodilla voi olla paluuarvo. Esim. tuttu koodinpätkä:

int luku = Integer.parseInt( lukija.nextLine() );

sisältää kaksi metodikutsua. Ensin kutsutaan sisempänä olevaa metodia lukija.nextLine. Metodilla on paluuarvonaan käyttäjän syöttämä merkkijono. Seuraavaksi kutsutaan metodia Integer.parseInt. Metodikutsun parametrina on merkkijono jonka lukija.nextLine:n kutsu palautti ja metodin paluuarvona on merkkijonoa vastaava kokonaisluku.

Metodin nimeen näyttää liittyvän piste, esim. lukija.nextLine(). Oikeastaan tässä metodin nimi onkin pisteen oikeanpuoleinen osa, eli nextLine(). Pisteen vasemmanpuoleinen osa, eli tässä lukija kertoo kenen metodista on kyse. Eli kyseessä on lukijan metodi nextLine. Opimme hiukan myöhemmin tarkemmin mistä tässä pisteen vasemmanpuoleisessa osassa on kyse. Tarkka lukija tietysti huomaa, että System.out.println():ssa on "kaksi pistettä". Metodin nimi tässä on println, ja System.out on se kenen metodista on kyse. Karkeasti ottaen System.out tarkoittaa koneen näyttöä.

Tähän mennessä käyttämämme metodit ovat kaikki olleet Javan valmiita metodeita. Nyt opettelemme tekemään omia metodeita.

Omat metodit

Olemme tähän mennessä ohjelmoineet ohjelmamme siten, että kaikki tapahtuu yhdessä jatkumossa ja koodia luetaan ylhäältä alas.

Edellä mainittiin että "metodi tarkoittaa koodinpätkää, jota voi kutsua muualta ohjelmakoodista". Javan valmiita metodeja on käytetty jo oikeastaan ensimmäisestä ohjelmasta lähtien.

Javan valmiiden metodien käytön lisäksi ohjelmoija voi kirjoittaa itse metodeja joita sovellus kutsuu. Oikeastaan on hyvin poikkeuksellista jos ohjelmassa ei ole yhtään itse kirjoitettua metodia. Tästälähtien lähes jokainen kurssilla tehty ohjelma sisältääkin itsekirjoitettuja metodeja.

Ohjelmarunkoon metodit kirjoitetaan main:in aaltosulkeiden ulkopuolelle mutta kuitenkin "uloimmaisten" aaltosulkeiden sisäpuolelle, joko mainin ylä- tai alapuolelle

import java.util.Scanner;

public class OhjelmaRunko {
  // omia metodeja tänne

  public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);
    // ohjelmakoodi
  }

  // omia metodeja tai tänne
}

Luodaan metodi tervehdi.

  public static void tervehdi() {
    System.out.println("Terveiset metodimaailmasta!");
  }

Ja asetetaan se metodeille kuuluvalle paikalle.

import java.util.Scanner;

public class OhjelmaRunko {
  public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);
    // ohjelmakoodi
  }

  // omat metodit
  public static void tervehdi() {
    System.out.println("Terveiset metodimaailmasta!");
  }
}

Metodin määrittely sisältää kaksi osaa. Metodimäärittelyn ensimmäisellä rivillä on metodin nimi eli tervehdi. Nimen vasemmalla puolella tässä vaiheessa määreet public static void. Metodin nimen sisältävän rivin alla on aaltosulkeilla erotettu koodilohko, jonka sisälle kirjoitetaan metodin koodi, eli ne komennot jotka metodia kutsuttaessa suoritetaan. Metodimme tervehdi ei tee muuta kuin kirjoittaa rivillisen tekstiä ruudulle.

Itsekirjoitetun metodin kutsu on helppoa, kirjoitetaan metodin nimi ja perään sulut ja puolipiste. Seuraavassa main eli pääohjelma kutsuu metodia ensin kerran ja sen jälkeen useita kertoja.

import java.util.Scanner;

public class OhjelmaRunko {
  public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);

    // ohjelmakoodi
    System.out.println("Kokeillaan pääsemmekö metodimaailmaan:");
    tervehdi();

    System.out.println("Näyttää siltä, kokeillaan vielä:");
    tervehdi(); 
    tervehdi(); 
    tervehdi();
  }

  // omat metodit
  public static void tervehdi() {
    System.out.println("Terveiset metodimaailmasta!");
  }
}

Ohjelman suoritus saa aikaan seuraavan tulosteen:

Kokeillaan pääsemmekö metodimaailmaan:
Terveiset metodimaailmasta!
Näyttää siltä, kokeillaan vielä:
Terveiset metodimaailmasta!
Terveiset metodimaailmasta!
Terveiset metodimaailmasta!

Huomionarvoista tässä on koodin suoritusjärjestys. Koodin suoritus etenee siten, että pääohjelman -- eli main:in -- rivit suoritetaan ylhäältä alas yksi kerrallaan. Koodirivin ollessa metodikutsu, mennään suorittamaan metodin koodirivit, jonka jälkeen palataan josta metodin kutsu tapahtui. Tarkemmin ottaen metodikutsun jälkeiselle riville.

Jos ollaan tarkkoja on pääohjelma eli main itsekin metodi. Kun ohjelma käynnistyy, kutsuu käyttöjärjestelmä main:ia. Metodi main on siis ohjelman käynnistyspiste, jonka ylimmältä riviltä ohjelman suoritus lähtee liikkeelle. Ohjelman suoritus loppuu kun päädytään mainin loppuun.

Jatkossa kun esittelemme metodeja, emme erikseen mainitse että niiden täytyy sijaita omalla paikallaan. Metodia ei esimerkiksi voi määritellä toisen metodin sisällä.

Tekstin tulostus

Tee metodi tulostaTeksti, joka tulostaa tekstin "Alussa olivat suo, kuokka ja Java." sekä rivinvaihdon.

public static void main(String[] args) {
    tulostaTeksti();
}

public static void tulostaTeksti() {
    // kirjoita koodia tähän
}

Ohjelman tulostus:

Alussa olivat suo, kuokka ja Java.

Monta tulostusta

Laajenna edellistä ohjelmaa siten, että pääohjelma kysyy käyttäjältä, montako kertaa teksti tulostetaan eli montako kertaa metodia kutsutaan.

public static void main(String[] args) {
    // kysy käyttäjältä, montako kertaa teksti tulostetaan
    // kutsu metodia tulostaTeksti while-komennon avulla useita kertoja
}

public static void tulostaTeksti() {
    // kirjoita koodia tähän
}

Ohjelman tulostus:

Kuinka monta?
7
Alussa olivat suo, kuokka ja Java.
Alussa olivat suo, kuokka ja Java.
Alussa olivat suo, kuokka ja Java.
Alussa olivat suo, kuokka ja Java.
Alussa olivat suo, kuokka ja Java.
Alussa olivat suo, kuokka ja Java.
Alussa olivat suo, kuokka ja Java.

huom: tulosta kehote Kuinka monta? omalle rivilleen!

Metodin parametrit

Metodista saa huomattavasti monikäyttöisemmän antamalla sille parametreja. Parametrit ovat muuttujia, jotka määritellään metodin ylimmällä rivillä metodin nimen jälkeen olevien sulkujen sisällä. Kun metodia kutsutaan, sen parametreille annetaan arvot kutsuvaiheessa.

Seuraavassa esimerkissä määritellään parametrillinen metodi tervehdi, jolla on String-tyyppinen parametri nimi.

public static void tervehdi(String nimi) {
  System.out.println("Hei " + nimi + ", terveiset metodimaailmasta!");
}

Kutsutaan metodia tervehdi siten, että parametrin nimi arvoksi asetetaan ensimmäisellä kutsulla Matti ja toisella kutsulla Arto.

public static void main(String[] args) {
  tervehdi("Matti");
  tervehdi("Arto");
}
Hei Matti, terveiset metodimaailmasta!
Hei Arto, terveiset metodimaailmasta!

Aivan kuten kutsuttaessa Javan valmista System.out.println()-metodia, voi oman metodin kutsussa parametrina käyttää monimutkaisempaa ilmausta:

public static void main(String[] args) {
  String nimi1 = "Antti";
  String nimi2 = "Mikkola";
  tervehdi( nimi1 + " " + nimi2 );

  int ika = 24;
  tervehdi("Juhana " + ika + " vuotta");
}
Hei Antti Mikkola, terveiset metodimaailmasta!
Hei Juhana 24 vuotta, terveiset metodimaailmasta!

Molemmissa tapauksissa metodilla on edelleen vain 1 parametri. Parametrin arvo lasketaan ennen metodin kutsumista. Ensimmäisessä tapauksessa parametrin arvo saadaan merkkijonokatenaationa nimi1 + " " + nimi2 joka siis on arvoltaan Antti Mikkola ja jälkimmäisessä tapauksessa merkkijonokatenaatiosta "Juhana " + ika + " vuotta".

Monta parametria

Metodille voidaan määritellä useita parametreja. Tällöin metodin kutsussa parametrit annetaan samassa järjestyksessä.

public static void tervehdi(String nimi, String mistaTerveiset) {
  System.out.println("Hei " + nimi + ", terveiset " + mistaTerveiset);
}
String kuka = "Matti";
String terveiset = "Kyröjoelta";

tervehdi(kuka, terveiset);
tervehdi(kuka, terveiset + " ja Kumpulasta");

Jälkimmäisessä tervehdi-funktion kutsussa toinen parametri muodostetaan katenoimalla muuttujaan terveiset teksti " ja Kumpulasta". Tämä suoritetaan ennen varsinaista funktion suoritusta.

Hei Matti, terveiset Kyröjoelta
Hei Matti, terveiset Kyröjoelta ja Kumpulasta	

Metodi kutsuu toista metodia

main ei ole suinkaan ainoa joka voi kutsua metodeita. Metodit voivat kutsua myös toisiaan. Tehdään metodi tervehdiMontaKertaa, joka tervehtii käyttäjää useasti metodin tervehdi avulla:

public static void tervehdi(String nimi) {
  System.out.println("Hei " + nimi + ", terveiset metodimaailmasta!");
}

public static void tervehdiMontaKertaa(String nimi, int kerrat) {
  int i = 0;
  while ( i < kerrat ) {
    tervehdi(nimi);
    i++;
  }
  
}

public static void main(String[] args) {
  tervehdiMontaKertaa("Antti", 3);
  System.out.println("ja");
  tervehdiMontaKertaa("Martin", 2);
}

Tulostuu:

Hei Antti, terveiset metodimaailmasta!
Hei Antti, terveiset metodimaailmasta!
Hei Antti, terveiset metodimaailmasta!
ja
Hei Martin, terveiset metodimaailmasta!
Hei Martin, terveiset metodimaailmasta!

Screencast aiheesta:

Tulostelua

Tähtien tulostus

Tee metodi tulostaTahtia, joka tulostaa annetun määrän tähtiä ja rivinvaihdon.

Tee metodi seuraavaan runkoon:

private static void tulostaTahtia(int maara) {
    // yhden tähden saat tulostettua komennolla
    // System.out.print("*");
    // kutsu tulostuskomentoa n kertaa
}

public static void main(String[] args) {
    tulostaTahtia(5);
    tulostaTahtia(3);
    tulostaTahtia(9);
}

Ohjelman tulostus:

*****
***
*********

huom: moniosaisen tehtävät voi palauttaa palvelimelle (painamalla testausnapin oikealla puolella olevaa nappia) vaikka kaikki osat eivät olisikaan tehty. Palvelin valittelee tällöin tekemättömien osien testeistä, tehdyt osat palvelin kirjaa.

Neliön tulostus

Tee metodi tulostaNelio(int sivunpituus) joka tulostaa neliön käyttäen tulostaTahtia-metodia. Siis esimerkiksi kutsu tulostaNelio(4) tulostaa seuraavaa:

****
****
****
****

Huom: tehtävässä ei riitä että tulostus näyttää oikealta, tulostaNelio-metodin sisällä neliön "rivien" tulostus tulee tehdä tulostaTahtia-metodia käyttäen.

Ohjelmaa tehdessäsi kannattaa varmistaa main:iin kirjoitetun testikoodin avulla että metodit toimivat vaaditulla tavalla.

Suorakulmion tulostus

Tee metodi tulostaSuorakulmio(int leveys, int korkeus) joka tulostaa suorakulmion käyttäen tulostaTahtia-metodia. Siis esimerkiksi kutsu tulostaSuorakulmio(17,3) tulostaa seuraavaa:

*****************
*****************
*****************

Vasemmalle nojaavan kolmion tulostus

Tee metodi tulostaKolmio(int koko) joka tulostaa kolmion käyttäen tulostaTahtia-metodia. Siis esimerkiksi kutsu tulostaKolmio(4) tulostaa seuraavaa:

*
**
***
****

Tulostelua Like A Boss

Tähtirivin ja tyhjien tulostus

Tee metodi tulostaTyhjaa(int koko) joka tulostaa koko kappaletta välilyöntejä. Metodi ei tulosta rivinvaihtoa.

Joudut myös joko kopioimaan edellisen tehtävän vastauksestasi metodin tulostaTahtia tai toteuttamaan sen uudelleen tämän tehtävän tehtäväpohjaan.

Oikealle nojaavan kolmion tulostus

Tee metodi tulostaKolmio(int koko) joka tulostaa kolmion käyttäen tulostaTyhjaa- ja tulostaTahtia-metodeja. Siis esimerkiksi kutsu tulostaKolmio(4) tulostaa seuraavaa:

   *
  **
 ***
****

Joulukuusen tulostus

Tee metodi jouluKuusi(int korkeus) joka tulostaa joulukuusen. Joulukuusi koostuu annetun korkuisesta kolmiosta ja jalasta. Jalka on kaksi tähteä korkea ja kolme tähteä leveä ja se on keskellä kolmion pohjaa. Kuusi tulee rakentaa käyttämällä tulostukseen metodeja tulostaTyhjaa ja tulostaTahtia

Esimerkiksi kutsu jouluKuusi(4) tulostaa seuraavaa:

   *
  ***
 *****
*******
  ***
  ***

Kutsu jouluKuusi(10) tulostaa:

         *
        ***
       *****
      *******
     *********
    ***********
   *************
  ***************
 *****************
*******************
        ***
        ***

Huom: Korkeuksien jotka ovat alle 3 ei tarvitse toimia!

Numerovisa

Tässä tehtävässä luodaan seuraavanlainen numerovisa:

Arvaa luku: 73
Luku on pienempi, tehtyjä arvauksia: 1
Arvaa luku: 22
Luku on suurempi, tehtyjä arvauksia: 2
Arvaa luku: 51
Luku on suurempi, tehtyjä arvauksia: 3
Arvaa luku: 62
Luku on suurempi, tehtyjä arvauksia: 4
Arvaa luku: 68
Luku on suurempi, tehtyjä arvauksia: 5
Arvaa luku: 71
Luku on pienempi, tehtyjä arvauksia: 6
Arvaa luku: 70
Onneksi olkoon, oikein arvattu!

Numeron arvaaminen

Tehtävänannon mukana tulevassa ohjelmassa tulee mukana komento arvoLuku, joka arpoo luvun suljetulta väliltä [0, 100] (0 ja 100 ovat myös mahdollisia). Toteuta ohjelma jossa arvataan arvottua lukua kerran. Ohjelman tulee tulostaa joko "Luku on pienempi", "Luku on suurempi" tai "Onneksi olkoon, oikein arvattu!" riippuen käyttäjän antamasta luvusta.

Arvaa luku: 12
Luku on suurempi
Arvaa luku: 66
Luku on pienempi
Arvaa luku: 42
Onneksi olkoon, oikein arvattu!

Toistuva arvaaminen

Lisää ohjelmaan toiminnallisuus jossa arvausta tehdään toistuvasti kunnes käyttäjä syöttää oikean numeron. Huomaa, että sinun tulee arpoa numero komentoa arvoLuku ennen toistolauseketta. Miksi? Mitä tapahtuu, jos luku arvotaan toistolausekkeen sisällä?

Alla olevassa esimerkissä kutsu komentoon arvoLuku palautti arvon 83.

Arvaa luku: 55
Luku on suurempi
Arvaa luku: 85
Luku on pienempi
Arvaa luku: 77
Luku on suurempi
Arvaa luku: 81
Luku on suurempi
Arvaa luku: 83
Onneksi olkoon, oikein arvattu!

Arvauskertojen laskeminen

Lisää ohjelmaan kokonaislukutyyppinen muuttuja, jonka avulla pidetään kirjaa tehtyjen arvausten määrästä. Tulosta arvausten määrä aina arvauksen yhteydessä.

Arvaa luku: 55
Luku on suurempi, tehtyjä arvauksia: 1
Arvaa luku: 85
Luku on pienempi, tehtyjä arvauksia: 2
Arvaa luku: 77
Luku on suurempi, tehtyjä arvauksia: 3
Arvaa luku: 81
Luku on suurempi, tehtyjä arvauksia: 4
Arvaa luku: 83
Onneksi olkoon, oikein arvattu!

Tekstikäyttöliittymä hirsipuu-pelille

Kaverisi ohjelmoi käyttöösi Hirsipuu-pelin, jolta puuttuu vielä käyttöliittymätoiminnallisuus. Hirsipuulla on seuraavat metodit:

Käytössäsi on palautusautomaatista tuleva ohjelmarunko, jossa on jo hieman toiminnallisuutta:

        Scanner lukija = new Scanner(System.in);
        Hirsipuu hirsipuu = new Hirsipuu();
        
        System.out.println("************");
        System.out.println("* HIRSIPUU *");
        System.out.println("************");
        System.out.println("");
        tulostaValikko();
        System.out.println("");
        
        // OHJELMOI TOTEUTUKSESI TÄNNE

	System.out.println("Kiitos pelistä!");

Rungon lisäksi käytössäsi on metodi tulostaValikko:

    public static void tulostaValikko() {
        System.out.println(" * valikko *");
        System.out.println("lopeta   - lopettaa pelin");
        System.out.println("tilanne  - tulostaa tarkemman tilanteen");
        System.out.println("yksittäinen kirjain arvaa annettua kirjainta");
        System.out.println("tyhjä rivi tulostaa tämän valikon");
    }

Tehtävä toteutetaan pienissä osissa.

Toistolauseke ja siitä poistuminen

Luo ohjelmaan toistolauseke, joka toimii käyttöliittymän pohjana. Lue toistolausekkeen sisällä käyttäjältä komento. Jos komento on "lopeta", poistu toistolauseesta.

Käytä whilen ehtona komentoa hirsipuu.peliKaynnissa(), eli toisto on muotoa:

while (hirsipuu.peliKaynnissa()) {
  String komento = lukija.nextLine();
  // ...
}

Ensi viikolla ymmärrämme mistä tässä hieman erikoiselta näyttävässä toiston lopetusehdossa on tarkkaan ottaen kysymys.

Tässä vaiheessa ohjelman tulisi toimia seuraavasti:

************
* HIRSIPUU *
************

 * valikko *
lopeta   - lopettaa pelin
tilanne  - tulostaa tarkemman tilanteen
yksittäinen kirjain arvaa annettua kirjainta
tyhjä rivi tulostaa tämän valikon 

Anna komento: 
ei lopeta

Anna komento: 
lopeta
Kiitos pelistä!

Tilanteen näyttäminen

Jos käyttäjä syöttää merkkijonon "tilanne", tulosta tilanne metodin hirsipuu.tulostaTilanne() avulla.

************
* HIRSIPUU *
************

 * valikko *
lopeta   - lopettaa pelin
tilanne  - tulostaa tarkemman tilanteen
yksittäinen kirjain arvaa annettua kirjainta
tyhjä rivi tulostaa tämän valikon

Anna komento: 
tilanne
Et ole vielä tehnyt yhtään arvausta.
Käyttämättömät kirjaimet: abcdefghijklmnopqrstuvwxyzåäö

Anna komento: 
lopeta
Kiitos pelistä!

Arvauksen tekeminen

Jos käyttäjä kirjoittaa komentona yhden merkin pituisen merkkijonon, eli yksittäisen kirjaimen, käytä syötettä arvaamiseen. Arvaaminen tapahtuu metodilla hirsipuu.arvaa(komento). Arvauskomennolla on oma tulostustoiminnallisuus, jolla se tulostaa lisätietoa arvauksen onnistumisesta.

Vihje: komennon toteaminen yksittäiseksi kirjaimeksi onnistuu seuraavasti:

String komento = lukija.nextLine();

if(komento.length() == 1) {  // komento on vain yksi kirjain, eli kyseessä on arvaus
  hirsipuu.arvaa(komento);
} 
...
Anna komento: 
a
Kirjainta a ei löytynyt sanasta.

Anna komento: 
b
Kirjainta b ei löytynyt sanasta.

Anna komento: 
c
Kirjain c löytyi sanasta!

Anna komento: 
lopeta
Kiitos pelistä!

Valikon tulostaminen

Jos käyttäjä syöttää tyhjän merkkijonon, eli merkkijonon jonka pituus on nolla, kutsu metodia tulostaValikko. Huomaa, että metodi tulostaValikko ei liity Hirsipuu-peliin, vaan omaan ohjelmaasi.

Vihje: merkkijonon tyhjyyden tarkastaminen onnistuu seuraavasti:

String nalle = "puh";
if(nalle.isEmpty()) {
    System.out.println("Tyhjä merkkijono");
} else {
    System.out.println("Löytyipäs sieltä jotain!");
}

Pelitilanteen tulostaminen

Jos käyttäjä ei ole syöttänyt komentoa lopeta, kutsu toistolausekkeen lopussa hirsipuupelin komentoja hirsipuu.tulostaUkko() ja hirsipuu.tulostaSana().

...
Anna komento: 
a
Kirjainta a ei löytynyt sanasta.
 ____
 |   
 |   
 |   
 |   
/|\
Arvattava sana: ????

Anna komento: 
m
Kirjain m löytyi sanasta!
 ____
 |   
 |   
 |   
 |   
/|\
Arvattava sana: m???

Anna komento: 
lopeta
Kiitos pelistä!
Viikko 3

Lisää metodeista

Edellisellä viikolla opettelimme kirjoittamaan omia metodeja. Metodien avulla voimme jäsennellä ohjelmaa pienemmiksi hyvin nimetyiksi. selkeän tehtävän omaaviksi loogisiksi kokonaisuuksiksi. Tämä sekä helpottaa ongelmanratkaisua että parantaa ohjelman luettavuutta niin ohjelmoijan kuin ohjelmaa myöhemmin mahdollisesti ylläpitävänkin osalta. Tästä lähtien oikeastaan jokainen tekemämme ohjelma sisältää metodeita.

Jatketaan edelleen metodien parissa.

Metodit ja muuttujien näkyvyys

Yritetään muuttaa metodin sisältä pääohjelman muuttujan arvoa.

// pääohjelma
public static void main(String[] args) {
  int luku = 1;
  kasvataKolmella();
}

// metodi
public static void kasvataKolmella() {
  luku = luku + 3;
}

Ohjelma ei kuitenkaan toimi, sillä metodi ei näe pääohjelman muuttujaa luku.

Yleisemminkin voi todeta, että pääohjelman muuttujat eivät näy metodien sisään, ja metodin muuttujat eivät näy muille metodeille tai pääohjelmalle. Ainoa keino viedä metodille tietoa ulkopuolelta on parametrin avulla.

// pääohjelma
public static void main(String[] args) {
  int luku = 1;
  System.out.println("Pääohjelman muuttujan luku arvo: " + luku);
  kasvataKolmella(luku);
  System.out.println("Pääohjelman muuttujan luku arvo: " + luku);
}

// metodi
public static void kasvataKolmella(int luku) {
  System.out.println("Metodin parametrin luku arvo: " + luku);
  luku = luku + 3;
  System.out.println("Metodin parametrin luku arvo: " + luku);
}

Yllä metodilla kasvataKolmella on parametri luku. Parametri luku kopioidaan metodin käyttöön. Kun yllä oleva ohjelma suoritetaan, nähdään seuraavanlainen tulostus.

Pääohjelman muuttujan luku arvo: 1
Metodin parametrin luku arvo: 1
Metodin parametrin luku arvo: 4
Pääohjelman muuttujan luku arvo: 1

Parametrina annettu luku siis kopioitiin metodin käyttöön. Jotta saisimme luvun uuden arvon myös pääohjelmaan, tulee metodin palauttaa arvo.

Metodin paluuarvot

Metodi voi palauttaa arvon. Tähän mennessä kurssilla olleissa esimerkeissä ja tehtävissä metodit eivät palauttaneet mitään. Tämä on merkitty kirjoittamalla metodin ylimmälle riville heti nimen vasemmalle puolelle void.

public static void kasvataKolmella() {
  ...

Arvon palauttavaa metodia määriteltäessä täytyy määritellä myös palautettavan arvon tyyppi. Paluuarvon tyyppi merkitään metodin nimen vasemmalle puolelle. Seuraavassa metodi joka palauttaa aina kokonaisluvun 10, metodin tyyppi on int. Palautus tapahtuu komennolla return:

public static int palautetaanAinaKymppi() {
  return 10;
}

Jotta metodin palauttamaa arvoa voisi käyttää, tulee paluuarvo ottaa talteen muuttujaan:

public static void main(String[] args) {
  int luku = palautetaanAinaKymppi();

  System.out.println( "metodi palautti luvun " + luku );
}

Metodin paluuarvo sijoitetaan int-tyyppiseen muuttujaan aivan kuin mikä tahansa muukin int-arvo. Paluuarvo voi toimia myös osana mitä tahansa lauseketta:

double luku = 4 * palautetaanAinaKymppi() + (palautetaanAinaKymppi() / 2) - 8;

System.out.println( "laskutoimituksen tulos " + luku );

Kaikki muuttujatyypit, mitä olemme tähän mennessä nähneet, voidaan palauttaa metodista:

public static void metodiJokaEiPalautaMitaan() {
  // metodin runko
}

public static int metodiJokaPalauttaaKokonaisLuvun() {
  // metodin runko, tarvitsee return-komennon
}

public static String metodiJokaPalauttaaTekstin() {
  // metodin runko, tarvitsee return-komennon
}

public static double metodiJokaPalauttaaLiukuluvun() {
  // metodin runko, tarvitsee return-komennon
}

Jos metodille määritellään paluuarvo, on sen myös pakko palauttaa arvo, esimerkiksi seuraava metodi on virheellinen.

public static String virheellinenMetodi() {
  System.out.println("Väitän palauttavani merkkijonon, mutten palauta sitä.");
}

Seuraavassa esimerkissä määritellään metodi summan laskemiseen. Tämän jälkeen metodia käytetään laskemaan luvut 2 ja 7 yhteen. Metodikutsusta saatava paluuarvo asetetaan muuttujaan lukujenSumma.

public static int summa(int eka, int toka) {
  return eka + toka;
}

Metodin kutsu:

int lukujenSumma = summa(2, 7);
// lukujenSumma on nyt 9

Laajennetaan edellistä esimerkkiä siten, että käyttäjä syöttää luvut.

public static void main(String[] args) {
  Scanner lukija = new Scanner(System.in);

  System.out.print("Anna ensimmäinen luku: ");
  int eka = Integer.parseInt( lukija.nextLine() );

  System.out.print("Anna toinen luku: ");
  int toka = Integer.parseInt( lukija.nextLine() );

  System.out.print("Luvut ovat yhteensä: " + summa(eka,toka) );
}

public static int summa(int eka, int toka) {
  return eka + toka;
}

Kuten huomataan, metodin paluuarvoa ei tarvitse välttämättä sijoittaa muuttujaan, se voi olla osana tulostuslausetta aivan kuten mikä tahansa muukin int-arvo.

Huomaa, että metodin parametrien nimillä ja metodin kutsujan puolella määritellyillä muuttujan nimillä ei ole mitään tekemistä keskenään. Edellisessä esimerkissä sekä pääohjelman muuttujat että metodin parametrit olivat "sattumalta" nimetty samoin (eli eka ja toka). Seuraava toimisi aivan yhtä hyvin:

public static void main(String[] args) {
  Scanner lukija = new Scanner(System.in);

  System.out.print("Anna ensimmäinen luku: ");
  int luku1 = Integer.parseInt( lukija.nextLine() );

  System.out.print("Anna toinen luku: ");
  int luku2 = Integer.parseInt( lukija.nextLine() );

  System.out.print("Luvut ovat yhteensä: " + summa(luku1, luku2) );
}

public static int summa(int eka, int toka) {
  return eka + toka;
}

Nyt pääohjelman muuttujan luku1 arvo kopioituu metodin parametrin eka arvoksi ja pääohjelman muuttujan luku2 arvo kopioituu metodin parametrin toka arvoksi.

Seuraavassa esimerkissä metodia summa kutsutaan kokonaisluvuilla, jotka saadaan summa-metodin paluuarvoina.

int eka = 3;
int toka = 2;

int monenLuvunSumma = summa(summa(1, 2), summa(eka, toka));
// 1) suoritetaan sisemmät metodit:
//    summa(1, 2) = 3   ja summa(eka, toka) = 5
// 2) suoritetaan ulompi metodi:
//    summa(3, 5) = 8 
// 3) muuttujan monenLuvunSumma arvoksi siis tulee 8

Metodin omat muuttujat

Seuraava metodi laskee syötteinään saamiensa lukujen keskiarvon. Metodi käyttää apumuuttujia summa ja ka. Metodin sisäisen muuttujan määrittely tapahtuu tutulla tavalla.

public static double keskiarvo(int luku1, int luku2, int luku3) {

  int summa = luku1 + luku2 + luku3;
  double ka = summa / 3.0;

  return ka;
}

Metodin kutsu voi tapahtua esim seuraavasti

public static void main(String[] args) {
  Scanner lukija = new Scanner(System.in);

  System.out.print("Anna ensimmäinen luku: ");
  int eka = Integer.parseInt( lukija.nextLine() );

  System.out.print("Anna toinen luku: ");
  int toka = Integer.parseInt( lukija.nextLine() );

  System.out.print("ja kolmas luku: ");
  int kolmas = Integer.parseInt( lukija.nextLine() );

  double keskiarvonTulos = keskiarvo(eka, toka, kolmas);

  System.out.print("Lukujen keskiarvo: " + keskiarvonTulos );
}

Huomaa että metodin sisäiset muuttujat summa ja ka eivät näy pääohjelmaan. Yksi yleinen aloittelijan virhe olisikin yrittää käyttää metodia seuraavasi:

public static void main(String[] args) {
  // annetaan arvot muuttujille eka, toka ja kolmas

  keskiarvo(eka, toka, kolmas);

  // yritetään käyttää metodin sisäistä muuttujaa, EI TOIMI!
  System.out.print("Lukujen keskiarvo: " + ka );
}

Myös seuraavanlaista virhettä näkee usein:

public static void main(String[] args) {
  // annetaan arvot muuttujille eka, toka ja kolmas

  keskiarvo(eka, toka, kolmas);

  // yritetään käyttää pelkkää metodin nimeä, EI TOIMI!
  System.out.print("Lukujen keskiarvo: " + keskiarvo );
}

Eli tässä yritettiin käyttää pelkkää metodin nimeä muuttujamaisesti. Toimiva tapa metodin tuloksen sijoittamisen apumuuttujaan lisäksi on suorittaa metodikutsu suoraan tulostuslauseen sisällä:

public static void main(String[] args) {
  int eka = 3;
  int toka = 8;
  int kolmas = 4; 

  // kutsutaan metodia tulostuslauseessa, TOIMII!
  System.out.print("Lukujen keskiarvo: " + keskiarvo(eka, toka, kolmas) );
}

Tässä siis ensin tapahtuu metodikutsu joka palauttaa arvon 5.0 joka sitten tulostetaan tulostuskomennon avulla.

Screencast aiheesta:

Lukujen summa

Tee metodi summa, joka laskee parametrina olevien lukujen summan.

Tee metodi seuraavaan runkoon:

public static int summa(int luku1, int luku2, int luku3, int luku4) {
    // kirjoita koodia tähän
    // muista että metodissa on oltava (lopussa) return!
}

public static void main(String[] args) {
    int vastaus = summa(4, 3, 6, 1);
    System.out.println("Summa: " + vastaus);
}

Ohjelman tulostus:

Summa: 14

Huom: kun tehtävässä sanotaan että metodin pitää palauttaa jotain, tarkoittaa tämä sitä että metodissa tulee olla määritelty paluutyyppi ja return-komento jolla haluttu asia palautetaan. Metodi ei itse tulosta (eli käytä komentoa System.out.println(..)), tulostuksen hoitaa metodin kutsuja, eli tässä tapauksessa pääohjelma.

Pienin

Tee metodi pienin, joka palauttaa parametrina saamistaan luvuista pienemmän arvon. Jos lukujen arvo on sama, voidaan palauttaa kumpi tahansa luvuista.

public static int pienin(int luku1, int luku2) {
    // kirjoita koodia tähän
    // älä tulosta metodin sisällä mitään
   
    // lopussa oltava komento return
}

public static void main(String[] args) {
    int vastaus =  pienin(2, 7);
    System.out.println("Pienin: " + vastaus);
}

Ohjelman tulostus:

Pienin: 2

Suurin

Tee metodi suurin, joka saa kolme lukua ja palauttaa niistä suurimman. Jos suurimpia arvoja on useita, riittää niistä jonkun palauttaminen. Tulostus tapahtuu pääohjelmassa.

public static int suurin(int luku1, int luku2, int luku3) {
    // kirjoita koodia tähän
}

public static void main(String[] args) {
    int vastaus =  suurin(2, 7, 3);
    System.out.println("Suurin: " + vastaus);
}

Ohjelman tulostus:

Suurin: 7

Lukujen keskiarvo

Tee metodi keskiarvo, joka laskee parametrina olevien lukujen keskiarvon. Metodin sisällä tulee käyttää apuna tehtävän 43 metodia summa!

Tee metodi seuraavaan runkoon:

public static int summa(int luku1, int luku2, int luku3, int luku4) {
    // kopioi koodi tehtävästä 43
}

public static double keskiarvo(int luku1, int luku2, int luku3, int luku4) {
    // kirjoita koodia tähän
    // laske alkioiden summa kutsumalla metodia summa
}

public static void main(String[] args) {
    double vastaus = keskiarvo(4, 3, 6, 1);
    System.out.println("Keskiarvo: " + vastaus);
}

Ohjelman tulostus:

Keskiarvo: 3.5

Muistathan miten kokonaisluku (int) muutetaan desimaaliluvuksi (double)!

Merkkijonot

Tässä luvussa tutustaan tarkemmin Javan merkkijonoihin, eli String:eihin. Olemme jo käyttäneet String-tyyppisiä muuttujia tulostuksen yhteydessä sekä oppineet vertailemaan merkkijonoja toisiinsa. Merkkijonoja vertailtiin toisiinsa kutsumalla merkkijonon equals()-metodia.

String elain = "Koira";

if( elain.equals("Koira") ) {
    System.out.println(elain + " sanoo vuh vuh");
} else if ( elain.equals("Kissa") ) {
    System.out.println(elain + " sanoo miau miau");
}

Merkkijonoilta voi kysyä niiden pituutta kirjoittamalla merkkijonon perään .length() eli kutsumalla merkkijonolle sen pituuden kertovaa metodia.

String banaani = "banaani";
String kurkku = "kurkku";
String yhdessa = banaani + kurkku;

System.out.println("Banaanin pituus on " + banaani.length());
System.out.println("Kurkku pituus on " + kurkku.length());
System.out.println("Sanan " + yhdessa + " pituus on " + yhdessa.length());

Edellä kutsutaan metodia length() kolmelle eri merkkijonolle. Kutsu banaani.length() kutsuu nimenomaan merkkijonon banaani pituuden kertovaa metodia, kun taas kurkku.length() on merkkijonon kurkku pituuden kertovan metodin kutsu, jne.Pisteen vasemman puoleinen osa kertoo kenen metodia kutsutaan.

Javassa on erillinen char-tietotyyppi kirjaimia varten. Yksittäiseen char-muuttujaan voi tallentaa yhden kirjaimen. Merkkijonolta voidaan kysyä sen kirjaimia kirjaimen paikan perusteella käyttämällä charAt()-metodia. Huomaa että laskeminen alkaa nollasta!

String kirja = "Kalavale";

char merkki = kirja.charAt(3);
System.out.println("Kirjan neljäs kirjain on " + merkki); //tulostaa "a"

Koska merkkijonon kirjaimet numeroidaan (eli teknisemmin ilmaistuna merkkijonoja indeksoidaan) alkaen paikasta 0, on merkkijonon viimeisen kirjaimen numero eli indeksi "merkkijonon pituus miinus yksi", eli kirja.charAt(kirja.length()-1). Esimerkiksi seuraava kaataa ohjelman: yritämme hakea kirjainta kohdasta jota ei ole olemassa.

char merkki = kirja.charAt(kirja.length());
NetBeans-vihje

Nimen pituus

Tee ohjelma, joka kysyy käyttäjän nimen ja ilmoittaa, kuinka monta kirjainta siinä on.

Anna nimi: Pekka
Kirjainmäärä: 5
Anna nimi: Katariina
Kirjainmäärä: 9

Huom! Rakenna ohjelmasi niin että laitat pituuden laskemisen omaan metodiinsa: public static int laskeKirjaimet(String merkkijono). Testit testaavat sekä metodia laskeKirjaimet että koko ohjelman toimintaa.

Ensimmäinen kirjain

Tee ohjelma, joka kysyy käyttäjän nimen ja ilmoittaa nimen ensimmäisen kirjaimen.

Anna nimi: Pekka
Ensimmäinen kirjain: P
Anna nimi: Katariina
Ensimmäinen kirjain: K

Huom! Rakenna ohjelmasi niin että laitat ensimmäisen kirjaimen hakemisen omaan metodiinsa: public static char ensimmainenKirjain(String merkkijono). Testit testaavat sekä metodia ensimmainenKirjain että koko ohjelman toimintaa.

Viimeinen kirjain

Tee ohjelma, joka kysyy käyttäjän nimen ja ilmoittaa nimen viimeisen kirjaimen.

Anna nimi: Pekka
Viimeinen kirjain: a
Anna nimi: Katariina
Viimeinen kirjain: a

Huom! Rakenna ohjelmasi niin että laitat viimeisen kirjaimen hakemisen omaan metodiinsa: public static char viimeinenKirjain(String merkkijono). Testit testaavat sekä metodia viimeinenKirjain että koko ohjelman toimintaa.

Ensimmäiset kirjaimet erikseen

Tee ohjelma, joka kysyy käyttäjän nimen ja ilmoittaa sen kolme ensimmäistä kirjainta erikseen. Jos nimen pituus on alle kolme, ei ohjelma tulosta mitään. Tehtävässä ei edellytetä erillisten metodien luomista.

Anna nimi: Pekka
1. kirjain: P
2. kirjain: e
3. kirjain: k
Anna nimi: me

Huom: ole tässä ja seuraavassa tehtävässä erityisen tarkka tulostusasun suhteen! Tulostuksessa tulee olla yksi välilyönti sekä pisteen että kaksoispisteen jälkeen!

Kirjaimet erikseen

Tee ohjelma, joka kysyy käyttäjän nimen ja ilmoittaa sen kirjaimet erikseen. Tehtävässä ei edellytetä erillisen metodin luomista.

Anna nimi: Pekka
1. kirjain: P
2. kirjain: e
3. kirjain: k
4. kirjain: k
5. kirjain: a

Vihje: while-toistolauseesta on tässä apua!

Anna nimi: Katariina
1. kirjain: K
2. kirjain: a
3. kirjain: t
4. kirjain: a
5. kirjain: r
6. kirjain: i
7. kirjain: i
8. kirjain: n
9. kirjain: a

Nimen kääntäminen

Tee ohjelma, joka kysyy käyttäjän nimen ja tulostaa sen väärinpäin. Erillistä metodia nimen kääntämiselle ei tarvitse tehdä.

Anna nimi: Pekka
Väärinpäin: akkeP
Anna nimi: Katariina
Väärinpäin: aniirataK

Vihje: Yksittäisen merkin saa tulostettua komennolla System.out.print()

Muita merkkijonojen metodeja

Merkkijonosta halutaan usein lukea jokin tietty osa. Tämä onnistuu mekkkijonojen eli String-luokan metodilla substring. Sitä voidaan käyttää kahdella tavalla: yksiparametrisenä palauttamaan merkkijonon loppuosa tai kaksiparametrisena palauttamaan parametrien valitsema osajono merkkijonosta:

String kirja = "Kalavale";

System.out.println(kirja.substring(4)); //tulostaa "vale"
System.out.println(kirja.substring(2,6)); //tulostaa "lava"

Koska substring-metodin paluuarvo on String-tyyppinen, voidaan metodin paluuarvo ottaa talteen String-tyyppiseen muuttujaan kirja..

String kirja = "8 veljestä";

String loppuosa = kirja.substring(2);
System.out.println("7 " + loppuosa); // tulostaa: 7 veljestä

String-luokan metodit tarjoavat myös mahdollisuuden etsiä tekstistä tiettyä sanaa. Esimerkiksi sana "erkki" sisältyy tekstiin "merkki". Metodi indexOf() etsii parametrinaan annettua sanaa merkkijonosta. Jos sana löytyi, se palauttaa sanan ensimmäisen kirjaimen indeksin, eli paikan (muista että paikkanumerointi alkaa nollasta!). Jos taas sanaa ei löytynyt merkkijonosta palautetaan arvo -1.

String sana = "merkkijono";

int indeksi = sana.indexOf("erkki"); //indeksin arvoksi tulee 1
System.out.println(sana.substring(indeksi)); //tulostetaan "erkkijono"

indeksi = sana.indexOf("jono"); //indeksin arvoksi tulee 6
System.out.println(sana.substring(indeksi)); //tulostetaan "jono"

indeksi = sana.indexOf("kirja"); //sana "kirja" ei sisälly sanaan "merkkijono"
System.out.println(indeksi); //tulostetaan -1
System.out.println(sana.substring(indeksi)); //virhe!

Screencast aiheesta:

Alkuosa

Tee ohjelma, joka tulostaa sanan alkuosan. Ohjelma kysyy käyttäjältä sanan ja alkuosan pituuden. Käytä ohjelmassa metodia substring.

Anna sana: esimerkki
Alkuosan pituus: 4
Tulos: esim
Anna sana: esimerkki
Alkuosan pituus: 7
Tulos: esimerk

Loppuosa

Tee ohjelma, joka tulostaa sanan loppuosan. Ohjelma kysyy käyttäjältä sanan ja loppuosan pituuden. Käytä ohjelmassa merkkijonon metodia substring.

Anna sana: esimerkki
Loppuosan pituus: 4
Tulos: rkki
Anna sana: esimerkki
Loppuosan pituus: 7
Tulos: imerkki

Sana sanassa

Tee ohjelma, joka kysyy käyttäjältä kaksi sanaa. Sitten ohjelma kertoo, onko toinen sana ensimmäisen sanan osana. Käytä ohjelmassa merkkijonon metodia indexOf.

Anna 1. sana: suppilovahvero
Anna 2. sana: ilo
Sana 'ilo' on sanan 'suppilovahvero' osana.
Anna 1. sana: suppilovahvero
Anna 2. sana: suru
Sana 'suru' ei ole sanan 'suppilovahvero' osana.

Huom: tulosta ohjelmassasi täsmälleen samassa muodossa kuin esimerkissä!

Merkkijonon kääntäminen

Tee metodi kaanna, joka kääntää annetun merkkijonon. Käytä metodille seuraavaa runkoa:

public static String kaanna(String merkkijono) {
    // kirjoita koodia tähän
}

public static void main(String[] args) {
    System.out.print("Anna merkkijono: ");
    String merkkijono = lukija.nextLine();
    System.out.println("Väärinpäin: " + kaanna(merkkijono));
}

Vihje: joudut todennäköisesti kokoamaan metodin sisällä käänteisen merkkijonon merkki kerrallaan. Kokoamisessa kannattaa käyttää apuna String-tyyppistä apumuuttujaa. Aluksi apumuuttujan arvo on tyhjä merkkijono. Tämän jälkeen merkkijonon perään laitetaan uusia merkkejä merkki kerrallaan.

  String apu = "";

  // ...
  // lisätään merkki apu-nimisen muuttujan perään
  apu = apu + merkki;

Ohjelman tulostus:

Anna merkkijono: esimerkki
ikkremise

Oliot ja metodit

Merkkijonot poikkeavat luonteeltaan hieman esim. kokonaisluvuista. Kokonaisluvut ovat "pelkkiä arvoja", niiden avulla voi laskea ja niitä voi tulostella ruudulle:

int x = 1;
int y = 3;

y = 3*x + 2;

System.out.println( "y:n arvo nyt: " + y );

Merkkijonot ovat hieman "älykkäämpiä" ja tietävät esimerkiksi pituutensa:

String sana1 = "Ohjelmointi";
String sana2 = "Java";

System.out.println( "merkkijonon "+ sana1 +" pituus: " + sana1.length() );

System.out.println( "merkkijonon "+ sana2 +" pituus: " + sana2.length() );

Tulostuu:

merkkijonon Ohjelmointi pituus on 11
merkkijonon Java pituus on 4

Pituus saadaan selville kutsumalla merkkijonon metodia length(). Merkkijonoilla on joukko muitakin metodeja. Kokonaisluvuilla eli int:eillä ei ole metodeja ollenkaan, ne eivät itsessään "osaa" mitään.

Merkkijonot ovat olioita eli "asioita joihin liittyy metodeja sekä arvo". Jatkossa tulemme näkemään hyvin paljon muitakin olioita kuin merkkijonoja.

Kuten edellisestä esimerkistä huomaamme, kutsutaan olion metodia lisäämällä olion nimen perään piste ja metodikutsu:

sana1.length()    // kutsutaan merkkijono-olion sana1 metodia length()
sana2.length()    // kutsutaan merkkijono-olion sana2 metodia length()

Metodikutsu kohdistuu nimenomaan sihen olioon, mille metodia kutsutaan. Yllä kutsumme ensin sana1-nimisen merkkijonon length()-metodia, sitten merkkijonon sana2 metodia length().

Vanha tuttumme lukija on myös olio:

  Scanner lukija = new Scanner(System.in);
Lukijat ja merkkijonot ovat molemmat oliota, ne ovat kuitenkin varsin erilaisia. Lukijoilla on mm. metodi nextLine() jota merkkijonoilla ei ole. Javassa oliot "synnytetään" eli luodaan melkein aina komennolla new, merkkijonot muodostavat tässä suhteessa poikkeuksen! -- Merkkijonoja voi luoda kahdella tavalla:

String banaani = new String("Banaani");
String porkkana = "porkkana";

Kumpikin ylläolevista riveistä luo uuden merkkijono-olion. Merkkijonojen luonnissa new-komentoa käytetään hyvin harvoin.

Olion "tyyppiä" sanotaan luokaksi. Merkkijonojen luokka on String, lukijoiden luokka taas on Scanner. Opimme jatkossa luokista ja olioista paljon lisää.

ArrayList eli "oliosäiliö"

Ohjelmoidessa tulee usein vastaan tilanteita joissa haluaisimme pitää muistissa esimerkiksi useita erilaisia merkkijonoja. Todella huono tapa olisi määritellä jokaiselle oma muuttujansa:

String sana1;
String sana2;
String sana3;
// ...
String sana10;

Tämä olisi aivan kelvoton ratkaisu ja sen huonoutta ei kannata oikeastaan edes perustella -- ajattele ylläoleva esimerkki vaikkapa sadalla tai tuhannella sanalla.

Java, kuten kaikki modernit ohjelmointikielet, tarjoaa erilaisia apuvälineitä joiden avulla on helppo säilyttää ohjelmassa monia olioita. Tutustumme nyt Javan ehkä eniten käytettyyn oliosäiliöön ArrayList:iin.

Seuraava ohjelmanpätkä ottaa käyttöönsä merkkijono-olioita tallentavan ArrayList:in sekä tallettaa listalle pari merkkijonoa.

import java.util.ArrayList;

public class ListaOhjelma {

    public static void main(String[] args) {
        ArrayList<String> sanaLista = new ArrayList<String>();

        sanaLista.add("Ensimmäinen");
        sanaLista.add("Toinen");
    }
}

Yllä olevan pääohjelman ensimmäinen rivi luo sanaLista-nimisen merkkijonoja tallettavan arraylistin. Listamuuttujan tyypin nimi on ArrayList<String>, eli merkkijonoja tallettava ArrayList. Itse lista luodaan sanomalla new ArrayList<String>();.

Huom: Jotta ArrayList toimisi, on ohjelman ylälaitaan kirjoitettava import java.util.ArrayList; tai import java.util.*;

Kun lista on luotu, siihen lisätään kaksi merkkijonoa kutsumalla listan metodia add. Tila ei lopu listalla missään vaiheessa kesken, eli periaatteessa listalle saa lisätä niin monta merkkijonoa kun "koneen" muistiin mahtuu.

Sisäisesti ArrayList on nimensä mukaisesti lista. Lisätyt merkkijonot menevät automaattisesti ArrayList:in loppuun.

ArrayList:in metodeja

ArrayList tarjoaa monia hyödyllisiä metodeita:

    public static void main(String[] args) {
        ArrayList<String> opettajat = new ArrayList<String>();

        opettajat.add("Antti");
        opettajat.add("Arto");
        opettajat.add("Pekka");
        opettajat.add("Juhana");
        opettajat.add("Martin");
        opettajat.add("Matti");

        System.out.println("opettajien lukumäärä " + opettajat.size() );

        System.out.println("listalla ensimmäisenä " + opettajat.get(0));
        System.out.println("listalla kolmantena " + opettajat.get(2));
       
        opettajat.remove("Arto");

        if ( opettajat.contains("Arto") ) {
            System.out.println("Arto on opettajien listalla");
        } else {
            System.out.println("Arto ei ole opettajien listalla");
        }
    }

Ensin luodaan merkkijonolista jolle lisätään 6 nimeä. size kertoo listalla olevien merkkijonojen lukumäärän. Huom: kun metodia kutsutaan, on kutsu muotoa opettajat.size(), eli metodin nimeä edeltää piste ja sen listan nimi kenen metodia kutsutaan.

Merkkijonot ovat listalla siinä järjestyksessä missä ne listalle laitettiin. Metodilla get(i) saadaan tietoon listan paikan i sisältö. Listan alkioiden paikkanumerointi alkaa nollasta, eli ensimmäisenä lisätty on paikassa numero 0, toisen a lisätty paikassa numero 1 jne.

Metodin remove avulla voidaan listalta poistaa merkkijonoja. Jos käytetään metodia muodossa remove("merkkejä"), poistetaan parametrina annettu merkkijono. Metodia voi käyttää myös siten, että annetaan parametriksi tietty luku. Esim. jos sanotaan remove(3) poistuu listalla neljäntenä oleva merkkijono.

Esimerkin lopussa kutsutaan metodia contains jonka avulla kysytään listalta sisältääkö se parametrina annettavan merkkijonon. Jos sisältää, palauttaa metodi arvon true.

Ohjelman tulostus:

opettajien lukumäärä 6
listalla ensimmäisena Antti
listalla kolmantena Pekka
Arto ei ole opettajien listalla

Huom! Metodit remove ja contains olettavat että listaan tallennetuilla olioilla on metodi equals. Palaamme tähän myöhemmin kurssilla.

ArrayList:in läpikäynti

Seuraavassa esimerkissä lisätään listalle 4 nimeä ja tulostetaan listan sisältö:

    public static void main(String[] args) {
        ArrayList<String> opettajat = new ArrayList<String>();

        opettajat.add("Antti");
        opettajat.add("Pekka");
        opettajat.add("Juhana");
        opettajat.add("Martin");
	
        System.out.println( opettajat.get(0) );
        System.out.println( opettajat.get(1) );
        System.out.println( opettajat.get(2) );
        System.out.println( opettajat.get(3) );
}

Ratkaisu on kuitenkin erittäin kömpelö. Entäs jos listalla olisi enemmän alkiota? Tai vähemmän? Entäs jos ei olisi edes tiedossa listalla olevien alkioiden määrää?

Tehdään ensin välivaiheen versio jossa pidetään:

    public static void main(String[] args) {
        ArrayList<String> opettajat = new ArrayList<String>();

        opettajat.add("Antti");
        opettajat.add("Pekka");
        opettajat.add("Juhana");
        opettajat.add("Martin");
        opettajat.add("Matti");
	
        int paikka = 0;
        System.out.println( opettajat.get(paikka) );
        paikka++;  
        System.out.println( opettajat.get(paikka) );  // paikka = 1
        paikka++;
        System.out.println( opettajat.get(paikka) );  // paikka = 2
        paikka++;
        System.out.println( opettajat.get(paikka) );  // paikka = 3
}

Vanhan tutun while-komennon avulla voimme kasvataa muuttujaa paikka niin kauan kunnes se kasvaa liian suureksi:

    public static void main(String[] args) {
        ArrayList<String> opettajat = new ArrayList<String>();

        opettajat.add("Antti");
        opettajat.add("Pekka");
        opettajat.add("Juhana");
        opettajat.add("Martin");
        opettajat.add("Matti");

        int paikka = 0;
        while ( paikka < opettajat.size() )  // muistatko miksi paikka <= opettajat.size() ei toimi?
            System.out.println( opettajat.get(paikka) );
            paikka++;
        }
}

Nyt tulostus toimii riippumatta listalla olevien alkioiden määrästä.

While-toistolauseen käyttö ja listan paikkojen "indeksointi" ei kuitenkaan ole yleensä järkevin tapa listan läpikäyntiin. Suositeltavampi on seuraavassa esiteltävä for-each -toistolauseke.

for-each

Vaikka komennosta käytetään nimitystä for-each, komennon nimi on pelkästään for. for:ista on olemassa kaksi versiota, perinteinen (jonka nopea esittely oli jo viimeviikolla mutta mitä alame varsinaisesti käyttämään vasta viikolla 6) ja "for-each" jota käytämme nyt.

ArrayList:in alkioiden läpikäynti for-each:illa on lasten leikkiä:

    public static void main(String[] args) {
        ArrayList<String> opettajat = new ArrayList<String>();

        opettajat.add("Antti");
        opettajat.add("Pekka");
        opettajat.add("Juhana");
        opettajat.add("Martin");
        opettajat.add("Matti");

        for (String opettaja : opettajat) {
            System.out.println( opettaja );
        }
}

Kuten huomaame, ei listalla olevien merkkijonojen paikkanumeroista tarvitse välittää, for käy listan sisällön läpi "automaattisesti".

Komennon for aaltosulkujen sisällä olevassa koodissa käytetään muuttujaa opettaja, joka on määritelty for-rivillä kaksoispisteen vasemmalla puolella. Käy niin, että kukin listalla opettajat oleva merkkijono tulee vuorollaan muuttujan opettaja arvoksi. Eli kun for:iin mennään, on opettaja ensin Antti, forin toisella toistolla opettaja on Pekka, jne

Vaikka for-komento voi tuntua aluksi hieman oudolta sitä kannattaa ehdottomasti totutella käyttämään!

Screencast aiheesta:

Sanat

Tee ohjelma, joka kysyy käyttäjältä sanoja, kunnes käyttäjä antaa tyhjän merkkijonon. Sitten ohjelma tulostaa käyttäjän antamat sanat uudestaan. Kokeile tässä for-toistolauseketta. Käytä ohjelmassa ArrayList-rakennetta, joka määritellään seuraavasti:

ArrayList<String> sanat = new ArrayList<String>();
Anna sana: Mozart
Anna sana: Schubert
Anna sana: Bach
Anna sana: Sibelius
Anna sana: Liszt
Anna sana:
Annoit seuraavat sanat:
Mozart
Schubert
Bach
Sibelius
Liszt

Vihje: tyhjä merkkijono voidaan havaita seuraavasti

String sana = lukija.nextLine();

if ( sana.isEmpty() ) {  // myös tämä tomisi: sana.equals("")
   // sana oli tyhjä eli pelkkä enterin painallus
}

Toistuva sana

Tee ohjelma, joka kysyy käyttäjältä sanoja, kunnes käyttäjä antaa saman sanan uudestaan. Käytä ohjelmassa ArrayList-rakennetta, joka määritellään seuraavasti:

ArrayList<String> sanat = new ArrayList<String>();

Kun sama sana toistuu, ilmoittaa ohjelma asiasta seuraavasti:

Anna sana: porkkana
Anna sana: selleri
Anna sana: nauris
Anna sana: lanttu
Anna sana: selleri
Annoit uudestaan sanan selleri

Vihje: Muista arraylistin metodi .contains()

Listan järjestäminen, kääntäminen ja sekoittaminen

ArrayList:n sisältö on helppo järjestää suuruusjärjestykseen. Suuruusjärjestys merkkijonojen yhteydessä tarkoittaa aakkosjärjestystä. Järjestäminen tapahtuu seuraavasti:

    public static void main(String[] args) {
        ArrayList<String> opettajat = new ArrayList<String>();

        // ...

        Collections.sort(opettajat);

        for (String opettaja : opettajat) {
            System.out.println( opettaja );
        }
    }

Tulostuu:

Antti
Arto
Juhana
Martin
Matti
Pekka

Annetaan siis lista parametriksi metodille Collections.sort. Jotta Collections:in apuvälineet toimisivat, on ohjelman yläosassa oltava import java.util.Collections; tai import java.util.*;

Collections:ista löytyy muutakin hyödyllistä:

Sanat käänteisesti

Tee ohjelma, joka kysyy käyttäjältä sanoja, kunnes käyttäjä antaa tyhjän merkkijonon. Sitten ohjelma tulostaa käyttäjän antamat sanat päinvastaisessa järjestyksessä, eli viimeinen syötetty sana ensin jne.

Anna sana: Mozart
Anna sana: Schubert
Anna sana: Bach
Anna sana: Sibelius
Anna sana: Liszt
Anna sana:
Annoit seuraavat sanat:
Liszt
Sibelius
Bach
Schubert
Mozart

Sanat aakkosjärjestyksessä

Tee edellistä tehtävää vastaava ohjelma, jossa sanat tulostetaan aakkosjärjestyksessä.

Anna sana: Mozart
Anna sana: Schubert
Anna sana: Bach
Anna sana: Sibelius
Anna sana: Liszt
Anna sana:
Annoit seuraavat sanat:
Bach
Liszt
Mozart
Schubert
Sibelius

ArrayList metodin parametrina

ArrayList:in voi antaa metodille parametrina:

    public static void tulosta(ArrayList<String> lista) {
        for (String sana : lista) {
            System.out.println( sana );
        }
    }

    public static void main(String[] args) {           
        ArrayList<String> lista = new ArrayList<String>();  
        lista.add("Java");
        lista.add("Python");
        lista.add("Ruby");
        lista.add("C++");
        
        tulosta(lista);         
    }

Parametrin tyyppi siis määritellään listaksi täsmälleen samalla tavalla eli ArrayList<String> kuin listamuuttujan määrittely tapahtuu.

Huomaa, että parametrin nimellä ei ole merkitystä:

    public static void tulosta(ArrayList<String> tulostettava) {
        for (String sana : tulostettava) {
            System.out.println( sana );
        }
    }

    public static void main(String[] args) { 
        ArrayList<String> ohjelmointikielet = new ArrayList<String>();  
        ohjelmointikielet.add("Java");
        ohjelmointikielet.add("Python");
        ohjelmointikielet.add("Ruby");
        ohjelmointikielet.add("C++");
        
        ArrayList<String> maat = new ArrayList<String>();
        maat.add("Suomi");
        maat.add("Ruotsi");
        maat.add("Norja");
        
        tulosta(ohjelmointikielet);    // annetaan metodille parametriksi lista ohjelmointikielet
        
        tulosta(maat);                 // annetaan metodille parametriksi lista maat
    }

Ohjelmassa on nyt kaksi listaa ohjelmointikielet ja maat. Metodille annetaan ensin tulostettavaksi lista ohjelmointikielet. Metodi tulosta käyttää parametriksi saamastaan listasta sisäisesti nimellä tulostettava! Seuraavaksi metodille annetaan tulostettavaksi lista maat. Jälleen metodi käyttää parametrinaan saamasta listasta sisäisesti nimeä tulostettava.

Listan alkioiden lukumäärä

Tee metodi public static int laskeAlkiot(ArrayList<String> lista) joka palauttaa listan alkioiden määrän. Metodisi ei siis tulosta mitään vaan palauttaa return:illa alkioiden lukumäärän seuraavan esimerkin mukaisesti

ArrayList<String> lista = new ArrayList<String>();
lista.add("Moi");
lista.add("Ciao");
lista.add("Hello");
System.out.println("Listalla on alkioita:");
System.out.println(laskeAlkiot(lista));
Listalla on alkioita:
3

Metodin sisällä on mahdollisuus vaikuttaa parametrina saadun listan sisältöön. Seuraavassa esimerkissä metodi poistaEnsimmainen nimensä mukaisesti poistaa listalla ensimmäisenä olevan merkkijonon (mitähän tapahtuu jos listalla ei ole mitään?).

    public static void tulosta(ArrayList<String> tulostettava) {
        for (String sana : tulostettava) {
            System.out.println( sana );
        }
    }
    
    public static void poistaEnsimmainen(ArrayList<String> lista) {
        lista.remove(0);  // poistetaan listalta ensimmäinen eli "nollas"
    }

   public static void main(String[] args) {
        ArrayList<String> ohjelmointikielet = new ArrayList<String>();  
        ohjelmointikielet.add("Pascal");
        ohjelmointikielet.add("Java");
        ohjelmointikielet.add("Python");
        ohjelmointikielet.add("Ruby");
        ohjelmointikielet.add("C++");
        

        tulosta(ohjelmointikielet);   
        
        poistaEnsimmainen(ohjelmointikielet);
        
        System.out.println();  // tulostetaan tyhjä rivi
        
        tulosta(ohjelmointikielet);      
    }

Tulostuu:

Pascal
Java
Python
Ruby
C++

Java
Python
Ruby
C++

Vastaavalla tavalla metodi voisi esim. lisätä parametrina saamaansa listaan lisää merkkijonoja.

Poista viimeinen

Tee metodi public static void poistaViimeinen(ArrayList<String> lista) joka poistaa listalla viimeisenä olevan alkion. Tällöin esimerkiksi seuraava koodi:

ArrayList<String> tyypit = new ArrayList<String>();
tyypit.add("Pekka");
tyypit.add("Mauri");
tyypit.add("Jore");
tyypit.add("Simppa");

System.out.println("Tyypit:");
System.out.println(tyypit);

// tyypit järjestykseen!
tyypit.sort();

// heitetään viimeinen mäkeen!
poistaViimeinen(tyypit);

System.out.println(tyypit);

Tulostaa:

Tyypit:
[Pekka, Mauri, Jore, Simppa]
[Jore, Mauri, Pekka]

Kuten edellisen tehtävän esimerkkitulostuksesta näemme, voi ArrayList:in tulostaa sellaisenaan. Tulostusmuoto ei kuitenkaan yleensä ole halutun kaltainen ja tulostus joudutaan hoitamaan itse esim. for-komennon avulla.

Lukuja ArrayList:issä

ArrayList:eihin voi tallettaa minkä tahansa tyyppisiä arvoja. Jos talletetaan kokonaislukuja eli int:ejä, tulee muistaa pari detaljia. int:ejä tallettava lista tulee määritellä ArrayList<Integer>, eli int:n sijaan tulee kirjoittaa Integer.

Kun listalle talletetaan int-lukuja, ei metodi remove toimi aivan odotetulla tavalla:

    public static void main(String[] args) {
        ArrayList<Integer> luvut = new ArrayList<Integer>();

        luvut.add(4);
        luvut.add(8);

        // yrittää poistaa luvun listan kohdasta 4, eli ei toimi odotetulla tavalla!
        luvut.remove(4);

        // tämä poistaa listalta luvun 4
        luvut.remove( Integer.valueOf(4) );

Eli luvut.remove(4) yrittää poistaa listalla kohdassa 4 olevan alkion. Listalla on vain 2 alkiota, joten komento aiheuttaa virheen. Jos halutaan poistaa luku 4, täytyy käyttää hieman monimutkaisempaa muotoa: luvut.remove( Integer.valueOf(4) );

Listalle voi tallettaa myös liukulukuja eli double:ja ja merkkejä eli char:eja. Tällöin listat luodaan seuraavasti:

        ArrayList<Double> doublet = new ArrayList<Double>();
        ArrayList<Character> merkit = new ArrayList<Character>();

Lukujen summa

Tee metodi summa, joka laskee parametrinaan saamansa kokonaislukuja sisältävän, eli tyyppiä ArrayList<Integer> olevan listan summan.

Tee metodi seuraavaan runkoon:

public static int summa(ArrayList<Integer> lista) {
    // kirjoita koodia tähän
}

public static void main(String[] args) {
    ArrayList<Integer> lista = new ArrayList<Integer>();
    lista.add(3);
    lista.add(2);
    lista.add(7);
    lista.add(2);

    System.out.println("Summa: " + summa(lista));

    lista.add(10);

    System.out.println("Summa: " + summa(lista));
}

Ohjelman tulostus:

Summa: 14
Summa: 24

Lukujen keskiarvo

Tee metodi keskiarvo, joka laskee parametrinaan saamansa kokonaislukuja sisältävän listan lukujen keskiarvon. Metodin on laskettava parametriensa summa käyttäen apuna edellisen tehtävän metodia summa.

Tee metodi seuraavaan runkoon:

public static double keskiarvo(ArrayList<Integer> lista) {
    // kirjoita koodia tähän
}

public static void main(String[] args) {
    ArrayList<Integer> lista = new ArrayList<Integer>();
    lista.add(3);
    lista.add(2);
    lista.add(7);
    lista.add(2);

    System.out.println("Keskiarvo: " + keskiarvo(lista));
}

Ohjelman tulostus:

Keskiarvo: 3.5

Suurin

Tee metodi suurin, joka palauttaa parametrina saamansa kokonaislukuja sisältävän listan suurimman luvun.

public static int suurin(ArrayList<Integer> lista) {
    // kirjoita koodia tähän
}

public static void main(String[] args) {
    ArrayList<Integer> lista = new ArrayList<Integer>();
    lista.add(3);
    lista.add(2);
    lista.add(7);
    lista.add(2);

    System.out.println("Suurin: " + suurin(lista));
}

Ohjelman tulostus:

Suurin: 7

Varianssi

Tee metodi varianssi, joka laskee palauttaa saamansa kokonaislukuja sisältävän listan otosvarianssin. Ohjeen varianssin laskemiseksi voit katsoa esimerkiksi Wikipediasta kohdasta populaatio- ja otosvarianssi.

Tee metodi käytäen apuna tehtävän 64 metodia keskiarvo, kutsu metodia kuitenkin vain kertaalleen yhden varianssin laskemisen aikana.

public static double varianssi(ArrayList<Integer> lista) {
    // kirjoita koodia tähän
}

public static void main(String[] args) {
    ArrayList<Integer> lista = new ArrayList<Integer>();
    lista.add(3);
    lista.add(2);
    lista.add(7);
    lista.add(2);

    System.out.println("Varianssi: " + varianssi(lista));
}

Ohjelman tulostus:

Varianssi: 5.666667

(Lukujen keskiarvo on 3.5, joten otosvarianssi on ((3 - 3.5)² + (2 - 3.5)² + (7 - 3.5)² + (2 - 3.5)²)/(4 - 1) ≈ 5,666667.)

Huom! Muistathan kokeillessasi ohjelmaa, että yhden alkion kokoisen listan (otos)varianssia ei ole määritelty! Kaavassa tapahtuu tällöin nollalla jakaminen. Java esittää nollalla jakamisen tuloksen epänumerona NaN

Totuusarvojen käyttö

Totuusarvoinen eli boolean-muuttuja voi saada vain kaksi arvoa true tai false. Seuraavassa esimerkki booleanin käytöstä:

        int luku1 = 1;
        int luku2 = 5;

        boolean ekaSuurempi = true;

        if (luku1 <= luku2) {
            ekaSuurempi = false;
        }

        if (ekaSuurempi==true) {
            System.out.println("luku1 suurempi");
        } else {
            System.out.println("luku1 ei ollut suurempi");
        }

Eli ensin asetetaan totuusarvon ekaSuurempi arvoksi tosi eli true. Ensimmäinen if tarkastaa onko luku1 pienempi tai yhtä pieni kuin luku2. Jos näin on, vaihdetaan totuusarvon arvoksi epätosi eli false. Myöhempi if valitsee tulostuksen totuusarvoon perustuen.

Totuusarvon käyttö ehtolauseessa on itseasiassa edellistä esimerkkiä yksinkertaisempaa, jälkimmäinen if voidaan kirjoittaa seuraavasti:

        if (ekaSuurempi) {  // tarkoittaa samaa kuin ekaSuurempi==true
            System.out.println("luku1 suurempi");
        } else {
            System.out.println("luku1 ei ollut suurempi");
        }

Eli jos halutaan tarkistaa että booleanmuuttujan arvo on tosi, eli ole tarvetta kirjoittaa ==true, pelkkä muuttujan nimi riittää!

Epätoden tarkastaminen onnistuu negaatio-operaation eli huutomerkin avulla:

        if (!ekaSuurempi) {  // tarkoittaa samaa kuin ekaSuurempi==false
            System.out.println("luku1 ei ollut suurempi");
        } else {
            System.out.println("luku1 suurempi");
        }

totuusarvon palauttava metodi

Totuusarvot ovat erityisen käteviä jonkun asian voimassaolon tarkistavien metodien paluuarvoina. Tehdään metodi joka tarkastaa sisältääkö sen parametrina saama lista ainoastaan positiivisia lukuja (tulkitaan 0 positiiviseksi). Tieto positiivisuudesta palautetaan totuusarvona.

    public static boolean kaikkiPositiivisia(ArrayList<Integer> luvut) {
        boolean eiNegatiivisia = true;

        for (int luku : luvut) {
            if (luku < 0) {
                eiNegatiivisia = false;
            }
        }

        // jos jonkun listan luvuista arvo oli pienempi kuin 0, on eiNegatiivisia nyt false
        return eiNegatiivisia;
    }

Metodilla on totuusarvoinen apumuuttuja eiNegatiivisia. Apumuuttujan arvoksi asetetaan ensin true. Metodi käy läpi kaikki listan luvut. Jos jonkun (siis vähintään yhden) luvun arvo on pienempi kuin nolla, asetetaan apumuuttujan arvoksi false. Lopuksi palautetaan apumuuttujan arvo. Apumuuttuja on edelleen true jos yhtään negatiivista lukua ei löytynyt, muuten false.

Metodia käytetään seuraavasti:

    public static void main(String[] args) {

        ArrayList<Integer> luvut = new ArrayList<Integer>();
        luvut.add(3);
        luvut.add(1);
        luvut.add(-1);

        boolean vastaus = kaikkiPositiivisia(luvut);

        if (vastaus) {  // tarkoittaa siis samaa kuin vastaus == true
            System.out.println("luvut positiivisia");
        } else {
            System.out.println("joukossa oli ainakin yksi negatiivinen");
        }
    }

Vastauksen tallettaminen ensin muuttujaan ei yleensä ole tarpeen, ja metodikutsu voidaan kirjottaa suoraan ehdoksi:

        ArrayList<Integer> luvut = new ArrayList<Integer>();
        luvut.add(4);
        luvut.add(7);
        luvut.add(12);
        luvut.add(9);

        if (kaikkiPositiivisia(luvut)) {
            System.out.println("luvut positiivisia");
        } else {
            System.out.println("joukossa oli ainakin yksi negatiivinen");
        }
    }

Komento return ja metodin lopetus

Metodin suoritus loppuu välittömästi kun metodissa suoritetaan return-käsky. Käyttämällä tätä tietoa hyväksi voimme kirjoittaa kaikkiPositiivisia-metodin hiukan suoraviivaisemmin ja selkeämmin.

    public static boolean kaikkiPositiivisia(ArrayList<Integer> luvut) {
        for (int luku : luvut) {
            if (luku < 0) {
                return false;
            }
        }

        // jos tultiin tänne asti, ei yhtään negatiivista löytynyt 
        // siispä palautetaan true
        return true;
    }
}

Eli jos lukujen listaa läpikäydessä törmätään negatiiviseen lukuun, voidaan metodista poistua heti palauttamalla false. Jos listalla ei ole yhtään negatiivista, päädytään loppuun ja voidaan palauttaa true. Olemme päässeet metodissa kokonaan eroon apumuuttujan käytöstä!

Onko luku listalla monta kertaa

Tee metodi onkoListallaUseasti, joka saa parametrinaan kokonaislukuja sisältävän listan ja int-luvun. Jos luku esiintyy listalla yli yhden kerran, metodi palauttaa true ja muulloin false.

Ohjelman rakenne on seuraava:

public static boolean onkoListallaUseasti(ArrayList<Integer> lista, int luku) {
    // kirjoita koodia tähän
}

public static void main(String[] args) {
    ArrayList<Integer> lista = new ArrayList<Integer>();
    lista.add(3);
    lista.add(2);
    lista.add(7);
    lista.add(2);

    System.out.println("Anna luku: ");
    int luku = Integer.parseInt(lukija.nextLine());
    if (onkoListallaUseasti(luvut, luku)) {
        System.out.println(luku + " on listalla useasti.");
    } else {
        System.out.println(luku + " ei ole listalla useasti.");
    }
}
Anna luku: 2
Luku on on listalla useasti.
Anna luku: 3
Luku ei ole listalla useasti.

Palindromi

Tee metodi palindromi, joka kertoo, onko merkkijono palindromi (merkkijonon sisältö on sama alusta loppuun ja lopusta alkuun luettuna).

Metodi voi käyttää apuna metodia kaanna tehtävästä 56. Metodin tyyppi on boolean, joten se pa lauttaa jokoarvon true (merkkijono on palindromi) tai false (merkkijono ei ole palindromi).

public static boolean palindromi(String merkkijono) {
    // kirjoita koodia tähän
}

public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);
  
    System.out.println("Anna merkkijono: ");
    String merkkijono = lukija.nextLine();    
    if (palindromi(merkkijono)) {
	   System.out.println("Merkkijono on palindromi!");
    } else {
	   System.out.println("Merkkijono ei ole palindromi!");
    }
}

Ohjelman tulostuksia:

Anna merkkijono: saippuakauppias
Merkkijono on palindromi!
Anna merkkijono: esimerkki
Merkkijono ei ole palindromi!

ArrayListin kopioiminen

Joskus on tarpeen muodostaa ArrayLististä kopio, johon voi tehdä muutoksia vaikuttamatta alkuperäisen ArrayListin sisältöön. Kopion voi muodostaa luomalla uuden ArrayListin käyttäen vanhaa ArrayListiä parametrina:

ArrayList<String> nimet = new ArrayList<String>();
nimet.add("Kyösti");
nimet.add("Risto");
nimet.add("Carl");
nimet.add("Urho");

//luodaan kopio nimet-listasta
ArrayList<String> kopio = new ArrayList<String>(nimet);
//järjestetään kopio
Collections.sort(kopio);

System.out.println(kopio);  //tulostuu [Carl, Kyösti, Risto, Urho]
System.out.println(nimet);  //tulostuu [Kyösti, Risto, Carl, Urho]

KaikkiEri

Tee metodi kaikkiEri, joka palauttaa true jos sen parametrina saamassa kokonaislukuja sisältävässä listassa olevat luvut ovat kaikki erisuuruisia. Metodi ei saa muuttaa listan sisältöä.

Seuraavassa kaksi esimerkkiä metodin toiminnasta:

public static void main(String[] args) {
    ArrayList<Integer> lista1 = new ArrayList<Integer>();
    lista1.add(3);
    lista1.add(7);
    lista1.add(1);

    boolean eri = kaikkiEri(lista1);
    // muuttujan eri arvo true

    ArrayList<Integer> lista2 = new ArrayList<Integer>();
    lista2.add(2);
    lista2.add(3);
    lista2.add(7);
    lista2.add(1);
    lista2.add(3);
    lista2.add(99);

    eri = kaikkiEri(lista2);
    // muuttujan eri arvo false sillä luku 3 on listalla kahteen kertaan
}

Metodit ja parametrien kopioituminen

Tarkastellaan muutamaa metodeihin liittyvää tärkeää yksityiskohtaa.

Luvussa 15 oli esimerkki, jossa yritettiin muuttaa pääohjelmassa olevan muuttujan arvoa metodin sisällä.

public static void main(String[] args) {
  int luku = 1;
  kasvataKolmella();
  System.out.println("luku on " + luku);
}

public static void kasvataKolmella() {
  luku = luku + 3;
}

Ohjelma ei toimi, sillä metodi ei pääse käsiksi pääohjelman muuttujaan luku.

Tämä johtuu siitä, että pääohjelman muuttujat eivät näy metodien sisään. Ja yleisemmin: minkään metodin muuttujat eivät näy muille metodeille. Koska pääohjelma main on myös metodi, pätee sääntö myös pääohjelmalle. Ainoa keino viedä metodille tietoa ulkopuolelta on parametrin avulla.

Yritetään korjata edellinen esimerkki välittämällä pääohjelman muuttuja luku parametrina metodille.

public static void main(String[] args) {
  int luku = 1;
  kasvataKolmella(luku);
  System.out.println(luku);  // tulostaa 1, eli arvo luku ei muuttunut
}

public static void kasvataKolmella(int luku) {
  luku = luku + 3;
}

Ohjelma ei toimi toivotulla tavalla. Metodissa olevat parametrit ovat eri muuttujia kuin pääohjelmassa esitellyt muuttujat. Edellä metodi siis kasvattaa samannimistä, mutta ei samaa parametria luku.

Kun metodille annetaan parametri, parametrin arvo kopioidaan uuteen muuttujaan metodissa käytettäväksi. Yllä olevassa esimerkissä metodille kasvataKolmella annetusta muuttujasta luku luodaan kopio, jota metodin sisällä lopulta käsitellään. Metodi käsittelee siis pääohjelmassa olevan muuttujan kopiota, ei alkuperäistä muuttujaa -- pääohjelmametodissa olevalle muuttujalle luku ei tehdä mitään.

Voidaan ajatella, että pääohjelmametodi main ja metodi kasvataKolmella toimivat kumpikin omassa kohtaa tietokoneen muistia. Allaolevassa kuvassa on main:in muuttujaa luku varten oma "lokero". Kun metodia kutsutaan, tehdään tälle oma muuttuja luku jonka arvoksi kopioituu main:in luku-muuttujan arvo eli 1. Molemmat luku-nimiset muuttujat ovat kuitenkin täysin erillisiä, eli kun metodissa kasvataKolmella muutetaan sen luku-muuttujan arvoa, ei muutos vaikuta millään tavalla pääohjelman muuttujaan luku.

Allaoleva kuva antaa lisävalaisua tilanteeseen.

Metodista saa toki välitettyä tietoa kutsujalle käyttäen paluuarvoa, eli palauttamalla arvon return-komennolla. Edellinen saadaan toimimaan muuttamalla koodia hiukan:

public static void main(String[] args) {
  int luku = 1;
  luku = kasvataKolmellaJaPalauta(luku);

  System.out.println(luku);  // tulostaa 4, sillä luku on saanut arvokseen metodin palauttaman arvon          
}

public static int kasvataKolmellaJaPalauta(int luku) {
  luku = luku + 3;

  return luku;
}

Edelleen on niin, että metodi käsittelee pääohjelman luku-muuttujan arvon kopiota. Pääohjelmassa metodin palauttama arvo sijoitetaan muuttujaan luku, joten muutos tulee tämän takia voimaan myös pääohjelmassa. Huomaa, että edellisessä ei ole mitään merkitystä sillä, mikä nimi metodin parametrilla on. Koodi toimii täysin samoin oli nimi mikä tahansa, esim.

public static void main(String[] args) {
  int luku = 1;
  luku = kasvataKolmellaJaPalauta(luku);

  System.out.println(luku);            
}

public static int kasvataKolmellaJaPalauta(int kasvatettavaLuku) {
  kasvatettavaLuku = kasvatettavaLuku + 3;

  return kasvatettavaLuku;
}

Huomasimme että metodissa olevat parametrit ovat eri muuttujia kuin metodin kutsujassa esitellyt muuttujat. Ainoastaan parametrin arvo kopioituu kutsujasta metodiin.

Asia ei kuitenkaan ole ihan näin yksinkertainen. Jos metodille annetaan parametrina ArrayList, käy niin että sama lista näkyy metodille ja kaikki metodin listalle tekemät muutokset tulevat kaikkialla voimaan.

    public static void poistaAlussaOleva(ArrayList<Integer> lista) {
        lista.remove(0); // poistaa paikassa 0 olevan luvun
    }  
    ArrayList<Integer> luvut = new ArrayList<Integer>(); 
    luvut.add(4);
    luvut.add(3);
    luvut.add(7);
    luvut.add(3);
        
    System.out.println(luvut); // tulostuu [4,3,7,3]
        
    poistaAlussaOleva(luvut);

    System.out.println(luvut); // tulostuu [3,7,3]

Toisin kuin int-tyyppinen parametri, lista ei kopioidu vaan metodi käsittelee suoraan parametrina annettua listaa.

Tilannetta valaisee allaoleva kuva. Toisin kuin int-tyyppinen muuttuja, ArrayList ei sijaitsekaan samalla tapaa "lokerossa", vaan muuttujan nimi, eli mainin tapauksessa luvut onkin ainoastaan viite paikkaan missä ArrayList sijaitsee. Yksi tapa ajatella asiaa, on että ArrayList on "langan päässä", eli listan nimi luvut on lanka jonka toisesta päästä lista löytyy. Kun metodikutsun parametrina on ArrayList, käykin niin että metodille annetaan "lanka" jonka päässä on sama lista jonka metodin kutsuja näkee. Eli main:illa ja metodilla on kyllä molemmilla oma lanka, mutta langan päässä on sama lista ja kaikki muutokset mitä metodi tekee listaan tapahtuvat täsmälleen samaan listaan jota pääohjelma käyttää. Tästä viikosta alkaen tulemme huomaamaan että Java:ssa hyvin moni asia on "langan päässä".

Huomaa jälleen että parametrin nimi metodin sisällä voi olla aivan vapaasti valittu, nimen ei tarvitse missään tapauksessa olla sama kuin kutsuvassa metodissa oleva nimi. Edellä listaa kutsutaan metodin sisällä nimellä lista, metodin kutsuja taas näkee saman listan luvut-nimisenä.

Miksi int-parametrista ainoastaan arvo kopioituu metodille mutta parametrin ollessa ArrayList metodi käsittelee suoraan listan sisältöä? Javassa ainoastaan alkeistietotyyppisten eli tyyppien int, double, char, boolean (ja muutamien muiden joita emme ole käsitelleet) arvot kopioidaan metodille. Muun tyyppisten parametrien tapauksessa metodille kopioidaan viite, ja metodista käsitellään viitteen takana olevaa parametria suoraan. Ei-alkeistyyppiset muuttujat -- eli viittaustyyppiset muuttujat ovat siis edellisen kuvan tapaan "langan päässä" -- metodille välitetään lanka parametriin, ja näin metodi käsittelee parametria suoraan.

ArrayListien yhdistäminen

Toteuta metodi public static void yhdista(ArrayList<Integer> eka, ArrayList<Integer> toka), joka lisää toisena parametrina toka olevassa ArrayListissa olevat luvut ensimmäisenä parametrina olevaan ArrayList:iin eka. Alkioiden talletusjärjestyksellä ei ole väliä, ja sama alkio voi päätyä listalle useamman kerran. Esimerkki metodin toiminnasta:

    ArrayList<Integer> lista1= new ArrayList<Integer>();
    ArrayList<Integer> lista2= new ArrayList<Integer>();  
    
    lista1.add(4);
    lista1.add(3);
    
    lista2.add(5);
    lista2.add(10);
    lista2.add(7);
    
    yhdista(lista1, lista2);
    
    System.out.println(lista1); // tulostuu [4, 3, 5, 10, 7]

    System.out.println(lista2); // tulostuu [5, 10, 7]

Listalle voi lisätä toisen listan sisällön ArrayList-luokan tarjoaman addAll-metodin avulla. Lista saa parametrinä toisen listan, jonka alkiot listalle lisätään.

Joukkoyhdistäminen

Toteuta metodi joukkoYhdista joka toimii muuten samoin kuin edellisen tehtävän yhdista-metodi, mutta parantele sitä niin, että yhdistäminen lisää listaan eka lukuja vain, jos ne eivät jo ennestään löydy listalta. Tehtävässä kannattaa käyttää hyväkseen ArrayListin contains-metodia, jolla voit tarkistaa sisältääkö lista jo jonkin luvun.

Ohjeita koodin kirjoittamiseen ja ongelmanratkaisuun

Yksi maailman johtavista ohjelmistonkehittäjistä, Kent Beck, on lausunut mm. seuraavasti:

Otamme viimeistään nyt ensimmäisiä askelia Kent Beckin viitoittamalla tiellä.

Oikein sisennetty ja "hengittävä" koodi

Tarkastellaan koodia joka ensin lisää listalle lukuja ja tulostaa listan sisällön. Tämän jälkeen listalta poistetaan kaikki tietyn luvun esiintymät ja tulostetaan lista uudelleen.

Kirjoitetaan koodi ensin huonosti ja jätetään se sisentämättä:

public static void main(String[] args) {
ArrayList<Integer> luvut = new ArrayList<Integer>();
luvut.add(4);
luvut.add(3);
luvut.add(7);
luvut.add(3);
System.out.println("luvut alussa:");
for (int luku : luvut) {
System.out.println(luku);
}
while (luvut.contains(Integer.valueOf(3))) {
luvut.remove(Integer.valueOf(3));
}
System.out.println("luvut poiston jälkeen:");
for (int luku : luvut) {
System.out.println(luku);
}

Vaikka sisentämätön koodi toimii, on sitä hyvin ikävä lukea. Sisennetään koodi oikein (NetBeansissa sisennyksen saa korjattua automaattisesti painamalla alt+shift+f), ja erotellaan loogiset kokonaisuudet rivinvaihdoin:

    public static void main(String[] args) {
        ArrayList<Integer> luvut = new ArrayList<Integer>();
        luvut.add(4);
        luvut.add(3);
        luvut.add(7);
        luvut.add(3);
        
        System.out.println("luvut alussa:");
        
        // tässä tulostetaan luvut
        for (int luku : luvut) {
            System.out.println(luku);
        }
        
        // tarkastetaan onko listalla luku 3
        while (luvut.contains(Integer.valueOf(3))) {
            luvut.remove(Integer.valueOf(3));  // jos löytyi, niin poistetaan se
        }
        // tehdään tämä whilessä jotta saadaan kaikki kolmoset poistetua!
        
        System.out.println("luvut poiston jälkeen:");
        
         // tässä tulostetaan luvut
        for (int luku : luvut) {
            System.out.println(luku);
        }

Nyt koodissa alkaa olla jo järkeä. Esimerkiksi tulostus ja kolmosten poisto ovat omia loogisia kokonaisuuksia, joten ne on erotettu rivinvaihdolla. Koodissa on ilmavuutta ja koodin lukeminen alkaa olla miellyttävämpää.

Koodiin on vieläpä kirjoitettu kommentteja selventämään muutaman kohdan toimintaa.

Copy-pasten eliminointi metodeilla

Ohjelmoijan lähes pahin mahdollinen perisynti on copy-paste -koodi, eli samanlaisen koodinpätkän toistaminen koodissa useaan kertaan. Esimerkissämme listan tulostus tapahtuu kahteen kertaan. Tulostuksen hoitava koodi on syytä erottaa omaksi metodikseen ja kutsua uutta metodia pääohjelmasta:

    public static void main(String[] args) {
        ArrayList<Integer> luvut = new ArrayList<Integer>();
        luvut.add(4);
        luvut.add(3);
        luvut.add(7);
        luvut.add(3);
        
        System.out.println("luvut alussa:");

        // tässä tulostetaan luvut
        tulosta(luvut);
        
        while (luvut.contains(Integer.valueOf(3))) {
            luvut.remove(Integer.valueOf(3));
        }
        
        System.out.println("luvut poiston jälkeen:");
        
        // tässä tulostetaan luvut
        tulosta(luvut);
    }

    public static void tulosta(ArrayList<Integer> luvut) {
        for (int luku : luvut) {
            System.out.println( luku );
        }
    }

Erillisten tehtävien erottaminen omiksi, selkeästi nimetyiksi metodeiksi

Koodi alkaa olla jo selkeämpää. Selvästi erillinen kokonaisuus, eli listan tulostus on oma helposti ymmärrettävä metodinsa. Uuden metodin esittelyn myötä myös pääohjelman luettavuus on kasvanut. Huomaa että uusi metodi on nimetty mahdollisimman kuvaavasti, eli siten että metodin nimi kertoo mitä metodi tekee. Ohjelmaan kirjoitetut kommentit tässä tulostetaan luvut ovatkin tarpeettomia, joten poistetaan ne.

Ohjelmassa on vielä hiukan siistimisen varaa. Pääohjelma on vielä sikäli ikävä, että siistien metodikutsujen seassa on vielä suoraan listaa käsittelevä "epäesteettinen" koodinpätkä. Erotetaan tämäkin omaksi metodikseen:

    public static void main(String[] args) {
        ArrayList<Integer> luvut = new ArrayList<Integer>();
        luvut.add(4);
        luvut.add(3);
        luvut.add(7);
        luvut.add(3);
        
        System.out.println("luvut alussa:");
        tulosta(luvut);
        
        poista(luvut, 3);
        
        System.out.println("luvut poiston jälkeen:");
        tulosta(luvut);
    }

    public static void tulosta(ArrayList<Integer> luvut) {
        for (int luku : luvut) {
            System.out.println( luku );
        }
    }

    public static void poista(ArrayList<Integer> luvut, int poistettava) {
        while (luvut.contains(Integer.valueOf(poistettava))) {
            luvut.remove(Integer.valueOf(poistettava));
        }
    }

Loimme yllä loogiselle kokonaisuudelle -- tietyn luvun kaikkien esiintymien poistolle -- oman kuvaavasti nimetyn metodin. Lopputuloksena oleva pääohjelma on nyt erittäin ymmärrettävä, lähes suomen kieltä. Molemmat metodit ovat myös erittäin yksinkertaisia ja selkeitä ymmärtää.

Kent Beck olisi varmaan tyytyväinen aikaansaannokseemme, koodi on helposti ymmärrettävää, helposti muokattavaa eikä sisällä copy-pastea.

Viikko 4

Olio-ohjelmointi

Pieni johdanto olio-ohjelmointiin ennen aloitusta.

Proseduraalisessa ohjelmoinnissa, eli tähän asti opiskelemassamme ohjelmointityylissä, ohjelma jäsennellään jakamalla se pienempiin osiin eli metodeihin. Metodi toimii ohjelman erillisenä osana, ja sitä voi kutsua mistä tahansa ohjelmasta. Metodia kutsuttaessa ohjelman suoritus siirtyy metodin alkuun, ja suorituksen päätyttyä palataan takaisin siihen kohtaan mistä metodia kutsuttiin.

Olio-ohjelmoinnissa, kuten proseduraalisessa ohjelmoinnissa, pyritään jakamaan ohjelma pieniin osiin. Olio-ohjelmoinnissa pienet osat ovat olioita. Jokaisella oliolla on oma yksittäinen vastuunsa eli se sisältää joukon yhteenkuuluvaa tietoa ja toiminnallisuutta. Olio-ohjelmat koostuvat useista olioista, joiden yhteinen toiminta määrittelee järjestelmän toiminnan.

Olio

Olemme käyttäneet jo monia Javan valmiita olioita. Esimerkiksi ArrayList:it ovat olioita. Jokainen yksittäinen lista koostuu yhteenkuuluvasta tiedosta, eli olion tilasta. Listaolioihin liittyy toiminnallisuutta, eli metodt joilla olion tilaa voidaan muuttaa. Esimerkiksi seuraavassa ohjelmanpätkässä on kaksi ArrayList-olioa kaupungit ja maat:

    public static void main(String[] args) {
        ArrayList<String> kaupungit = new ArrayList<String>();
        ArrayList<String> maat = new ArrayList<String>();

        maat.add("Suomi");
        maat.add("Saksa");
        maat.add("Hollanti");

        kaupungit.add("Berliini");
        kaupungit.add("Nijmegen");
        kaupungit.add("Turku");
        kaupungit.add("Helsinki");
        
        System.out.println("maita " + maat.size() );
        System.out.println("kaupunkeja " + kaupungit.size() );
    }

Sekä maat-olio että kaupungit-olio elää omaa elämäänsä. Molempien "tila" on toisten olioiden tilasta riippumaton. Esim. olion maat tila koostuu listalla olevista merkkijonoista "Suomi", "Saksa" ja "Hollanti" ja todennäköisesti myös tiedosta kuinka monta maata listalla on.

Olioon liittyvää metodikutsua tehdessä (esimerkiksi maat.add("Suomi");) pisteen vasemmalle puolelle tulee sen olion nimi, jolle metodia kutsutaan, oikealle metodin nimi. Kun kysytään monta merkkijonoa listalla maat on, kutsu on muotoa maat.size() eli kutsutaan maat oliolle sen metodia size. Metodin palauttama tulos riippuu olion maat tilasta, eli muut oliot kuten kaupungit eivät vaikuta metodin suoritukseen millään tavalla.

Olemme jo useasti käyttämään komentoa new. Esimerkiksi listan (ArrayList) ja lukijan (Scanner) luominen on tapahtunut new-komennolla. Syy on se, että kumpikin näistä on Luokkia, joista olio luodaan. Java-kielessä oliot luodaan aina new-komennolla muutamaa poikkeusta lukuunottamatta.

Yksi näistä poikkeuksista ovat merkkijonot, joiden luomiseen ei aina tarvita new:iä. Tuttu tapa merkkijonon luomiseen on oikeastaan Javan lyhennysmerkintä new:in käytölle. Merkkijonon voi luoda myös new:illä kuten muutkin oliot:

        String teksti = "tekstiä";       // lyhennysmerkintä merkkijono-olion luomiselle
        String toinenTeksti = new String("lisää tekstiä");   

On myös tilanteita, missä Javan valmis kalusto kutsuu new:iä piilossa ohjelmoijalta.

Luokka

On selvää että kaikki oliot eivät ole keskenään samankaltaisia. Esim. ArrayList-oliot poikkeavat ratkaisevasti String-olioista. Kaikilla ArrayList:eillä on samat metodit (add, contains, remove, size, ...) ja vastaavasi kaikilla String-olioilla on samat metodit (substring, length, charAt, ...). ArrayList- ja String-olioiden metodit eivät ole samat sillä ne ovat eri tyyppisiä olioita.

Tietyn olioryhmän tyyppiä kutsutaan luokaksi. ArrayList on luokka, String on luokka, Scanner on luokka, jne... Oliot taas ovat luokasta tehtyjä ilmentymiä.

Luokan kaikilla olioilla on samat metodit, sekä samankaltainen tila. Esim. kaikkien ArrayList-olioiden tila koostuu listalle tallennetuista alkioista. String-olioiden tila taas koostuu merkkijonon muodostavista kirjaimista.

Luokka ja sen oliot

Luokka määrittelee minkälaisia luokan oliot ovat:

Luokka kuvaa siitä luotavien olioiden "rakennuspiirustukset".

Otetaan analogia tietokoneiden ulkopuoleisesta maailmasta. Rintamamiestalot lienevät kaikille suomalaisille tuttuja. Voidaan ajatella, että jossain on olemassa piirustukset jotka määrittelevät minkälainen rintamamiestalo on. Piirrustukset ovat luokka, eli ne määrittelevät luokasta luotavien olioiden luonteen:

Yksittäiset oliot eli rintamamiestalot on tehty samojen piirustusten perusteella, eli ne ovat saman luokan ilmentymiä. Yksittäisten olioiden tila eli ominaisuudet (esim. seinien väri, katon rakennusmateriaali ja väri, kivijalan väri, ovien rakennusmateriaali ja väri, ...) vaihtelevat. Seuraavassa yksi "rintamamiestalo-luokan olio":

Luokasta luodaan olio aina kutsumalla olion luovaa metodia eli konstruktoria komennon new avulla. Esimerkiksi Scanner-luokasta luodaan uusi ilmentymä eli olio kun kutsutaan new Scanner(..):

    Scanner lukija = new Scanner(System.in);

Konstruktorit saavat parametreja kuten muutkin metodit.

Tilejä

Tehtäväpohjan mukana tulee valmis luokka Tili. Luokan Tili olio esittää pankkitiliä, jolla on saldo (eli jossa on jokin määrä rahaa). Tilejä käytetään näin:

Tili artonTili = new Tili("Arton tili",100.00);
Tili artonSveitsilainenTili = new Tili("Arton tili Sveitsissä",1000000.00);

System.out.println("Alkutilanne");
System.out.println(artonTili);
System.out.println(artonSveitsilainenTili);

artonTili.otto(20);
System.out.println("Arton tilin saldo on nyt: "+artonTili.saldo());
artonSveitsilainenTili.pano(200);
System.out.println("Arton toisen tilin saldo on nyt: "+artonSveitsilainenTili.saldo());

System.out.println("Lopputilanne");
System.out.println(artonTili);
System.out.println(artonSveitsilainenTili);

Ensimmäinen tilisi

Huom: tämän tehtävän jokaista alikohtaa varten on oma tehtäväpohja, tee tämä tehtävä pohjaan 072.1

Tee ohjelma, joka luo tilin jonka saldo on 100.0, panee tilille 20.0 ja tulostaa tilin. Huom! tee kaikki nämä operaatiot täsmälleen tässä järjestyksessä.

Ensimmäinen tilisiirtosi

Huom: tämän tehtävän jokaista alikohtaa varten on oma tehtäväpohja, tee tämä tehtävä pohjaan 072.2

Tee ohjelma joka:

  1. Luo tilin nimeltä "Matin tili" saldolla 1000
  2. Luo tilin nimeltä "Oma tili" saldolla 0
  3. Nostaa matin tililtä 100.0
  4. Panee omalle tilille 100.0
  5. Tulosta molemmat tilit

Tilisiirtoja

Huom: tämän tehtävän jokaista alikohtaa varten on oma tehtäväpohja, tee tämä tehtävä pohjaan 072.3

Yllä siirsit rahaa tililtä toiselle. Tehdään seuraavaksi metodi joka tekee saman!

Toteuta ohjelmapohjaan metodi public static void tilisiirto(Tili mista, Tili minne, double paljonko) joka siirtää rahaa tililtä toiselle. Sinun ei tarvitse tarkistaa että mista-tilin saldo riittää.

Tämän jälkeen tee main-metodissasi seuraavaa:

  1. Luo tili "A" saldolla 100.0
  2. Luo tili "B" saldolla 0.0
  3. Luo tili "C" saldolla 0.0
  4. Siirrä 50.0 tililtä A tilille B.
  5. Siirrä 25.0 tililtä B tilille C.

Oman luokan määritteleminen - oliomuuttujat

Luokka määritellään jotain mielekästä kokonaisuutta varten. Usein "mielekäs kokonaisuus" kuvaa jotain reaalimaailman asiaa. Jos tietokoneohjelman pitää käsitellä henkilötietoja, voisi olla mielekästä määritellä erillinen luokka Henkilo joka kokoaa yhteen henkilöön liittyvät metodit ja ominaisuudet.

Aloitetaan. Oletetaan että meillä on projektirunko jossa on tyhjä pääohjelma:

public class Main {

    public static void main(String[] args) {       
    }

}

Luomme nyt projektiimme eli ohjelmaamme uuden luokan. Tämä tapahtuu valitsemalla NetBeansissa vasemmalta projects-kohdasta hiiren oikealla napilla new ja java class. Avautuvaan dialogiin annetaan luokalle nimi.

Kuten muuttujien ja metodien nimien, myös luokan nimen on aina oltava mahdollisimman kuvaava. Joskus ohjelmoinnin edetessä luokka elää ja muuttaa muotoaan. Tällaisissa tilanteissa luokan voi nimetä uudelleen (ks. NetBeans-ohje).

Luodaan luokka nimeltä Henkilo. Luokasta muodostuu oma tiedostonsa Henkilo.java. Eli ohjelma koostuu nyt kahdesta tiedostosta, sillä pääohjelma on omassa tiedostossaan. Aluksi luokka on tyhjä:

public class Henkilo {

}

Luokan tulee määritellä mitä metodeja ja ominaisuuksia luokan olioilla on. Päätetään, että jokaisella henkilöllä on nimi ja ikä. Nimi on luonnollista esittää merkkijonona, eli Stringinä, ja ikä taas kokonaislukuna. Lisätään nämä rakennuspiirustuksiimme:

public class Henkilo {
    private String nimi;
    private int ika;
}

Määrittelimme yllä että kaikilla Henkilo-luokan olioilla on nimi ja ika. Määritettely tapahtuu hiukan kuten normaalin muuttujan määrittely. Eteen on kuitenkin nyt laitettu avainsana private. Tämä tarkoittaa sitä, että nimi ja ikä eivät näy suoraan olion ulkopuolelle vaan ovat "piilossa" olion sisällä. Olion sisälle piilottamista kutsutaan kapseloinniksi.

Luokan sisälle määriteltyjä muuttujia kutsutaan oliomuuttujiksi tai olion kentiksi tai olion attribuuteiksi. Rakkaalla lapsella on monta nimeä.

Eli olemme määritelleet rakennuspiirrustukset -- luokan -- henkilöoliolle. Kaikilla henkilöolioilla on muuttujat nimi ja ika. Henkilöiden "tila" koostuu niiden nimeen ja ikään asetetuista arvoista.

Oman luokan määritteleminen - konstruktori eli tilan alustus

Luotavalle oliolle halutaan asettaa luontivaiheessa jokin alkutila. Itse määritellyn olion luominen tapahtuu hyvin samaan tapaan kuin Javan valmiiden olioiden kuten ArrayList:ien luominen. Oliot siis luodaan new-komennolla. Olion synnyttämisen yhteydessä olisikin kätevä pystyä antamaan arvo joillekin olioiden muuttujille. Esim. henkilön synnytyksessä olisi kätevää pystyä antamaan nimi jo syntymähetkellä:

    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka");
        // ...
    }

Tämä onnistuu määrittelemällä olion synnyttävä metodi, eli konstruktori. Seuraavassa on määritelty Henkilo-luokalle konstruktori, joka luo uuden Henkilo-olion. Konstruktorissa luotavan henkilön iäksi asetetaan 0 ja nimeksi parametrina tuleva merkkijono:

public class Henkilo {
    private String nimi;
    private int ika;

    public Henkilo(String nimiAlussa) {
        this.ika = 0;
        this.nimi = nimiAlussa;       
    }
}

Konstruktori on siis nimeltään sama kuin luokan nimi. Yllä luokka (class) on Henkilo, ja konstruktori public Henkilo(String nimiAlussa). Konstruktorille parametrina tuleva arvo tulee sulkuihin konstruktorin nimen perään. Konstruktorin voi ajatella olevan metodi, jonka Java suorittaa kun olio luodaan sanomalla new Henkilo("Pekka"); Aina kun jostain luokasta luodaan olio, kutsutaan kyseisen luokan konstruktoria.

Muutama huomio: konstruktorin sisällä on komento this.ika = 0. Tässä asetetaan arvo 0 juuri tämän olion, eli "this"-olion sisäiselle muuttujalle ika. Toinen komento this.nimi = nimiAlussa; taas asettaa juuri tämän olion sisäiselle muuttujalle nimi arvoksi parametrina annetun merkkijonon. Olion muuttujat ika ja nimi näkyvät konstruktorissa ja muuallakin olion sisällä automaattisesti. Niihin viitataan this-etuliitteellä. Koska niissä on private-määre, niin olion ulkopuolelle ne eivät näy.

Vielä yksi huomio: jos ohjelmoija ei tee luokalle konstruktoria, tekee Java automaattisesti luokalle oletuskonstruktorin. Oletuskonstruktori on konstruktori joka ei tee mitään. Eli jos konstruktoria ei jostain syystä tarvita, ei sellaista tarvitse ohjelmoida.

Oman luokan määritteleminen - metodit

Alkaa olla korkea aika päästä käyttämään Henkilo-olioita. Osaamme luoda olion ja alustaa olion muuttujat. Järkevään toimintaan pystyäkseen olioilla on oltava myös metodeja. Tehdään Henkilo-luokalle metodi jonka avulla olio tulostaa itsensä ruudulle:

public class Henkilo {
    private String nimi;
    private int ika;

    public Henkilo(String nimiAlussa) {
        this.ika = 0;
        this.nimi = nimiAlussa;       
    }

    public void tulostaHenkilo() {
        System.out.println(this.nimi + ", ikä " + this.ika + " vuotta");
    }
}

Metodi siis kirjoitetaan luokan sisälle, metodin nimen eteen tulee public void sillä metodin on tarkoitus näkyä ulkomaailmalle ja metodi ei palauta mitään. Huomaa, että sana static ei nyt ilmene missään. Olioiden yhteydessä static-määrettä ei käytetä. Selitämme ensi viikolla hieman tarkemmin mistä tässä on kysymys.

Metodin tulostaHenkilo sisällä on yksi koodirivi joka käyttää hyvakseen oliomuuttujia nimi ja ika. Juuri tämän olion muuttujiin viitataan taas etuliitteellä this. Kaikki olion muuttujat ovat siis näkyvillä ja käytettävissä metodin sisällä.

Luodaan pääohjelmassa kolme henkilöä ja pyydetään niitä tulostamaan itsensä:

public class Main {

    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka");
        Henkilo antti = new Henkilo("Antti");
        Henkilo martin = new Henkilo("Martin");

        pekka.tulostaHenkilo();
        antti.tulostaHenkilo();
        martin.tulostaHenkilo();
    }
}

Tulostuu:

Pekka, ikä 0 vuotta
Antti, ikä 0 vuotta
Martin, ikä 0 vuotta

Sama screencastina:

Tuote

Luo luokka Tuote joka esittää kaupan tuotetta jolla on hinta, lukumäärä ja nimi.

Uuden luokan saa lisättyä seuraavasti: Ruudun vasemmalla reunalla on projektilistaus. Paina projektin nimen 073.Tuote kohdalla hiiren oikeaa nappia. Valitse avautuvasta valikosta New ja Java Class. Anna luokan nimeksi (Class Name) Tuote.

Luokalla tulee olla:

lisää metodeja

Lisätään aiemmin rakentamallemme Henkilölle metodi, joka kasvattaa henkilön ikää vuodella:

public class Henkilo {
    // ...

    public void vanhene() {
        this.ika = this.ika + 1;
    }
}

Metodi siis kirjoitetaan tulostaHenkilo-metodin tapaan luokan Henkilo sisälle. Metodissa kasvatetaan oliomuuttujan ika arvoa yhdellä.

Kutsutaan metodia ja katsotaan mitä tapahtuu:

public class Main {

    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka");
        Henkilo antti = new Henkilo("Antti");

        pekka.tulostaHenkilo();
        antti.tulostaHenkilo();

        System.out.println("");

        pekka.vanhene();
        pekka.vanhene();

        pekka.tulostaHenkilo();
        antti.tulostaHenkilo();
    }
}

Ohjelman tulostus on seuraava:

Pekka, ikä 0 vuotta
Antti, ikä 0 vuotta

Pekka, ikä 2 vuotta
Antti, ikä 0 vuotta

Eli "syntyessään" molemmat oliot ovat nollavuotiaita (konstruktorissa suoritetaan mm. rivi this.ika = 0;). Olion pekka metodia vanhene kutsutaan kaksi kertaa. Kuten tulostus näyttää, tämä saa aikaan sen että Pekan ikä vanhenemisen jälkeen 2 vuotta. Kutsumalla metodia Pekkaa vastaavalle oliolle, toisen henkilöolion ikä ei muutu.

Jokaisella oliolla on siis oma sisäinen tilansa.

Aivan kuiten edellisellä viikolla käsittelemämme olioihin liittymättömät metodit, myös olioihin liittyvät metodit voivat palauttaa arvon. Lisätään Henkilölle metodi joka palauttaa henkilön iän:

public class Henkilo {
    // ...

    public int palautaIka() {
        return this.ika;
    }
}

Eli koska kyseessä olioon liittyvä metodi, ei määrittelyssä ole sanaa static. Olioiden arvon palauttavia metodeja käytetään kuten mitä tahansa arvon palauttavia metodeja:

public class Main {

    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka");
        Henkilo antti = new Henkilo("Antti");

        pekka.vanhene();
        pekka.vanhene();

        antti.vanhene();

        System.out.println( "Pekan ikä: "+pekka.palautaIka() );
        System.out.println( "Antin ikä: "+antti.palautaIka() );

        int yht = pekka.palautaIka() + antti.palautaIka();

        System.out.println( "Pekka ja Antti yhteensä "+yht+ " vuotta" );
    }
}

Ohjelman tulostus on seuraava:

Pekan ikä 2
Antin ikä 1 

Pekka ja Antti yhteensä 3 vuotta

Kertoja

Luo luokka Kertoja jolla on:

Esimerkki luokan käytöstä:

Kertoja kolmellaKertoja = new Kertoja(3);
System.out.println("kolmellaKertoja.kerro(2): " + kolmellaKertoja.kerro(2));
Kertoja neljallaKertoja = new Kertoja(4);
System.out.println("neljallaKertoja.kerro(2): " + neljallaKertoja.kerro(2));
System.out.println("kolmellaKertoja.kerro(1): " + kolmellaKertoja.kerro(1));
System.out.println("neljallaKertoja.kerro(1): " + neljallaKertoja.kerro(1));

Tulostus

kolmellaKertoja.kerro(2): 6
neljallaKertoja.kerro(2): 8
kolmellaKertoja.kerro(1): 3
neljallaKertoja.kerro(1): 4

Vähenevä laskuri

Tehtäväpohjan mukana tulee osittain valmiiksi toteutettu luokka VahenevaLaskuri:

public class VahenevaLaskuri {
    private int arvo;   // oliomuuttuja joka muistaa laskurin arvon
    
    public VahenevaLaskuri(int arvoAlussa) {
        this.arvo = arvoAlussa;
    }

    public void tulostaArvo() {
        System.out.println("arvo: " + this.arvo);  
    }

    public void vahene() {
        // kirjoita tänne metodin toteutus
        // laskurin arvon on siis tarkoitus vähentyä yhdellä
    }
    
    // ja tänne muut metodit
}

Seuraavassa esimerkki miten pääohjelma käyttää vähenevää laskuria:

public class Paaohjelma {
    public static void main(String[] args) {
        VahenevaLaskuri laskuri = new VahenevaLaskuri(10);
        
        laskuri.tulostaArvo();
        
        laskuri.vahene();
        laskuri.tulostaArvo();

        laskuri.vahene();
        laskuri.tulostaArvo();
    }
}

Pitäisi tulostua:

arvo: 10
arvo: 9
arvo: 8

VahenevaLaskuri-luokan konstruktorille annetaan parametrina alkuarvo. Esimerkin oliota laskuri luodessa laskurille välitetään parametrina arvo 10. Esimerkin laskuri-olioon liittyvään oliomuuttujaan arvo asetetaan siis aluksi arvo 10. Laskurin arvon voi tulostaa metodilla tulostaArvo(). Laskurilla tulee myös olla metodi vahene() joka vähentää laskurin arvoa yhdellä.

Metodin vahene() toteutus

Täydennä luokan runkoon metodin vahene() toteutus sellaiseksi, että se vähentää kutsuttavan olion oliomuuttujan arvo arvoa yhdellä. Kun olet toteuttanut metodin vahene(), edellisen esimerkin pääohjelman tulee toimia esimerkkitulosteen mukaan.

Laskurin arvo ei saa olla negatiivinen

Täydennä metodin vahene() toteutus sellaiseksi, ettei laskurin arvo mene koskaan negatiiviseksi. Eli jos laskurin arvo on jo 0, ei vähennys sitä enää vähennä:

public class Paaohjelma {
    public static void main(String[] args) {
        VahenevaLaskuri laskuri = new VahenevaLaskuri(2);
        
        laskuri.tulostaArvo();
        
        laskuri.vahene();
        laskuri.tulostaArvo();

        laskuri.vahene();
        laskuri.tulostaArvo();

        laskuri.vahene();
        laskuri.tulostaArvo();
    }
}

Tulostuu:

arvo: 2
arvo: 1
arvo: 0
arvo: 0

Laskurin arvon nollaus

Tee laskurille metodi public void nollaa() joka nollaa laskurin arvon, esim:

public class Paaohjelma {
    public static void main(String[] args) {
        VahenevaLaskuri laskuri = new VahenevaLaskuri(100);
        
        laskuri.tulostaArvo();
        
        laskuri.nollaa();
        laskuri.tulostaArvo();

        laskuri.vahene();
        laskuri.tulostaArvo();
    }
}

Tulostuu:

arvo: 100
arvo: 0
arvo: 0

Laskurin arvon palautus

Tee laskurille metodi public void palautaAlkuarvo(), joka palauttaa laskurille arvon joka sillä oli alussa:

public class Paaohjelma {
    public static void main(String[] args) {
        VahenevaLaskuri laskuri = new VahenevaLaskuri(100);
        
        laskuri.tulostaArvo();
        
        laskuri.vahene();
        laskuri.tulostaArvo();

        laskuri.vahene();
        laskuri.tulostaArvo();

        laskuri.nollaa();
        laskuri.tulostaArvo();

        laskuri.palautaAlkuarvo();
        laskuri.tulostaArvo();
    }
}

Tulostuu:

arvo: 100
arvo: 99
arvo: 98
arvo: 0
arvo: 100

Vihje jotta alkuarvon voi palauttaa, se täytyy "muistaa" toisen oliomuuttujan avulla! Joudut siis lisäämään ohjelmaan oliomuuttujan johon talletetaan laskurin alussa saama arvo.

Ruokalista

Kumpulan kampuksella Helsingissä toimivaan Unicafe-nimiseen gourmet-ravintolaan tarvitaan uusi ruokalista. Keittiömestari tietää ohjelmoinnista, ja haluaa listan hallinnointiin tietokonejärjestelmän. Toteutetaan tässä tehtävässä järjestelmän sydän, luokka Ruokalista.

Tehtäväpohjan mukana tulee Main-luokka, jossa voit testata ruokalistan toimintaa. Ruokalistan toteuttamista varten saat seuraavanlaisen tehtäväpohjan:

import java.util.ArrayList;

public class Ruokalista {

    private ArrayList<String> ateriat;

    public Ruokalista() {
        this.ateriat = new ArrayList<String>();
    }

    // toteuta tänne tarvittavat metodit
}

Ruokalistaoliolla on siis oliomuuttujana ArrayList, jonka on tarkoitus tallentaa ruokalistalla olevien ruokalajien nimet.

Ruokalistan tulee tarjota metodit public void lisaaAteria(String ateria), public void tulostaAteriat(), ja public void tyhjennaRuokalista().

Aterian lisääminen

Toteuta metodi public void lisaaAteria(String ateria), joka lisää uuden aterian ruokalistan ateriat-listaan. Jos lisättävä ateria on jo listassa, sitä ei lisätä uudelleen.

Aterioiden tulostaminen

Toteuta metodi public void tulostaAteriat(), joka tulostaa ateriat. Esimerkiksi kolmen aterian lisäyksen jälkeen tulostuksen tulee olla seuraavanlainen.

ensimmäisenä lisätty ateria
toisena lisätty ateria
kolmantena lisätty ateria

Ruokalistan tyhjentäminen

Toteuta metodi public void tyhjennaRuokalista() joka tyhjentää ruokalistan. ArrayList-luokalla on metodi josta on tässä hyötyä. NetBeans osaa vihjata käytettävissä olevista metodeista kun kirjoitat olion nimen ja pisteen. Yritä kirjoittaa ateriat. metodirungon sisällä ja katso mitä käy.

Henkilo-luokka laajenee

Jatketaan taas Henkilo-luokan laajentamista. Luokan tämänhetkinen versio on seuraava:

public class Henkilo {
    private String nimi;
    private int ika;

    public Henkilo(String nimiAlussa) {
        this.ika = 0;
        this.nimi = nimiAlussa;       
    }

    public void tulostaHenkilo() {
        System.out.println(this.nimi + ", ikä " + this.ika + " vuotta");
    }

    public void vanhene() {
        this.ika = this.ika + 1;
    }
}

Tehdään henkilölle metodi, jonka avulla voidaan selvittää onko henkilö täysi-ikäinen. Metodi palauttaa totuusarvon -- joko true tai false:

public class Henkilo {
    // ...

    public boolean taysiIkainen(){
        if ( this.ika < 18 ) {
            return false;
        }

        return true;
    }

   /*
      huom. metodin voisi kirjoittaa lyhyemmin seuraavasti:
  
      public boolean taysiIkainen(){
        return this.ika >= 18;
      }
   */
}

Ja testataan:

    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka");
        Henkilo antti = new Henkilo("Antti");

        int i = 0;
        while ( i < 30 ) {
            pekka.vanhene();
            i++;
        }

        antti.vanhene();

        System.out.println("");

        if ( antti.taysiIkainen() ) {
            System.out.print("täysi-ikäinen: ");
            antti.tulostaHenkilo();
        } else {
            System.out.print("alaikäinen: ");
            antti.tulostaHenkilo();
        }

        if ( pekka.taysiIkainen() ) {
            System.out.print("täysi-ikäinen: ");
            pekka.tulostaHenkilo();
        } else {
            System.out.print("alaikäinen: ");
            pekka.tulostaHenkilo();
        }
    }
alaikäinen: Antti, ikä 1 vuotta
täysi-ikäinen: Pekka, ikä 30 vuotta

Viritellään ratkaisua vielä hiukan. Nyt henkilön pystyy "tulostamaan" ainoastaan siten, että nimen lisäksi tulostuu ikä. On tilanteita, joissa haluamme tietoon pelkän olion nimen. Eli tehdään tarkoitusta varten oma metodi:

public class Henkilo {
    // ...

    public String getNimi() {
        return this.nimi;
    }
}

Metodi getNimi palauttaa oliomuuttujan nimi kutsujalle. Metodin nimi on hieman erikoinen. Javassa on usein tapana nimetä oliomuuttujan palauttava metodi juuri näin, eli getMuuttujanNimi. Tälläisiä metodeja kutsutaan usein "gettereiksi".

Muotoillaan pääohjelma käyttämään uutta "getteri"-metodia:

    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka");
        Henkilo antti = new Henkilo("Antti");

        int i = 0;
        while ( i < 30 ) {
            pekka.vanhene();
            i++;
        }

        antti.vanhene();

        System.out.println("");

        if ( antti.taysiIkainen() ) {
            System.out.println( antti.getNimi() + " on täysi-ikäinen" );
        } else {
            System.out.println( antti.getNimi() + " on alaikäinen" );
        }

        if ( pekka.taysiIkainen() ) {
            System.out.println( pekka.getNimi() + " on täysi-ikäinen" );
        } else {
            System.out.println( pekka.getNimi() + " on alaikäinen " );
        }
    }

Tulostus alkaa olla jo aika siisti:

Antti on alaikäinen
Pekka on täysi-ikäinen

toString

Olemme syyllistyneet edellä osittain huonoon ohjelmointityyliin tekemällä metodin jonka avulla olio tulostetaan, eli metodin tulostaHenkilo. Suositeltavampi tapa on määritellä oliolle metodi jonka palauttaa olion "merkkijonoesityksen". Merkkijonoesityksen palauttavan metodin nimi on Javassa aina toString. Määritellään seuraavassa henkilölle tämä metodi:

public class Henkilo {
    // ...

    public String toString() {
        return this.nimi + ", ikä " + this.ika + " vuotta";
    }
}

Metodi toString toimii kuten tulostaHenkilo, mutta se ei itse tulosta mitään vaan palauttaa merkkijonoesityksen, jotta metodin kutsuja voi halutessaan suorittaa tulostamisen.

Metodia käytetään hieman yllättävällä tavalla:

    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka");
        Henkilo antti = new Henkilo("Antti");

        int i = 0;
        while ( i < 30 ) {
            pekka.vanhene();
            i++;
        }

        antti.vanhene();

        System.out.println( antti ); // sama kun System.out.println( antti.toString() ); 
        System.out.println( pekka ); // sama kun System.out.println( pekka.toString() ); 
    }

Periaatteena on, että System.out.println-metodi pyytää olion merkkijonoesityksen ja tulostaa sen. Merkkijonoesityksen palauttavan toString-metodin kutsua ei tarvitse kirjoittaa itse, sillä Java lisää sen automaattisesti. Ohjelmoijan kirjoittaessa:

        System.out.println( antti );       

Java täydentää suorituksen aikana kutsun muotoon:

        System.out.println( antti.toString() ); 

Käy niin, että oliolta pyydetään sen merkkijonoesitys. Olion palauttama merkkijonoesitys tulostetaan normaaliin tapaan System.out.println-komennolla.

Voimme nyt poistaa turhaksi käyneen tulostaOlio-metodin.

Olioscreencastin toinen osa:

Lyyra-kortti

Helsingin Yliopiston opiskelijaruokaloissa eli Unicafeissa opiskelijat maksavat lounaansa käyttäen Lyyra-korttia.

Tässä tehtäväsäsarjassa tehdään luokka LyyraKortti, jonka tarkoituksena on jäljitellä Lyyra-kortin käyttämistä Unicafessa.

Luokan runko

Projektiin tulee kuulumaan kaksi kooditiedostoa:

Tehtäväpohjan eli projektin Viikko4_077.Lyyrakortti mukana tulee kooditiedosto Paaohjelma jonka sisällä on main-metodi.

Lisää projektiin uusi luokka nimeltä LyyraKortti. Uuden luokan saa lisättyä seuraavasti: Ruudun vasemmalla reunalla on projektilistaus. Paina projektin nimen Viikko4_077.Lyyrakortti kohdalla hiiren oikeaa nappia. Valitse avautuvasta valikosta New ja Java Class. Anna luokan nimeksi (Class Name) LyyraKortti.

Tee ensin LyyraKortti-olion konstruktori, jolle annetaan kortin alkusaldo ja joka tallentaa sen olion sisäiseen muuttujaan. Tee sitten toString-metodi, joka palauttaa kortin saldon muodossa "Kortilla on rahaa X euroa".

Seuraavassa on luokan LyyraKortti runko:

public class LyyraKortti {
    private double saldo;

    public LyyraKortti(double alkusaldo) {
        // kirjoita koodia tähän
    }

    public String toString() {
        // kirjoita koodia tähän
    }
}

Seuraava pääohjelma testaa luokkaa:

public class Paaohjelma {
    public static void main(String[] args) {
        LyyraKortti kortti = new LyyraKortti(50);
        System.out.println(kortti);
    }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Kortilla on rahaa 50.0 euroa

Kortilla maksaminen

Täydennä LyyraKortti-luokkaa seuraavilla metodeilla:

    public void syoEdullisesti() {
        // kirjoita koodia tähän
    }

    public void syoMaukkaasti() {
        // kirjoita koodia tähän
    }

Metodin syoEdullisesti tulisi vähentää kortin saldoa 2.50 eurolla ja metodin syoMaukkaasti tulisi vähentää kortin saldoa 4.00 eurolla.

Seuraava pääohjelma testaa luokkaa:

public class Paaohjelma {
    public static void main(String[] args) {
        LyyraKortti kortti = new LyyraKortti(50);
        System.out.println(kortti);

        kortti.syoEdullisesti();
        System.out.println(kortti);

        kortti.syoMaukkaasti();
        kortti.syoEdullisesti();
        System.out.println(kortti);
    }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Kortilla on rahaa 50.0 euroa
Kortilla on rahaa 47.5 euroa
Kortilla on rahaa 41.0 euroa

Ei-negatiivinen saldo

Mitä tapahtuu, jos kortilta loppuu raha kesken? Ei ole järkevää, että saldo muuttuu negatiiviseksi. Muuta metodeita syoEdullisesti ja syoMaukkaasti niin, että ne eivät vähennä saldoa, jos saldo menisi negatiiviseksi.

Seuraava pääohjelma testaa luokkaa:

public class Paaohjelma {
    public static void main(String[] args) {
        LyyraKortti kortti = new LyyraKortti(5);
        System.out.println(kortti);

        kortti.syoMaukkaasti();
        System.out.println(kortti);

        kortti.syoMaukkaasti();
        System.out.println(kortti);
    }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Kortilla on rahaa 5.0 euroa
Kortilla on rahaa 1.0 euroa
Kortilla on rahaa 1.0 euroa

Yllä toinen metodin syoMaukkaasti kutsu ei vaikuttanut saldoon, koska saldo olisi mennyt negatiiviseksi.

Kortin lataaminen

Lisää LyyraKortti-luokkaan seuraava metodi:

    public void lataaRahaa(double rahamaara) {
        // kirjoita koodia tähän
    }

Metodin tarkoituksena on kasvattaa kortin saldoa parametrina annetulla rahamäärällä. Kuitenkin kortin saldo saa olla korkeintaan 150 euroa, joten jos ladattava rahamäärä ylittäisi sen, saldoksi tulisi tulla silti tasan 150 euroa.

Seuraava pääohjelma testaa luokkaa:

public class Paaohjelma {
    public static void main(String[] args) {
        LyyraKortti kortti = new LyyraKortti(10);
        System.out.println(kortti);

        kortti.lataaRahaa(15);
        System.out.println(kortti);

        kortti.lataaRahaa(10);
        System.out.println(kortti);

        kortti.lataaRahaa(200);
        System.out.println(kortti);
    }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Kortilla on rahaa 10.0 euroa
Kortilla on rahaa 25.0 euroa
Kortilla on rahaa 35.0 euroa
Kortilla on rahaa 150.0 euroa

Kortin lataus negatiivisella arvolla

Muuta metodia lataaRahaa vielä siten, että jos yritetään ladata negatiivinen rahamäärä, ei kortilla oleva arvo muutu.

Seuraava pääohjelma testaa luokkaa:

public class Paaohjelma {
    public static void main(String[] args) {
        LyyraKortti kortti = new LyyraKortti(10);
        System.out.println(kortti);
        kortti.lataaRahaa(-15);
        System.out.println(kortti);
    }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Kortilla on rahaa 10.0 euroa
Kortilla on rahaa 10.0 euroa

Monta korttia

Tee pääohjelma, joka sisältää seuraavan tapahtumasarjan:

Pääohjelman runko on seuraava:

public class Main {
    public static void main(String[] args) {
        LyyraKortti pekanKortti = new LyyraKortti(20);
        LyyraKortti matinKortti = new LyyraKortti(30);

        // kirjoita koodia tähän
    }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Pekka: Kortilla on rahaa 16.0 euroa
Matti: Kortilla on rahaa 27.5 euroa
Pekka: Kortilla on rahaa 36.0 euroa
Matti: Kortilla on rahaa 23.5 euroa
Pekka: Kortilla on rahaa 31.0 euroa
Matti: Kortilla on rahaa 73.5 euroa

Lisää metodeja

Jatketaan taas Henkilo-luokan parissa. Päätetään että haluamme laskea henkilöiden painoindeksejä. Tätä varten teemme henkilölle metodit pituuden ja painon asettamista varten, sekä metodin joka laskee painoindeksin. Henkilön uudet ja muuttuneet osat seuraavassa:

   public class Henkilo {
    private String nimi;
    private int ika;
    private int paino;
    private int pituus;

    public Henkilo(String nimiAlussa) {
        this.ika = 0;
        this.paino = 0;
        this.pituus = 0;
        this.nimi = nimiAlussa;
    }

    public void setPituus(int uusiPituus) {
        this.pituus = uusiPituus;
    }

    public void setPaino(int uusiPaino) {
        this.paino = uusiPaino;
    }

    public double painoIndeksi(){
        double pituusPerSata = this.pituus / 100.0;
        return this.paino / ( pituusPerSata * pituusPerSata );
    }

    // ...
}      

Eli henkilölle lisättiin oliomuuttujat pituus ja paino. Näille voi asettaa arvon metodeilla setPituus ja setPaino. Jälleen käytössä Javaan vakiintunut nimeämiskäytäntö, eli jos metodin tehtävänä on ainoastaan asettaa arvo oliomuuttujaan, on metodi tapana nimetä setMuuttujanNimi:ksi. Arvon asettavia metodeja kutsutaan usein "settereiksi". Seuraavassa käytämme uusia metodeja:

    public static void main(String[] args) {
        Henkilo matti = new Henkilo("Matti");
        Henkilo juhana = new Henkilo("Juhana");

        matti.setPituus(180);
        matti.setPaino(86);
        
        juhana.setPituus(175);
        juhana.setPaino(64);
        
        System.out.println( matti.getNimi() + ", painoindeksisi on " + matti.painoIndeksi() );
        System.out.println( juhana.getNimi() + ", painoindeksisi on " + juhana.painoIndeksi() );

Tulostus:

Matti, painoindeksisi on 26.54320987654321
Juhana, painoindeksisi on 20.897959183673468

Parametrilla ja oliomuuttujalla sama nimi!

Edellä metodissa setPituus asetetaan oliomuuttujaan pituus parametrin uusiPituus arvo:

    public void setPituus(int uusiPituus) {
        this.pituus = uusiPituus;
    }

Parametrin nimi voisi olla myös sama kuin oliomuuttujan nimi, eli seuraava toimisi myös:

    public void setPituus(int pituus) {
        this.pituus = pituus;
    }

Nyt metodissa pituus tarkottaa nimenomaan pituus-nimistä parametria ja this.pituus saman nimistä oliomuuttujaa. Esim. seuraava ei toimisi sillä koodi ei viittaa ollenkaan oliomuuttujaan pituus:

    public void setPituus(int pituus) {
        // EI TOIMI ÄLÄ TEE NÄIN!!!
        pituus = pituus;
    }

Liukuluvun "siisti" tulostaminen

Desimaalien määrä edellisessä tulostuksessa on hieman liioiteltu. Yksi tapa päästä määräämään tulostettavien desimaalien määrä on seuraava:

    System.out.println( matti.getNimi() + ", painoindeksisi on " + String.format( "%.2f", matti.painoIndeksi() ) );
    System.out.println( juhana.getNimi() + ", painoindeksisi on " + String.format( "%.2f", juhana.painoIndeksi() ) );

Eli jos luku on liukuluku, voi siitä tehdä komennolla String.format( "%.2f", luku ) merkkijonon, jossa luku on otettu mukaan kahden desimaalin tarkkuudella. Pisteen ja f:n välissä oleva numero säätää mukaan tulevan desimaalien määrän.

Nyt tulostus on siistimpi:

Matti, painoindeksisi on 26,54
Juhana, painoindeksisi on 20,90

String.format ei ehkä ole kaikkein monikäyttöisin tapa Javassa tulostuksen muotoiluun. Se kuitenkin lienee yksinkertaisin ja kelpaa meille hyvin nyt.

Kello laskurin avulla

Tässä tehtävässä tehdään luokka YlhaaltaRajoitettuLaskuri ja sovelletaan sitä kellon tekemiseen.

Rajoitettu laskuri

Tehdään luokka YlhaaltaRajoitettuLaskuri. Luokan olioilla on seuraava toiminnallisuus:

Tehtäväpohjassa on valmiina pääohjelmaa varten tiedosto Paaohjelma. Aloita tekemällä luokka YlhaaltaRajoitettuLaskuri vastaavasti kuin LyyraKortti-tehtävässä. Näin tehdään myös tulevissa tehtäväsarjoissa.

Luokan rungoksi tulee seuraava:

public class YlhaaltaRajoitettuLaskuri {
    private int arvo;
    private int ylaraja;

    public YlhaaltaRajoitettuLaskuri(int ylarajanAlkuarvo) {
        // kirjoita koodia tähän
    }

    public void seuraava() {
        // kirjoita koodia tähän
    }

    public String toString() {
        // kirjoita koodia tähän
    }
}

Vihje: et voi palauttaa toStringissä suoraan kokonaislukutyyppisen oliomuuttujan laskuri arvoa. Kokonaislukumuuttujasta arvo saa merkkijonomuodon esim. lisäämällä sen eteen tyhjän merkkijonon eli kirjoittamalla "" + arvo.

Seuraavassa on pääohjelma, joka käyttää laskuria:

public class Paaohjelma {
    public static void main(String[] args) {
        YlhaaltaRajoitettuLaskuri laskuri = new YlhaaltaRajoitettuLaskuri(4);
        System.out.println("arvo alussa: " + laskuri );

        int i = 0; 
        while ( i < 10) {
            laskuri.seuraava();
            System.out.println("arvo: " + laskuri );
            i++;
        }
    }
}

Laskurille asetetaan konstruktorissa ylärajaksi 4, joten laskurin arvo on luku 0:n ja 4:n väliltä. Huomaa, miten metodi seuraava vie laskurin arvoa eteenpäin, kunnes se pyörähtää 4:n jälkeen 0:aan:

Ohjelman tulostuksen tulisi olla seuraava:

arvo alussa: 0
arvo: 1
arvo: 2
arvo: 3
arvo: 4
arvo: 0
arvo: 1
arvo: 2
arvo: 3
arvo: 4
arvo: 0

Etunolla tulostukseen

Tee toString-metodista sellainen, että se lisää arvon merkkijonoesitykseen etunollan, jos laskurin arvo on vähemmän kuin 10. Eli jos laskurin arvo on esim. 3, palautetaan merkkijono "03", jos arvo taas on esim. 12, palautetaan normaaliin tapaan merkkijono "12".

Muuta pääohjelma seuraavaan muotoon ja varmista, että tulos on haluttu.

public class Paaohjelma {
    public static void main(String[] args) {
        YlhaaltaRajoitettuLaskuri laskuri = new YlhaaltaRajoitettuLaskuri(14);
        System.out.println("arvo alussa: " + laskuri );

        int i = 0; 
        while ( i < 16){
            laskuri.seuraava();
            System.out.println("arvo: " + laskuri );
            i++; 
        }
    }
}
arvo alussa: 00
arvo: 01
arvo: 02
arvo: 03
arvo: 04
arvo: 05
arvo: 06
arvo: 07
arvo: 08
arvo: 09
arvo: 10
arvo: 11
arvo: 12
arvo: 13
arvo: 14
arvo: 00
arvo: 01

Kello, ensimmäinen versio

Käyttämällä kahta laskuria voimme muodostaa kellon. Tuntimäärä on laskuri, jonka yläraja on 23, ja minuuttimäärä on laskuri jonka yläraja on 59. Kuten kaikki tietävät, kello toimii siten, että aina kun minuuttimäärä pyörähtää nollaan, tuntimäärä kasvaa yhdellä.

Tee ensin laskurille metodi arvo, joka palauttaa laskurin arvon:

    public int arvo() {
        // kirjoita koodia tähän
    }

Tee sitten kello täydentämällä seuraava pääohjelmarunko (kopioi tämä pääohjelmaksesi sekä täydennä tarvittavilta osin kommenttien ohjaamalla tavalla):

public class Paaohjelma {
    public static void main(String[] args) {
        YlhaaltaRajoitettuLaskuri minuutit = new YlhaaltaRajoitettuLaskuri(59);
        YlhaaltaRajoitettuLaskuri tunnit = new YlhaaltaRajoitettuLaskuri(23);     

        int i = 0;
        while ( i < 121 ) {
            System.out.println( tunnit + ":" + minuutit);   // tulostetaan nykyinen aika
            // minuuttimäärä kasvaa
            // jos minuuttimäärä menee nollaan, tuntimäärä kasvaa

            i++;
        }
    }
}

Jos kellosi toimii oikein, sen tulostus näyttää suunnilleen seuraavalta:

00:00
00:01
...
00:59
01:00
01:01
01:02
...
01:59
02:00

Kello, toinen versio

Laajenna kelloasi myös sekuntiviisarilla. Tee lisäksi luokalle YlhaaltaRajoitettuLaskuri metodi asetaArvo, jolla laskurille pystyy asettamaan halutun arvon. Jos laskurille yritetään asettaa kelvoton arvo eli negatiivinen luku tai ylärajaa suurempi luku, ei laskurin arvo muutu.

Tämän metodin avulla voit muuttaa kellon ajan heti ohjelman alussa haluamaksesi.

Voit testata kellon toimintaa seuraavalla ohjelmalla

public class Paaohjelma {
    public static void main(String[] args)  {
        Scanner lukija = new Scanner(System.in);
        YlhaaltaRajoitettuLaskuri sekunnit = new YlhaaltaRajoitettuLaskuri(59);
        YlhaaltaRajoitettuLaskuri minuutit = new YlhaaltaRajoitettuLaskuri(59);
        YlhaaltaRajoitettuLaskuri tunnit = new YlhaaltaRajoitettuLaskuri(23);

        System.out.print("sekunnit: ");
        int sek = // kysy sekuntien alkuarvo käyttäjältä
        System.out.print("minuutit: ");
        int min = // kysy minuuttien alkuarvo käyttäjältä
        System.out.print("tunnit: ");
        int tun = // kysy tuntien alkuarvo käyttäjältä

        sekunnit.asetaArvo(sek);
        minuutit.asetaArvo(min);
        tunnit.asetaArvo(tun);

        int i = 0;
        while ( i < 121 ) {
          // lisää edelliseen myös sekuntiviisari
         i++;
        }

    }
}

Kokeile laittaa kellosi alkamaan ajasta 23:59:50 ja varmista, että vuorokauden vaihteessa kello toimii odotetusti!

Bonus-tehtävä: ikuisesti käyvä kello (tehtävää ei palauteta!)

Ennen kuin alat tekemään tätä tehtävää, palauta jo tekemäsi kello!

Muuta pääohjelmasi seuraavaan muotoon:

public class Paaohjelma {
    public static void main(String[] args) throws Exception {
        YlhaaltaRajoitettuLaskuri sekunnit = new YlhaaltaRajoitettuLaskuri(59);
        YlhaaltaRajoitettuLaskuri minuutit = new YlhaaltaRajoitettuLaskuri(59);
        YlhaaltaRajoitettuLaskuri tunnit = new YlhaaltaRajoitettuLaskuri(23);

        sekunnit.asetaArvo(50);
        minuutit.asetaArvo(59);
        tunnit.asetaArvo(23);

        while ( true ) {
            System.out.println( tunnit + ":" + minuutit + ":" + sekunnit );
            Thread.sleep(1000);
            // lisää kellon aikaa sekunnilla eteenpäin
        }
    }
}

Nyt kello käy ikuisesti ja kasvattaa arvoaan sekunnin välein. Sekunnin odotus tapahtuu komennolla Thread.sleep(1000);, komennon parametri kertoo nukuttavan ajan millisekunteina. Jotta komento toimisi, pitää main:in esittelyriville tehdä pieni lisäys: public static void main(String[] args) throws Exception {, eli tummennettuna oleva throws Exception.

Saat ohjelman lopetettua painamalla NetBeans-konsolin (eli sen osan johon kello tulostaa arvonsa) vasemmalla laidalla olevasta punaisesta laatikosta.

Tärkeitä kommentteja liittyen olioiden käyttöön. Lue nämä ehdottomasti.

Olio-ohjelmoinnissa on kyse pitkälti käsitteiden eristämisestä omiksi kokonaisuuksikseen tai toisin ajatellen abstraktioiden muodostamisesta. Voisi ajatella, että on turha luoda oliota jonka sisällä on ainoastaan luku, eli että saman voisi tehdä suoraan int-muuttujilla. Asia ei kuitenkaan ole näin. Jos kello koostuu pelkästään kolmesta int-muuttujasta joita kasvatellaan, muuttuu ohjelma lukijan kannalta epäselvemmäksi, koodista on vaikea "nähdä" mistä on kysymys. Aiemmin materiaalissa mainitsimme jo kokeneen ja kuuluisan ohjelmoijan Kent Beckin neuvon "Any fool can write code that a computer can understand. Good programmers write code that humans can understand", eli koska viisari on oikeastaan oma selkeä käsitteensä, on siitä ohjelman ymmärrettävyyden parantamiseksi hyvä tehdä oma luokka, eli YlhaaltaRajoitettuLaskuri.

Käsitteen erottaminen omaksi luokaksi on monellakin tapaa hyvä idea. Ensinnäkin tiettyjä yksityiskohtia (esim. laskurin pyörähtäminen) saadaan piilotettua luokan sisään (eli abstrahoitua). Sen sijaan että kirjoitetaan if-lause ja sijoitusoperaatio, riittää, että laskurin käyttäjä kutsuu selkeästi nimettyä metodia seuraava(). Aikaansaatu laskuri sopii kellon lisäksi ehkä muidenkin ohjelmien rakennuspalikaksi, eli selkeästä käsitteestä tehty luokka voi olla monikäyttöinen. Suuri etu saavutetaan myös sillä, että koska laskurin toteutuksen yksityiskohdat eivät näy laskurin käyttäjille, voidaan yksityiskohtia tarvittaessa muuttaa.

Totesimme että kello sisältää kolme viisaria, eli koostuu kolmesta käsitteestä. Oikeastaan kello on itsekin käsite ja teemme ensi viikolla luokan Kello, jotta voimme luoda selkeitä Kello-olioita. Kello tulee siis olemaan olio jonka toiminta perustuu "yksinkertaisimpiin" olioihin eli viisareihin. Tämä on juuri olio-ohjelmoinnin suuri idea: ohjelma rakennetaan pienistä selkeistä yhteistoiminnassa olevista olioista.

Nyt otamme varovaisia ensiaskelia oliomaailmassa. Kurssin lopussa oliot alkavat kuitenkin olla jo selkärangassa ja nyt ehkä käsittämättömältä tuntuva lausahdus, ohjelma rakennetaan pienistä selkeistä yhteistoiminnassa olevista olioista alkaa tuntua meistä ehkä järkeenkäyvältä ja itsestäänselvältä.

Oman metodin kutsu

Olio voi kutsua myös omia metodeitaan. Jos esim. halutaan, että toString-metodin palauttama merkkijonoesitys kertoisi myös henkilön painoindeksin, kannattaa toString:istä kutsua olion omaa metodia painoIndeksi:

    public String toString() {
        return this.nimi + ", ikä " + this.ika + " vuotta, painoindeksini on " + this.painoIndeksi();
    }

Eli kun olio kutsuu omaa metodiaan, riittää etuliite this ja pelkkä metodin nimi. Vaihtoehtoinen tapa on tehdä oman metodin kutsu muodossa painoIndeksi() jolloin ei korosteta, että kutsutaan "olion itsensä" metodia painoindeksi:

    public String toString() {
        return this.nimi + ", ikä " + this.ika + " vuotta, painoindeksini on " + painoIndeksi();
    }

Olioscreencastin kolmas osa:

Nyt on aika harjoitella lisää yksinkertaisten olioiden tekemistä käytännössä:

Lukutilasto

Lukujen määrä

Tee luokka Lukutilasto (tiedosto luomaasi luokkaa varten on tehtäväpohjassa valmiina), joka tuntee seuraavat toiminnot :

Luokan ei tarvitse tallentaa mihinkään lisättyjä lukuja, vaan riittää muistaa niiden määrä. Metodin lisaaLuku ei tässä vaiheessa tarvitse edes ottaa huomioon, mikä luku lisätään tilastoon, koska ainoa tallennettava asia on lukujen määrä.

Luokan runko on seuraava:

public class Lukutilasto {
    private int lukujenMaara;

    public Lukutilasto() {
        // alusta tässä muuttuja lukujenMaara
    }
    
    public void lisaaLuku(int luku) {
        // kirjoita koodia tähän
    }

    public int haeLukujenMaara() {
        // kirjoita koodia tähän
    }
}

Seuraava ohjelma esittelee luokan käyttöä:

public class Paaohjelma {
   public static void main(String[] args) {
      Lukutilasto tilasto = new Lukutilasto();
      tilasto.lisaaLuku(3);
      tilasto.lisaaLuku(5);
      tilasto.lisaaLuku(1);
      tilasto.lisaaLuku(2);
      System.out.println("Määrä: " + tilasto.haeLukujenMaara());
    }
}

Ohjelman tulostus on seuraava:

Määrä: 4

Summa ja keskiarvo

Laajenna luokkaa seuraavilla toiminnoilla:

Luokan runko on seuraava:

public class Lukutilasto {
    private int lukujenMaara;
    private int summa;

    public Lukutilasto() {
        // alusta tässä muuttujat maara ja summa
    }
    
    public void lisaaLuku(int luku) {
        // kirjoita koodia tähän
    }

    public int haeLukujenMaara() {
        // kirjoita koodia tähän
    }

    public int summa() {
        // kirjoita koodia tähän
    }

    public double keskiarvo() {
        // kirjoita koodia tähän
    }
}

Seuraava ohjelma esittelee luokan käyttöä:

public class Main {
    public static void main(String[] args) {
        Lukutilasto tilasto = new Lukutilasto();
        tilasto.lisaaLuku(3);
        tilasto.lisaaLuku(5);
        tilasto.lisaaLuku(1);
        tilasto.lisaaLuku(2);
        System.out.println("Määrä: " + tilasto.haeLukujenMaara());
        System.out.println("Summa: " + tilasto.summa());
        System.out.println("Keskiarvo: " + tilasto.keskiarvo());
    }
}

Ohjelman tulostus on seuraava:

Määrä: 4
Summa: 11
Keskiarvo: 2.75

Summa käyttäjältä

Tee ohjelma, joka kysyy lukuja käyttäjältä, kunnes käyttäjä antaa luvun -1. Sitten ohjelma ilmoittaa lukujen summan.

Ohjelmassa tulee käyttää Lukutilasto-olioa summan laskemiseen.

Anna lukuja:
4
2
5
4
-1
Summa: 15

Monta summaa

Muuta edellistä ohjelmaa niin, että ohjelma laskee myös parillisten ja parittomien lukujen summaa.

HUOM: Määrittele ohjelmassa kolme oliota luokasta Lukutilasto: ensimmäinen laskee kaikkien summan, toinen laskee parillisten lukujen summan ja kolmas laskee parittomien lukujen summan.

Jotta testi toimisi, on oliot luotava pääohjelmassa edellä mainitussa järjestyksessä (eli ensin kaikkien summan laskeva olio, toisena parillisten summan laskeva ja viimeisenä parittomien summan laskeva olio)!

Ohjelman tulee toimia seuraavasti:

Anna lukuja:
4
2
5
2
-1
Summa: 13
Parillisten summa: 8
Parittomien summa: 5

Satunnaisuus

Ohjelmoidessa satunnaisuutta tarvitaan silloin tällöin. Satunnaisuutta -- esimerkiksi sään arvaamattomuutta tai tietokoneen tietokonepeleissä tekemien yllättäviä siirtoja -- voidaan useimmiten simuloida tietokoneelta pyydettävien satunnaislukujen avulla. Satunnaislukujen pyytäminen onnistuu käyttäen Javasta löytyvää Random-luokkaa. Random-luokkaa voi käyttää seuraavalla tavalla.

import java.util.Random;

public class Arvontaa {
    public static void main(String[] args) {
        Random arpoja = new Random(); // luodaan arpoja apuväline
        int i = 0;

        while (i < 10) {
            // Arvotaan ja tulostetaan jokaisella kierroksella satunnainen luku
            System.out.println(arpoja.nextInt(10)); 
            i++;
        }
    }
}

Yllä olevassa koodissa luodaan ensin Random-luokan ilmentymä käyttäen avainsanaa new -- samoin kuin muitakin olioita luodessa. Random-olio tarjoaa metodin nextInt, jolle annetaan parametrina kokonaisluku. Metodi palauttaa satunnaisen kokonaisluvun väliltä 0..(annettu kokonaisluku - 1).

Ohjelman tulostus voisi olla vaikka seuraavanlainen:

2
2
4
3
4
5
6
0
7
8

Tarvitsemme liukulukuja esimerkiksi todennäköisyyslaskennan yhteydessä. Tietokoneella todennäköisyyksiä simuloidaan yleensä väliltä [0..1] olevilla luvuilla. Random-oliolta saa satunnaisia liukulukuja metodilla nextDouble. Tarkastellaan seuraavia säämahdollisuuksia:

Luodaan edellä olevista arvioista sääennustaja.

import java.util.ArrayList;
import java.util.Random;

public class SaaEnnustaja {
    private Random random;

    public SaaEnnustaja() {
        this.random = new Random();
    } 

    public String ennustaSaa() {
        double todennakoisyys = this.random.nextDouble();

        if (todennakoisyys <= 0.1) {
            return "Sataa räntää";
        } else if (todennakoisyys <= 0.4) { // 0.1 + 0.3
            return "Sataa lunta";
        } else { // loput, 1.0 - 0.4 = 0.6
            return "Aurinko paistaa";
        } 
    }

    public int ennustaLampotila() {
        return (int) ( 4 * this.random.nextGaussian() - 3 ); 
    }
}

Metodi ennustaLampotila on monella tapaa mielenkiintoinen. Metodin sisällä tehtävä kutsu this.random.nextGaussian() on tavallinen metodikutsu, jonka kaltaisia olemme nähneet aikaisemminkin. Kiinnostavaa tässä Random-luokan ilmentymän tarjoamassa metodissa on se, että metodin palauttama luku on normaalijakautunut (jos et koe mielenkiintoa satunnaisuuden eri lajeihin se ei haittaa!).

    public int ennustaLampotila() {
        return (int) ( 4 * this.random.nextGaussian() - 3 ); 
    }

Edellisessä lausekkeessa kiinnostava on myös osa (int). Tämä kohta lausekkeessa muuttaa suluissa olevan liukuluvun kokonaisluvuksi. Vastaavalla menetelmällä voidaan muuttaa myös kokonaislukuja liukuluvuiksi kirjoittamalla (double) kokonaisluku. Tätä kutsutaan eksplisiittiseksi tyyppimuunnokseksi.

Luodaan vielä pääohjelma josta luokkaa SaaEnnustaja käytetään.

public class Ohjelma {

    public static void main(String[] args) {
        SaaEnnustaja ennustaja = new SaaEnnustaja();
        
        // käytetään listaa apuvälineenä
        ArrayList<String> paivat = new ArrayList<String>();
        Collections.addAll(paivat, "Ma", "Ti", "Ke", "To", "Pe", "La", "Su");

        System.out.println("Seuraavan viikon sääennuste:");
        for(String paiva : paivat) {
            String saaEnnuste = ennustaja.ennustaSaa();
            int lampotilaEnnuste = ennustaja.ennustaLampotila(); 

            System.out.println(paiva + ": " + saaEnnuste + " " + lampotilaEnnuste + " astetta.");
        }
    }
}

Ohjelman tulostus voisi olla esimerkiksi seuraavanlainen:

Seuraavan viikon sääennuste:
Ma: Sataa lunta 1 astetta.
Ti: Sataa lunta 1 astetta.
Ke: Aurinko paistaa -2 astetta.
To: Aurinko paistaa 0 astetta.
Pe: Sataa lunta -3 astetta.
La: Sataa lunta -3 astetta.
Su: Aurinko paistaa -5 astetta

Nopan heittäminen

Tehtäväpohjassa on luokka Noppa, jolla on seuraavat toiminnot:

Luokan runko on seuraava:

import java.util.Random;

public class Noppa {
    private Random random = new Random();
    private int tahkojenMaara;

    public Noppa(int tahkojenMaara) {
        // Alusta muuttuja tahkojenMaara tässä
    }

    public int heita() {
        // arvotaan luku väliltä 1-tahkojenMaara
    }
}

Täydennä luokkaa Noppa siten, että noppa palauttaa jokaisella heitolla arvotun luvun väliltä 1...tahkojen määrä. Seuraavassa noppaa testaava pääohjelma:

public class Ohjelma {
    public static void main(String[] args) {
        Noppa noppa = new Noppa(6);
        
        int i = 0;
        while ( i < 10 ) {
            System.out.println( noppa.heita() );
            i++;
        }
    }
}

Tulostus voisi olla esimerkiksi seuraava:

1
6
3
5
3
3
2
2
6
1

Salasanan arpoja

Tehtävänäsi on täydentää luokkaa SalasananArpoja, jossa on seuraavat toiminnot:

Luokan runko on seuraava:

import java.util.Random;

public class SalasananArpoja {
    // Määrittele muuttuja tässä

    public SalasananArpoja(int pituus) {
        // Alusta muuttuja tässä
    }

    public String luoSalasana() {
        // Kirjoita tähän koodi, joka palauttaa uuden salasanan
    }
}

Seuraavassa ohjelma joka käyttää SalasananArpoja-olioa:

public class Ohjelma {
    public static void main(String[] args) {
        SalasananArpoja arpoja = new SalasananArpoja(13);
        System.out.println("Salasana: " + arpoja.luoSalasana());
        System.out.println("Salasana: " + arpoja.luoSalasana());
        System.out.println("Salasana: " + arpoja.luoSalasana());
        System.out.println("Salasana: " + arpoja.luoSalasana());
    }
}

Ohjelman tulostus voisi näyttää seuraavalta:

Salasana: mcllsoompezvs
Salasana: urcxboisknkme
Salasana: dzaccatonjcqu
Salasana: bpqmedlbqaopq

Vihje1: näin muutat kokonaisluvun luku kirjaimeksi:

int luku = 17;
char merkki = "abcdefghijklmnopqrstuvxyz".charAt(luku);

Vihje2: tehtävän 58 vihjeestä lienee tässäkin tehtävässä apua.

Lottoarvonta

Tehtävänäsi on täydentää luokkaa LottoRivi, joka arpoo viikon lottonumerot. Lottonumerot ovat väliltä 1–39 ja niitä arvotaan 7. Lottorivi koostuu siis 7:stä eri numerosta väliltä 1–39. Luokassa on seuraavat toiminnot:

Luokan runko on seuraava:

import java.util.ArrayList;
import java.util.Random;

public class LottoRivi {
    private ArrayList<Integer> numerot;

    public LottoRivi() {
        // Alustetaan lista numeroille
        this.numerot = new ArrayList<Integer>();
        // Arvo numerot heti LottoRivin luomisen yhteydessä
        this.arvoNumerot();
    }

    public ArrayList<Integer> numerot() {
        return this.numerot;
    }

    public void arvoNumerot() {
        // Kirjoita numeroiden arvonta tänne käyttämällä metodia sisaltaaNumeron()
    }

    public boolean sisaltaaNumeron(int numero) {
        // Testaa tässä onko numero jo arvottujen numeroiden joukossa
    }
}

Tehtäväpohjan mukana tulee seuraava pääohjelma:

import java.util.ArrayList;

public class Ohjelma {
    public static void main(String[] args) {
        LottoRivi lottoRivi = new LottoRivi();
        ArrayList<Integer> lottonumerot = lottoRivi.numerot();

        System.out.println("Lottonumerot:");
        for (int numero : lottonumerot) {
            System.out.print(numero + " ");
        }
        System.out.println("");
    }
}

Ohjelman mahdollisia tulostuksia ovat seuraavat:

Lottonumerot:
3 5 10 14 15 27 37
Lottonumerot:
2 9 11 18 23 32 34

Huom! Sama numero saa esiintyä lottorivissä vain kerran.

Viikon huipennus: hirsipuun logiikka

Hirsipuu-pelistä pitävä kaverisi ohjelmoi hirsipuupelin joka näyttää seuraavalta.

Hänen koneestaan hajosi kovalevy, eikä hän ollut tehnyt varmuuskopiota. Hän on kuitenkin luvannut pelin lahjaksi pikkuveljelleen, ja aikataulukiireidensä takia tarvitsee apuasi pelin toteutukseen. Tässä tehtäväsarjassa toteutat kaverisi hirsipuu-pelille arvauslogiikan ja annetun sanan salaamisen.

Kaverisi on luonut pelille käyttöliittymän sekä valmiin rungon sovelluksen logiikkaan. Kun lataat projektin TMC:ltä, näet käyttöliittymä- ja testiluokkien lisäksi allaolevan ohjelmarungon.

public class HirsipuuLogiikka {

    private String sana;
    private String arvatutKirjaimet;
    private int virheidenLukumaara;

    public HirsipuuLogiikka(String sana) {
        this.sana = sana.toUpperCase();
        this.arvatutKirjaimet = "";
        this.virheidenLukumaara = 0;
    }

    public int virheidenLukumaara() {
        return this.virheidenLukumaara;
    }

    public String arvatutKirjaimet() {
        return this.arvatutKirjaimet;
    }
    
    public int virheitaHavioon() {
        return 12;
    }

    public void arvaaKirjain(String kirjain) {
        // Ohjelmoi tänne toiminnallisuus kirjaimen arvaamiseksi.
        
        // Arvattua kirjainta ei saa arvata uudestaan. Jos arvattava sana 
        // ei sisällä kirjainta, virheiden lukumäärän tulee kasvaa.
        // Lisää arvattu kirjain arvattuihin kirjaimiin

    }

    public String salattuSana() {
        // Ohjelmoi tänne toiminnallisuus salatun sanan luomiseksi
        // ja antamiseksi. 
        
        // Luo uusi salattu sana this.sana-merkkijonoa kirjain kirjaimelta 
        // läpikäyden. Jos kirjain on arvatuissa kirjaimissa, voit lisätä sen 
        // sellaisenaan. Jos ei, lisää uuteen sanaan merkki "_".
        
        // Muista varmistaa ettet ohjelmoi päättymätöntä toistolauseketta!
        
        // palauta lopuksi salattua sanaa kuvaava merkkijono.
        
        return "";
    }
}

Tässä tehtävässä sinun tulee lisätä toiminnallisuutta vain luokan HirsipuuLogiikka metodeihin arvaaKirjain(String kirjain) ja salattuSana().

Logiikan testaaminen

Tehtäväpohja sisältää kaksi testiluokkaa. Luokka Main käynnistää kaverisi toteuttaman graafisen version pelistä. Luokkaa Testiohjelma taas voit käyttää HirsipuuLogiikka-luokan testaamiseen.

Kirjaimen arvaaminen

Muokkaa tässä tehtävässä vain metodin arvaaKirjain(String kirjain) toiminnallisuutta.

Kun käyttäjä arvaa jonkin kirjaimen, kutsuu hirsipuun käyttöliittymä hirsipuulogiikan metodia arvaaKirjain jonka tulee tarkistaa onko arvattava kirjain jo arvattujen kirjainten (joita hirsipuulogiikka säilyttää oliomuuttujassa this.arvatutKirjaimet) joukossa. Jos kirjain on jo arvattujen kirjainten joukossa, metodin ei tule tehdä mitään. Jos arvattavaa kirjainta ei ole arvattu aiemmin, on tarkastettava onko se arvattavassa sanassa (jota hirsipuulogiikka säilyttää oliomuuttujassa this.sana). Jos ei, virheiden lukumäärää (oliomuuttuja this.virheidenLukumaara) pitää kasvattaa yhdellä. Arvattu kirjain pitää lopuksi lopuksi osaksi arvattuja kirjaimia, jos se ei ole jo siellä.

Esimerkki arvaaKirjain-metodin toiminnasta:

  HirsipuuLogiikka l = new HirsipuuLogiikka("kissa");
  System.out.println("Arvataan: A, D, S, F, D");
  l.arvaaKirjain("A");   // oikea
  l.arvaaKirjain("D");   // väärä
  l.arvaaKirjain("S");   // oikea
  l.arvaaKirjain("F");   // väärä
  l.arvaaKirjain("D");   // Tämän ei pitäisi kasvattaa virheiden lukumäärää koska D oli jo arvattu
  System.out.println("Arvatut kirjaimet: "+l.arvatutKirjaimet());
  System.out.println("Virheiden lukumäärä: "+l.virheidenLukumaara());
Arvataan: A, D, S, F, D
Arvatut kirjaimet: ADSF
Virheiden lukumäärä: 2

Salatun sanan luominen

Hirsipuun käyttöliittymä näyttää käyttäjälle salatun muodon arvattavana olevasta sanasta. Yllä olevassa kuvassa salattu sana on METO_I, eli arvattavana olevasta sanasta näytetään vain arvatut kirjaimet, muut korvataan alaviivalla. Salatun sanan muodostaa hirsipuulogiikan metodi salattuSana() jonka sisältöä tässä tehtävässä on tarkoitus muokata.

Salattu sana luodaan luomalla arvattavasta sanasta (this.sana) uusi versio. Jokainen kirjain jota ei ole vielä arvattu tulee vaihtaa merkkiin "_". Jos kirjain on jo arvattu, eli se löytyy arvatuista kirjaimista, voidaan se lisätä sellaisenaan salattuun sanaan.

Avainsanoista while, charAt ja contains lienee tästä hyötyä. Saat muutettua yksittäisen merkin merkkijonoksi seuraavasti:

char merkki = 'a';
String merkkijono = "" + merkki;

Esimerkki metodin toiminnasta:

HirsipuuLogiikka l = new HirsipuuLogiikka("kissa");
System.out.println("Sana on: "+l.salattuSana());

System.out.println("Arvataan: A, D, S, F, D");
l.arvaaKirjain("A");
l.arvaaKirjain("D");
l.arvaaKirjain("S");
l.arvaaKirjain("F");
l.arvaaKirjain("D");
System.out.println("Arvatut kirjaimet: "+l.arvatutKirjaimet());
System.out.println("Virheiden lukumäärä: "+l.virheidenLukumaara());
System.out.println("Sana on: "+l.salattuSana());
Sana on: _____
Arvataan: A, D, S, F, D
Arvatut kirjaimet: ADSF
Virheiden lukumäärä: 2
Sana on: __SSA

Testaa lopuksi sinun ja kaverisi yhteistyössä tekemää ohjelmaa Main-luokan avulla! Voit muuttaa arvattavaa sanaa muuttamalla HirsipuuLogiikka-luokan konstruktorille parametrina annettavaa merkkijonoa:

        HirsipuuLogiikka logiikka = new HirsipuuLogiikka("parametri");
        HirsipuuIkkuna peliIkkuna = new HirsipuuIkkuna(logiikka);
        peliIkkuna.pelaa();

Peliä pelataan näppäimistöltä, pelin voi lopettaa painamalla peli-ikkunan vasemmassa yläkulmassa olevaa x-merkkiä.

Viikko 5

Lisää luokista ja olioista

Useita konstruktoreja

Palataan jälleen henkilöitä käsittelevän luokan pariin. Luokka Henkilo näyttää tällä hetkellä seuraavalta:

public class Henkilo {

    private String nimi;
    private int ika;
    private int pituus;
    private int paino;

     public Henkilo(String nimi) {
        this.nimi = nimi;
        this.ika = 0;
        this.paino = 0;
        this.pituus = 0;
    }

    public void tulostaHenkilo() {
        System.out.println(this.nimi + " olen " + this.ika + " vuotta vanha");
    }

    public void vanhene() {
        this.ika++;
    }

    public boolean taysiIkainen(){
        if ( this.ika < 18 ) {
            return false;
        }

        return true;
    }
    
    public double painoindeksi(){
        double pituusMetreina = this.pituus/100.0;
        
        return this.paino / (pituusMetreina*pituusMetreina);
    }    

    public String toString(){ 
        return this.nimi + " olen " + this.ika + " vuotta vanha, painoindeksini on " + this.painoindeksi();
    } 

    public void setPituus(int pituus){
        this.pituus = pituus;
    }
    
    public int getPituus(){
        return this.pituus;
    }

    public int getPaino() {
        return this.paino;
    }

    public void setPaino(int paino) {
        this.paino = paino;
    }   
    
    public String getNimi(){
        return this.nimi;
    }   
}

Kaikki henkilöoliot ovat luontihetkellä 0-vuotiaita, sillä konstruktori asettaa uuden henkilön ika-oliomuuttujan arvoksi 0:

    public Henkilo(String nimi) {
        this.nimi = nimi;
        this.ika = 0;
        this.paino = 0;
        this.pituus = 0;
    }

Haluaisimme myös luoda henkilöitä siten, että nimen lisäksi konstruktorin parametrina annettaisiin ikä. Tämä onnistuu helposti, sillä konstruktoreja voi olla useita. Tehdään vaihtoehtoinen konstruktori. Vanhaa konstruktoria ei tarvise poistaa.

    public Henkilo(String nimi) {
        this.nimi = nimi;
        this.ika = 0;
        this.paino = 0;
        this.pituus = 0;
    }

    public Henkilo(String nimi, int ika) {
        this.nimi = nimi;
        this.ika = ika;
        this.paino = 0;
        this.pituus = 0;
    }

Nyt olioiden luonti onnistuu kahdella vaihtoehtoisella tavalla:

    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka", 24);
        Henkilo esko = new Henkilo("Esko");

        System.out.println( pekka );
        System.out.println( esko );
    }
Pekka, ikä 24 vuotta
Esko, ikä 0 vuotta

Tekniikkaa jossa luokalla on kaksi konstruktoria, kutsutaan konstruktorin kuormittamiseksi. Luokalla voi siis olla useita konstruktorjea, jotka poikkeavat toisistaanparametriensa määrältä tai tyypeiltä. Ei kuitenkaan ole mahdollista tehdä kahta erilaista konstruktoria joilla on täysin saman tyyppiset parametrit. Emme siis voi edellisten lisäksi lisätä konstruktoria public Henkilo(String nimi, int paino) sillä Javan on mahdoton erottaa tätä kaksiparametrisesta konstruktorissa, jossa luku tarkoittaa ikää.

Oman konstruktorin kutsuminen

Mutta hetkinen, luvussa 21 todettiin että "copy-paste"-koodi ei ole hyvä idea. Kun tarkastellaan edellä tehtyjä kuormitettuja konstruktoreita, niissä on aika paljon samaa. Emme ole oikein tyytyväisiä tilanteeseen.

Konstruktoreista ylempi on oikeastaan alemman erikoistapaus. Entä jos ylempi konstruktori voisi "kutsua" alempaa konstruktoria? Tämä onnistuu, sillä konstruktorin sisältä voi kutsua toista konstruktoria juuri tähän olioon liittyvän this-ilmauksen avulla!

Muutetaan ylempää konstruktoria siten, että se ei itse tee mitään vaan ainoastaan kutsuu alempaa konstruktoria ja pyytää sitä asettamaan iäksi 0:

    public Henkilo(String nimi) {
        this(nimi, 0);  // suorita tässä toisen konstruktorin koodi ja laita ika-parametrin arvoksi 0
    }

    public Henkilo(String nimi, int ika) {
        this.nimi = nimi;
        this.ika = ika;
        this.paino = 0;
        this.pituus = 0;
    }

Oman konstruktorin kutsu this(nimi, 0); saattaa vaikuttaa erikoiselta. Asiaa voi vaikka ajatella siten, että kutsun kohdalle tulee "copy-pastena" automaattisesti alemman konstruktorin koodi, siten että ika parametrin arvoksi tulee 0.

Metodin kuormittaminen

Konstruktorien tapaan myös metodeja voi kuormittaa, eli samannimisestä metodista voi olla useita versioita. Jälleen eri versioiden parametrien tyyppien on oltava erilaiset. Tehdään vanhene-metodista toinen versio, joka mahdollistaa henkilön vanhentamisen parametrina olevalla vuosimäärällä:

    public void vanhene() {
        this.ika = this.ika + 1;
    }
    
    public void vanhene(int vuodet) {
        this.ika = this.ika + vuodet;
    }

Seuraavassa "Pekka" syntyy 24-vuotiaana, vanhenee ensin vuoden ja sitten 10 vuotta:

    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka", 24);

        System.out.println( pekka );
        pekka.vanhene();
        System.out.println( pekka );
        pekka.vanhene(10);
        System.out.println( pekka );

Tulostuu:

Pekka, ikä 24 vuotta
Pekka, ikä 25 vuotta
Pekka, ikä 35 vuotta

Henkilöllä on siis vanhene-nimisiä metodeja 2 kappaletta. Se kumpi metodeista valitaan suoritettavaksi, riippuu metodikutsussa käytettyjen parametrien määrästä. Metodin vanhene voi myös toteuttaa metodin vanhene(int vuodet) avulla:

    public void vanhene() {
        this.vanhene(1);
    }
    
    public void vanhene(int vuodet) {
        this.ika = this.ika + vuodet;
    }

Kuormitettu laskuri

Monta konstruktoria

Toteuta luokka Laskuri, joka sisältää luvun, jota voi vähentää ja suurentaa. Laskurissa on lisäksi valinnainen tarkistus joka estää laskurin menemisen miinukselle. Luokalla tulee olla seuraavat konstruktorit:

ja seuraavat metodit:

Vaihtoehtoiset metodit

Tee laskurin metodeista lisaa ja vahenna myös yksiparametriset versiot:

Olio on langan päässä

Luvussa 20 mainittiin, että ArrayList on "langan päässä". Myös oliot ovat langan päässä. Mitä tämä oikein tarkoittaa? Tarkastellaan seuraavaa esimerkkiä:

    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka", 24);

        System.out.println( pekka );
    }

Kun suoritamme lauseen Henkilo pekka = new Henkilo("Pekka", 24); syntyy olio. Olioon päästään käsiksi muuttujan pekka avulla. Teknisesti ottaen olio ei ole muuttujan pekka "sisällä" (eli lokerossa pekka) vaan pekka viittaa syntyneeseen olioon. Toisin sanonen olio on pekka-nimisestä muuttujasta lähtevän "langan päässä". Kuvana asiaa voisi havainnollistaa seuraavasti:

Lisätään ohjelmaan Henkilo-tyyppinen muuttuja henkilo ja annetaan sille alkuarvoksi pekka. Mitä nyt tapahtuu?

    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka", 24);

        System.out.println( pekka );

        Henkilo henkilo = pekka;
        henkilo.vanhene(25);

        System.out.println( pekka );
    }

Tulostuu:

Pekka, ikä 24 vuotta
Pekka, ikä 49 vuotta

Eli Pekka on alussa 24-vuotias, henkilo-muuttujaan liittyvän langan päässä olevaa Henkilö-oliota vanhennetaan 25:llä vuodella ja sen seurauksena Pekka vanhenee! Mistä on kysymys?

Komento Henkilo henkilo = pekka; saa aikaan sen, että henkilo rupeaa viittaamaan samaan olioon kuin mihin pekka viittaa. Eli ei synnykään kopiota oliosta, vaan molemmissa muuttujissa on langan päässä sama olio. Komennossa Henkilo henkilo = pekka; syntyy kopio langasta. Kuvana (Huom: kuvassa p ja h tarkottavat pääohjelman muuttujia pekka ja henkilo. Kuvien muuttujanimiä on lyhennelty myös muutamassa seuraavassa kuvassa.):

Esimerkissä "vieras henkilö henkilo ryöstää Pekan identiteetin". Seuraavassa esimerkkiä on jatkettu siten, että luodaan uusi olio ja pekka alkaa viittaamaan uuteen olioon:

    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka", 24);

        System.out.println( pekka );

        Henkilo henkilo = pekka;
        henkilo.vanhene(25);

        System.out.println( pekka );

        pekka = new Henkilo("Pekka Mikkola", 24);
        System.out.println( pekka );
    }

Tulostuu:

Pekka, ikä 24 vuotta
Pekka, ikä 49 vuotta
Pekka Mikkola, ikä 24 vuotta

Muuttuja pekka viittaa siis ensin yhteen olioon, mutta rupeaa sitten viittaamaan toiseen olion. Seuraavassa kuva tilanteesta viimeisen koodirivin jälkeen:

Jatketaan vielä esimerkkiä laittamalla henkilo viittaamaan "ei mihinkään", eli null:iin:

    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka", 24);

        System.out.println( pekka );

        Henkilo henkilo = pekka;
        henkilo.vanhene(25);

        System.out.println( pekka );

        pekka = new Henkilo("Pekka Mikkola", 24);
        System.out.println( pekka );

        henkilo = null;
        System.out.println( henkilo );
    }

Viimeisen koodirivin jälkeen näyttää seuraavalta:

Alempaan olioon ei nyt viittaa kukaan. Oliosta on tullut "roska". Javan roskienkerääjä käy siivoamassa aika ajoin roskaksi joutuneet oliot. Jos näin ei tehtäisi, jäisivät ne kuluttamaan turhaan koneen muistia ohjelman suorituksen loppuun asti.

Huomaamme, että viimeisellä rivillä yritetään vielä tulostaa "ei mitään" eli null. Käy seuraavasti:

Pekka, ikä 24 vuotta
Pekka, ikä 49 vuotta
Pekka Mikkola, ikä 24 vuotta
null

Mitä tapahtuu jos yritämme kutsua "ei minkään" metodia, esimerkiksi metodia painoIndeksi:

    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka", 24);

        System.out.println( pekka );

        Henkilo henkilo = null;
        System.out.println( henkilo.painoIndeksi() );
    }

Tulos:

Pekka, ikä 24 vuotta
Exception in thread "main" java.lang.NullPointerException
        at Main.main(Main.java:20)
Java Result: 1

Eli käy huonosti. Tämän on ehkä ensimmäinen kerta elämässäsi kun näet tekstin NullPointerException. Voimme luvata, että tulet näkemään sen vielä uudelleen. NullPointerException on poikkeustila, joka syntyy kun null-arvoisen olion metodeja yritetään kutsua.

Metodin parametrina olio

Olemme nähneet että metodien parametrina voi olla esim. int, double, String tai ArrayList. ArrayListit ja merkkijonot ovat olioita, joten kuten arvata saattaa, metodi voi saada parametriksi minkä tahansa tyyppisen olion. Demonstroidaan tätä esimerkillä.

Painonvartijoihin hyväksytään jäseniksi henkilöitä, joiden painoindeksi ylittää jonkun annetun rajan. Kaikissa painonvartijayhdistyksissä raja ei ole sama. Tehdään painonvartijayhdistystä vastaava luokka. Olioa luotaessa konstruktorille annetaan parametriksi pienin painoindeksi, jolla yhdistyksen jäseneksi pääsee.

public class PainonvartijaYhdistys {
    private double alinPainoindeksi;

    public PainonvartijaYhdistys(double indeksiRaja) {
        this.alinPainoindeksi = indeksiRaja;
    }
    
}

Tehdään sitten metodi, jonka avulla voidaan tarkastaa hyväksytäänkö tietty henkilö yhdistyksen jäseneksi, eli onko henkilön painoindeksi tarpeeksi suuri. Metodi palauttaa true jos parametrina annettu henkilö hyväksytään, false jos ei.

public class PainonvartijaYhdistys {
    // ...
    
    public boolean hyvaksytaanJaseneksi(Henkilo henkilo) {
        if ( henkilo.painoIndeksi() < this.alinPainoindeksi ) {
            return false;
        }

        return true;
    }
}

Painonvartijayhdistys-olion metodi hyvaksytaanJaseneksi saa siis parametriksi Henkilo-olion (tarkemmin sanottuna "langan" henkilöön) ja kutsuu parametrina saamansa henkilön metodia painoIndeksi.

Seuraavassa testipääohjelma jossa painonvartijayhdistyksen metodille annetaan ensin parametriksi henkilöolio matti ja sen jälkeen henkilöolio juhana:

    public static void main(String[] args) {
        Henkilo matti = new Henkilo("Matti");
        matti.setPaino(86);
        matti.setPituus(180);

        Henkilo juhana = new Henkilo("Juhana");
        juhana.setPaino(64);
        juhana.setPituus(172);

        PainonvartijaYhdistys kumpulanPaino = new PainonvartijaYhdistys(25);

        if ( kumpulanPaino.hyvaksytaanJaseneksi(matti) ) {
            System.out.println( matti.getNimi() + " pääsee jäseneksi");
        } else {
            System.out.println( matti.getNimi() + " ei pääse jäseneksi");
        }

        if ( kumpulanPaino.hyvaksytaanJaseneksi(juhana) ) {
            System.out.println( juhana.getNimi() + " pääsee jäseneksi");
        } else {
            System.out.println( juhana.getNimi() + " ei pääse jäseneksi");
        }
    }

Ohjelma tulostaa:

Matti pääsee jäseneksi
Juhana ei pääse jäseneksi
Muutama NetBeans-vihje

Kasvatuslaitos

Tehtäväpohjassasi on valmiina jo tutuksi tullut luokka Henkilo sekä runko luokalle Kasvatuslaitos. Kasvatuslaitosoliot käsittelevät ihmisiä eri tavalla, esim. punnitsevat ja syöttävät ihmisiä. Rakennamme tässä tehtävässä kasvatuslaitoksen. Luokan Henkilö koodiin ei tehtävässä ole tarkoitus koskea!

Henkilöiden punnitseminen

Kasvatuslaitoksen luokkarungossa on valmiina runko metodille punnitse:

public class Kasvatuslaitos {

    public int punnitse(Henkilo henkilo) {
       // palautetaan parametrina annetun henkilön paino
       return -1;
    }
}

Metodi saa parametrina henkilön ja metodin on tarkoitus palauttaa kutsujalleen parametrina olevan henkilön paino. Paino selviää kutsumalla parametrina olevan henkilön henkilo sopivaa metodia. Eli täydennä metodin koodi!

Seuraavassa on pääohjelma jossa kasvatuslaitos punnitsee kaksi henkilöä:

    public static void main(String[] args) {
        // esimerkkipääohjelma tehtävän ensimmäiseen kohtaan

        Kasvatuslaitos haaganNeuvola = new Kasvatuslaitos();

        Henkilo eero = new Henkilo("Eero", 1, 110, 7);
        Henkilo pekka = new Henkilo("Pekka", 33, 176, 85);

        System.out.println(eero.getNimi() + " paino: " + haaganNeuvola.punnitse(eero) + " kiloa");
        System.out.println(pekka.getNimi() + " paino: " + haaganNeuvola.punnitse(pekka) + " kiloa");
    }

Tulostuksen pitäisi olla seuraava:

Eero paino: 7 kiloa
Pekka paino: 85 kiloa

syötä

Parametrina olevan olion tilaa on mahdollista muuttaa. Tee kasvatuslaitokselle metodi public void syota(Henkilo henkilo) joka kasvattaa parametrina olevan henkilön painoa yhdellä.

Seuraavassa esimerkki, jossa henkilöt ensin punnitaan, ja tämän jälkeen neuvolassa syötetään eeroa kolme kertaa. Tämän jälkeen henkilöt taas punnitaan:

    public static void main(String[] args) {
        Kasvatuslaitos haaganNeuvola = new Kasvatuslaitos();

        Henkilo eero = new Henkilo("Eero", 1, 110, 7);
        Henkilo pekka = new Henkilo("Pekka", 33, 176, 85);

        System.out.println(eero.getNimi() + " paino: " + haaganNeuvola.punnitse(eero) + " kiloa");
        System.out.println(pekka.getNimi() + " paino: " + haaganNeuvola.punnitse(pekka) + " kiloa");

        haaganNeuvola.syota(eero);
        haaganNeuvola.syota(eero);
        haaganNeuvola.syota(eero);
        
        System.out.println("");
        
        System.out.println(eero.getNimi() + " paino: " + haaganNeuvola.punnitse(eero) + " kiloa");
        System.out.println(pekka.getNimi() + " paino: " + haaganNeuvola.punnitse(pekka) + " kiloa");
    }

Tulostuksen pitäisi paljastaa että Eeron paino on noussut kolmella:

Eero paino: 7 kiloa
Pekka paino: 85 kiloa

Eero paino: 10 kiloa
Pekka paino: 85 kiloa

laske punnitukset

Tee kasvatuslaitokselle metodi public int punnitukset() joka kertoo kuinka monta punnitusta kasvatuslaitos on ylipäätään tehnyt. Testipääohjelma:

    public static void main(String[] args) {
        // esimerkkipääohjelma tehtävän ensimmäiseen kohtaan

        Kasvatuslaitos haaganNeuvola = new Kasvatuslaitos();

        Henkilo eero = new Henkilo("Eero", 1, 110, 7);
        Henkilo pekka = new Henkilo("Pekka", 33, 176, 85);

        System.out.println("punnituksia tehty "+haaganNeuvola.punnitukset());
        
        haaganNeuvola.punnitse(eero);
        haaganNeuvola.punnitse(pekka);
        
        System.out.println("punnituksia tehty "+haaganNeuvola.punnitukset());

        haaganNeuvola.punnitse(eero);
        haaganNeuvola.punnitse(eero);
        haaganNeuvola.punnitse(eero);
        haaganNeuvola.punnitse(eero);
        
        System.out.println("punnituksia tehty "+haaganNeuvola.punnitukset());
    }

Tulostuu:

punnituksia tehty 0
punnituksia tehty 2
punnituksia tehty 6

Lyyra-kortti ja Kassapääte

"Tyhmä" Lyyra-kortti

Teimme viime viikolla luokan LyyraKortti. Kortilla oli metodit edullisesti ja maukkaasti syömistä sekä rahan lataamista varten.

Viime viikon tyylillä tehdyssä Lyyra-kortissa oli kuitenkin ongelma. Kortti tiesi lounaiden hinnan ja osasi sen ansiosta vähentää saldoa oikean määrän. Entä kun hinnat nousevat? Tai jos myyntivalikoimaan tulee uusia tuotteita? Hintojen muuttaminen tarkoittaisi, että kaikki jo käytössä olevat Lyyra-kortit pitäisi korvata uusilla, uudet hinnat tuntevilla korteilla.

Parempi ratkaisu on tehdä kortit "tyhmiksi", hinnoista ja myytävistä tuotteista tietämättömiksi pelkän saldon säilyttäjiksi. Kaikki äly kannattaakin laittaa erillisiin olioihin, kassapäätteisiin.

Toteutetaan ensin Lyyra-kortista "tyhmä" versio. Kortilla on ainoastaan metodit saldon kysymiseen, rahan lataamiseen ja rahan ottamiseen. Täydennä alla (ja tehtäväpohjassa) olevaan luokkaan metodin public boolean otaRahaa(double maara) ohjeen mukaan:

public class LyyraKortti {
    private double saldo;

    public LyyraKortti(double saldo) {
        this.saldo = saldo;
    }

    public double saldo() {
        return this.saldo;
    }

    public void lataaRahaa(double lisays) {
        this.saldo += lisays;
    }

    public boolean otaRahaa(double maara){
       // toteuta metodi siten että se ottaa kortilta rahaa vain jos saldo on vähintään maara
       // onnistuessaan metodi palauttaa true ja muuten false
    }
}

Testipääohjelma:

public class Paaohjelma {
    public static void main(String[] args) {
        LyyraKortti pekanKortti = new LyyraKortti(10);

        System.out.println("rahaa " + pekanKortti.saldo() );
        boolean onnistuiko = pekanKortti.otaRahaa(8);
        System.out.println("onnistuiko otto: " + onnistuiko );
        System.out.println("rahaa " + pekanKortti.saldo() );

        onnistuiko = pekanKortti.otaRahaa(4);
        System.out.println("onnistuiko otto: " + onnistuiko );
        System.out.println("rahaa " + pekanKortti.saldo() );
    }
}

Tulostuksen kuuluisi olla seuraavanlainen

rahaa 10.0
onnistuiko otto: true
rahaa 2.0
onnistuiko otto: false
rahaa 2.0

Kassapääte ja käteiskauppa

Unicafessa asioidessa asiakas maksaa joko käteisellä tai Lyyra-kortilla. Myyjä käyttää kassapäätettä kortin velottamiseen ja käteismaksujen hoitamiseen. Tehdään ensin kassapäätteestä käteismaksuihin sopiva versio.

Kassapäätteen runko. Metodien kommentit kertovat halutun toiminnallisuuden:

public class Kassapaate {
    private double rahaa;  // kassassa olevan käteisen määrä
    private int edulliset; // myytyjen edullisten lounaiden määrä
    private int maukkaat;  // myytyjen maukkaiden lounaiden määrä

    public Kassapaate() {
	// kassassa on aluksi 1000 euroa rahaa
    }

    public double syoEdullisesti(double maksu) {
        // edullinen lounas maksaa 2.50 euroa. 
        // kasvatetaan kassan rahamäärää edullisenlounaan hinnalla ja palautetaan vaihtorahat
        // jos parametrina annettu maksu ei ole riittävän suuri, ei lounasta myydä ja metodi palauttaa koko summan
    }

    public double syoMaukkaasti(double maksu) {
        // maukas lounas maksaa 4.00 euroa. 
        // kasvatetaan kassan rahamäärää maukkaan lounaan hinnalla ja palautetaan vaihtorahat
        // jos parametrina annettu maksu ei ole riittävän suuri, ei lounasta myydä ja metodi palauttaa koko summan
    }

    public String toString() { 
        return "kassassa rahaa "+rahaa+" edullisia lounaita myyty "+edulliset+" maukkaita lounaita myyty "+maukkaat;
    }
}

Kassapäätteessä on aluksi rahaa 1000 euroa. Toteuta ylläolevan rungon metodit ohjeen ja alla olevan pääohjelman esimerkkitulosteen mukaan toimiviksi.

public class Paaohjelma {
    public static void main(String[] args) {
        Kassapaate unicafeExactum = new Kassapaate();
        
        double vaihtorahaa = unicafeExactum.syoEdullisesti(10);
        System.out.println("vaihtorahaa jäi " + vaihtorahaa );

        vaihtorahaa = unicafeExactum.syoEdullisesti(5);
        System.out.println("vaihtorahaa jäi "  + vaihtorahaa );

        vaihtorahaa = unicafeExactum.syoMaukkaasti(4);
        System.out.println("vaihtorahaa jäi "  + vaihtorahaa );

        System.out.println( unicafeExactum );
   }
}
vaihtorahaa jäi 7.5
vaihtorahaa jäi 2.5
vaihtorahaa jäi 0.0
kassassa rahaa 1009.0 edullisia lounaita myyty 2 maukkaita lounaita myyty 1

Kortilla maksaminen

Laajennetaan kassapäätettä siten että myös kortilla voi maksaa. Teemme kassapäätteelle siis metodit joiden parametrina kassapääte saa lyyrakortin jolta se vähentää valitun lounaan hinnan. Seuraavassa uusien metodien rungot ja ohje niiden toteuttamiseksi:

public class Kassapaate {    
    // ...

    public boolean syoEdullisesti(LyyraKortti kortti) {
        // edullinen lounas maksaa 2.50 euroa. 
        // jos kortilla on tarpeeksi rahaa, vähennetään hinta kortilta ja palautetaan true
        // muuten palautetaan false
    }

    public boolean syoMaukkaasti(LyyraKortti kortti) {
        // edullinen lounas maksaa 4.00 euroa.
        // jos kortilla on tarpeeksi rahaa, vähennetään hinta kortilta ja palautetaan true
        // muuten palautetaan false
    }

    // ...
}

Huom: kortilla maksaminen ei lisää kassapäätteessä olevan käteisen määrää.

Seuraavassa testipääohjelma ja haluttu tulostus:

public class Paaohjelma {
    public static void main(String[] args) {
        Kassapaate unicafeExactum = new Kassapaate();

        double vaihtorahaa = unicafeExactum.syoEdullisesti(10);
        System.out.println("vaihtorahaa jäi " + vaihtorahaa );

        LyyraKortti antinKortti = new LyyraKortti(7);      

        boolean onnistuiko = unicafeExactum.syoMaukkaasti(antinKortti);
        System.out.println("riittikö raha: " + onnistuiko);
        onnistuiko = unicafeExactum.syoMaukkaasti(antinKortti);
        System.out.println("riittikö raha: " + onnistuiko);
        onnistuiko = unicafeExactum.syoEdullisesti(antinKortti);
        System.out.println("riittikö raha: " + onnistuiko);

        System.out.println( unicafeExactum );
    }
}
vaihtorahaa jäi 7.5
riittikö raha: true
riittikö raha: false
riittikö raha: true
kassassa rahaa 1002.5 edullisia lounaita myyty 2 maukkaita lounaita myyty 1

Rahan lataaminen

Lisätään vielä kassapäätteelle metodi jonka avulla kortille voidaan ladata lisää rahaa. Muista, että rahan lataamisen yhteydessä ladattava summa viedään kassapäätteeseen. Metodin runko:

    public void lataaRahaaKortille(LyyraKortti kortti, double summa) {
       // ...
    }   

Testipääohjelma ja esimerkkisyöte:

public class Paaohjelma {
    public static void main(String[] args) {
        Kassapaate unicafeExactum = new Kassapaate();
        System.out.println( unicafeExactum );

        LyyraKortti antinKortti = new LyyraKortti(2);

        System.out.println("kortilla rahaa " + antinKortti.saldo() + " euroa");

        boolean onnistuiko = unicafeExactum.syoMaukkaasti(antinKortti);
        System.out.println("riittikö raha: " + onnistuiko);

        unicafeExactum.lataaRahaaKortille(antinKortti, 100);

        onnistuiko = unicafeExactum.syoMaukkaasti(antinKortti);
        System.out.println("riittikö raha: " + onnistuiko);

        System.out.println("kortilla rahaa " + antinKortti.saldo() + " euroa");

        System.out.println( unicafeExactum );
    }
}
kassassa rahaa 1000.0 edullisia lounaita myyty 0 maukkaita lounaita myyty 0
kortilla rahaa 2.0 euroa
riittikö raha: false
riittikö raha: true
kortilla rahaa 98.0 euroa
kassassa rahaa 1100.0 edullisia lounaita myyty 0 maukkaita lounaita myyty 1

Metodin parametrina toinen samantyyppinen olio

Jatkamme edelleen luokan Henkilo parissa. Kuten muistamme, henkilöt tietävät ikänsä:

public class Henkilo {

    private String nimi;
    private int ika;
    private int pituus;
    private int paino;

    // ...
}

Haluamme vertailla kahden henkilön ikää. Vertailu voidaan hoitaa usealla tavalla. Henkilölle voitaisiin määritellä getterimetodi getIka ja kahden henkilön iän vertailu tapauhtuisi tällöin seuraavasti:

   Henkilo pekka = new Henkilo("Pekka");
   Henkilo juhana = new Henkilo("Juhana") 
 
   if ( pekka.getIka() > juhana.getIka() ) {
       System.out.println( pekka.getNimi() + " on vanhempi kuin " + juhana.getNimi() );
   }

Opettelemme kuitenkin nyt hieman "oliohenkisemmän" tavan kahden henkilön ikävertailun tekemiseen.

Teemme Henkilö-luokalle metodin boolean vanhempiKuin(Henkilo verrattava) jonka avulla tiettyä henkilö-olioa voi verrata parametrina annettuun henkilöön iän perusteella.

Metodia on tarkoitus käyttää seuraavaan tyyliin:

    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka", 24);
        Henkilo antti = new Henkilo("Antti", 22);

        if ( pekka.vanhempiKuin(antti) ) {  //  sama kun pekka.vanhempiKuin(antti)==true
            System.out.println( pekka.getNimi() + " on vanhempi kuin " + antti.getNimi() );
        } else {
            System.out.println( pekka.getNimi() + " ei ole vanhempi kuin " + antti.getNimi() );
        }

Tässä siis kysytään Pekalta onko hän Anttia vanhempi, Pekka vastaa true jos on ja false muuten. Käytännössä kutsutaan "Pekkaa" vastaavan olion johon pekka viittaa metodia vanhempiKuin, jolle annetaan parametriksi "Anttia" vastaavan olion viite antti.

Ohjelman tulostaa:

Pekka on vanhempi kuin Antti

Metodi saa parametrikseen henkilöolion (tarkemmin sanottuna viitteen henkilöolioon, eli "langan päässä" olevan henkilön) ja vertaa omaa ikäänsä this.ika verrattavaksi annetun henkilön ikään verrattava.ika. Toteutus näyttää seuraavalta:

public class Henkilo {
    // ... 

    public boolean vanhempiKuin(Henkilo verrattava) {
        if ( this.ika > verrattava.ika ) {
            return true;
        }

        return false;
    }
}

Vaikka ika onkin olion yksityinen (private) oliomuuttuja, pystymme lukemaan muuttujan arvon kirjoittamalla verrattava.ika. Tämä johtuu siitä, että private-muuttujat ovat luettavissa kaikissa metodeissa, jotka kyseinen luokka sisältää. Huomaa, että syntaksi (kirjoitusasu) vastaa tässä jonkin olion metodin kutsumista. Toisin kuin metodia kutsuttaessa, viittaamme olion kenttään, jolloin metodikutsun osoittavia sulkeita ei kirjoiteta.

Päiväys oliona

Toinen esimerkki samasta teemasta. Tehdään luokka, jonka avulla voidaan esittää päiväyksiä.

Olion sisällä päiväys esitetään kolmella oliomuuttujalla. Tehdään myös metodi jolla voi vertailla onko päivämäärä aiemmin kuin parametrina annettu päivämäärä:

public class Paivays {
    private int paiva;
    private int kuukausi;
    private int vuosi;

    public Paivays(int paiva, int kuukausi, int vuosi) {
        this.paiva = paiva;
        this.kuukausi = kuukausi;
        this.vuosi = vuosi;
    }

    public String toString() {
        return this.paiva + "." + this.kuukausi + "." + this.vuosi;
    }

    public boolean aiemmin(Paivays verrattava) {
        // ensin verrataan vuosia
        if ( this.vuosi < verrattava.vuosi ) {
            return true;
        }

        // jos vuodet ovat samat, verrataan kuukausia
        if ( this.vuosi == verrattava.vuosi && this.kuukausi < verrattava.kuukausi ) {
            return true;
        }

        // vuodet ja kuukaudet samoja, verrataan päivää
        if ( this.vuosi == verrattava.vuosi && this.kuukausi == verrattava.kuukausi &&
                this.paiva < verrattava.paiva ) {
            return true;
        }

        return false;
    }
}

Käyttöesimerkki:

    public static void main(String[] args) {
        Paivays p1 = new Paivays(14, 2, 2011);
        Paivays p2 = new Paivays(21, 2, 2011);
        Paivays p3 = new Paivays(1, 3, 2011);
        Paivays p4 = new Paivays(31, 12, 2010);

        System.out.println( p1 + " aiemmin kuin " + p2 + ": " + p1.aiemmin(p2));
        System.out.println( p2 + " aiemmin kuin " + p1 + ": " + p2.aiemmin(p1));

        System.out.println( p2 + " aiemmin kuin " + p3 + ": " + p2.aiemmin(p3));
        System.out.println( p3 + " aiemmin kuin " + p2 + ": " + p3.aiemmin(p2));

        System.out.println( p4 + " aiemmin kuin " + p1 + ": " + p4.aiemmin(p1));
        System.out.println( p1 + " aiemmin kuin " + p4 + ": " + p1.aiemmin(p4));
}
14.2.2011 aiemmin kuin 21.2.2011: true
21.2.2011 aiemmin kuin 14.2.2011: false
21.2.2011 aiemmin kuin 1.3.2011: true
1.3.2011 aiemmin kuin 21.2.2011: false
31.12.2010 aiemmin kuin 14.2.2011: true
14.2.2011 aiemmin kuin 31.12.2010: false

Asuntovertailu

Asuntovälitystoimiston tietojärjestelmässä myynnissä olevaa asuntoa kuvataan seuraavan luokan olioilla:

public class Asunto {
  private int huoneita;
  private int nelioita;
  private int neliohinta;

  public Asunto(int huoneita, int nelioita, int neliohinta){
    this.huoneita = huoneita;
    this.nelioita = nelioita;
    this.neliohinta = neliohinta;
  }
}

Tehtävänä on toteuttaa muutama metodi, joiden avulla myynnissä olevia asuntoja voidaan vertailla.

Suurempi

Tee metodi public boolean suurempi(Asunto verrattava) joka palauttaa true jos asunto-olio, jolle metodia kutsutaan on suurempi kuin verrattavana oleva asunto-olio.

Esimerkki metodin toiminnasta:

  Asunto eiraYksio = new Asunto(1, 16, 5500);
  Asunto kallioKaksio = new Asunto(2, 38, 4200);
  Asunto jakomakiKolmio = new Asunto(3, 78, 2500);

  System.out.println( eiraYksio.suurempi(kallioKaksio) );       // false
  System.out.println( jakomakiKolmio.suurempi(kallioKaksio) );  // true

Hintaero

Tee metodi public int hintaero(Asunto verrattava) joka palauttaa asunto-olion jolle metodia kutsuttiin ja parametrina olevan asunto-olion hintaeron. Hintaero on asuntojen hintojen (=neliöhinta*neliöt) itseisarvo.

Esimerkki metodin toiminnasta:

  Asunto eiraYksio = new Asunto(1, 16, 5500);
  Asunto kallioKaksio = new Asunto(2, 38, 4200);
  Asunto jakomakiKolmio = new Asunto(3, 78, 2500);

  System.out.println( eiraYksio.hintaero(kallioKaksio) );        // 71600
  System.out.println( jakomakiKolmio.hintaero(kallioKaksio) );   // 35400

Kalliimpi

Tee metodi public boolean kalliimpi(Asunto verrattava) joka palauttaa true jos asunto-olio, jolle metodia kutsutaan on kalliimpi kuin verrattavana oleva asunto-olio.

Esimerkki metodin toiminnasta:

  Asunto eiraYksio = new Asunto(1, 16, 5500);
  Asunto kallioKaksio = new Asunto(2, 38, 4200);
  Asunto jakomakiKolmio = new Asunto(3, 78, 2500);

  System.out.println( eiraYksio.kalliimpi(kallioKaksio) );       // false
  System.out.println( jakomakiKolmio.kalliimpi(kallioKaksio) );   // true

Olioita listalla

Olemme käyttäneet ArrayList:ejä jo monessa esimerkissä ja tehtävässä. ArrayList-olioon pystyy lisäämään esimerkiksi merkkijonoja ja listalla olevien merkkijonojen läpikäynti, etsiminen, poistaminen, järjestäminen ym. ovat vaivattomia toimenpiteitä.

ArrayList:eihin voidaan laittaa minkä tahansa tyyppisiä oliota. Luodaan seuraavassa henkilölista, eli tyyppiä ArrayList<Henkilo> oleva ArrayList ja laitetaan sinne muutama henkilöolio:

    public static void main(String[] args) {
        ArrayList<Henkilo> opettajat = new ArrayList<Henkilo>();

        // voidaan ensin ottaa henkilö muuttujaan
        Henkilo opettaja = new Henkilo("Juhana");
        // ja lisätä se sitten listalle
        opettajat.add(opettaja);

        // tai voidaan myös luoda olio lisättäessä:
        opettajat.add( new Henkilo("Matti") );
        opettajat.add( new Henkilo("Martin") );

        System.out.println("opettajat vastasyntyneenä: ");
        for ( Henkilo hlo : opettajat ) {
            System.out.println( hlo );
        }

        for ( Henkilo hlo : opettajat ) {
            hlo.vanhene( 30 );
        }

        System.out.println("30 vuoden kuluttua: ");
        for ( Henkilo hlo : opettajat ) {
            System.out.println( hlo );
        }

Ohjelman tulostus:

opettajat vastasyntyneenä: 
Juhana, ikä 0 vuotta
Matti, ikä 0 vuotta
Martin, ikä 0 vuotta
30 vuoden kuluttua: 
Juhana, ikä 30 vuotta
Matti, ikä 30 vuotta
Martin, ikä 30 vuotta

Opiskelija-luokka

Opiskelija-luokka

Tee luokka Opiskelija, johon tallennetaan seuraavat tiedot opiskelijasta:

Tee luokkaan seuraavat metodit:

Voit testata luokan toimintaa seuraavalla koodilla:

public class Main {
    public static void main(String[] args) {
	Opiskelija pekka = new Opiskelija("Pekka Mikkola", "013141590");
        System.out.println("Nimi: " + pekka.haeNimi());
        System.out.println("Opiskelijanumero: " + pekka.haeOpiskelijanumero());
	System.out.println(pekka);
    }
}

Ohjelman tulostuksen tulisi olla seuraava:

Nimi: Pekka Mikkola
Opiskelijanumero: 013141590
Pekka Mikkola (013141590)

Opiskelijalista

Tee edellisen tilalle uusi pääohjelma, joka kysyy alla olevan esimerkkitulostuksen tyyliin opiskelijoiden tietoja (ensin kysytään nimi ja sen jälkeen opiskelijanumero). Ohjelma luo jokaisesta opiskelijasta uuden olion ja tallentaa sen listaan. Kun käyttäjä antaa nimeksi tyhjän merkkijonon, ohjelma tulostaa listalla olevat opiskelijat.

Listan määrittelyn tulisi olla seuraava:

ArrayList<Opiskelija> lista = new ArrayList<Opiskelija>();

Seuraavassa on esimerkki ohjelman suorituksesta:

Nimi: Alfred Apina
Opiskelijanumero: 017635727
Nimi: Bruno Banaani
Opiskelijanumero: 011288989
Nimi: Cecilia Cembalo
Opiskelijanumero: 013672548
Nimi: 

Alfred Apina (017635727)
Bruno Banaani (011288989)
Cecilia Cembalo (013672548)

Opiskelijahaku

Laajenna edellisen tehtävän opiskelijalistaa siten, että listan syöttämisen jälkeen pääohjelma kysyy hakusanan, jonka avulla voi hakea opiskelijat, joiden nimessä on annettu hakusana. Tehtävänäsi on siis toteuttaa hakutoiminto.

Vihje: Käy opiskelijat läpi silmukassa ja tarkista String-luokan contains-metodilla, onko hakusana opiskelijan nimessä.

Seuraavassa on esimerkki ohjelman suorituksesta:

Nimi: Saku Silmukka
Opiskelijanumero: 015696234
Nimi: Cecilia Cembalo
Opiskelijanumero: 013672548
Nimi: Taina Taulukko
Opiskelijanumero: 014662803
Nimi: 

Saku Silmukka (015696234)
Cecilia Cembalo (013672548)
Taina Taulukko (014662803)

Anna hakusana: ukk
Tulokset:
Saku Silmukka (015696234)
Taina Taulukko (014662803)

Olion sisällä olio

Olioiden sisällä voi olla olioita, ei pelkästään merkkijonoja vaan myös itse määriteltyjä oliota. Jatketaan taas Henkilo-luokan parissa ja lisätään henkilölle syntymäpäivä. Syntymäpäivä on luonnollista esittää aiemmin tehdyn Paivays-olion avulla:

public class Henkilo {
    private String nimi;
    private int ika;
    private int paino;
    private int pituus;
    private Paivays syntymaPaiva;

    // ...

Tehdään henkilölle uusi konstruktori, joka mahdollistaa syntymäpäivän asettamisen:

    public Henkilo(String nimi, int paiva, int kuukausi, int vuosi) {
        this.nimi = nimi;
        this.paino = 0;
        this.pituus = 0;
        this.syntymaPaiva = new Paivays(paiva, kuukausi, vuosi);
    }

Eli konstruktorin parametrina annetaan erikseen päiväyksen osat (päivä, kuukausi, vuosi), niistä luodaan päiväysolio joka sijoitetaan oliomuuttujaan syntymaPaiva.

Muokataan toString siten, että iän sijaan se näyttää syntymäpäivän:

    public String toString() {
        return this.nimi + ", syntynyt " + this.syntymaPaiva;
    }

Ja kokeillaan miten uusittu Henkilö-luokka toimii:

    public static void main(String[] args) {
        Henkilo martin = new Henkilo("Martin", 24, 4, 1983);

        Henkilo juhana = new Henkilo("Juhana", 17, 9, 1985);

        System.out.println( martin );
        System.out.println( juhana );
   }

Tulostuu:

Martin, syntynyt 24.4.1983
Juhana, syntynyt 17.9.1985

Luvussa 24.4 todettiin, että oliot ovat "langan päässä". Kertaa nyt luku 24.4.

Henkilö-oliolla on oliomuuttujat nimi joka on merkkijono-olio ja syntymaPaiva joka on Päiväys-olio. Henkilön oliomuuttujat siis ovat molemmat olioita, eli teknisesti ottaen ne eivät sijaitse henkilö-olion sisällä vaan ovat "langan päässä", ts. henkilöllä on oliomuuttujissa tallessa viite niihin. Kuvana:

Pääohjelmalla on nyt siis langan päässä kaksi Henkilö-olioa. Henkilöllä on nimi ja syntymäpäivä. Koska molemmat ovat olioita, ovat ne henkilöllä langan päässä.

Syntymäpäivä vaikuttaa hyvältä laajennukselta Henkilö-luokkaan. Huomaamme kuitenkin, että oliomuuttuja ikä uhkaa jäädä turhaksi ja lienee syytä poistaa se. Iän pystyy nimittäin tarvittaessa selvittämään helposti nykyisen päivämäärän ja syntymäpäivän perusteella. Javassa nykyinen päivä selviää esim. seuraavasti:

        int paiva = Calendar.getInstance().get(Calendar.DATE);
        int kuukausi = Calendar.getInstance().get(Calendar.MONTH) + 1; // tammikuun numero 0 joten  lisätään 1
        int vuosi = Calendar.getInstance().get(Calendar.YEAR);
        System.out.println("tänään on " + paiva + "." + kuukausi + "." + vuosi );

Kun ikä poistetaan, täytyy vanhempiKuin-metodi muuttaa toimimaan syntymäpäiviä vertaamalla. Teemme muutoksen harjoitustehtävänä.

Kellosta olio

Edellisen viikon tehtävässä 78 tehtiin ensin luokka YlhaaltaRajoitettuLaskuri ja rakennettiin laskurien avulla pääohjelmaan kello. Tehdään nyt myös itse kellosta olio. Luokan kello runko näyttää seuraavalta:


public class Kello {
    private YlhaaltaRajoitettuLaskuri tunnit;
    private YlhaaltaRajoitettuLaskuri minuutit;
    private YlhaaltaRajoitettuLaskuri sekunnit;

    public Kello(int tunnitAlussa, int minuutitAlussa, int sekunnitAlussa) {
      // luodaan kello joka asetetaan parametrina annettuun aikaan
    }

    public void etene(){
      // kello etenee sekunnilla
    }

    public String toString() {
        // palauttaa kellon merkkijonoesityksen
    }
}

Kopioi uuteen projektiin viime viikon tehtävän 78 YlhaaltaRajoitettuLaskuri-luokka.

Toteuta luokan Kello konstruktori ja puuttuvat metodit. Voit testata kelloasi seuraavalla pääohjelmalla:

public class Main {
    public static void main(String[] args) {
        Kello kello = new Kello(23, 59, 50);

        int i = 0;
        while( i < 20) {
            System.out.println( kello );
            kello.etene();
            i++;
        }
    }
}

Tulostuksen tulisi edetä seuraavasti:

23:59:50
23:59:51
23:59:52
23:59:53
23:59:54
23:59:55
23:59:56
23:59:57
23:59:58
23:59:59
00:00:00
00:00:01
...

Olion sisällä listallinen olioita

Laajennetaan PainonvartijaYhdistys-oliota siten, että yhdistys tallettaa kaikki jäsenensä ArrayList-olioon. Listalle tulee siis Henkilo-olioita. Yhdistykselle annetaan laajennetussa versiossa konstruktorin parametrina nimi:

public class PainonvartijaYhdistys {
    private double alinPainoindeksi;
    private String nimi;
    private ArrayList<Henkilo> jasenet;

    public PainonvartijaYhdistys(String nimi, double alinPainoindeksi) {
        this.alinPainoindeksi = alinPainoindeksi;
        this.nimi = nimi;
        this.jasenet = new ArrayList<Henkilo>();
    }

    //..
} 

Tehdään metodi jolla henkilö liitetään yhdistykseen. Metodi ei liitä yhdistykseen kuin tarpeeksi suuren painoindeksi omaavat henkilöt. Tehdään myös toString jossa tulostetaan jäsenten nimet:

public class PainonvartijaYhdistys {
    // ...

    public boolean hyvaksytaanJaseneksi(Henkilo henkilo) {
        if ( henkilo.painoIndeksi() < this.alinPainoindeksi ) {
            return false;
        }

        return true;
    }

    public void lisaaJaseneksi(Henkilo henkilo) {
        if ( !hyvaksytaanJaseneksi(henkilo) ) { // sama kuin hyvaksytaanJaseneksi(henkilo) == false
            return;
        }

        this.jasenet.add(henkilo);
    }

    public String toString() {
        String jasenetMerkkijonona = "";

        for ( Henkilo jasen : this.jasenet ) {
            jasenetMerkkijonona += "  " + jasen.getNimi() + "\n";
        }

        return "Painonvartijayhdistys " + this.nimi + " jäsenet: \n" + jasenetMerkkijonona;
    }
}

Metodi lisaaJaseneksi käyttää aiemmin tehtyä metodia hyvaksytaanJaseneksi.

Kokeillaan laajentunutta painonvartijayhdistystä:

    public static void main(String[] args) {
        PainonvartijaYhdistys painonVartija = new PainonvartijaYhdistys("Kumpulan paino", 25);

        Henkilo matti = new Henkilo("Matti");
        matti.setPaino(86);
        matti.setPituus(180);
        painonVartija.lisaaJaseneksi(matti);

        Henkilo juhana = new Henkilo("Juhana");
        juhana.setPaino(64);
        juhana.setPituus(172);
        painonVartija.lisaaJaseneksi(juhana);

        Henkilo harri = new Henkilo("Harri");
        harri.setPaino(104);
        harri.setPituus(182);
        painonVartija.lisaaJaseneksi(harri);

        Henkilo petri = new Henkilo("Petri");
        petri.setPaino(112);
        petri.setPituus(173);
        painonVartija.lisaaJaseneksi(petri);

        System.out.println( painonVartija );
}

Tulostuksesta huomaamme, että Juhanaa ei kelpuutettu jäseneksi:

Painonvartijayhdistys Kumpulan paino jäsenet: 
  Matti
  Harri
  Petri

Joukkueet ja pelaajat

Joukkue-luokka

Tee luokka Joukkue, johon tallennetaan joukkueen nimi (String). Tee luokkaan seuraavat metodit:

Seuraava pääohjelma testaa luokan toimintaa:

public class Main {
    public static void main(String[] args) {
	Joukkue tapiiri = new Joukkue("FC Tapiiri");
	System.out.println("Joukkue: " + tapiiri.haeNimi());
    }
}

Ohjelman tulostus on seuraava:

Joukkue: FC Tapiiri

Pelaaja

Luo luokka Pelaaja, johon tallennetaan pelaajan nimi ja tehtyjen maalien määrä. Tee luokkaan kaksi konstruktoria: yksi jolle annetaan vain pelaajan nimi, toinen jolle annetaan sekä pelaajan nimi että pelaajan tekemien maalien määrä. Lisää pelaajalle myös metodit:

public class Main {
    public static void main(String[] args) {
	Joukkue tapiiri = new Joukkue("FC Tapiiri");
	System.out.println("Joukkue: " + tapiiri.haeNimi());

        Pelaaja matti = new Pelaaja("Matti");
        System.out.println("Pelaaja: " + matti);
        
        Pelaaja pekka = new Pelaaja("Pekka", 39);
        System.out.println("Pelaaja: " + pekka);
    }
}
Joukkue: FC Tapiiri
Pelaaja: Matti, maaleja 0
Pelaaja: Pekka, maaleja 39

Pelaajat joukkueisiin

Lisää luokkaan Joukkue seuraavat metodit:

Tallenna joukkueessa olevat pelaajat Joukkue-luokan sisäiseen ArrayList-listaan.

Seuraava pääohjelma testaa luokan toimintaa:

public class Main {
    public static void main(String[] args) {
	Joukkue tapiiri = new Joukkue("FC Tapiiri");

        Pelaaja matti = new Pelaaja("Matti");
        Pelaaja pekka = new Pelaaja("Pekka", 39);

        tapiiri.lisaaPelaaja(matti);
        tapiiri.lisaaPelaaja(pekka);
	tapiiri.lisaaPelaaja(new Pelaaja("Mikael", 1)); //vaikutus on sama kuin edellisillä

	tapiiri.tulostaPelaajat();
    }
}

Ohjelman tulostuksen tulisi olla seuraava:

Matti, maaleja 0
Pekka, maaleja 39
Mikael, maaleja 1

Joukkueen maksimikoko ja nykyinen koko

Lisää luokkaan Joukkue seuraavat metodit:

Joukkueen suurin sallittu pelaajamäärä on oletusarvoisesti 16. Metodin asetaMaksimikoko avulla tätä rajaa voi muuttaa. Muuta metodia lisaaPelaaja niin, että se ei lisää pelaajaa joukkueeseen, jos sallittu pelaajamäärä ylittyisi.

HUOM: muista lisätä oletusarvoinen maksimikoko koodiisi sillä muuten arvoksi tulee 0. Tämä aiheuttaa edellisen kohdan testien hajoamisen, sillä testit luovat oletusmaksimikokoisia joukkueita ja jos joukkueen maksimikoko on 0, ei joukkueeseen voi lisätä yhtään pelaajaa.

Seuraava pääohjelma testaa luokan toimintaa:

public class Main {
    public static void main(String[] args) {
	Joukkue tapiiri = new Joukkue("FC Tapiiri");
        tapiiri.asetaMaksimikoko(1);

        Pelaaja matti = new Pelaaja("Matti");
        Pelaaja pekka = new Pelaaja("Pekka", 39);
        tapiiri.lisaaPelaaja(matti);
        tapiiri.lisaaPelaaja(pekka);
	tapiiri.lisaaPelaaja(new Pelaaja("Mikael", 1)); //vaikutus on sama kuin edellisillä

        System.out.println("Pelaajia yhteensä: " + tapiiri.koko());
    }
}
Pelaajia yhteensä: 1

Joukkueen maalit

Lisää luokkaan Joukkue metodi:

Seuraava pääohjelma testaa luokan toimintaa:

public class Main {
    public static void main(String[] args) {
	Joukkue tapiiri = new Joukkue("FC Tapiiri");

        Pelaaja matti = new Pelaaja("Matti");
        Pelaaja pekka = new Pelaaja("Pekka", 39);
        tapiiri.lisaaPelaaja(matti);
        tapiiri.lisaaPelaaja(pekka);
	tapiiri.lisaaPelaaja(new Pelaaja("Mikael", 1)); //vaikutus on sama kuin edellisillä

        System.out.println("Maaleja yhteensä: " + tapiiri.maalit());
    }
}
Maaleja yhteensä: 40

Metodi palauttaa olion

Olemme nähneet metodeja jotka palauttavat totuusarvoja, lukuja, listoja ja merkkijonoja. On helppoa arvata, että metodi voi palauttaa minkä tahansa tyyppisen olion. Tehdään painovartijayhdistykselle metodi, jolla saadaan tietoon yhdistyksen suurimman painoindeksin omaava henkilö.

public class PainonvartijaYhdistys {
    // ...

    public Henkilo suurinPainoindeksinen() {
        // jos jasenlista on tyhjä, palautetaan null-viite
        if ( this.jasenet.isEmpty() ) {
            return null;
        }

        Henkilo painavinTahanAsti = this.jasenet.get(0);

        for ( Henkilo henkilo : this.jasenet) {
            if ( henkilo.painoIndeksi() > painavinTahanAsti.painoIndeksi() ) {
                painavinTahanAsti = henkilo;
            }
        }
        
        return painavinTahanAsti;
    }

Logiikaltaan metodi toimii samaan tapaan kuin suurimman luvun etsiminen taulukosta. Käytössä on apumuuttuja painavinTahanAsti joka laitetaan aluksi viittaamaan listan ensimmäiseen henkilöön. Sen jälkeen käydään lista läpi ja katsotaan tuleeko vastaan suuremman painoindeksin omaavia henkilöitä, jos tulee, niin otetaan viite talteen muuttujaan painavinTahanAsti. Lopuksi palautetaan muuttujan arvo eli viite henkilöolioon.

Tehdään lisäys edelliseen pääohjelmaan. Pääohjelma ottaa vastaan metodin palauttaman viitteen muuttujaan painavin.

public static void main(String[] args) {
        PainonvartijaYhdistys painonVartija = new PainonvartijaYhdistys("Kumpluan paino", 25);

        // ..

        Henkilo painavin = painonVartija.suurinPainoindeksinen();
        System.out.print("suurin painoindeksinen jäsen: " + painavin.getNimi() );
        System.out.println(" painoindeksi " + String.format( "%.2f", painavin.painoIndeksi() ) );
}

Tulostuu:

suurin painoindeksinen jäsen: Petri
painoindeksi 37,42

Metodi palauttaa luomansa olion

Edellisessä esimerkissä metodi palautti yhden painonVartija-olion sisältämistä Henkilo-olioista. On myös mahdollista, että metodi palauttaa kokonaan uuden olion. Seuraavassa yksinkertainen laskuri, jolla on metodi kloonaa, jonka avulla laskurista voidaan tehdä klooni, eli uusi laskurio-olio, jolla on luomishetkellä sama arvo kuin kloonattavalla laskurilla:

public Laskuri {
  private int arvo;

  public Laskuri(){
     this(0);
  }

  public Laskuri(int alkuarvo){
     this.arvo = alkuarvo;
  }

  public void kasvata(){
     this.arvo++;
  }

  public String toString(){
     return "arvo: "+arvo;
  }

  public Laskuri kloonaa(){
     // luodaan uusi laskuriolio, joka saa alkuarvokseen kloonattavan laskurin arvon
     Laskuri klooni = new Laskuri(this.arvo);
   
     // palautetaan klooni kutsujalle
     return klooni;
  } 
}

Seuraavassa käyttöesimerkki:

  Laskuri laskuri = new Laskuri();
  laskuri.kasvata();
  laskuri.kasvata();

  System.out.println(laskuri);         // tulostuu 2

  Laskuri klooni = laskuri.kloonaa();  

  System.out.println(laskuri);         // tulostuu 2
  System.out.println(klooni);          // tulostuu 2
 
  laskuri.kasvata();
  laskuri.kasvata();
  laskuri.kasvata();
  laskuri.kasvata();

  System.out.println(laskuri);         // tulostuu 6
  System.out.println(klooni);          // tulostuu 2

  klooni.kasvata();

  System.out.println(laskuri);         // tulostuu 6
  System.out.println(klooni);          // tulostuu 3

Kloonattavan ja kloonin arvo on siis kloonauksen tapahduttua sama. Kyseessä on kuitenkin kaksi erillistä olioa, eli jatkossa kun toista laskureista kasvatetaan, ei kasvatus vaikuta toisen arvoon millään tavalla.

Päiväys

Tehtäväpohjan mukana tulee luvussa 24.7 esitelty luokka Paivays, jossa päivämäärä talletetaan oliomuuttujien vuosi, kuukausi, ja paiva avulla:

public class Paivays {
    private int paiva;
    private int kuukausi;
    private int vuosi;

    public Paivays(int paiva, int kuukausi, int vuosi) {
        this.paiva = paiva;
        this.kuukausi = kuukausi;
        this.vuosi = vuosi;
    }

    public String toString() {
        return this.paiva + "." + this.kuukausi + "." + this.vuosi;
    }

    public boolean aiemmin(Paivays verrattava) {
        // ensin verrataan vuosia
        if ( this.vuosi < verrattava.vuosi ) {
            return true;
        }

        // jos vuodet ovat samat, verrataan kuukausia
        if ( this.vuosi == verrattava.vuosi && this.kuukausi < verrattava.kuukausi ) {
            return true;
        }

        // vuodet ja kuukaudet samoja, verrataan päivää
        if ( this.vuosi == verrattava.vuosi && this.kuukausi == verrattava.kuukausi &&
                this.paiva < verrattava.paiva ) {
            return true;
        }

        return false;
    }
}

Tässä tehtäväsarjassa laajennetaan luokkaa.

Seuraava päivä

Toteuta metodi public void etene(), joka siirtää päiväystä yhdellä päivällä. Tässä tehtävässä oletetaan, että jokaisessa kuukaudessa on 30 päivää. Huom! Sinun tulee tietyissä tilanteissa muuttaa kuukauden ja vuoden arvoa.

Tietty määrä päiviä eteenpäin

Toteuta metodi public void etene(int montakoPaivaa), joka siirtää päiväystä annetun päivien määrän verran. Käytä apuna edellisessä tehtävässä toteutettua metodia etene().

Ajan kuluminen

Lisätään Paivays-olioon mahdollisuus edistää aikaa. Tee oliolle metodi Paivays paivienPaasta(int paivia), joka luo uuden Paivays-olion, jonka päiväys on annetun päivien lukumäärän verran suurempi kuin oliolla, jolle sitä kutsuttiin. Voit edelleen olettaa, että jokaisessa kuukaudessa on 30 päivää. Huomaa, että vanhan päiväysolion on pysyttävä muuttumattomana!

Koska metodissa on luotava uusi olio, tulee rungon olla suunnilleen seuraavanlainen:

    public Paivays paivienPaasta(int paivia){
        Paivays uusiPaivays = new Paivays( ... );
        
        // tehdään jotain...

        return uusiPaivays;
    }

Ohessa on esimerkki metodin toiminnasta.

    public static void main(String[] args) {
        Paivays pvm = new Paivays(25, 2, 2011);
        Paivays uusi_pvm = pvm.paivienPaasta(7);
        for (int i = 1; i <= 7; ++i) {
            System.out.println("Perjantai " + i + " viikon kuluttua on " + uusi_pvm);
            uusi_pvm = uusi_pvm.paivienPaasta(7);
        }
        System.out.println("Tämän viikon perjantai on " + pvm);
        System.out.println("Päivämäärä 790:n päivän päästä tämän viikon perjantaista on  " + pvm.paivienPaasta(790));
  }

Ohjelma tulostaa:

Perjantai 1 viikon kuluttua on 2.3.2011
Perjantai 2 viikon kuluttua on 9.3.2011
Perjantai 3 viikon kuluttua on 16.3.2011
Perjantai 4 viikon kuluttua on 23.3.2011
Perjantai 5 viikon kuluttua on 30.3.2011
Perjantai 6 viikon kuluttua on 7.4.2011
Perjantai 7 viikon kuluttua on 14.4.2011
Tämän viikon perjantai on 25.2.2011
Päivämäärä 790:n päivän päästä tämän viikon perjantaista on  5.5.2013

Huom! Sen sijaan, että muuttaisimme vanhan olion tilaa palautamme uuden olion. Kuvitellaan, että Paivays-luokalle on olemassa metodi edista, joka toimii vastaavasti kuin ohjelmoimamme metodi, mutta se muuttaa vanhan olion tilaa. Tällöin seuraava koodin pätkä tuottaisi ongelmia.

Paivays nyt = new Paivays(20,2,2011);
Paivays viikonPaasta = nyt;
viikonPaasta.edista(7);
System.out.println("Nyt: " + nyt);
System.out.println("Viikon päästä: " + viikonPaasta);

Ohjelman tulostus olisi seuraavanlainen:

Nyt 27.2.2011
Viikon päästä 27.2.2011

Tämä johtuu siitä, että tavallinen sijoitus kopioi ainoastaan viitteen olioon. Siis itse asiassa ohjelman oliot nyt ja viikonPaasta viittavaat yhteen ja samaan Paivays-olioon.

Lisää oliotehtäviä

Viikon teoria on esitelty, seuraavassa vielä joukko samoja teemoja käsitteleviä tehtäviä.

Päivämäärien erotus

Jatketaan luokan Päiväys laajentamista. Tämä tehtävä ei riipu edellisestä tehtävästä, saat tehtäväpohjan mukana Paivays-luokan jossa ei ole edellisen tehtävän lisäyksiä.

Kahden päiväyksen erotus vuosissa

Lisää päiväykselle metodi public int erotusVuosissa(Paivays verrattava), jonka avulla saadaan selville päiväyksen ja verrattavan päiväyksen ero vuosissa. Huomioi seuraavat:

Seuraava pääohjelma demonstroi metodin käyttöä:

public class Paaohjelma {
    public static void main(String[] args) {
        Paivays eka = new Paivays(24, 12, 2009);
        Paivays toka = new Paivays(1, 1, 2011);
        Paivays kolmas = new Paivays(25, 12, 2010);

        System.out.println( toka + " ja " + eka + " ero vuosissa: " + toka.erotusVuosissa(eka) );

        System.out.println( kolmas + " ja " + eka + " ero vuosissa: " + kolmas.erotusVuosissa(eka) );

        System.out.println( toka + " ja " + kolmas + " ero vuosissa: " + toka.erotusVuosissa(kolmas) );
    }
}

Tulos näyttää seuraavalta:

1.1.2011 ja 24.12.2009 ero vuosissa: 2
25.12.2010 ja 24.12.2009 ero vuosissa: 1
1.1.2011 ja 25.12.2010 ero vuosissa: 1

Tarkennettu versio

Vuosien laskenta ei edellisessä versiossa ollut vielä kovin tarkkaa. Esim. 1.1.2011 ja 25.12.2010 välillä ilmoitettiin olevan vuoden ero. Tarkennetaan metodin toiminta sellaiseksi, että se osaa laskea vuodet kunnolla. Laske erotukseen mukaan vain täydet vuodet. Eli vaikka päiväysten ero olisi 1 vuosi ja 364 päivää, ilmoittaa metodi eroksi vuoden.

Metodin tämänkin version tarvitsee toimia ainoastaan siten, että parametriksi annettava päivämäärä on aiempi kuin se päivämäärä jolle metodia kutsutaan.

Edellisen esimerkin tulos on nyt:

1.1.2011 ja 24.12.2009 ero vuosissa: 1
25.12.2010 ja 24.12.2009 ero vuosissa: 1
1.1.2011 ja 25.12.2010 ero vuosissa: 0

Ja lopullinen versio

Laitetaan metodi toimimaan samoin riippumatta onko parametrina annettava päiväys myöhempi vai aiempi kuin päiväys mille metodia kutsutaan. Esimerkkipääohjelma:

public class Paaohjelma {
    public static void main(String[] args) {
        Paivays eka = new Paivays(24, 12, 2009);
        Paivays toka = new Paivays(1, 1, 2011);
        Paivays kolmas = new Paivays(25, 12, 2010);

        System.out.println( eka + " ja " + toka + " ero vuosissa: " + toka.erotusVuosissa(eka) );
        System.out.println( toka + " ja " + eka + " ero vuosissa: " + eka.erotusVuosissa(toka) );
        System.out.println( eka + " ja " + kolmas + " ero vuosissa: " + kolmas.erotusVuosissa(eka) );
        System.out.println( kolmas + " ja " + eka + " ero vuosissa: " + eka.erotusVuosissa(kolmas) );
        System.out.println( kolmas + " ja " + toka + " ero vuosissa: " + toka.erotusVuosissa(kolmas) );
        System.out.println( toka + " ja " + kolmas + " ero vuosissa: " + kolmas.erotusVuosissa(toka) );
    }
}
24.12.2009 ja 1.1.2011 ero vuosissa: 1
1.1.2011 ja 24.12.2009 ero vuosissa: 1
24.12.2009 ja 25.12.2010 ero vuosissa: 1
25.12.2010 ja 24.12.2009 ero vuosissa: 1
1.1.2011 ja 25.12.2010 ero vuosissa: 0
25.12.2010 ja 1.1.2011 ero vuosissa: 0

Luokan Henkilö muokkaaminen

Iän laskeminen syntymäpäivän perusteella

Luvussa 24.9. lisättiin henkilölle oliomuuttujaksi syntymäpäivän kertova Paivays-olio. Samalla todettiin, että oliomuuttuja ika kannattaa poistaa sillä iän pystyy laskemaan päiväyksen ja syntymäpäivän avulla.

Toteuta metodi ika joka palauttaa henkilön iän.

Huom: edellisessä tehtävässä lisättiin luokalle Paivays metodi public int erotusVuosissa(Paivays verrattava). Kannattaa kopioida metodi tässä tehtävässä olevaan luokkaan, se helpottaa tehtävän tekemistä oleellisesti!

import java.util.Calendar;

public class Henkilo {
    private String nimi;
    private Paivays syntymaPaiva;

    public Henkilo(String nimi, int pp, int kk, int vv) {
        this.nimi = nimi;
        this.syntymaPaiva = new Paivays(pp, kk, vv);
    }

    public int ika() {
        // laske henkilön ikä syntymäpäivän ja tämän päivän perusteella
        // tämä päivä saadaan selville seuraavasti
        // Calendar.getInstance().get(Calendar.DATE);
        // Calendar.getInstance().get(Calendar.MONTH) + 1; // tammikuun numero on 0 joten lisätään 1
        // Calendar.getInstance().get(Calendar.YEAR);
    }

    public String getNimi() {
        return this.nimi;
    }

    public String toString() {
        return this.nimi +", syntynyt "+ this.syntymaPaiva;
    }
}

Voit testata Henkilöä seuraavalla pääohjelmalla. Lisää myös itsesi ohjelmaan ja varmista että ikäsi tulostuu oikein.

public class Main {
    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka", 15, 2, 1993);
        Henkilo antti = new Henkilo("Antti", 1, 3, 1955);

        System.out.println( antti.getNimi() + " ikä " + antti.ika() + " vuotta");
        System.out.println( pekka.getNimi() + " ikä " + pekka.ika() + " vuotta");
    }
}

Tulostus:

Antti ikä 57 vuotta
Pekka ikä 19 vuotta

Iän vertailu syntymäpäivien perusteella

Tee henkilölle metodi jonka avulla se vertaa ikäänsä parametrina annettuun henkilöön. Jos henkilö on vanhempi eli syntynyt aiemmin, palauttaa metodi true ja muuten false.

public class Henkilo {
    // ...

    public boolean vanhempiKuin(Henkilo verrattava) {
       // vertaa henklöiden ikiä käyttäen henkilöiden syntymäpäivää
    }
}

Ja testaa laajennettua Henkilö-luokkaa esim. seuraavasti:

public class Main {
    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka", 15, 2, 1983);
        Henkilo martin = new Henkilo("Martin", 1, 3, 1983);

        System.out.println( martin.getNimi() + " vanhempi kuin " +  pekka.getNimi() + ": "+ martin.vanhempiKuin(pekka) );
        System.out.println( pekka.getNimi() + " vanhempi kuin " +  martin.getNimi() + ": "+ pekka.vanhempiKuin(martin) );
    } 
}  

Tulostus:

Martin vanhempi kuin Pekka: false
Pekka vanhempi kuin Martin: true

Uusia konstruktoreja

Tee Henkilo-luokalle kaksi uutta konstruktoria:

Testaa uusia konstruktoreja esim. seuraavasti:

public class Main {
    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka", new Paivays(15, 2, 1983));
        Henkilo sepe = new Henkilo("Sepe");

        System.out.println( pekka );
        System.out.println( sepe );
    } 
}  a

Esimerkkitulostus:

Pekka, syntynyt 15.2.1983
Sepe, syntynyt 9.2.2012

Huom: jälkimmäinen rivi riippuu päivämäärästä, jolloin koodi ajetaan!

Viikko 6

Nopea kertaus

Aloitetaan viikko muutamalla viime viikon tärkeimpiä teemoja kertaavalla tehtävällä. Kertaa tarvittaessa luku 24.10 ennen tehtävää 94 ja luvut 24.6 ja 24.12 ennen tehtävää 95.

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

Puhelinmuistio

Tehtävässä tehdään yksinkertainen puhelinmuistio.

Henkilö

Tee ensin luokka Henkilo Luokan pitää toimia seuraavan esimerkin osoittamalla tavalla:

public static void main(String[] args) {
    Henkilo pekka = new Henkilo("Pekka Mikkola", "040-123123");

    System.out.println( pekka.haeNimi() );
    System.out.println( pekka.haeNumero() );

    System.out.println( pekka );

    pekka.vaihdaNumeroa("050-333444");
    System.out.println( pekka );
}

Tulostuu:

Pekka Mikkola
040-123123
Pekka Mikkola  puh: 040-123123
Pekka Mikkola  puh: 050-333444

Tee siis luokalle

Henkilöiden lisäys puhelinmuistioon

Tee luokka Puhelinmuistio joka tallettaa sisällään olevaan ArrayListiin Henkilo-olioita. Tässä vaiheessa luokalle tehdään seuraavat metodit:

Esimerkki muistion toiminnasta:

public static void main(String[] args) {
    Puhelinmuistio muistio = new Puhelinmuistio();

    muistio.lisaa("Pekka Mikkola", "040-123123");
    muistio.lisaa("Antti Laaksonen", "045-456123");
    muistio.lisaa("Juhana Laurinharju", "050-222333");

    muistio.tulostaKaikki();

Ohjelman tulostus oikein toteutetuilla luokilla on:

Pekka Mikkola  puh: 040-123123
Antti Laaksonen  puh: 045-456123
Juhana Laurinharju  puh: 050-222333

Numerojen haku muistiosta

Tehdään puhelinmuistiolle metodi public String haeNumero(String nimi), joka palauttaa parametrina annetun henkilön numeron. Jos henkilö ei ole muistiossa, palautetaan merkkijono "numero ei tiedossa". Esimerkki metodin toiminnasta:

public static void main(String[] args) {
    Puhelinmuistio muistio = new Puhelinmuistio();
    muistio.lisaa("Pekka Mikkola", "040-123123");
    muistio.lisaa("Antti Laaksonen", "045-456123");
    muistio.lisaa("Juhana Laurinharju", "050-222333");

    String numero = muistio.haeNumero("Pekka Mikkola");
    System.out.println( numero );

    numero = muistio.haeNumero("Martti Tienari");
    System.out.println( numero );

Tulostuu:

040-123123
numero ei tiedossa

Raha

Lyyrakortti-tehtävässä käytimme rahamäärän tallettamiseen double-tyyppistä oliomuuttujaa. Todellisissa sovelluksissa näin ei kannata tehdä, sillä kuten jo olemme nähneet, doubleilla laskenta ei ole tarkkaa. Onkin järkevämpää toteuttaa rahamäärän käsittely oman luokkansa avulla. Seuraavassa on luokan runko:

public class Raha {

    private final int euroa;
    private final int senttia;

    public Raha(int euroa, int senttia) {
        this.euroa = euroa;
        this.senttia = senttia;
    }

    public int eurot(){
        return euroa;
    }
    
    public int sentit(){
        return senttia;
    }

    public String toString() {
        String nolla = "";
        if (senttia <= 10) {
            nolla = "0";
        }

        return euroa + "." + nolla + senttia + "e";
    }
}

Määrittelyssä pistää silmään oliomuuttujien määrittelyn yhteydessä käytetty sana final, tällä saadaan aikaan se, että oliomuuttujien arvoa ei pystytä muuttamaan sen jälkeen kun ne on konstruktorissa asetettu. Raha-luokan oliot ovatkin muuttumattomia eli immutaabeleita, eli jos halutaan esim. kasvattaa rahamäärää, on luotava uusi olio, joka kuvaa kasvatettua rahasummaa.

Luomme seuraavassa muutaman operaation rahojen käsittelyyn.

Plus

Tee ensin metodi public Raha plus(Raha lisattava), joka palauttaa uuden raha-olion, joka on arvoltaan yhtä suuri kuin se olio jolle metodia kutsuttiin ja parametrina oleva olio yhteensä.

Metodin runko on seuraavanlainen:

    public Raha plus(Raha lisattava) {
        Raha uusi = new Raha(...); // ludaan uusi Raha-olio jolla on oikea arvo
        
        // palautetaan uusi Raha-olio
        return uusi;
    }

Seuraavassa esimerkkejä metodin toiminnasta

  Raha a = new Raha(10,0);
  Raha b = new Raha(5,0);

  Raha c = a.plus(b);

  System.out.println(a);  // 10.00e
  System.out.println(b);  // 5.00e
  System.out.println(c);  // 15.00e

  a = a.plus(c);          // HUOM: tässä syntyy uusi Raha-olio, joka laitataan "a:n langan päähän"
                          //       vanha a:n langan päässä ollut 10 euroa häviää ja Javan roskien kerääjä korjaa sen pois
  
  System.out.println(a);  // 25.00e
  System.out.println(b);  // 5.00e
  System.out.println(c);  // 15.00e

Vähemmän

Tee metodi public boolean vahemman(Raha verrattava), joka palauttaa true jos raha-olio jolle metodia kutsutaan on arvoltaan pienempi kuin raha-olio, joka on metodin parametrina.

  Raha a = new Raha(10,0);
  Raha b = new Raha(3,0);
  Raha c = new Raha(5,0);

  System.out.println(a.vahemman(b));  // false
  System.out.println(b.vahemman(c));  // true

Miinus

Tee metodi public Raha miinus(Raha vahennettava), joka palauttaa uuden raha-olion, joka arvoksi tulee sen olion jolle metodia kutsuttiin ja parametrina olevan olion arvojen erotus. Jos erotus olisi negatiivinen, tulee luotavan raha-olion arvoksi 0.

Seuraavassa esimerkkejä metodin toiminnasta

  Raha a = new Raha(10,0);
  Raha b = new Raha(3,50);

  Raha c = a.miinus(b);

  System.out.println(a);  // 10.00e
  System.out.println(b);  // 3.50e
  System.out.println(c);  // 6.50e

  c = c.miinus(a);        // HUOM: tässä syntyy uusi Raha-olio, joka laitataan "c:n langan päähän"
                          //       vanha c:n langan päässä ollut 6.5 euroa häviää ja Javan roskien kerääjä korjaa sen pois
  
  System.out.println(a);  // 10.00e
  System.out.println(b);  // 3.50e
  System.out.println(c);  // 0.00e

Merkkijonot ovat immutaabeleja

Javan String-oliot ovat Raha-luokan olioiden tyyliin muuttumattomia, eli immutaabelena. Jos esim. merkkijonon perään katenoidaan eli liitetään +-operaatiolla uusi merkkijono, ei alkuperäistä merkkijonoa pidennetä vaan syntyy uusi merkkijono-olio:

String jono = "koe";
jono + "häntä";

System.out.println( jono );  // koe

Merkkijonoa ei siis voi muuttaa, mutta voimme ottaa katenoimalla syntyvän uuden merkkijonon talteen vanhaan muuttujaan:

String jono = "koe";
jono = jono + "häntä";   // tai jono += "häntä";

System.out.println( jono );  // koehäntä

Nyt siis muuttuja jono viittaa uuteen merkkijono-olioon, joka luotiin yhdistämällä muuttujan aiemmin viittaama merkkijono "koe" ja merkkijono "häntä". Merkkijono-olioon "koe" ei enää viitata.

Taulukko

Olemme käyttäneet kurssin aikana lukemattomia kertoja ArrayList:ejä erilaisten olioiden säilömiseen. ArrayList on helppokäyttöinen sillä se tarjoaa paljon valmiita työvälineitä jotka helpottavat ohjelmoijan elämää: muun muassa automaattisen listan kasvatuksen, jonka ansioista listalta ei lopu tila kesken (elle lista kasva niin suureksi että se käyttää loppuun ohjelmalle varatun muistimäärän).

ArrayListin helppokäyttöisyydesta huolimatta ohjelmissa on joskus tarvetta ArrayListin esi-isälle eli taulukolle.

Taulukko on olio, joka voidaan käsittää eräänlaisena järjestettynä lokerikkona arvoille. Taulukon pituus tai koko on lokerikon paikkojen lukumäärä, eli kuinka monta arvoa taulukkoon voi laittaa. Taulukon arvoja kutsutaan taulukon alkioiksi. ArrayLististä poiketen taulukon kokoa (eli sen alkioiden määrää) ei voi muuttaa, taulukon kasvattaminen vaatii siis aina uuden taulukon luomista ja vanhassa olevien alkioiden kopiointia uuteen.

Taulukon voi luoda kahdella eri tavalla. Tutustutaan ensin tapaan jossa taulukolle annetaan sisältö luomisen yhteydessä. Kolmen alkion kokonaislukutyyppinen taulukko määritellään seuraavasti:

int[] luvut = {100, 1, 42};

Taulukko-olion tyyppi merkitään int[], joka tarkoittaa taulukkoa, jonka alkiot ovat tyyppiä int. Taulukko-olion nimi on esimerkissä luvut ja se sisältää kolme lukua {100, 1, 42}. Taulukko alustetaan lohkolla, jossa taulukkoon asetettavat arvot on esitelty pilkulla eriteltyinä.

Taulukon arvot voivat olla mitä tahansa aikaisemmin nähtyjä muuttujatyyppejä. Alla on esitelty ensin merkkijonoja sisältävä taulukko, jonka jälkeen esitellään liukulukuja sisältävä taulukko.

String[] merkkijonotaulukko = {"Matti P.", "Matti V."};
double[] liukulukutaulukko = {1.20, 3.14, 100.0, 0.6666666667};

Taulukon alkioihin viitataan indeksillä, joka on kokonaisluku. Indeksi kertoo alkion paikan taulukossa. Taulukon ensimmäinen alkio on paikassa nolla, seuraava kohdassa yksi ja niin edelleen. Taulukon tietyssä indeksissä olevaa arvoa tutkittaessa indeksi annetaan taulukko-olion nimen perään hakasulkeiden sisällä.

// indeksi       0   1    2    3   4   5     6     7
int[] luvut = {100,  1,  42,  23,  1,  1, 3200, 3201};

System.out.println(luvut[0]);	// tulostaa luvun taulukon indeksistä 0, eli luvun 100
System.out.println(luvut[2]);	// tulostaa luvun taulukon indeksistä 2, eli luvun 42

Yllä olevan taulukon koko (eli pituus) on 8.

Huomaat todennäköisesti että ArrayListin metodi get käyttäytyy hyvin samalla tavalla kuin taulukon tietystä indeksistä haku. Taulukon kohdalla vain syntaksi, eli merkintätapa, on erilainen.

Yksittäisen arvon asettaminen taulukon tiettyyn paikkaan tapahtuu kuten arvon asetus tavalliseen muuttujaan, mutta taulukkoon asetettaessa paikka eli indeksi tulee kertoa. Indeksi kerrotaan hakasulkeiden sisällä.

int[] luvut = {100,1,42};

luvut[0] = 1;    // asetetaan luku 1 indeksiin 0
luvut[1] = 101;  // asetetaan luku 101 indeksiin 1

// luvut-taulukko on nyt {1,101,42}

Jos indeksillä osoitetaan taulukon ohi, eli alkioon jota ei ole olemassa, niin saadaan virheilmoitus ArrayIndexOutOfBoundsException, joka kertoo että indeksiä johon osoitimme ei ole olemassa. Taulukon ohi, eli indeksiin joka on pienempi kuin 0 tai suurempi tai yhtäsuuri kuin taulukon koko ei siis saa viitata.

Huomaamme, että taulukko on selvästi sukua ArrayList:ille. Aivan kuten listoilla, myös taulukossa alkiot ovat tietyssä paikassa!

Taulukon läpikäynti

Taulukko-olion koon saa selville kirjoittamalla koodiin taulukko.length, huomaa että ilmauksessa ei tule käyttää sulkuja eli taulukko.length() ei toimi!

Taulukon alkioiden läpikäynti on helppo toteuttaa while-komennon avulla:

int[] luvut = {1, 8, 10, 3, 5};

int i = 0; 
while (i < luvut.length ) {
    System.out.println(luvut[i]);
    i++;
}

Esimerkissä käydään muuttujan i avulla läpi indeksit 0, 1, 2, 3, ja 4, ja tulostetaan taulukon kussakin indeksissä olevan muuttujan arvo. Ensin siis tulostuu luvut[0], sitten luvut[1] jne. Muuttujan i kasvatus loppuu kun koko taulukko on käyty läpi, eli kun sen arvo on yhtäsuuri kuin taulukon pituus.

Taulukon läpikäynnissä ei ole aina todellista tarvetta taulukon indeksien luetteluun, vaan ainoa kiinnostava asia ovat taulukon arvot. Tällöin voidaan käyttää aiemmin tutuksi tullutta for-each-rakennetta arvojen läpikäyntiin. Nyt toistolauseen rungossa annetaan vain muuttujan nimi, johon kukin taulukon arvo asetetaan vuorollaan, ja taulukon nimi kaksoispisteellä erotettuna.

int[] luvut = {1,8,10,3,5};

for (int luku : luvut) {
  System.out.println(luku);
}
String[] nimet = {"Juhana L.", "Matti P.", "Matti L.", "Pekka M."};

for (String nimi : nimet) {
  System.out.println(nimi);
}

Huom: for-each-tyylisellä läpikäynnillä taulukon alkioihin ei voi asettaa arvoja! Seuraavaksi nähtävällä for-lauseen toisella muodolla sekin onnistuu.

for-komennon toinen muoto

Olemme toistaiseksi käyttäneet toistolauseissa whileä tai for-lauseen ns. "for-each"-muotoa. Toistolauseesta for on olemassa myös toinen muoto, joka on kätevä erityisesti taulukoiden käsittelyn yhteydessä. Seuraavassa tulostetaan for-toistolauseen avulla luvut 0, 1 ja 2:

for (int i = 0; i < 3; i++ ) {
    System.out.println(i);
}

Esimerkin for toimii täsmälleen samalla tavalla kuin alla oleva while:

int i = 0;  // toistossa käytettävän muuttujan alustus
while ( i < 3 ) {  // toistoehto
    System.out.println(i);
    i++;   // toistossa käytettävän muuttujan päivitys 
}

for-komento, kuten yllä esitelty for (int i = 0; i < 3; i++ ) sisältää kolme osaa: looppimuuttujien alustus; toistoehto; looppimuuttujien päivitys:

for on hieman while:ä selkeämpi tapa toteuttaa toistoja joissa toistojen määrä perustuu esim. laskurin kasvatukseen. Taulukkojen läpikäynnissä tilanne on yleensä juuri tämä. Seuraavassa tulostetaan taulukon luvut sisältö for:illa

int[] luvut = {1, 3, 5, 9, 17, 31, 57, 105};

for(int i = 3; i < 7; i++) {
    System.out.println(luvut[i]);
}

Forilla voidaan aloittaa läpikäynti luonnollisesti muualtakin kuin nollasta ja läpikäynti voi edetä "ylhäältä alas". Esimerkiksi taulukon paikoissa 6, 5, 4, ja 3 olevat alkiot voidaan tulostaa seuraavasti:

int[] luvut = {1, 3, 5, 9, 17, 31, 57, 105};

for(int i = 6; i>2 ; i--) {
    System.out.println(luvut[i]);
}

for ja taulukon pituus

Kaikkien taulukon alkioiden läpikäynti for:in avulla onnistuu seuraavasti:

int[] luvut = {1, 8, 10, 3, 5};

for (int i = 0; i < luvut.length; i++ ) {
    System.out.println(luvut[i]);
}

Huomaa, että toistoehdossa i < luvut.length verrataan looppimuuttujan arvoa taulukolta kysyttyyn pituuteen. Ehtoa ei kannata missään tapauksessa "kovakoodata" tyyliin i < 5 sillä yleensä taulukon pituudesta ei ole etukäteen varmuutta.

Taulukko parametrina

Taulukkoja voidaan käyttää metodin parametrina aivan kuten muitakin olioita. Huomaa, että kuten kaikkien olioiden tapauksessa, metodi saa parametrina viitteen taulukkoon, eli kaikki metodissa tapahtuvat taulukon sisältöön vaikuttavat muutokset näkyvät myös pääohjelmassa.

public static void listaaAlkiot(int[] kokonaislukuTaulukko) {

  System.out.println("taulukon alkiot ovat: ");
  for( int luku : kokonaislukuTaulukko) {
    System.out.print(luku + " ");  
  }

  System.out.println("");
}

public static void  main(String[] args) {
  int[] luvut = { 1, 2, 3, 4, 5 };
  listaaAlkiot(luvut);
}

Kuten jo tiedämme, parametrin nimi metodin sisällä voi olla aivan vapaasti valittu, nimen ei tarvitse missään tapauksessa olla sama kuin kutsuvassa. Edellä taulukkoa kutsutaan metodin sisällä nimellä kokonaislukuTaulukko, metodin kutsuja taas näkee saman taulukon luvut-nimisenä.

Taulukon lukujen summa

Tee metodi public static int laskeTaulukonLukujenSumma(int[] taulukko), joka palauttaa taulukossa olevien lukujen summan.

Ohjelman runko on seuraava:

public class Main {
  public static void main(String[] args) {
    // Tässä voit testata metodia
    int[] taulukko = {5, 1, 3, 4, 2};
    System.out.println(laskeTaulukonLukujenSumma(taulukko));
  }

  public static int laskeTaulukonLukujenSumma(int[] taulukko) {
    // Kirjoita koodia tänne
    return 0;
  }
}

Ohjelman tulostus on seuraava:

15

Huom: tämän tehtävän metodi samoin kuin muutamien seuraavien tehtävien taulukkoa käsittelevät metodit ovat viikkojen 2 ja 3 tapaan static eli staattisia metodeja. Karkeasti ottaen tämä johtuu siitä, että metodi ei liity mihinkään olioon, vaan saa kaiken datan jota se käyttää eli tässä tapauksessa taulukon, parametrina. Luvussa 28 palaamme tarkemmin aiheeseen staattiset vs. olioihin liittyvät metodit.

Tyylikäs tulostus

Tee metodi public static void tulostaTyylikkaasti(int[] taulukko), joka tulostaa taulukossa olevat luvut tyylikkäästi. Lukujen väliin tulee pilkku ja välilyönti. Viimeisen luvun jälkeen ei pilkkua tule.

Ohjelman runko on seuraava:

public class Main {
  public static void main(String[] args) {
    // Tässä voit testata metodia
    int[] taulukko = {5, 1, 3, 4, 2};
    tulostaTyylikkaasti(taulukko);
  }

  public static void tulostaTyylikkaasti(int[] taulukko) {
    // Kirjoita koodia tänne
  }
}

Ohjelman tulostus on seuraava:

5, 1, 3, 4, 2

Uuden taulukon luonti

Jos taulukon koko ei ohjelmassa ole aina sama, eli se riippuu esim. käyttäjän syötteestä, ei äsken esitelty taulukon luontitapa kelpaa. Taulukko on mahdollista luoda myös siten, että sen koko määritellään muuttujan avulla:

int alkioita = 99;
int[] taulukko = new int[alkioita];

Yllä luodaan int-tyyppinen taulukko, jossa on 99 paikkaa. Tällä vaihtoehtoisella tavalla taulukon luominen tapahtuu siis kuten muidenkin olioiden luominen, eli new-komennolla. Komentoa new seuraa taulukon sisältämien muuttujien tyyppi, ja hakasuluissa taulukon koko.

int alkioita = 99;
int[] taulukko = new int[alkioita]; //luodaan muuttujan alkioita sisältämän arvon kokoinen taulukko

if(taulukko.length == alkioita) {
    System.out.println("Taulukon pituus on " + alkioita);
} else {
    System.out.println("Jotain epätodellista tapahtui. Taulukon pituus on eri kuin " + alkioita);
}

Seuraavassa esimerkissä on ohjelma, joka kysyy käyttäjältä lukujen määrän ja joukon lukuja. Tämän jälkeen ohjelma tulostaa luvut uudestaan samassa järjestyksessä. Käyttäjän antamat luvut tallennetaan taulukkoon.

System.out.print("Kuinka monta lukua? ");
int lukuja = Integer.parseInt(lukija.nextLine());

int[] luvut = new int[lukuja];

System.out.println("Anna luvut:");
for(int i = 0; i < lukuja; i++) {
    luvut[i] = Integer.parseInt(lukija.nextLine());
}

System.out.println("Luvut uudestaan:");
for(int i = 0; i < lukuja; i++) {
    System.out.println(luvut[i]);
}

Eräs ohjelman suorituskerta voisi olla seuraavanlainen:

Kuinka monta lukua? 4
Anna luvut:
4
8
2
1
Luvut uudestaan:
4
8
2
1

Taulukko paluuarvona

Koska metodit voivat palauttaa olioita, voivat ne palauttaa myös taulukkoja. Eräs merkkijonotaulukon palauttava metodi on seuraavannäköinen -- huomaa että taulukkoihin voi aivan hyvin siis laittaa myös olioita.

public static String[] annaMerkkijonoTaulukko() {
  String[] opet = new String[3];

  opet[0] = "Bonus";
  opet[1] = "Ihq";
  opet[2] = "Lennon";

  return opet;
}

public static void main(String[] args){
  String[] opettajat = annaMerkkijonoTaulukko();

  for ( String opettaja : opettajat)
    System.out.println( opettaja );
}

Taulukon kopiointi ja kääntaminen

Kopiointi

Tee metodi public static int[] kopioi(int[] taulukko) joka luo kopion parametrina saadusta taulukosta. Vihje: koska metodin on luotava taulukosta kopio, tulee metodin sisällä luoda uusi taulukko ja kopioida vanhan taulukon sisältö uudelle taulukolle alkio alkiolta.

Seuraavassa esimerkki metodin käytöstä (koodissa myös Arrays-luokan tarjoama kätevä apuväline taulukon sisällön tulostamiseen):

    public static void main(String[] args) {
        int[] alkuperainen = {1, 2, 3, 4};
        int[] kopio = kopioi(alkuperainen);
        
        // muutetaan kopioa
        kopio[0] = 99;
        
        // tulostetaan molemmat
        System.out.println( "alkup: " + Arrays.toString(alkuperainen));
        System.out.println( "kopio: " + Arrays.toString(kopio));
    }

Kuten tulostuksesta huomaa, ei kopioon tehty muutos vaikuta alkuperäiseen:

alkup: [1, 2, 3, 4]
kopio: [99, 2, 3, 4]

Kääntäminen

Tee metodi public static int[] kaanna(int[] taulukko) joka luo käänteisessä järjestyksessä olevan kopion parametrinaan saamastaan taulukosta.

Eli jos parametrina on taulukko jossa esim. luvut 5, 6, 7 palauttaa metodi uuden taulukon jonka sisältönä luvut 7, 6, 5. Parametrina oleva taulukko ei saa muuttua.

Seuraavassa esimerkki metodin käytöstä:

    public static void main(String[] args) {
        int[] alkuperainen = {1, 2, 3, 4};
        int[] kaannetty = kaanna(alkuperainen);
        
        // tulostetaan molemmat
        System.out.println( "alkup: " +Arrays.toString(alkuperainen));
        System.out.println( "käännetty: " +Arrays.toString(kaannetty));        
    }

Tulostuksesta pitäisi selvitä, että alkuperäinen taulukko on muuttumaton:

alkup: [1, 2, 3, 4]
käännetty: [4, 3, 2, 1]

Lohkoista ja sisäkkäisistä toistolauseista

Aaltosululla { alkavaa ja aaltosululla } koodia sisältävää aluetta siis sanotaan lohkoksi. Kuten on jo nähty, lohkoja käytetään muun muassa ehto- ja toistolauseiden alueen rajaamisessa. Tärkeä lohkon ominaisuus on se, että lohkossa määritellyt muuttujat ovat voimassa vain lohkon sisällä.

Seuraavassa esimerkissä määritellään ehtolausekkeeseen liittyvän lohkon sisällä tekstimuuttuja lohkonSisallaMaariteltyTeksti, joka on olemassa vain lohkon sisällä. Lohkossa esitellyn muuttujan tulostus lohkon ulkopuolella ei toimi!

int luku = 5;

if( luku == 5 ){ 
  String lohkonSisallaMaariteltyTeksti = "Jea!";
}

System.out.println(lohkonSisallaMaariteltyTeksti); // ei toimi!

Lohkossa voidaan toki käyttää ja muuttaa sen ulkopuolella määriteltyjä muuttujia.

int luku = 5;

if( luku == 5 ) {
  luku = 6;
}

System.out.println(luku); // tulostaa luvun 6

Lohkon sisällä voi olla mitä tahansa koodia. Esimerkiksi for-toistolauseen määrittelemän lohkon sisällä voi olla toinen for-toistolauseke tai vaikkapa while-toistolauseke. Tarkastellaan seuraavaa ohjelmaa:

for(int i = 0; i < 3; i++) {
   System.out.print(i + ": ");

   for(int j = 0; j < 3; j++) {
      System.out.print(j + " ");       
   }

   System.out.println(); 
}

Ohjelman tulostus on seuraava:

0: 0 1 2 
1: 0 1 2 
2: 0 1 2 

Eli mitä ohjelmassa tapahtuukaan? Jos ajatellaan pelkkää ulommaista for:ia, on toiminnallisuus helppo ymmärtää:

for(int i = 0; i < 3; i++) {
   System.out.print(i + ": ");

   // sisemmäinen for

   System.out.println(); 
}

Eli ensin i=0 ja tulostuu 0: ja rivinvaihto. Tämän jälkeen i kasvaa ja tulostuu ykkönen, jne., eli ulompi for saa aikaan seuraavan:

0:
1:
2:

Myös sisempi for on helppo ymmärtää erillään. Se saa aina aikaan tulosteen 0 1 2. Kun yhdistämme nämä kaksi, huomaamme, että sisin for suorittaa tulosteensa aina juuri ennen uloimman for:in tulostamaa rivinvaihtoa.

for-toistolauseen ulkopuolella määriteltyjen muuttujien käyttö toiston ehtona

Tutkitaan seuraavaa muunnosta edelliseen esimerkkiin:

for(int i = 0; i < 3; i++) {
   System.out.print(i + ": ");

   for(int j = 0; j <= i; j++) {
      System.out.print(j + " ");       
   }

   System.out.println(); 
}

Sisemmän for:in toistojen määrä riippuukin nyt ulomman for:in muuttujan i arvosta. Eli kun i=0, tulostaa sisin looppi luvun 0, kun i=1, tulostaa sisempi toistolauseke 0 1, jne. Koko ohjelman tulostus on seuraava:

0: 0 
1: 0 1 
2: 0 1 2

Seuraava ohjelma tulostaa lukujen 1..10 kertotaulun.

for(int i = 1; i <= 10; i++) {

    for(int j = 1; j <= 10; j++) {
        System.out.print(i * j + " ");
    }

    System.out.println();
}

Tulostus näyttää seuraavalta:

1 2 3 4 5 6 7 8 9 10 
2 4 6 8 10 12 14 16 18 20 
3 6 9 12 15 18 21 24 27 30 
4 8 12 16 20 24 28 32 36 40 
5 10 15 20 25 30 35 40 45 50 
6 12 18 24 30 36 42 48 54 60 
7 14 21 28 35 42 49 56 63 70 
8 16 24 32 40 48 56 64 72 80 
9 18 27 36 45 54 63 72 81 90 
10 20 30 40 50 60 70 80 90 100 

Ylimmällä rivillä luvun 1 kertotaulu. Alussa i=1 ja sisimmän loopin muuttuja j saa arvot 1...10. Jokaisella i, j arvoparilla tulostetaan niiden tulo. Eli alussa i=1, j=1, sitten i=1, j=2, ..., i=1, j=10 seuraavaksi i=2, j=1, jne.

Kertotaulu-ohjelman voi toki pilkkoa pienempiin osiin. Voimme määritellä metodit public void tulostaKertotaulunRivi(int kerroin, int montakokertaa) ja public void tulostaKertotaulu(int mihinAsti), jolloin ohjelman rakenne olisi esimerkiksi seuraavanlainen seuraavanlainen:

public class Kertotaulu {

  public void tulosta(int mihinAsti) {
    for(int i = 1; i <= mihinAsti; i++) {
      tulostaKertotaulunRivi(i, mihinAsti);
      
      System.out.println();
    }
  }


  public void tulostaKertotaulunRivi(int kerroin, int montakokertaa) {
    for(int i = 1; i <= montakokertaa; i++) {
        System.out.print(i * kerroin + " ");
    }
  }
}

Nyt kutsu new Kertotaulu().tulosta(5); tulostaa allaolevan kertotaulun.

1 2 3 4 5    
2 4 6 8 10   
3 6 9 12 15  
4 8 12 16 20 
5 10 15 20 25

Taulukko tähtinä

Kirjoita metodi public static void tulostaTaulukkoTahtina(int[] taulukko), joka tulostaa jokaista taulukossa olevaa lukua vastaavan pituisen rivin tähtiä.

Ohjelman runko on seuraava:

public class Main {
  public static void main(String[] args) {
    // Tässä voit testata metodia
    int[] taulukko = {5, 1, 3, 4, 2};
    tulostaTaulukkoTahtina(taulukko);
  }

  public static void tulostaTaulukkoTahtina(int[] taulukko) {
    // Kirjoita tulostuskoodi tänne
  }
}

Edellisen esimerkin mukaisella syötteellä ohjelman tulostus on seuraava:

*****
*
***
****
**

Eli koska taulukon nollannessa paikassa on luku 5, tulee ensimmäiselle riville 5 tähteä. Seuraavalla 1 tähti jne.

Tähtitaivas

Luodaan ohjelma tähtitaivaan tulostamiseen. Tähtitaivaan tähtien määrä kerrotaan tiheyden avulla. Esimerkiksi jos tähtitaivaan tiheys on 0.2, on noin 10% tähtitaivaasta peitettynä tähdillä. Pääset harjoittelemaan siis myös satunnaisuuslukujen käyttöä.

Käytä tähtien tulostamiseen *-merkkiä. Alla on esimerkki lopullisen Tahtitaivas-luokan käytöstä ja käyttöä vastaavasti tulostuksesta.

    Tahtitaivas tahtitaivas = new Tahtitaivas(0.1, 39, 10);
    tahtitaivas.tulosta();
    System.out.println("Tähtiä: " + tahtitaivas.tahtiaViimeTulostuksessa());
    System.out.println("");
    
    tahtitaivas = new Tahtitaivas(0.2, 15, 6);
    tahtitaivas.tulosta();
    System.out.println("Tähtiä: " + tahtitaivas.tahtiaViimeTulostuksessa());
        *     *                  *      
    *             * *         *      ** 
                                     *  
        *       *      *         *  *   
 *     *                     *          
*            * *                   *    
*  * *           *          * *  **     
                            *  *        
          *               *             
     *                             *    
Tähtiä: 36

 * * *     *   
     * *   *   
*     *        
   *  *       *
*       *   * *
* ** **     *  
Tähtiä: 22

Huom! tehtävissä kannattaa käyttää for-lauseketta. Vaikka edellinen luku puhuukin sisäkkäisistä toistolauseista, tässä tehtävässä "sisempi" toisto piilotetaan metodin sisälle.

Tahtitaivas-luokka ja yhden rivin tulostaminen

Luo luokka Tahtitaivas, jolla on kolme oliomuuttujaa: tiheys (double), leveys (int), ja korkeus (int). Luo luokalle myös kolme konstruktoria:

Lisää luokalle Tahtitaivas metodi tulostaRivi, joka tulostaa yhden rivin. Rivin leveyden määrää oliomuuttuja leveys. Oliomuuttuja tiheys kertoo todennäköisyyden tähdelle. Arvo jokaisen merkin kohdalla tulostetaanko tähti vai ei Random-luokan nextDouble-metodin avulla.

Testaa ohjelmaasi, esimerkkinä seuraava kutsu ja esimerkkitulostus.

    Tahtitaivas tahtitaivas = new Tahtitaivas(0.1);
    tahtitaivas.tulostaRivi();
            *  *                  *     

Tähtitaivaan tulostus

Luo Tahtitaivas-luokalle metodi tulosta, joka tulostaa koko tähtitaivaan. Käytä tässä hyödyksesi aiempaa tulostaRivi-metodia.

    Tahtitaivas tahtitaivas = new Tahtitaivas(8, 4);
    tahtitaivas.tulosta();
    *   
        
  *     
    *   

Tähtien laskeminen

Lisää Tahtitaivas-luokalle oliomuuttuja tahtiaViimeTulostuksessa (int) ja metodi tahtiaViimeTulostuksessa(), joka palauttaa viime tulostuksessa tulostuneiden tähtien lukumäärän. Toteuta ohjelmaasi tähtien laskeminen.

    Tahtitaivas tahtitaivas = new Tahtitaivas(8, 4);
    tahtitaivas.tulosta();
    System.out.println("Tähtiä: " + tahtitaivas.tahtiaViimeTulostuksessa());
    System.out.println("");
    
    tahtitaivas.tulosta();
    System.out.println("Tähtiä: " + tahtitaivas.tahtiaViimeTulostuksessa());
        
        
 *      
        
Tähtiä: 1

 *      
      * 
*       
        
Tähtiä: 3

static:illa vai ilman?

Kun aloimme olioden käytön, materiaalissa oli neuvo jättää sana static pois olioiden metodien määrittelystä. Viikkoon 3 asti taas kaikissa metodeissa esiintyi määre static. Mistä on kysymys?

Seuraavassa esimerkissä on metodi nollaaTaulukko joka toimii nimensä mukaisesti eli asettaa nollan parametrina saamansa taulukon kaikkien lokeroiden arvoksi.

public class Ohjelma {

  public static void nollaaTaulukko(int[] taulukko) {
    for ( int i=0; i < taulukko.length; i++ )
      taulukko[i] = 0;
  }

  public static void main(String[] args) {
    int[] luvut = { 1, 2, 3, 4, 5 };

    for ( int luku : luvut ) {
      System.out.print( luku + " " );  // tulostuu 1, 2, 3, 4, 5
    }

    System.out.println();

    nollaaTaulukko(luvut);

    for ( int luku : luvut ) {
      System.out.print( luku + " " );  // tulostuu 0, 0, 0, 0, 0
    }
  }
}

Huomaamme, että metodin määrittelyssä on nyt määre static. Syynä on se, että metodi ei liity mihinkään olioon vaan kyseessä on luokkametodi. Luokkametodeja kutsutaan usein myös staattisiksi metodeiksi. Toisin kuin olioiden metodit (joilla ei ole määrettä static), staattiseen metodiin ei liity mitään olioa. Staattinen metodi ei siis voi viitata this-määreellä olioon itseensä toisin kuin oliometodit.

Staattiselle metodille voi toki antaa olion parametrina. Staattinen metodi ei voi käsitellä mitään muita lukuja, merkkijonoja, taulukoita tai oliota kuin niitä, jotka on sille parametrina annettu. Toisin sanoin, staattisen metodin kutsuja antaa metodille aina käsiteltävät arvot ja oliot.

Koska staattinen metodi ei liity mihinkään olioon, ei sitä kutsuta oliometodien tapaan olionNimi.metodinNimi(), vaan ylläolevan esimerkin tapaan käytetään pelkkää staattisen metodin nimeä.

Jos staattisen metodin koodi on eri luokan sisällä kuin sitä kutsuva metodi, tapahtuu kutsu muodossa LuokanNimi.staattisenMetodinNimi(). Edellinen esimerkki alla muutettuna siten, että pääohjelma ja metodi ovat omissa luokissaan (eli eri tiedostoissa):

public class Ohjelma {
  public static void main(String[] args) {
    int[] luvut = { 1, 2, 3, 4, 5 };

    for ( int luku : luvut ) {
      System.out.print( luku + " " );  // tulostuu 1, 2, 3, 4, 5
    }

    System.out.println();

    TaulukonKasittely.nollaaTaulukko(luvut);

    for ( int luku : luvut ) {
      System.out.print( luku + " " );  // tulostuu 0, 0, 0, 0, 0
    }
  }
}
public class TaulukonKasittely {
  public static void nollaaTaulukko(int[] taulukko) {
    for ( int i=0; i < taulukko.length; i++ ) {
      taulukko[i] = 0;
    }
  }
}

Toisen luokan sisällä määriteltyä staattista metodia kutsutaan nyt muodossa TaulukonKasittely.nollaaTaulukko(parametri);.

Milloin staattisia metodeja tulisi käyttää

Kaikki olion tilaa käsittelevät metodit tulee määritellä normaaleina oliometodeina. Esim. edellisillä viikoilla määrittelemiemme luokkien Henkilo, Paivays, Kello, Joukkue, ... kaikki metodit tulee määritellä normaaleina oliometodeina eli ei static:eina.

Palataan vielä luokkaan Henkilo. Seuraavassa on osa luokan määritelmästä. Kaikkiin oliomuuttujiin viitataan this-määreen avulla sillä korostamme, että metodeissa käsitellään olion "sisällä" olevia oliomuuttujia.

public class Henkilo {
    private String nimi;
    private int ika;

    public Henkilo(String nimi) {
        this.ika = 0;
        this.nimi = nimi;       
    }

    public boolean taysiIkainen(){
        if ( this.ika < 18 ) {
            return false;
        }

        return true;
    }

    public void vanhene() {
        this.ika++;
    }
 
    public String getNimi() {
        return this.nimi;
    }
}

Koska metodit käsittelevät olioa, ei niitä voi määrittää static:eiksi eli "olioon kuulumattomiksi". Jos näin yritetään tehdä, ei metodi toimi:

public class Henkilo {
    //...

    public static void vanhene() {
        this.ika++;
    }
}

Seurauksena on virheilmoitus non static variable ika can not be referenced from static context, joka tarkoittaa että staattinen metodi ei pysty käsittelemään oliomuuttujaa.

Eli milloin staattista metodia sitten kannattaa käyttää? Tarkastellaan luvusta 23 tuttua henkilöolioita käsittelevää esimerkkiä:

public class Ohjelma {
    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka");
        Henkilo antti = new Henkilo("Antti");
        Henkilo juhana = new Henkilo("Juhana");

        for ( int i=0; i < 30; i++ ) {
            pekka.vanhene();
            juhana.vanhene();
        }

        antti.vanhene();

        if ( antti.taysiIkainen() ) {
            System.out.println( antti.getNimi() + " on täysi-ikäinen" );
        } else {
            System.out.println( antti.getNimi() + " on alaikäinen" );
        }

        if ( pekka.taysiIkainen() ) {
            System.out.println( pekka.getNimi() + " on täysi-ikäinen" );
        } else {
            System.out.println( pekka.getNimi() + " on alaikäinen " );
        }

        if ( juhana.taysiIkainen() ) {
            System.out.println( juhana.getNimi() + " on täysi-ikäinen" );
        } else {
            System.out.println( juhana.getNimi() + " on alaikäinen " );
        }
    }
}

Huomaamme, että henkilöiden täysi-ikäisyyden ilmottamiseen liittyy koodinpätkä joka on copy-pastettu peräkkäin kolme kertaa. Todella rumaa!

Henkilön täysi-ikäisyyden ilmoittaminen on mainio kohde staattiselle metodille. Kirjoitetaan ohjelma uudelleen metodia hyödyntäen:

public class Main {

    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka");
        Henkilo antti = new Henkilo("Antti");
        Henkilo juhana = new Henkilo("Juhana");

        for ( int i=0; i < 30; i++ ) {
            pekka.vanhene();
            juhana.vanhene();
        }

        antti.vanhene();

        ilmoitaTaysiIkaisyys(antti);
   
        ilmoitaTaysiIkaisyys(pekka);
   
        ilmoitaTaysiIkaisyys(juhana);
    }

    private static void ilmoitaTaysiIkaisyys(Henkilo henkilo) {
        if ( henkilo.taysiIkainen() ) {
            System.out.println(henkilo.getNimi() + " on täysi-ikäinen");
        } else {
            System.out.println(henkilo.getNimi() + " on alaikäinen");
        }
    }
}        

Metodi ilmoitaTaysiIkaisyys on määritelty staattiseksi, eli se ei liity mihinkään olioon, mutta metodi saa parametrikseen henkilöolion. Metodia ei ole nyt määritelty Henkilö-luokan sisälle sillä vaikka se käsittelee parametrinaan saamaan henkilöolioa, se on juuri kirjoitetun pääohjelman apumetodi, jonka avulla main on saatu kirjoitettua selkeämmin.

Kirjaston tietojärjestelmä

Kumpulan tiedekirjasto tarvitsee uuden järjestelmän kirjojen hallintaan. Tässä tehtävässä toteutetaan prototyyppi, jossa toteutetaan kirjan haku nimen, julkaisijan tai julkaisuvuoden perusteella.

Rakennetaan järjestelmä osista, ensin toteutetaan oleelliset luokat eli Kirja ja Kirjasto. Luokka Kirja sisältää kirjaan liittyvät tiedot, luokka Kirjasto tarjoaa erilaisia hakutoiminnallisuuksia kirjoihin liittyen.

Kirja

Luodaan ensiksi luokka Kirja. Kirjalla on oliomuuttujina nimike, eli kirjan nimi, julkaisija, eli kirjan julkaisija, ja julkaisuvuosi eli vuosi jolloin kirja on julkaistu. Kaksi ensimmäistä muuttujaa on merkkijonotyyppisiä, viimeisin on kokonaisluku. Oletamme tässä että kirjalla on aina vain yksi kirjoittaja.

Toteuta luokka Kirja. Kirjalla tulee olla myös konstruktori public Kirja(String nimike, String julkaisija, int julkaisuvuosi) sekä metodit public String nimike(), public String julkaisija(), public int julkaisuvuosi() ja public String toString(). Arvannet mitä metodien tulee tehdä, alla esimerkki.

Testaa luokan toimintaa:

Kirja cheese = new Kirja("Cheese Problems Solved", "Woodhead Publishing", 2007);
System.out.println(cheese.nimike());
System.out.println(cheese.julkaisija());
System.out.println(cheese.julkaisuvuosi());

System.out.println(cheese);
Cheese Problems Solved
Woodhead Publishing
2007
Cheese Problems Solved, Woodhead Publishing, 2007

Kirjasto

Kirjaston tehtävä on antaa käyttäjälle mahdollisuus kirjojen lisäämiseen ja niiden hakemiseen. Luo luokka Kirjasto, jolla on konstruktori public Kirjasto() ja metodit public void lisaaKirja(Kirja uusiKirja) ja public void tulostaKirjat()

Kirjasto kirjasto = new Kirjasto();

Kirja cheese = new Kirja("Cheese Problems Solved", "Woodhead Publishing", 2007);
kirjasto.lisaaKirja(cheese);

Kirja nhl = new Kirja("NHL Hockey", "Stanley Kupp", 1952);
kirjasto.lisaaKirja(nhl);

kirjasto.lisaaKirja(new Kirja("Battle Axes", "Tom A. Hawk", 1851));

kirjasto.tulostaKirjat();
Cheese Problems Solved, Woodhead Publishing, 2007
NHL Hockey, Stanley Kupp, 1952
Battle Axes, Tom A. Hawk, 1851

Hakutoiminnallisuus

Kirjastosta tulee pystyä etsimään kirjoja nimikkeiden ja julkaisijoiden perusteella. Lisää kirjastolle metodit public ArrayList<Kirja> haeKirjaNimikkeella(String nimike), public ArrayList<Kirja> haeKirjaJulkaisijalla(String julkaisija) ja public ArrayList<Kirja> haeKirjaJulkaisuvuodella(int julkaisuvuosi). Metodit palauttavat listan kirjoista, joissa on haluttu nimike, julkaisija tai julkaisuvuosi.

Huom: joudut siis tehdä metodin jonka paluuarvona on ArrayList. Tämä onnustuu seuraavaa metodirunkoa hyödyntäen:

public class Kirjasto {
   // ...

   public ArrayList<Kirja> haeKirjaNimikkeella(String nimike) {
     ArrayList<Kirja> loydetyt = new ArrayList<Kirja>();

     // käy läpi kaikki kirjat ja lisää ne joilla haetun kaltainen nimike listalle loydetyt

     return loydetyt;  
   }

Huom! Kun haet teet hakua merkkijonon avulla, älä tee tarkkaa hakua (metodi equals) vaan käytä String-luokan metodia contains. Huomaat todennäköisesti myös että sinulla on ns. copy-paste -koodia Kirjasto-luokan koodissa. Keksitkö tavan päästä siitä eroon?

Kirjasto kirjasto = new Kirjasto();

kirjasto.lisaaKirja(new Kirja("Cheese Problems Solved", "Woodhead Publishing", 2007));
kirjasto.lisaaKirja(new Kirja("The Stinky Cheese Man and Other Fairly Stupid Tales", "Penguin Group", 1992));
kirjasto.lisaaKirja(new Kirja("NHL Hockey", "Stanley Kupp", 1952));
kirjasto.lisaaKirja(new Kirja("Battle Axes", "Tom A. Hawk", 1851));

ArrayList<Kirja> hakutulos = kirjasto.haeKirjaNimikkeella("Cheese");
for (Kirja kirja: hakutulos) {
    System.out.println(kirja);
}

System.out.println("---");
for (Kirja kirja: kirjasto.haeKirjaJulkaisijalla("Penguin Group  ")) {
    System.out.println(kirja);
}

System.out.println("---");
for (Kirja kirja: kirjasto.haeKirjaJulkaisuvuodella(1851)) {
    System.out.println(kirja);
}
Cheese Problems Solved, Woodhead Publishing, 2007
The Stinky Cheese Man and Other Fairly Stupid Tales, Penguin Group, 1992
---
---
Battle Axes, Tom A. Hawk, 1851

Paranneltu hakutoiminnallisuus

Hakutoiminnallisuutemme on jo hyvä, mutta se ei ymmärrä isojen ja pienten kirjainten eroa. Yllä olleessa esimerkissä haku nimikkeellä "cheese" ei olisi tuottanut yhtäkään tulosta. Myös toinen esimerkki, jossa oli ylimääräisiä välilyöntejä, ei näyttänyt haluttua tulosta. Haluamme että nimikkeiden ja julkaisijoiden nimillä haettaessa ei välitetä merkkien koosta, ja että käyttäjä voi syöttää ylimääräisiä välilyöntejä kirjan nimen alkuun tai loppuun (meidän ei tarvitse välittää sanojen välillä olevista tyhjistä!). Toteutetaan pieni apukirjasto StringUtils merkkijonojen vertailuun.

Luo luokka StringUtils, ja lisää sille staattinen metodi public static boolean sisaltaa(String sana, String haettava), joka tarkistaa sisältääkö merkkijono sana merkkijonon haettava. Jos jommankumman merkkijonon arvo on null, metodin tulee palauttaa arvo false. Metodin tarjoaman vertailun tulee olla välittämättä merkin koosta.

Lisää metodille sisaltaa myös toiminnallisuus, joka poistaa merkkijonojen sana ja haettava alusta ja lopusta ylimääräiset välilyönnit. Käytä tähän String-luokan metodia trim, esim. trimmattu = trimmattava.trim()

Vinkki! String-luokan metodista toUpperCase() on hyötyä kun haluat verrata ovatko kaksi merkkijonoa samat -- riippumatta niiden alkuperäisestä merkkikoosta.

Kun olet saanut metodin valmiiksi, käytä sitä Kirjasto-luokassa. Alla esimerkki:

if(StringUtils.sisaltaa(kirja.nimike(), haettuNimike)) {
   // kirja löytyi!
}
Kirjasto kirjasto = new Kirjasto();

kirjasto.lisaaKirja(new Kirja("Cheese Problems Solved", "Woodhead Publishing", 2007));
kirjasto.lisaaKirja(new Kirja("The Stinky Cheese Man and Other Fairly Stupid Tales", "Penguin Group", 1992));
kirjasto.lisaaKirja(new Kirja("NHL Hockey", "Stanley Kupp", 1952));
kirjasto.lisaaKirja(new Kirja("Battle Axes", "Tom A. Hawk", 1851));

for (Kirja kirja: kirjasto.haeKirjaNimikkeella("CHEESE")) {
    System.out.println(kirja);
}

System.out.println("---");
for (Kirja kirja: kirjasto.haeKirjaJulkaisijalla("PENGUIN  ")) {
    System.out.println(kirja);
}
Cheese Problems Solved, Woodhead Publishing, 2007
The Stinky Cheese Man and Other Fairly Stupid Tales, Penguin Group, 1992
---
The Stinky Cheese Man and Other Fairly Stupid Tales, Penguin Group, 1992

Tehtäviä joissa saat itse keksiä ohjelmalle sopivan rakenteen

Arvosanajakauma

Tehtävä vastaa kolmea yksiosaista tehtävää.

HUOM: Ohjelmassa saa käyttää vain yhtä Scanner-olioa, eli vain kertaalleen saa sanoa new Scanner. Jos tarvitset Scanneria monessa kohtaa, voit välittää sen muualle parametrina seuraavaan tyyliin:

  public static void main(String[] args) {
     Scanner lukija = new Scanner(System.in);

     // ...
     
     teeJotain(lukija);  
  }

  public static void teeJotain(Scanner lukija) {
     String rivi = lukija.nextLine();
     // ...
  }

Tai jos toinen olio tarvitsee Scanneria, voi sen välittää konstruktoriparametrina ja tallettaa oliomuuttujaan jolloin Scanner on olion kaikkien metodien käytössä.

Ohjelman syöte on joukko kokonaislukuja, jotka kuvaavat opiskelijoiden kokeesta saamia pistemääriä. Käyttäjä syöttää yhden pistemäärän per rivi. Kun syöte on -1, lopettaa ohjelma pistemäärien kyselyn.

Pisteiden syöttö toimii seuraavasti:

Syötä koepisteet, -1 lopettaa:
34
41
53
36
55
27
43
40
-1

Pisteiden syöttämisen jälkeen ohjelma tulostaa kurssin arvosanajakauman ja hyväksymisprosentin seuraavassa muodossa:

Arvosanajakauma:
5: **
4:   
3: ***
2: *
1: *
0: *
Hyväksymisprosentti: 87.5 

Arvosananajakauma muodostetaan seuraavasti:

Hyväksyttyjä ovat muut paitsi arvosanan 0 saaneet. Hyväksyttyjä edellä on siis 7 osallistujaa 8:sta. Hyväksymisprosentti lasketaan kaavalla 100*hyväksytyt/osallistujat.

Lintubongarin tietokanta

HUOM: Ohjelmassa saa käyttää vain yhtä Scanner-olioa, eli vain kertaalleen saa sanoa new Scanner. Jos tarvitset Scanneria monessa kohtaa, voit välittää sen muualle parametrina.

Tehtävä vastaa kolmea yksiosaista tehtävää.

Tässä tehtävässä suunnittelet ja toteutat tietokannan lintubongareille. Tietokanta sisältää lintuja, joista jokaisella on nimi (merkkijono) ja latinankielinen nimi (merkkijono). Tämän lisäksi tietokanta laskee kunkin linnun havaintokertoja.

Ohjelmasi täytyy toteuttaa seuraavat komennot:

Lisäksi virheelliset syötteet pitää käsitellä. (Ks. Simo alla). Tässä vielä esimerkki ohjelman toiminnasta:

? Lisaa
Nimi: Korppi
Latinankielinen nimi: Corvus Corvus
? Lisaa
Nimi: Haukka
Latinankielinen nimi: Dorkus Dorkus
? Havainto
Mikä havaittu? Haukka
? Havainto
Mikä havaittu? Simo
Ei ole lintu!
? Havainto
Mikä havaittu? Haukka
? Tilasto
Haukka (Dorkus Dorkus): 2 havaintoa
Korppi (Corvus Corvus): 0 havaintoa
? Nayta
Mikä? Haukka
Haukka (Dorkus Dorkus): 2 havaintoa
? Lopeta

Huom! Ohjelmasi rakenne on täysin vapaa. Testaamme vain että Tietokanta luokan main-metodi toimii kuten tässä on kuvailtu. Yritä miettiä millaisista luokista ja olioista olisi hyötyä ohjelman toteuttamisessa!

Debuggeri ja muuta hyödyllistä

Ohjelmien muuttuessa monimutkaisemmiksi, tulee virheiden löytämisestäkin koko ajan haastavampaa. NetBeansiin integroitu debuggeri voi olla avuksi virheiden löytämisessä. Seuraavalla screencastilla esitellään debuggerin käyttöä. Screencast esittelee myös miten projekteja voidaan luoda, avata ja sulkea sekä miten ohjelmia voidaan suorittaa NetBeansin ulkopuolella.

Taulukon järjestäminen

Palaamme jälleen taulukkojen pariin.

Taulukon järjestäminen Javan valmiilla työkaluilla.

Kuten olemme nähneet, Javassa on valmiina paljon kaikenlaista hyödyllistä. Esimerkiksi ArrayListin käsittelyyn löytyi useita hyödyllisiä apumetodeja luokasta Collections. Taulukoille löytyy vastaavia apuvälineitä luokasta Arrays. Taulukon saa järjestettyä komennolla Arrays.sort(taulukko).

Huom: Komennon käyttäminen vaatii, että ohjelmatiedoston yläosassa lukee seuraava määrittely:

import java.util.Arrays;

Jos unohdat import-rivin, NetBeans tarjoaa apua sen kirjoittamiseen. Kokeile klikata punaisella alleviivatun koodirivin vasempaan laitaan ilmestyvää lampun kuvaa.

Seuraava ohjelma luo taulukon ja järjestää taulukossa olevat luvut Arrays.sort -komennon avulla.

    int[] luvut = {-3, -111, 7, 42};
    Arrays.sort(luvut);
    for(int luku: luvut) {
        System.out.println(luku);
    } 
-111
-3
7
42

Järjestämisalgoritmin toteuttaminen

Taulukon järjestäminen on helppoa Javan valmiin kaluston avulla. Ohjelmoijan yleissivistykseen kuuluu kuitenkin ainakin yhden järjestämisalgoritmin (eli tavan järjestää taulukko) tuntemus. Tutustutaan yhteen "klassiseen" järjestämisalgoritmiin, valintajärjestämiseen. Tutustuminen tapahtuu harjoitustehtävien avulla.

Järjestäminen

Huom: tässä tehtävässä on tarkoitus järjestää taulukko itse. Et saa käyttää Arrays.sort()-metodia tai ArrayListejä apunasi!

Pienin

Tee metodi pienin, joka palauttaa taulukon pienimmän luvun.

Metodin runko on seuraava:

public static int pienin(int[] taulukko) {
    // kirjoita koodia tähän
}

HUOM: parametrina olevaa taulukkoa ei saa muuttaa!

Seuraava koodi esittelee metodin toimintaa:

int[] luvut = {6, 5, 8, 7, 11};
System.out.println("Pienin: " + pienin(luvut));
Pienin: 5

Pienimmän indeksi

Tee metodi pienimmanIndeksi, joka palauttaa taulukon pienimmän luvun indeksin (eli luvun kohdan taulukossa).

Metodin runko on seuraava:

public static int pienimmanIndeksi(int[] taulukko) {
    // kirjoita koodia tähän
}

HUOM: parametrina olevaa taulukkoa ei saa muuttaa!

Seuraava koodi esittelee metodin toimintaa:

// indeksit:   0  1  2  3  4
int[] luvut = {6, 5, 8, 7, 11};
System.out.println("Pienimmän indeksi: " + pienimmanIndeksi(luvut));
Pienimmän indeksi: 1

Taulukon pienin luku on 2, ja sen indeksi eli sijaintipaikka taulukossa on 1. Muistathan, että taulukon numerointi alkaa 0:sta.

Pienimmän indeksi taulukon loppuosassa

Tee metodi pienimmanIndeksiAlkaen, joka toimii samalla tavalla kuin edellisen tehtävän metodi mutta ottaa huomioon vain taulukon loppuosan jostain indeksistä alkaen. Metodille annetaan taulukon lisäksi aloitusindeksi, josta lähtien pienintä lukua etsitään.

Metodin runko on seuraava:

public static int pienimmanIndeksiAlkaen(int[] taulukko, int aloitusIndeksi) {
    // kirjoita koodia tähän
}

HUOM: parametrina olevaa taulukkoa ei saa muuttaa!

Seuraava koodi esittelee metodin toimintaa:

// indeksit:    0  1  2  3   4
int[] luvut = {-1, 6, 9, 8, 12};
System.out.println(pienimmanIndeksiAlkaen(luvut, 1));
System.out.println(pienimmanIndeksiAlkaen(luvut, 2));
System.out.println(pienimmanIndeksiAlkaen(luvut, 4));
1
3
4	

Esimerkissä ensimmäinen metodikutsu etsii pienimmän luvun indeksin aloittaen indeksistä 1. Indeksistä 1 alkaen pienin luku on 6, ja sen indeksi on 1. Vastaavasti toinen metodikutsu etsii pienimmän luvun indeksiä indeksistä 2 aloittaen. Tällöin pienin luku on 8, ja sen indeksi on 3. Viimeinen kutsu etsii pienimmän luvun indeksiä taulukon viimeisestä indeksistä eli kohdasta 4 aloittaen. Tällöinen muita paikkoja ei ole, joten pienin on paikassa 4.

Lukujen vaihtaminen

Tee metodi vaihda, jolle annetaan taulukko ja kaksi sen indeksiä. Metodi vaihtaa indekseissä olevat luvut keskenään.

Metodin runko on seuraava:

public static void vaihda(int[] taulukko, int indeksi1, int indeksi2) {
    // kirjoita koodia tähän
}

Seuraavassa estellään metodin toimintaa. Taulukon tulostamisessa käytetään apuna taulukon merkkijonoksi muotoilevaa Arrays.toString-metodia:

int[] luvut = {3, 2, 5, 4, 8};	

System.out.println( Arrays.toString(luvut) );

vaihda(luvut, 1, 0);
System.out.println( Arrays.toString(luvut) );

vaihda(luvut, 0, 3);
System.out.println( Arrays.toString(luvut) );
[3, 2, 5, 4, 8]
[2, 3, 5, 4, 8]
[4, 3, 5, 2, 8]

Järjestäminen

Nyt koossa on joukko hyödyllisiä metodeja, joiden avulla voimme toteuttaa järjestämisalgoritmin nimeltä vaihtojärjestäminen.

Vaihtojärjestämisen idea on seuraava:

Toisin sanoen:

Toteuta metodi jarjesta, joka perustuu yllä olevaan ideaan. Metodissa on syytä olla silmukka, joka käy läpi taulukon indeksejä. Metodeista pieninIndeksiAlkaen ja vaihda on varmasti hyötyä. Tulosta myös taulukon sisältö ennen järjestämistä ja jokaisen kierroksen jälkeen, jotta voit varmistaa algoritmin toimivan oikein.

Metodin runko on seuraava:

public static void jarjesta(int[] taulukko) {
}

Testaa metodin toimintaa ainakin seuraavalla esimerkillä:

int[] luvut = {8, 3, 7, 9, 1, 2, 4};
jarjesta(luvut);

Ohjelman tulosteen tulisi olla seuraavanlainen. Huomaa että sinun tulee tulostaa taulukon sisältö jokaisen vaihtamisen jälkeen!

[8, 3, 7, 9, 1, 2, 4]
[1, 3, 7, 9, 8, 2, 4]
[1, 2, 7, 9, 8, 3, 4]
[1, 2, 3, 9, 8, 7, 4]
[1, 2, 3, 4, 8, 7, 9]
[1, 2, 3, 4, 7, 8, 9]
[1, 2, 3, 4, 7, 8, 9]

Huomaat, miten taulukko tulee pikkuhiljaa järjestykseen alkaen alusta ja edeten loppua kohti.

Etsintä

Järjestämisen lisäksi toinen hyvin tyypillinen ongelma johon ohjelmoija törmää, on tietyn arvon etsiminen taulukosta. Olemme aiemmin jo toteuttaneet metodeja, jotka etsivät arvoja listoista ja taulukoista. Taulukkojen tapauksessa lukuja ja merkkijonoja voi etsiä seuraavasti:

  public static boolean onkoTaulukossa(int[] taulukko, int etsittava) {
    for ( int luku : taulukko ) {
       if ( luku == etsittava )  {
           return true;
       }
    }    

    return false;
  }

  public static boolean onkoSanaTaulukossa(String[] taulukko, String etsittava) {
    for ( String sana: taulukko ) {
       if ( sana.equals(etsittava) )  {
           return true;
       }
    }    

    return false;
  }

Tämänkaltainen toteutus on paras mihin olemme tähän asti pystyneet. Metodin huono puoli on se, että jos taulukossa on hyvin suuri määrä lukuja, kuluu etsintään paljon aikaa. Metodi käy läpi pahimmassa tapauksessa jokaisen taulukossa olevan alkion. Tämä tarkoittaa sitä, että taulukon, jossa on esimerkiksi 16777216 alkiota, läpikäynti vaatii 16777216 alkion tutkiskelua.

Toisaalta, jos taulukossa olevat luvut ovat suuruusjärjestyksessä, voidaan etsiminen tehdä huomattavasti tehokkaammin soveltamalla tekniikkaa nimeltään binäärihaku. Tutkitaan binäärihaun ideaa seuraavan taulukon kautta:

// indeksit   0   1   2   3    4   5    6   7   8   9  10
// luvut     -7  -3   3   7   11  15   17  21  24  28  30

Oletetaan että haluamme löytää luvun 17. Hyödynnetään tietoa siitä että taulukon arvot ovat järjestyksessä sen sijaan, että kävisimme taulukon lukuja läpi taulukon alusta lähtien. Tutkitaan taulukon keskimmäistä alkiota. Taulukon puolessa välissä olevan alkion indeksi on isoin indeksi 10 jaettuna kahdella eli 5. Keskimmäinen alkio on merkattu seuraavaan tähdellä:

                                   * 
// indeksit   0   1   2   3    4   5    6   7   8   9  10
// luvut     -7  -3   3   7   11  15   17  21  24  28  30

Puolessa välissä on luku 15, joka ei ollut hakemamme luku. Etsimme lukua 17, joten koska taulukon alkiot ovat suuruusjärjestyksessä, ei etsitty luku voi missään tapauksessa olla luvun 15 vasemmalla puolella. Voimme siis päätellä että kaikki indeksit, jotka ovat pienempiä tai yhtäsuuria kuin 5, eivät missään nimessä sisällä hakemaamme arvoa.

Alue, jolta etsimme haettavaa lukua voidaan nyt rajata lukuihin, jotka sijaitsevat indeksin 5 oikealla puolella, eli indekseihin välillä [6, 10] (6, 7, 8, 9, 10). Seuraavassa on merkitty harmaalla se osa taulukkoa jossa etsitty ei voi olla:

// indeksit    0   1   2   3   4    5    6   7   8   9  10
// luvut      -7  -3   3   7  11   15   17  21  24  28  30

Tutkitaan seuraavaksi jäljellä olevan etsintäalueen, eli indeksien 6-10 keskimmäistä indeksiä. Keskimmäinen indeksi löytyy ottamalla etsintäalueen pienimmän ja suurimman indeksin summa ja jakamalla se kahdella, eli (6+10)/2 = 16/2 = 8. Indeksi 8 on merkitty alle tähdellä.

                                                 *
// indeksit    0   1   2   3   4    5    6   7   8   9  10
// luvut      -7  -3   3   7  11   15   17  21  24  28  30

Indeksissä 8 oleva luku on 24, joka ei ollut hakemamme luku. Koska luvut taulukossa ovat suuruusjärjestyksessä, ei etsittävä luku voi missään nimessä olla luvun 24 oikealla puolella. Voimme siis päätellä että kaikki indeksit, jotka ovat suurempia tai yhtäsuuria kuin 8, eivät missään nimessä sisällä hakemaamme arvoa. Etsintäalue rajautuu taas, harmaat alueet on käsitelty:

// indeksit    0   1   2   3   4    5    6   7   8   9  10
// luvut      -7  -3   3   7  11   15   17  21  24  28  30

Etsintä jatkuu. Tutkitaan jäljellä olevan etsintäalueen, eli indeksien 6-7, keskimmäistä indeksiä. Keskimmäinen indeksi löytyy taas ottamalla etsintäalueen pienimmän ja suurimman indeksin summa ja jakamalla se kahdella, eli (6+7)/2 = 6,5, joka pyöristyy alaspäin luvuksi 6. Kohta on merkitty alle tähdellä.

                                         *
// indeksit    0   1   2   3   4    5    6   7   8   9  10
// luvut      -7  -3   3   7  11   15   17  21  24  28  30

Indeksissä 6 on luku 17, joka on sama kuin hakemamme luku. Voimme lopettaa haun ja ilmoittaa että etsitty luku on taulukossa. Jos luku ei olisi ollut taulukossa -- esimerkiksi jos haettava luku olisi ollut 16, etsintäalue olisi jäänyt lopulta tyhjäksi.

                                         *
// indeksit    0   1   2   3   4    5    6   7   8   9  10
// luvut      -7  -3   3   7  11   15   17  21  24  28  30

Jotta binäärihaun idea tulee sinulle tutuksi, simuloi kynällä ja paperilla miten binäärihaku toimii kun taulukkona on alla oleva taulukko ja haet ensin lukua 33, sitten lukua 1.

// indeksit   0   1   2   3   4   5   6   7   8   9  10  11  12  13
// luvut     -5  -2   3   5   8  11  14  20  22  26  29  33  38  41

Binäärihaun avulla haemme alkiota puolittamalla aina tutkittavan alueen kahteen osaan. Tämä mahdollistaa hyvin tehokkaan hakemisen. Esimerkiksi taulukko, jossa on 16 alkiota, voidaan jakaa kahteen osaan korkeintaan 4 kertaa, eli 16 -> 8 -> 4 -> 2 -> 1. Toisaalta, taulukko, jossa on 16777216 alkiota, voidaan jakaa kahteen osaan korkeintaan 24 kertaa. Tämä tarkoittaa sitä, että binäärihaun avulla 16777216-alkioisessa taulukossa tarvitsee tarkastella korkeintaan 24 alkiota haetun alkion löytymiseksi.

Binäärihaun tehokkuutta voi tutkia logaritmien avulla. Kaksikantainen logaritmi (log2) luvusta 16777216 on 24 -- voimme siis laskea kaksikantaisen logaritmin avulla kuinka monta kertaa jonkun luvun voi puolittaa. Vastaavasti luvun 4294967296 kaksikantainen logaritmi, (log2 4294967296) on 32. Tämä tarkoittaa että 4294967296 eri arvoa sisältävästä järjestyksessä olevasta taulukosta hakeminen vaatisi korkeintaan 32 eri alkion tarkastamista. Tehokkuus on oleellinen osa tietojenkäsittelytiedettä. Esimerkiksi tietojenkäsittelytieteen ensimmäisen vuoden kurssi Tietorakenteet keskittyy tehokkaiden tietorakenteiden toteuttamiseen.

Arvauspeli

Tehdään tässä tehtävässä tekoäly, joka arvaa pelaajan ajatteleman luvun. Tekoäly olettaa, että luku on välillä alaraja...yläraja. Pelin käynnistäjä antaa nämä rajat pelin toteuttavalle metodille parametrina. Tekoäly kysyy käyttäjältä kysymyksiä muotoa "Onko lukusi suurempi kuin X?" ja päättelee oikean luvun käyttäjän vastausten perusteella.

Tekoäly pitää kirjaa hakualueesta muuttujien alaraja ja yläraja avulla. Tekoäly kysyy aina, onko käyttäjän luku suurempi kuin näiden lukujen keskiarvo, jolloin vastauksen perusteella hakualue aina puolittuu. Lopulta alaraja ja yläraja ovat samat, ja käyttäjän ajattelema luku on paljastunut.

Seuraavassa esimerkissä käyttäjä valitsee luvun 44:

Ajattele jotain lukua väliltä 1...100.
Lupaan pystyä arvaamaan ajattelemasi luvun 7 kysymyksellä.

Esitän sinulle seuraavaksi sarjan kysymyksiä. Vastaa niihin rehellisesti.

Onko lukusi suurempi kuin 50? (k/e)
e
Onko lukusi suurempi kuin 25? (k/e)
k
Onko lukusi suurempi kuin 38? (k/e)
k
Onko lukusi suurempi kuin 44? (k/e)
e
Onko lukusi suurempi kuin 41? (k/e)
k
Onko lukusi suurempi kuin 43? (k/e)
k
Valitsemasi luku on 44.

Yllä olevassa esimerkissä mahdollinen lukualue on aluksi 1...100. Kun käyttäjä kertoo, että luku ei ole yli 50, mahdollinen lukualue on 1...50. Kun käyttäjä kertoo, että luku on yli 25, mahdollinen lukualue on 26...50. Samanlainen päättely jatkuu, kunnes saavutaan lukuun 44.

Puolitus- eli binäärihaun mukaisesti tässä puolitetaan mahdollinen hakualue jokaisella kysymyksellä, jolloin kysymyksiä tarvitaan vähän. Jopa lukuvälillä 1..1000000 pitäisi kulua korkeintaan 20 kysymystä.

Pelin toteuttavan luokan Arvauspeli runko on seuraavanlainen:

public class Arvauspeli {

    private Scanner lukija;

    public Arvauspeli() {
        this.lukija = new Scanner(System.in);
    }

    public void pelaa(int alaraja, int ylaraja) {
        tulostaOhjeet(ylaraja, alaraja);

        // Kirjoita pelin koodi tänne        
    }

    // tee metodit onkoSuurempiKuin ja keskiarvo tänne

    public void tulostaOhjeet(int ylaraja, int alaraja) {
        int kysymyksiaKorkeintaan = kuinkaMontaKertaaVoiJakaaKahteen(ylaraja - alaraja);

        System.out.println("Ajattele jotain lukua väliltä " + alaraja + "..." + ylaraja + ".");

        System.out.println("Lupaan pystyä arvaamaan ajattelemasi luvun " + kysymyksiaKorkeintaan + " kysymyksellä.");
        System.out.println("");
        System.out.println("Esitän sinulle seuraavaksi sarjan kysymyksiä. Vastaa niihin rehellisesti.");
        System.out.println("");
    }

    // apumetodi 
    public int kuinkaMontaKertaaVoiJakaaKahteen(int luku) {
        // luodaan kaksikantainen logaritmi annetusta luvusta, logaritmeista
        // löytyy lisää tietoa mm. osoitteessa

        // http://www02.oph.fi/etalukio/pitka_matematiikka/kurssi8/maa8_teoria7.html

        // Alla vaihdamme kantalukua alkuperäisestä kaksikantaisiin logaritmeihin!
        return (int) (Math.log(luku) / Math.log(2)) + 1;
    }
}

Peli käynnistetään seuraavasti:

    // luodaan peli-olio
    Arvauspeli peli = new Arvauspeli();

    // pelataan pari kierrosta
    peli.pelaa(1,10);  // arvattava luku nyt välillä 1-10
    peli.pelaa(10,99);  // arvattava luku nyt välillä 10-99

Toteutetaan tämä tehtävä askeleittain.

Onko suurempi kuin

Toteuta luokalle Arvauspeli metodi public boolean onkoSuurempiKuin(int luku), joka esittää käyttäjälle kysymyksen:

"Onko lukusi suurempi kuin annettu luku? (k/e)"

Metodi palauttaa arvon true jos käyttäjä vastaa "k", muulloin false.

Huom: metodin tulee lukea käyttäjän syöte luokan Arvauspeli oliomuuttujassa this.lukija olevaa lukijaa hyväksikäyttäen. Metodissa ei saa luoda uutta lukijaa!

Testaa metodisi toimintaa

    Arvauspeli peli = new Arvauspeli();
  
    System.out.println(peli.onkoSuurempiKuin(32));
    System.out.println(peli.onkoSuurempiKuin(99));
Onko lukusi suurempi kuin 32? (k/e)
k
true
Onko lukusi suurempi kuin 99? (k/e)
e
false

Keskiarvo

Toteuta luokalle Arvauspeli metodi public int keskiarvo(int ekaLuku, int tokaLuku), joka laskee annettujen lukujen keskiarvon. Huomaa että Java pyöristää liukuluvut luvut automaattisesti alaspäin, tämä on meidän tapauksessamme täysin toivottavaa.

    Arvauspeli peli = new Arvauspeli();  

    System.out.println(peli.keskiarvo(3, 4));
    System.out.println(peli.keskiarvo(6, 12));
3
9

Arvauslogiikka

Kirjoita varsinainen arvauslogiikka luokan Arvauspeli metodin public void pelaa(int alaraja, int ylaraja) runkoon. Tarvitset ainakin toistolauseen, sekä kyselyn jossa kysyt onko käyttäjän ajattelema luku suurempi kuin ala- ja ylärajan keskiarvo. Käytä edellisten kohtien metodeja kyselyyn ja keskiarvon selvittämiseen. Muokkaa toistolauseessa ala- tai ylärajaa käyttäjän vastauksesta riippuen.

Jatka toistolausekkeen toistoa kunnes alaraja ja yläraja ovat samat! Voit testata peliä myös pienemmillä ala- ja ylärajan arvoilla:

Ajattele jotain lukua väliltä 1...4.
Lupaan pystyä arvaamaan ajattelemasi luvun 2 kysymyksellä.

Esitän sinulle seuraavaksi sarjan kysymyksiä. Vastaa niihin rehellisesti.

Onko lukusi suurempi kuin 2? (k/e)
k
Onko lukusi suurempi kuin 3? (k/e)
k
Ajattelemasi luku on 4.

Binäärihaun toteutus

Testiautomaatista tulevassa rungossa on pohja binäärihaun toteutukselle. Luokka BinaariHaku sisältää metodin public static boolean hae(int[] taulukko, int etsittavaLuku), jonka tehtävänä on selvittää binäärihakua käyttäen, onko parametrina annettu luku parametrina annetussa järjestyksessä olevassa taulukossa.

Metodi hae ei kuitenkaan toimi vielä. Viimeistele metodin toteutus oikeaksi binäärihauksi.

Ohjelman testausta varten on erillinen pääohjelma luokassa Main, jonka runko on seuraava:


import java.util.Arrays;
import java.util.Scanner;

public class Main {
  public static void main(String[] args) {
    // Tässä voit testata binäärihakua
    int[] taulukko = { -3, 2, 3, 4, 7, 8, 12 };
    Scanner lukija = new Scanner(System.in);

    System.out.print("Taulukon luvut: " + Arrays.toString(taulukko));
    System.out.println();

    System.out.print("Anna haettava luku: ");
    String etsittavaLuku = lukija.nextLine();
    System.out.println();

    boolean tulos = BinaariHaku.hae(taulukko, Integer.parseInt(etsittavaLuku));

    // Tulosta tässä binäärihaun tulos
  }
}

Ohjelman suoritus näyttää seuraavalta:

Taulukon luvut: [-3, 2, 3, 4, 7, 8, 12]

Anna haettava luku: 8

Luku 8 on taulukossa
Taulukon luvut: [-3, 2, 3, 4, 7, 8, 12]

Anna haettava luku: 99

Luku 99 ei ole taulukossa

Taulukoista ja olioista

Jos tarvetta on, taulukkoon voi luonnollisesti laittaa minkä tahansa tyyppisen olion. Seuraavassa esimerkki taulukosta johon talletetaan Henkilo-olioita:

    public static void main(String[] args) {
        Henkilo[] henkilot = new Henkilo[3];

        henkilot[0] = new Henkilo("Pekka");
        henkilot[1] = new Henkilo("Antti");
        henkilot[2] = new Henkilo("Juhana");

        for ( int i=0; i < 30; i++ ) {
            henkilot[0].vanhene();
            henkilot[1].vanhene();
            henkilot[2].vanhene();
        }

        for ( Henkilo henkilo : henkilot ) {       
            ilmoitaTaysiIkaisyys(henkilo);
        }
    }

Alussa luodaan taulukko johon mahtuu 3 henkilöolioa. Laitetaan Pekka lokeroon 0, Antti lokeroon 1 ja Juhana lokeroon 2. Vanhennetaan kaikkia 30 vuotta ja tarkastetaan kaikkien täysi-ikäisyys edellisen luvun metodia hyödyntäen.

Sama esimerkki ArrayListien avulla:

    public static void main(String[] args) {
        ArrayList<Henkilo> henkilot = new ArrayList<Henkilo>();

        henkilot.add( new Henkilo("Pekka") );
        henkilot.add( new Henkilo("Antti") );
        henkilot.add( new Henkilo("Juhana") );

        for ( int i=0; i < 30; i++ ) {
            for ( Henkilo henkilo : henkilot ) {
                henkilo.vanhene();
            }

            // tai henkilot.get(0).vanhene();
            //     henkilot.get(1).vanhene();
            //     ...
        }

        for ( Henkilo henkilo : henkilot ) {
            ilmoitaTaysiIkaisyys(henkilo);
        }
    }

Useimmissa sovellustilanteissa taulukon sijaan kannattaa käyttää ArrayListiä. Voi kuitenkin olla joitain tilanteita joissa taulukko riittää ja on hieman yksinkertaisempi käyttää.

Viikko koostuu aina seitsemästä päivästä. Viikko-olio olisikin mielekästä koostaa tasan seitsemästä päiväoliosta. Koska päiva-olioita on aina täsmälleen 7, sopii taulukko tilanteeseen hyvin:

public class Paiva {
    private String nimi;
    // ...
}

public class Viikko {
    private Paiva[] paivat;
    
    public Viikko(){
        paivat = new Paiva[7];
        paivat[0] = new Paiva("Maanantai");
        paivat[1] = new Paiva("Tiistai"); 
        // ...
    }
}

Kurssipalaute

Kurssipalaute

Olemme saaneet paljon arvokasta palautetta TMC:n kautta. Näin kurssin viimeisenä kysymyksenä haluaisimme koko kurssin sisältöä koskevan palautteen. Anna palaute täyttämällä täältä löytyvälomake. Palaute on anonyymi.

Jotta saat merkatuksi tämän tehtävän, aja tehtävän TMC-testit ja lähetä tehtävä palvelimelle.

Hylätty materiaali

Ongelman ratkaiseminen paloittain

Käydään vielä kurssin lopuksi läpi esimerkki jossa tehdään ohjelma seuraavaan tilanteeseen jossa käyttäjältä kysellään sanoja, kunnes käyttäjä antaa saman sanan uudestaan. Tehdään oletus, että käyttäjä antaa korkeintaan 1000 sanaa.

Anna sana: porkkana
Anna sana: selleri
Anna sana: nauris
Anna sana: lanttu
Anna sana: selleri
Annoit saman sanan uudestaan!

Osin ongelmien syy saattoi olla, että oli vaikea päättää miten lähestyä tehtävää, eli miten jäsentää ongelmaa ja mistä aloittaa.

Ongelma koostuu oikeastaan kahdesta "aliongelmasta". Ensimmäinen on sanojen toistuva lukeminen käyttäjältä kunnes tietty ehto toteutuu. Tämä voitaisiin hahmotella seuraavaan tapaan ohjelmarungoksi:

public static void main(String[] args) {
  while ( true ) {
    String sana = lukija.nextLine();

    if ( "pitää lopettaa" ) {
      break;
    }
  }
   
  System.out.println("Annoit saman sanan uudestaan");
}

Sanojen kysely pitää lopettaa siinä vaiheessa kun on syötetty jokin jo aiemmin syötetty sana. Päätetään tehdä metodi, joka huomaa että sana on jo syötetty. Vielä ei tiedetä miten metodi kannattaisi tehdä, joten tehdään siitä vasta runko:

public static void main(String[] args) {
   while ( true ) {
      String sana = lukija.nextLine();
  
      if ( onJoSyotetty( sana ) ) {
        break;
      }
   }

   System.out.println("Annoit saman sanan uudestaan");
}

public static boolean onJoSyotetty(String sana) {
   // tänne jotain
}

Ohjelmaa on hyvä testata koko ajan, joten tehdään metodista kokeiluversio:

public static boolean onJoSyotetty(String sana) {
  if ( sana.equals("loppu") ) {
     return true;
  }

  return false;
}

Eli toisto jatkuu nyt niin kauan kunnes syötteenä on sana loppu:

Anna sana: porkkana
Anna sana: selleri
Anna sana: nauris
Anna sana: lanttu
Anna sana: loppu
Annoit saman sanan uudestaan!

Ohjelma ei siis toimi vielä kokonaisuudessaan, mutta ensimmäinen osaongelma eli ohjelman pysäyttäminen kunnes tietty ehto toteutuu on saatu toimimaan.

Toinen osaongelma on aiemmin syötettyjen sanojen muistaminen. Taulukko sopii tietysti mainiosti tähän tarkoitukseen. Tarvitaan myös muuttuja joka muistaa kuinka monta sanaa on syötetty:

public static void main(String[] args) {
   String[] aiemmatSanat = new String[1000];
   int sanojaSyotetty = 0;

   // ...
}

Kun uusi sana syötetään, on se lisättävä syötettyjen sanojen joukkoon. Tämä tapahtuu lisäämällä main:in while-silmukkaan taulukkoa ja sanojen laskuria päivittävät rivit:

   while ( true ) {
      String sana = lukija.nextLine();
  
      if ( onJoSyotetty( sana, aiemmatSanat, sanojaSyotetty ) ) {
        break;
      }

      // lisätään uusi sana aiempien sanojen taulukkoon
      aiemmatSanat[sanojaSyotetty] = sana;
      sanojaSyotetty++;
   }

Jälleen kannattaa testata, että ohjelma toimii edelleen. Voi olla hyödyksi esim. lisätä ohjelman loppuun testitulostus, joka varmistaa että syötetyt sanat todella menivät taulukkoon:

   while ( true ) {
      // ...

      // lisätään uusi sana aiempien sanojen taulukkoon
      aiemmatSanat[sanojaSyotetty] = sana;
      sanojaSyotetty++;
   }

   // testitulostus joka varmistaa että kaikki toimii edelleen
   for ( int i=0; i < sanojaSyotetty; i++ ) {
     System.out.println( aiemmatSanat[sanojaSyotetty] );
   }

Muokataan vielä äsken tekemämme metodi onJoSyotetty tutkimaan onko kysytty sana jo syötettyjen joukossa, eli taulukon käytössä olevassa alkuosassa:

public static boolean onJoSyotetty(String sana, String[] aiemmatSanat, int sanojaSyotetty) {
   for ( int i=0; i < sanojaSyotetty; i++ ) {
      if ( sana.equals( aiemmatSanat[i] ) ) 
         return true;
   }

   return false;
}

public static void main(String[] args) {
   String[] aiemmatSanat = new String[1000];
   int sanojaSyotetty = 0;

   while ( true ) {
      String sana = lukija.nextLine();
  
      if ( onJoSyotetty( sana, aiemmatSanat, sanojaSyotetty ) ) 
        break;

      aiemmatSanat[sanojaSyotetty] = sana;
      sanojaSyotetty++;
   }

   System.out.println("Annoit saman sanan uudestaan");
}

Nyt koodi on valmis ja alkaa olla kohtuullisen luettava. Emme kuitenkaan ole koodiin täysin tyytyväisiä. Valitettavasti emme vielä osaa tarpeeksi Javaa tehdäksemme "lopullista" siistiä versiota. Ensi viikolla kuitenkin otamme ratkaisevan askeleen jolla saamme koodin siistiksi.

Eli ohjelmoidessasi, seuraa aina näitä neuvoja:

Hyvät ja kokeneet ohjelmoijat noudattavat näitä käytänteitä sen takia että ohjelmointi olisi helpompaa, ja että ohjelmien lukeminen, ylläpitäminen ja muokkaaminen olisi helpompaa.

Koodissa on käsite! Tehdään siitä olio!

Rakensimme äsken askel askeleelta ratkaisun seuraavaan tehtävään:

Tee ohjelma, joka kysyy käyttäjältä sanoja, kunnes käyttäjä antaa saman sanan uudestaan. Voit olettaa, että käyttäjä antaa korkeintaan 1000 sanaa.

Anna sana: porkkana
Anna sana: selleri
Anna sana: nauris
Anna sana: lanttu
Anna sana: selleri
Annoit saman sanan uudestaan!

Päädyimme ratkaisuun

public static boolean onJoSyotetty(String sana, String[] aiemmatSanat, int sanojaSyotetty) {
   for ( int i=0; i < sanojaSyotetty; i++ ) {
      if ( sana.equals( aiemmatSanat[i] ) ) 
         return true;
   }

   return false;
}

public static void main(String[] args) {
   String[] aiemmatSanat = new String[1000];
   int sanojaSyotetty = 0;

   while ( true ) {
      String sana = lukija.nextLine();
  
      if ( onJoSyotetty( sana, aiemmatSanat, sanojaSyotetty ) ) 
        break;

      aiemmatSanat[sanojaSyotetty] = sana;
      sanojaSyotetty++;
   }

   System.out.println("Annoit saman sanan uudestaan");
}

Totesimme, että vaikka ratkaisu alkaa olla ymmärrettävyydeltään ihan kohtuullinen, jokin siinä edelleen jäi vaivaamaan.

Pääohjelman käyttämät apumuuttujat, eli taulukko aiemmatSanat ja kokonaisluku sanojaSyotetty ovat todella ikäviä "matalan tason" detaljeja pääohjelman kannalta. Pääohjelman kannaltahan on oleellista, että muistetaan niiden sanojen joukko jotka on nähty jo aiemmin. Sanojen joukko, tämähän on selkeä erillinen "käsite", tai abstraktio. Tälläiset selkeät käsitteet ovat potentiaalisia olioita ja huomattuaan koodissaan "käsitteen" viisas ohjelmoija eristää sen luokaksi.

Eli päätämme tehdä luokan Sanajoukko. Haluaisimme, että pääohjelma näyttää seuraavalta:

    public static void main(String[] args) {
        Sanajoukko aiemmatSanat = new Sanajoukko();

        while (true) {
            String sana = lukija.nextLine();

            if ( aiemmatSanat.sisaltaa(sana) ) {
                break;
            }

            aiemmatSanat.lisaa(sana);
        }

        System.out.println("Annoit saman sanan uudestaan");
    }

Pääohjelman, eli sanajoukon käyttäjän kannalta Sanajoukko olisi hyvä jos sillä olisi metodit boolean sisaltaa(String sana) jolla tarkastetaan sisältyykö annettu sana jo sanajoukkoon ja void lisaa(String sana) jolla annettu sana lisätään joukkoon.

Huomaamme, että näin kirjoitettuna pääohjelman luettavuus on huomattavasti parempi kuin taulukkoa käyttäen. Pääohjelma on lähes suomen kieltä!

Luokan Sanajoukko runko näyttää seuraavanlaiselta:

public class Sanajoukko {
    // sopivia oliomuuttujia 	

    public Sanajoukko() {
       // ...
    }

    public boolean sisaltaa(String sana) {
       // ...
    }

    public void lisaa(String sana) {
       // ...
    }
}

Voimme toteuttaa sanajoukon siirtämällä aiemman ratkaisumme taulukon sanajoukon oliomuuttujaksi:

public class Sanajoukko {
    private String[] joukonSanat;
    private int sanojaSyotetty;

    public Sanajoukko() {
        joukonSanat = new String[1000];
        sanojaSyotetty = 0;
    }

    // ...
}

Oliomuuttujien alustus tapahtuu tuttuun tapaan konstruktorissa.

Uuden sanan lisääminen on helppoa. Koska sanojaSyotetty muistaa monta sanaa taulukossa jo on, ja taulukon indeksit alkavat nollasta, tulee uusi sana juuri tähän paikkaan. Muuttujan arvo pitää muistaa vielä kasvattaa:

public class Sanajoukko {
    private String[] joukonSanat;
    private int sanojaSyotetty;

    public Sanajoukko() {
        joukonSanat = new String[1000];
        sanojaSyotetty = 0;
    }

    public void lisaa(String sana) {
        joukonSanat[ sanojaSyotetty ] = sana;
        sanojaSyotetty++;
    }

    // ...
}

Ja vielä metodi, jolla tarkistetaan onko sana jo joukossa. Eli käydään sanat muistavan taulukon alkuosaa läpi syötettyjen sanojen verran ja palautetaan true jos etsitty sana löytyy:

public class Sanajoukko {
    private String[] joukonSanat;
    private int sanojaSyotetty;

    public Sanajoukko() {
        joukonSanat = new String[1000];
        sanojaSyotetty = 0;
    }

    public boolean sisaltaa(String sana) {
        for ( int i=0; i < sanojaSyotetty; i++ ) {
            if ( joukonSanat[i].equals(sana) )
                return true;
        }

        return true;
    }

    public void lisaa(String sana) {
        joukonSanat[ sanojaSyotetty ] = sana;
        sanojaSyotetty++;
    }
}

Ratkaisu on nyt varsin elegantti. Erillinen käsite on saatu erotettua ja pääohjelma näyttää siistiltä. Kaikki "likaiset yksityiskohdat" (taulukko ja lukumäärä sanoista) on saatu siivottua eli kapseloitua olion sisälle.

Yhtäkkiä mieleemme palaa toissa viikolla esitelty ArrayList. Sanojen muistaminenhan voidaan toteuttaa helposti ArrayList:illa! Päätämme korvata luokan Sanajoukko sisällä olevan taulukon listalla:

import java.util.ArrayList;

public class Sanajoukko {
    private ArrayList<String> joukonSanat;

    public Sanajoukko() {
        joukonSanat = new ArrayList<String> ();
    }

    public boolean sisaltaa(String sana) {
        return joukonSanat.contains(sana);
    }

    public void lisaa(String sana) {
        joukonSanat.add(sana);
    }

Näin päädytään ratkaisuun jossa Sanajoukko on ainoastaan ArrayList:in "wräpperi". Onko tässä järkeä? Kenties. Voimme nimittäin halutessamme tehdä Sanajoukolle muitakin muutoksia. Ennen pitkään saatamme esim. huomata, että sanajoukko pitää tallettaa tiedostoon. Jos tekisimme nämä muutokset Sanajoukon koodiin, ei pääohjelmaa tarvitsisi muuttaa mitenkään.

Voi olla, että jatkossa ohjelmaa halutaan laajentaa siten, että Sanajoukko-luokan olisi osattava uusia asiota. Jos esim. pääohjelmassa haluttaisiin tietää kuinka moni syötetyistä sanoista oli palindromi, voidaan sanajoukkoa laajentaa metodilla palindromeja:

    public static void main(String[] args) {
        Sanajoukko aiemmatSanat = new Sanajoukko();

        while (true) {
            String sana = lukija.nextLine();

            if ( aiemmatSanat.sisaltaa(sana) ) {
                break;
            }

            aiemmatSanat.lisaa(sana);
        }

        System.out.println("Annoit saman sanan uudestaan");
        System.out.println("Sanoistasi " + aiemmatSanat.palindromeja() + "oli palindromeja");
    }

Pääohjelma säilyy siistinä, viimeisellä rivillä kutsutaan uutta hyvin nimettyä metodia, palindromien laskeminen jää Sanajoukko-olion huoleksi. Metodin toteutus voisi olla seuraavanlainen:

public class Sanajoukko {
    private ArrayList<String>  joukonSanat;

    // ...

    public int palindromeja() {
        int palindromit = 0;

        for ( String sana : joukonSanat ) {
            if ( onPalindromi(sana) )
                palindromit++;
        }

        return palindromit;
    }

    private boolean onPalindromi(String sana){
        int loppu = sana.length()-1;
        for ( int i=0; i %lt; sana.length()/2; i++ ){
            if ( sana.charAt(i) != sana.charAt(loppu-i) )
                return false;
        }

        return true;
    }

Eli metodi palindromeja käy läpi kaikki joukon sanat ja apumetodin onPalindromi avulla laskee paindromien määrän.

Koska apumetodi onPalindromi on tarkoitettu ainoastaan olion sisäiseen käyttöön, on sen näkyvyysmääreeksi määritelty private. Olion käyttäjän siis ei ole mahdollista kutsua metodia, metodia voi kutsua ainoastaan olion sisältä.

Jos sanojen talletukseen olisi käytetty suoraan ArrayList:iä, olisi palindromien laskeminen vaatinut pääohjelman koodiin suurempia muutoksia kuin Sanajoukko-luokan käyttö. Joskus on toki tilanteita, joissa kannattaa käyttää suoraan valmista kalustoa eikä oman luokan määrittelylle ole tarvetta. Kuitenkin jos koodin luettavuus alkaa kärsimään, on selkeistä käsitteistä syytä tehdä omat luokkansa. Kuten näimme tämä parhaassa tapauksessa mahdollistaa ohjelman laajennettavuutta ja muokattavuutta.

Tehtävien hautausmaa

Loppuivatko tehtävät kesken? Ei hätää: saatavilla on lisätehtäväsarja Ohpesokkelo. Siinä tehdään graafinen labyrinttipeli!

Arvauspeli

Tee yksinkertainen arvauspeli. Pelin toiminta alkaa sillä, että käyttäjältä kysytään arvattavat luvut:

Kuinka monta lukua? 3
Anna luvut:
5
2
11
Kiitos. Siirrytään arvausvaiheeseen.

Kun luvut on syötetty, tulostetaan 40 rivinvaihtoa kutsumalla komentoa System.out.println("") 40 kertaa.

Tämän jälkeen käyttäjälle (joka on kenties eri ihminen kun lukujen syöttäjä) annetaan 3 arvausta. Arvausten jälkeen ilmoitetaan, kuinka monta arvauksista oli oikein.

Arvaa mitä lukuja taulukossa on.
Saat kolme arvausta.
4
99
2
Kiitos arvauksista. 1 oikein.

Tee peli käyttäen kahden edellisen tehtävän metodeja. Pääohjelma muotoutuu seuraavaan tapaan:

public static void main(String[] args) {
    int [] luvut = kysyLuvut();
    // tulosta tyhjää

    int oikeatArvaukset = 0;    
    // kolme arvausta
    for ( int i=0; i<3; i++ ) {
       // lue arvaus
       // tarkasta metodin onkoLukuTaulukossa avulla menikö arvaus oikein ja päivitä statistiikka
    }
    // tulosta oikeiden vastausten määrä    
}

Arvauspeli versio 2.0

Laajennetaan edellistä tehtävää niin, että pelaajalla on rajaton määrä arvauksia. Kun käyttäjä antaa syötteenä -99, loppuu peli ja käyttäjälle ilmoitetaan arvausten lukumäärä ja oikeiden arvausten lukumäärä. Peli etenee seuraavasti:

Arvaa mitä lukuja taulukossa on.
Kun kyllästyt, syötä -99.
4
7
2
100
11
-99
Arvauksia oli 5, joista oikein 2.

Arvauspeli versio 3.0

Laajennetaan edellistä tehtävää siten, että ohjelma kysyy pelin jälkeen, haluaako käyttäjä pelata uudestaan. Lopuksi ohjelma ilmoittaa, kuinka monta peliä pelattiin yhteensä.

Arvaa mitä lukuja taulukossa on. Kun kyllästyt, syötä -99.
4
7
2
100
11
-99
Arvauksia oli 5, joista oikein 2.

Uusi peli (k/e)? k

Arvaa mitä lukuja taulukossa on. Kun kyllästyt, syötä -99.
-1
5
2
-99
Arvauksia oli 3, joista oikein 0.

Uusi peli (k/e)? e

Pelaajia tänään 2.

Kivi, paperi, sakset

Tässä tehtäväsarjassa tehdään tekoäly kivi, paperi, sakset -peliin.

Tekoälyn runko

Tee luokka KPSTekoaly, josta tulee tekoäly kivi, paperi, sakset -peliin. Luokkaan tulee metodi teeSiirto, joka palauttaa tekoälyn tekemän siirron. Merkkijonot "k", "p" ja "s" vastaavat siirtoja kivi, paperi ja sakset.

Tee tekoälystä ensin sellainen, että sen siirto on aina kivi.

Luokan runko on seuraava:

import java.util.*;
public class KPSTekoaly {
    public String teeSiirto() {
        // kirjoita koodia tähän
    }
}

Pääluokka näyttää seuraavalta:

import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        static Scanner lukija = new Scanner(System.in);

        KPSTekoaly tekoaly = new KPSTekoaly();
        System.out.print("Kuinka monta kierrosta? ");
        int kierrokset = Integer.parseInt(lukija.nextLine());
        for (int i = 0; i < kierrokset; i++) {
            System.out.print("Anna siirto (k, p tai s): ");
            String pelaajanSiirto = lukija.nextLine();
            String tekoalynSiirto = tekoaly.teeSiirto();
            System.out.println("Pelaajan siirto: " + pelaajanSiirto);
            System.out.println("Tekoälyn siirto: " + tekoalynSiirto);
        }
    }
}

Peli voi edetä esimerkiksi seuraavasti:

Kuinka monta kierrosta? 3
Anna siirto (k, p tai s): s
Pelaajan siirto: s
Tekoälyn siirto: k
Anna siirto (k, p tai s): p
Pelaajan siirto: p
Tekoälyn siirto: k
Anna siirto (k, p tai s): k
Pelaajan siirto: k
Tekoälyn siirto: k

Voittajan tarkistus

Lisää projektiin vielä yksi luokka KPSTuomari, jonka runko on seuraava:

public class KPSTuomari {
    public void maaritaVoittaja(String pelaajanSiirto, String tekoalynSiirto) {
        // kirjoita koodia tähän
    }
}

Metodin maaritaVoittaja tarkoituksena on selvittää pelaajan ja tekoälyn siirtojen perusteella, kumpi on voittaja vai tuliko tasapeli. Toteuta metodi niin, että se tulostaa tiedon voittajasta.

Kivi, paperi, sakset -pelissä kivi voittaa sakset, sakset voittavat paperin ja paperi voittaa kiven.

Pääluokka on nyt seuraava:

import java.util.Scanner;

public class Main {
    private static Scanner lukija = new Scanner(System.in);

    public static void main(String[] args) {
        KPSTekoaly tekoaly = new KPSTekoaly();
        KPSTuomari tuomari = new KPSTuomari();
        System.out.print("Kuinka monta kierrosta? ");
        int kierrokset = Integer.parseInt(lukija.nextLine());
        for (int i = 0; i < kierrokset; i++) {
            System.out.print("Anna siirto (k, p tai s): ");
            String pelaajanSiirto = lukija.nextLine();
            String tekoalynSiirto = tekoaly.teeSiirto();
            System.out.println("Tekoälyn siirto: " + tekoalynSiirto);
            tuomari.maaritaVoittaja(pelaajanSiirto, tekoalynSiirto);
        }
    }
}

Peli voi edetä esimerkiksi seuraavasti:

Kuinka monta kierrosta? 3
Anna siirto (k, p tai s): s
Tekoälyn siirto: k
Tekoäly voitti!
Anna siirto (k, p tai s): p
Tekoälyn siirto: k
Pelaaja voitti!
Anna siirto (k, p tai s): k
Tekoälyn siirto: k
Tuli tasapeli!

Satunnainen tekoäly

Nyt on aika tehdä tekoälystä parempi, koska ei ole järkevää, että tekoäly valitsee aina kiven.

Tee tekoälystä satunnainen: se valitsee yhtä todennäköisesti kiven, saksen ja paperin.

Pisteenlasku

Laajenna tuomariluokkaa niin, että se pitää kirjaa pelaajan ja tekoälyn voitoista. Lisää luokkaan myös metodi tulostaTilasto, joka tulostaa tilaston voitoista. Lisää tämän metodin kutsu pääohjelman loppuun.

Kuinka monta kierrosta? 5
Anna siirto (k, p tai s): s
Tekoälyn siirto: s
Tuli tasapeli!
Anna siirto (k, p tai s): p
Tekoälyn siirto: s
Tekoäly voitti!
Anna siirto (k, p tai s): k
Tekoälyn siirto: k
Tuli tasapeli!
Anna siirto (k, p tai s): s
Tekoälyn siirto: p
Pelaaja voitti!
Anna siirto (k, p tai s): s
Tekoälyn siirto: k
Tekoäly voitti!

Tilasto:
Pelaaja: 1 voittoa
Tekoäly: 2 voittoa

Muistava tekoäly

Lisää tekoälyyn muisti: tekoäly ottaa siirtonsa valinnassa huomioon, mitä käyttäjä on valinnut aiemmin eri tilanteissa. Päätä itse, millä tavoin tarkalleen tekoäly hyödyntää muistia.

Lisää tekoälyyn metodi kirjaaSiirto, jota kutsutaan käyttäjän siirron jälkeen. Sen avulla tekoäly saa tietoonsa, mitä siirtoja käyttäjä on tehnyt.

Pääluokkaan tehdään seuraava muutos:

...
        String pelaajanSiirto = lukija.nextLine();
        String tekoalynSiirto = tekoaly.teeSiirto();
        tekoaly.kirjaaSiirto(pelaajanSiirto);
        System.out.println("Tekoälyn siirto: " + tekoalynSiirto);
...

Jos tekoäly käyttää muistiaan hyvin, se voi sopeutua esimerkiksi tilanteeseen, jossa käyttäjä valitsee aina saman siirron:

Kuinka monta kierrosta? 5
Anna siirto (k, p tai s): s
Tekoälyn siirto: p
Pelaaja voitti!
Anna siirto (k, p tai s): s
Tekoälyn siirto: k
Tekoäly voitti!
Anna siirto (k, p tai s): s
Tekoälyn siirto: k
Tekoäly voitti!
Anna siirto (k, p tai s): s
Tekoälyn siirto: k
Tekoäly voitti!
Anna siirto (k, p tai s): s
Tekoälyn siirto: k
Tekoäly voitti!

Tilasto
Pelaaja: 1 voittoa
Tekoäly: 4 voittoa

Taulukoista ja olioista

Oliot taulukossa

Tutkitaan vielä olioiden lisäämistä taulukkoon:

    public static void main(String[] args) {
        Henkilo[] henkilot = new Henkilo[3];

        henkilot[0] = new Henkilo("Pekka");
        henkilot[1] = new Henkilo("Antti");
        henkilot[2] = new Henkilo("Juhana");

        for ( int i=0; i < 30; i++ ) {
            henkilot[0].vanhene();
            henkilot[1].vanhene();
            henkilot[2].vanhene();
        }

        for ( Henkilo henkilo : henkilot )        
            ilmoitaTaysiIkaisyys(henkilo);
    }

Alussa luodaan taulukko johon mahtuu 3 henkilöolioa. Laitetaan Pekka lokeroon 0, Antti lokeroon 1 ja Juhana lokeroon 2. Vanhennetaan kaikkia 30 vuotta ja tarkastetaan kaikkien täysi-ikäisyys edellisen luvun metodia hyödyntäen.

Sama esimerkki ArrayListien avulla:

    public static void main(String[] args) {
        ArrayList<Henkilo> henkilot = new ArrayList<Henkilo>();

        henkilot.add( new Henkilo("Pekka") );
        henkilot.add( new Henkilo("Antti") );
        henkilot.add( new Henkilo("Juhana") );

        for ( int i=0; i < 30; i++ ) {
            for ( Henkilo henkilo : henkilot ) {
                henkilo.vanhene();
            }

            // tai henkilot.get(0).vanhene();
            //     henkilot.get(1).vanhene();
            //     ...
        }

        for ( Henkilo henkilo : henkilot ) {
            ilmoitaTaysiIkaisyys(henkilo);
        }
    }

Usein tahdotaan myös tehdä olioista loogisesti riippumattomista apuri-metodeista staattisia. Tästä sopiva esimerkki olisi luvun 24 Sanajoukko-esimerkissä esiintynyt onPalindromi-metodi. Kuten koodista nähdään, ei metodissa viitata lainkaan oliomuuttujiin.

public class Sanajoukko {
    /* Muu luokan määrittely */

    private static boolean onPalindromi(String sana){
        int loppu = sana.length()-1;
        for ( int i=0; i < sana.length()/2; i++ ){
            if ( sana.charAt(i) != sana.charAt(loppu-i) )
                return false;
        }

        return true;
    }
}

Teksturi

HUOM: tehtävä on osoittautunut melko haastavaksi ollakseen viikon alkupään tehtävä. Jos et heti ymmärrä mitä tehtävässä ajetaan takaa, hyppää tehtävän yli ja palaa tähän siinä vaiheessa kun olet tehnyt viikon muut tehtävät.

Tehtäväpohja sisältää Kurt Koodarin tekemän tekstinkäsittelyohjelman. Ohjelma on kuitenkin koodattu yhteen metodiin! Tehtävänäsi on refaktoroida eli siistiä Kurtin ohjelma edellisen tehtävän mallin mukaan.

Komennot pois mainista

Luo jokaiselle komennolle (paitsi lopeta) oma metodeihinsa. Komentoa vastaava metodi saa parametrinaan lukijan (jotta se voi kysyä käyttäjältä asioita) ja listan rivejä (jotta se voi muokata niitä). Toteuta siis metodit:

Tämän jälkeen muuta main-metodi käyttämään lisäämiäsi metodeja komentojen toteuttamiseen. Uusi main-metodi siis vain kysyy komennon nimen ja sen jälkeen kutsuu oikeaa metodia ylläolevalta listalta. Ohjelman toiminnan pitää pysyä samanlaisena siistimisestä huolimatta!

Rivinumeron lukeminen

Edellisen askeleen jälkeen koodissa on kuitenkin vielä toistoa jäljellä: rivinumeroitten lukeminen. Lisää siis ohjelmaan metodi public static int kysyRivinumero(String kysymys, Scanner lukija). Tämä metodi tulostaa kysymyksen ja lukee rivinumeron. Palautusarvo on -1 jos käyttäjä syötti tyhjän rivin.

vihje: voit ottaa toteutuksesi pohjaksi seuraavan metodirungon:

    public static int kysyRivinumero(String kysymys, Scanner lukija) {
        System.out.println(kysymys);
        String rivi = // lue käytäjältä merkkijono

        // jos merkkijono oli tyhjä, palautetaan -1;
        if (rivi.isEmpty()) {
            return -1;
        }

        // muuten muuta rivi kokonaisluvuksi Integer.parseInt:illä ja palauta se
        return ...
    }

Kutsu kysyRivinumero-metodia metodista poista

Metodi poista lukee käyttäjältä rivinumeron. Muuta siis poista-metodi käyttämään edellisessä askeleessa toteuttamaasi kysyRivinumero-metodia

Kutsu kysyRivinumero-metodia metodista lisaa

Myös metodi lisaa lukee käyttäjältä rivinumeron. Käytä rivinumeron lukemiseen kysyRivinumero-metodia

Pienin yhteinen jaettava

Tässä tehtävässä lasketaan kahden luvun pienin yhteinen jaettava näiden lukujen suurimman yhteisen tekijän avulla.

Pienin yhteinen jaettava on pienin kokonaisluku, joka on tasan jaollinen kyseessä olevilla luvuilla. Se voidaan laskea helposti, jos tiedetään kyseessä olevien lukujen suurin yhteinen tekijä. Suurin yhteinen tekijä taas tarkoittaa suurinta sellaista lukua, joka jakaa kyseessä olevat luvut niin, että lopputulos on kokonaisluku (eli lukujen jakojäännös on nolla).

Esimerkiksi lukujen 32 ja 6 suurin yhteinen tekijä on 2, koska 32 / 2 = 16 ja 6 / 2 = 3, eikä löydy suurempaa lukua kuin 2, jolla molemmat luvut, 32 ja 6, voitaisiin jakaa siten, että tuloksena olisi kokonaisluku.

Lukujen 32 ja 6 pienin yhteinen jaettava on 96, koska 96 / 32 = 3 ja 96 / 6 = 16, eikä löydy pienempää lukua kuin 96, joka voitaisiin jakaa molemilla luvuilla, 32 ja 6, siten, että tuloksena olisi kokonaisluku.

Suurimman yhteisen tekijän laskeminen

Luo luokkaan PieninYhteinenJaettava metodi public static int suurinYhteinenTekija(int luku1, int luku2), joka laskee parametreina annettujen lukujen suurimman yhteisen tekijän ja palauttaa vastaukseksi saadun luvun.

Eräs tapa laskea suurin yhteinen tekijä on Eukleideen algoritmi, joka on yksinkertainen toteuttaa ohjelmakoodissa.

Eukleideen algoritmi toimii seuraavasti, kun lukuina ovat a ja b (muuttujaa c käytetään vain apuna):

  1. Jos b on 0, niin algoritmi päättyy ja a on suurin yhteinen tekijä.
  2. c = a
  3. a = b
  4. b = c % b (%-operaatio tarkoittaa jakojäännöstä eli moduloa)
  5. Palaa kohtaan 1.

Esimerkiksi lukujen 32 ja 20 suurin yhteinen tekijä löytyy seuraavasti:

Vinkki: Tarvitset siis silmukkaa algoritmin toteuttamisessa.

Pienimmän yhteisen jaettavan laskeminen suurimman yhteisen tekijän avulla

Luo luokkaan PieninYhteinenJaettava metodi public static int pieninYhteinenJaettava(int luku1, int luku2), joka laskee parametreina annettujen lukujen pienimmän yhteisen tekijän käyttämällä apuna lukujen suurinta yhteistä tekijää.

Pienin yhteinen jaettava voidaan laskea helposti seuraavalla kaavalla luvuille a ja b, kun syt laskee lukujen suurimman yhteisen tekijän:

(a * b) / syt(a, b)

Esimerkiksi:

a = 32
b = 6

syt(32, 6) = 2

(32 * 6) / syt(32, 6) = 192 / 2 = 96

Huom: Metodin pieninYhteinenJaettava on siis kutsuttava edellisessä kohdassa tehtyä metodia suurinYhteinenTekija.

Pankki

Tehtäväpohjan mukana tulee kaksi luokkaa, Pankki ja Tili, sekä pääohjelmaluokka Ohjelma. Luokalla Tili on:

Luokalla pankki on:

Tehtäväsi on luoda pääohjelmaan nimeltä Ohjelma ohjelma joka

  1. luo kaksi Pankki-oliota, nimiltään MegaPankki, ja HassuPankki
    muistutus: olio siis luodaan seuraavasti Pankki megaPankki = new Pankki("MegaPankki")
  2. luo tilin nimeltään Pekka, tallettaa siihen 100, ja lisää tilin MegaPankkiin
  3. luo tilin nimeltään Simo, tallettaa siihen 20, ja lisää sen MegaPankkiin.
  4. luo tilin nimeltään Korianteri, tallettaa siihen 5, ja lisää sen Hassupankkiin
  5. luo tilin nimeltään Terminaattori, tallettaa siihen 7, ja lisää sen HassuPankkiin.
  6. siirtää Pekan tililtä 50 Simon tilille
    muistutus: käytä Pankki-luokan tilisiirto-metodia.
  7. siirtää Korianterin tililtä 0.5 Terminaattorin tilille
  8. tulostaa pankkien saldot allaolevassa muodossa (summat alla ovat vääriä):
    MegaPankki: 45.0
    HassuPankki: 70.0
    

Huom! suorita ohjelmassasi vain kuvaillut toimenpiteet.

ArrayList metodin paluuarvona

ArrayList voi toimia myös metodin paluuarvona. Seuraavassa esimerkkinä metodi joka luo listan, lisää sille parametrinaan saamansa kolme kokonaislukua ja palauttaa lopuksi listan. Huomaa miten pääohjelma tallettaa metodin palauttaman listan muuttujaan jonka tyyppi on sama kuin metodin palauttamaan listan tyyppi:

public class Main {

    public static ArrayList<Integer> laitaLuvutListalle(int luku1, int luku2, int luku3){
        ArrayList<Integer> lista = new ArrayList<Integer>();
        
        lista.add(luku1);
        lista.add(luku2);
        lista.add(luku3);
        
        return lista;
    }
    
    public static void main(String[] args) {
        ArrayList<Integer> luvut = laitaLuvutListalle(3, 5, 2);
        
        for (int luku : luvut) {
            System.out.println( luku );
        }
    }
}

Merkkijonojen pituudet

Tee metodi pituudet, joka palauttaa ArrayListissä saamiensa merkkijonojen pituudet, ja palauttaa ne ArrayListinä samassa järjestyksessä.

public static ArrayList<Integer> pituudet(ArrayList<String> lista) {
    // kirjoita koodia tähän
}

public static void main(String[] args) {
    ArrayList<String> lista = new ArrayList<String>();
    lista.add("Hei");
    lista.add("Moi");
    lista.add("Benvenuto!");
    lista.add("badger badger badger badger");
    ArrayList<Integer> pituudet = pituudet(lista);

    System.out.println("Pituudet: " + pituudet);
}

Ohjelman tulostus:

Pituudet: [3, 3, 10, 27]

Wikipedia

Tehtäväpohjan mukana tulee luokka Wikipedia, josta luotu olio yhdistää internetin yli (englanninkieliseen) wikipediaan ja kyselee sieltä tietoja. Luokalla on metodi ArrayList<String> getLinks(String title) joka hakee kaikkien niiden sivujen nimet, joille annetulta sivulta on linkki. Esimerkiksi seuraava ohjelmanpätkä:

Wikipedia wiki = new Wikipedia();
ArrayList<String> linkit = wiki.getLinks("Helsinki");

Collections.sort(linkit);
Collections.reverse(linkit);

System.out.println("1: " + linkit.get(0));
System.out.println("2: " + linkit.get(1));
System.out.println("3: " + linkit.get(2));

Tulostaa (tällä hetkellä, wikipedia muuttuu jatkuvasti):

1: Pasila
2: Parliament of Finland
3: Parliament House, Helsinki

Toteuta ohjelma, joka ensin kysyy käyttäjältä wikipediasivun nimen ja tämän jälkeen hakee kyseisen sivun linkit ja tulostaa niiden määrän. Tämän jälkeen käyttäjä voi suorittaa kyselyitä linkkilistaan. Ohjelma päättyy kun käyttäjä syöttää tyhjän rivin. Esimerkki:

Anna wikipediasivun nimi:
Helsinki
Sivulta Helsinki on 130 linkkiä.
Linkkikysely:
Pasila
Sivulta Helsinki on linkki sivulle Pasila
Linkkikysely:
Barack Obama
Sivulta Helsinki ei ole linkkiä sivulle Barack Obama
Linkkikysely:

Hienoja laatikoita

Toteuta neljä tulostaLaatikko-metodia:

HUOM: Toteuta kolme viimeistä metodia niin, että ne vain kutsuvat metodia tulostaLaatikko(String teksti, char merkki) oikeilla arvoilla.

Muistutus! Javassa yksittäistä merkkiä merkitään näin: 'K'. Huomaa ero yhden pituiseen merkkijonoon "K"!

Muistutus! Tässä tehtävässä tehdään jälleen static-metodeja, kuten esim. viikolla 3. Sanan static tarkka merkitys kerrotaan ensi viikolla, lyhyesti sanottuna kyse on metodeista, jotka eivät liity mihinkään olioon vaan saavat kaiken käsittelemänsä tiedon parametrina.

Onko sana taulukossa?

Tee metodi public static boolean onkoSanaTaulukossa(String[] taulukko), joka palauttaa totuusarvon sen mukaan onko käyttäjän antama sana taulukossa. Kirjoita lisäksi main-metodiin ohjelma, joka testaa onkoSanaTaulukossa-metodia allaolevan esimerkkitulostuksen mukaisesti kertoen onko sana taulukossa vai ei.

Ohjelman runko on seuraava:

import java.util.Scanner;

public class Main {
  public static void main(String[] args) {
    // Tässä voit testata metodia
    String[] taulukko = {"apina", "banaani", "cembalo"};

    Scanner lukija = new Scanner(System.in);
    // Kirjoita tähän koodi, joka kysyy sanan ja kertoo oliko sana taulukossa vai ei
    // Muista käyttää metodia onkoSanaTaulukossa()!
  }

  public static boolean onkoSanaTaulukossa(String[] taulukko, String sana) {
    // Tarkista täällä onko sana taulukossa ja palauta tulos
    return false;
  }
}

Esimerkkitulostuksia:

Anna sana: apina
Sana on taulukossa.
Anna sana: siili
Sanaa ei ole taulukossa.

Muista, mitä erityistä merkkijonojen vertailussa on!

Taulukon pienin luku

Tee metodi public static int etsiTaulukonPieninLuku(int[] taulukko), joka palauttaa taulukon pienimmän luvun.

Ohjelman runko on seuraava:

public class Main {
  public static void main(String[] args) {
    // Tässä voit testata metodia
    int[] taulukko = {5, 1, 3, 4, 2};
    System.out.println(etsiTaulukonPieninLuku(taulukko));
  }

  public static int etsiTaulukonPieninLuku(int[] taulukko) {
    // Kirjoita pienimmän luvun etsivä koodi tänne
    return 0;
  }
}

Ohjelman tulostus on seuraava:

1

Arrays.sort

Tee ohjelma, joka kysyy käyttäjältä lukujen määrän ja joukon lukuja. Tämän jälkeen ohjelma tulostaa luvut järjestyksessä. Ohjelman tulee toimia seuraavasti:

Kuinka monta lukua? 4
Anna luvut:
5
2
8
1
Luvut järjestyksessä:
1
2
5
8

Korttipakka

Tässä tehtävässä luodaan korttipakka, jonka kehitystä jatketaan seuraavilla viikoilla. Tehtävässä luodaan luokka Kortti, josta luodaan jokaista eri pelikorttia varten luodaan oma ilmentymä. Tehtävässä luodaan myös luokat Korttipakka ja Kasi jotka sisältävät Kortti-olioita. Korttipakasta taas voidaan nostaa kortteja joko yksitellen tai kädellinen.

Tehtävässä toteutetaan ohjelma kolmessa askeleessa. Luokat ja toteutettavien metodien rungot ovat mukana tehtäväpohjassa.

Kortti-luokan toteuttaminen

Kortti-olio tallentaa arvonsa nollasta alkavilla luvuilla - arvo on vähintään nolla (ässä) ja enintään 12 (kuningas). Kortin maa on 0 (risti), 1 (ruutu), 2 (hertta) tai 3 (pata). Sinun ei tarvitse vielä tässä tehtävässä varmistaa, että kortteja luodaan vain näille arvoalueille sopivilla arvoilla.

Toteuta seuraavat metodit:

Tulostuksen helpottamiseksi Kortti-luokalla on myös valmiiksi toteutetut ArrayListit maidenNimet maita ja arvojenNimet kortin arvoja varten. maidenNimet.get(maa) on maan selkokielinen merkkijonoesitys ("Risti", "Ruutu" jne...). arvojenNimet.get(arvo) on arvon korttipakkaan sopiva lyhenne (A, J, Q, K normaalien lukumerkintöjen lisäksi).

toString-metodin esimerkkitulostuksia:

Risti 4
Pata K
Pata 5
Hertta Q
Risti 6
Risti A
Hertta 9
Ruutu 8
Ruutu 7
Ruutu J

Kasi-luokan toteuttaminen

Kasi-olio esittää korttikättä, eli kokoelmaa korteista, jotka ovat peräisin samasta korttipakasta. Käsi voi olla myös tyhjä, jolloin siinä ei ole kortteja.

Toteuta seuraavat luokan Kasi metodit

Korttipakka-luokan toteuttaminen

Korttipakka-olio esittää tavallista korttipakkaa, joka koostuu 52 kortista. Täysi korttipakka sisältää jokaisen neljän maan (pata, risti, ruutu ja hertta) kaikki 13 korttia ässästä kuninkaaseen. Kortin poistamista korttipakasta kutsutaan nostamiseksi.

Toteuta seuraavat luokan Korttipakka metodit

Huom: Korttipakasta kortteja nostavat metodit voivat aiheuttaa helposti poikkeustilanteita, jos kortit loppuvat kesken. Tee metodien toteutukset huolellisesti, ettei näin pääse käymään.

Hotelli

Tämä tehtävä toteutetaan kolmessa osassa.

Vieras-luokan toteuttaminen

Toteuta seuraavat metodit luokkaan Vieras

Vieras-luokan esimerkki toString metodin tulosteita

Pekka: 14
Timo: 15
Jaakko: 16

Huone-luokan toteuttaminen

Toteuta seuraavat metodit luokkaan Huone

Huone luokan toString() metodin esimerkkitulostuksia, alla viiden huoneen tulostukset, joista viimeinen on tyhjä:

Aino: 2, Timo: 3
Timo: 4, Snorri: 5
Markku: 6, Noris: 7
Timo: 124, Saku: 114, Teppo: 29
Huone tyhjä

Hotelli-luokan toteutus

toteuta seuraavat metodit luokkaan Hotelli

Hotellilla on valmiiksi toteutettuna metodi getHuoneet() joka palauttaa viitteen huoneet-ArrayListiin.

Hotelli-luokan toString metodin esimerkkitulostus:

kalle: 1, Simo: 2
kalle: 3, Timo: 4
kalle: 5, Snorri: 6
kalle: 7, Noris: 8
kalle: 9, Malloc: 10
kalle: 11, Rase: 12
kalle: 13, Dogo: 14
Huone tyhjä
Huone tyhjä
Huone tyhjä
Huone tyhjä
Huone tyhjä
Huone tyhjä
Huone tyhjä
Huone tyhjä