Tehtävät
Viidennen osan tavoitteet

Ymmärtää alkeis- ja viittaustyyppisten muuttujien erot. Osaa käsitellä olioita listarakenteessa sekä käyttää listaa oliomuuttujana. Ymmärtää automaattisen testaamisen periaatteet ja osaa luoda yksinkertaisen luokan toimintaa testaavia automaattisia testejä.

Alkeis- ja viittaustyyppiset muuttujat

Javan muuttujat voi jakaa alkeis- ja viittaustyyppisiin muuttujiin. Alkeistyyppisten muuttujien sisältämä tieto on tallennettu muuttujan arvoksi, kun taas viittaustyyppisten muuttujien arvona on viite muuttujaan liittyvään tietoon. Tarkastellaan näitä muuttujatyyppejä kahden esimerkin kautta.

int arvo = 10;
System.out.println(arvo);  
10
Nimi leevi = new Nimi("Leevi");
System.out.println(leevi);
Nimi@4aa298b7

Ensimmäisessä esimerkissä luodaan alkeistyyppinen int-muuttuja, jonka arvoksi kopioidaan luku 10. Kun muuttuja annetaan System.out.println-metodille tulostettavaksi, tulostuu arvo 10. Toisessa esimerkissä taas luodaan viittaustyyppinen Nimi-muuttuja, jonka arvoksi kopioidaan Nimi-luokassa määritellyn konstruktorin kutsun palauttama viite olioon. Kun muuttuja annetaan System.out.println-metodille tulostettavaksi, tulostuu merkkijono Nimi@4aa298b7. Mistä tässä oikein on kyse?

Metodikutsu System.out.println tulostaa muuttujan arvon. Alkeistyyppiset muuttujat sisältävät konkreettisen arvon, joka tulostuu tulostuskutsun yhteydessä. Myös viittaustyyppiset muuttujat sisältävät konkreettisen arvon, mutta viittaustyyppisten muuttujien arvo on viite paikkaan, missä olion tiedot ovat. Voidaan ajatella, että merkkijono Nimi@4aa298b7 kertoo, että kyse on Nimi-tyyppisestä muuttujasta paikassa 4aa298b7.

Tutustutaan näihin muuttujatyyppeihin tarkemmin.

Alkeistyyppiset muuttujat

Javassa on yhteensä kahdeksan erilaista alkeistyyppistä muuttujaa. Nämä ovat boolean (totuusarvo), byte (8 bittiä sisältävä tavu), char (yhtä merkkiä kuvaava 16-bittiä sisältävä kirjainarvo), short (pientä lukua kuvaava 16 bittiä sisältävä arvo), int (keskikokoista lukua kuvaava 32 bittiä sisältävä arvo), long (isohkoa lukua kuvaava 64 bittiä sisältävä arvo), float (32-bittiä sisältävä liukuluku) ja double (64-bittiä sisältävä liukuluku).

Olemme käyttäneet näistä ensisijaisesti totuusarvomuuttujia (boolean), kokonaislukumuuttujia (int), ja liukulukumuuttujia (double).

boolean totuusarvo = false;
int kokonaisluku = 42;
double liukuluku = 4.2;

System.out.println(totuusarvo);
System.out.println(kokonaisluku);
System.out.println(liukuluku);
false
42
4.2
Bitit ja binääriluvut

Tietokoneen muisti on käytännössä taulukko, missä jokainen taulukon lokero sisältää joko luvun 0 tai 1. Näitä yksittäisiä lukuja kutsutaan biteiksi (lyhenne sanasta binary digit). Koneen käyttämät luvut kuvataan bittien avulla binäärilukuna: esimerkiksi short-tyyppinen muuttuja sisältää 16 bittiä ja int-tyyppinen muuttuja 32 bittiä.

Tarkastellaan miten binääriluvut toimivat. Alla on kuvattuna 8 bittiä pitkä alue tietokoneen muistista, missä jokainen bitin arvo on 0.

0 0 0 0 0 0 0 0

Binäärilukukujen avulla jokainen luku voidaan esittää luvun 2 potensseina. Esimerkiksi yllä olevan kahdeksan bitin luvun arvo kymmenjärjestelmässä on: 0 * 20 + 0 * 21 + ... + 0 * 27. Laskeminen aloitetaan tyypillisesti luvun oikeasta laidasta. Alla on kuvattuna toinen esimerkki.

0 0 0 0 0 1 0 1

Yllä kuvattu binääriluku on kymmenjärjestelmässä: 1 * 20 + 0 * 21 + 1 * 22 + 0 * 23 + ... + 0 * 27 = 5.

Vastaavasti toiseen suuntaan siirryttäessä luku 3 voidaan ilmaista binäärilukuna "11", ja esimerkiksi luku 256 binäärilukuna "100000000". Binäärilukujen avulla kuvataan lukujen lisäksi käytännössä kaikki tietokoneen tallentama tieto. Tähän teemaan tutustutaan tarkemmin kurssilla Tietokoneen toiminta (TKT-10005).

Alkeistyyppisillä muuttujilla muuttujan esittely varaa muistipaikan, johon kopioidaan arvo. Alla olevassa esimerkissä luodaan kolme muuttujaa. Jokaisella muuttujalla on erillinen muistipaikka, johon asetettava arvo kopioidaan.

int eka = 10;
int toka = eka;
int kolmas = toka;
System.out.println(eka + " " + toka + " " + kolmas);
toka = 5;
System.out.println(eka + " " + toka + " " + kolmas);
10 10 10
10 5 10

Muuttujan arvon asetus yhtäsuuruusmerkillä aiheuttaa muuttujan arvon kopioimisen. Esimerkiksi lause int eka = 10 luo int-tyyppisen muuttujan nimeltä eka, jonka arvoksi kopioidaan luku 10. Vastaavasti lause int toka = eka; luo int-tyyppisen muuttujan nimeltä toka, jonka arvoksi kopioidaan muuttujan eka arvo.

Muuttujien arvot kopioituvat myös metodikutsujen yhteydessä. Käytännössä tämä tarkoittaa sitä, että metodikutsun yhteydessä metodin parametriksi annetun muuttujan arvo ei muutu metodia kutsuvassa metodissa. Alla olevassa esimerkissä main-metodissa esitellään muuttuja luku, jonka arvo kopioidaan metodin kutsu parametriksi. Metodissa kutsu parametrina saatu arvo tulostetaan, jonka jälkeen arvoa kasvatetaan yhdellä, jonka jälkeen arvo tulostetaan vielä kerran. Lopulta metodin kutsu suoritus loppuu, ja palataan main-metodiin, missä luku-muuttujan arvo ei ole muuttunut.

Viittaustyyppiset muuttujat

Lähes kaikki Javan muuttujat ovat viittaustyyppisiä, ja ohjelmoija voi luoda Javaan myös uusia viittaustyypin muuttujatyyppejä. Käytännössä jokainen annetusta luokasta luotu olio on viittaustyyppinen muuttuja.

Tarkastellaan alussa ollutta esimerkkiä, missä luotiin Nimi-tyyppinen muuttuja leevi.

Nimi leevi = new Nimi("Leevi");

Kutsun osat ovat seuravat:

  • Mitä tahansa uutta muuttujaa esiteltäessä tulee ensin kertoa esiteltävän muuttujan tyyppi. Alla esitellään muuttuja, jonka tyyppi on Nimi. Jotta ohjelman suorittaminen onnistuu, tulee ohjelmassa olla luokka nimeltä Nimi.
    Nimi leevi = new Nimi("Leevi");
    
  • Muuttujan esittelyn yhteydessä kerrotaan myös muuttujan nimi. Muuttujan arvoon voi myöhemmin viitata muuttujan nimen perusteella. Alla muuttujan nimeksi tulee leevi.
    Nimi leevi = new Nimi("Leevi");
    
  • Muuttujaan halutaan asettaa arvo. Luokista luodaan olioita kutsumalla niiden konstruktoria, joka määrittelee luotavan olion muuttujiin asetettavat arvot. Alla oletetaan, että luokassa Nimi on konstruktori, joka saa parametrikseen merkkijonon.
    Nimi leevi = new Nimi("Leevi");
    
  • Konstruktorikutsu palauttaa arvon, joka on viite luotuun olioon. Yhtäsuuruusmerkki kertoo ohjelmalle, että yhtäsuuruusmerkin oikealla puolella olevan lausekkeen arvo tulee kopioida yhtäsuuruusmerkin vasemmalla puolella olevan muuttujan arvoksi.
    Nimi leevi = new Nimi("Leevi");
    

On mahdollista, että Nimi-luokalle on määritelty metodeja, joiden avulla Nimi-luokasta tehtyjen olioiden sisäistä tilaa voidaan muuttaa.

Suurin ero alkeis- ja viittaustyyppisten muuttujien välillä on se, että alkeistyyppiset muuttujat (jotka ovat lähes poikkeuksetta numeroita) ovat muuttumattomia, kun taas viittaustyyppiset muuttujat voivat muuttua. Tämä ilmiö liittyy siihen, että alkeistyyppisten muuttujien arvo on tallennettu suoraan muuttujaan, kun taas viittaustyyppisten muuttujien arvo on viite muuttujan tietoihin.

Alkeistyyppisille muuttujille löytyy laskuoperaatioita kuten plus, miinus, kerto jne -- nämä operaatiot eivät muuta alkuperäisten muuttujien arvoja. Laskuoperaatioiden avulla voidaan luodaan uusia arvoja, jotka varastoidaan muuttujiin tarvittaessa. Toisaalta, viittaustyyppisten muuttujien arvoa ei voi muuttaa plus, miinus, kerto ym. laskuoperaatioiden avulla.

Viittaustyyppisen muuttujan arvo -- eli viite -- osoittaa paikkaan muistissa, mistä löytyy viittaustyyppiseen muuttujaan liittyvät tiedot. Oletetaan, että käytössä on luokka Henkilo, jossa on määritelty oliomuuttujaksi ika. Jos luokasta on luotu henkilöolio, voi henkilöolion viitettä seuraamalla päästä käsiksi muuttujaan ika, jonka arvoa voi tarvittaessa muuttaa.

Muuttujat ja tietokoneen muisti

Totesimme aiemmin, että alkeistyyppisten muuttujien arvo on tallennettuna suoraan muuttujaan, kun taas viittaustyyppisten muuttujien arvo sisältää viitteen olioon. Totesimme myös, että muuttujan arvon asettaminen yhtäsuuruusmerkillä kopioi oikealla olevan (muuttujan) arvon vasemmalla olevan muuttujan arvoksi. Vastaava toiminnallisuus on myös metodikutsujen yhteydessä -- metodikutsun yhteydessä parametrina annettava arvo kopioidaan metodin käyttöön.

Tarkastellaan tätä käytännössä. Oletetaan, että käytössämme on seuraava luokka Henkilo.

public class Henkilo {
    private String nimi;
    private int syntymavuosi;

    public Henkilo(String nimi) {
        this.nimi = nimi;
        this.syntymavuosi = 1970;
    }

    public int getSyntymavuosi() {
        return this.syntymavuosi;
    }
  
    public void setSyntymavuosi(int syntymavuosi) {
        this.syntymavuosi = syntymavuosi;
    }

    public String toString() {
        return this.nimi + " (" + this.syntymavuosi + ")";
    }
}

Tarkastellaan seuraavan ohjelman toimintaa askeleittain.

public class Esimerkki {
    public static void main(String[] args) {
        Henkilo eka = new Henkilo("Eka");
  
        System.out.println(eka);
        nuorenna(eka);
        System.out.println(eka);
  
        Henkilo toka = eka;
        nuorenna(toka);
  
        System.out.println(eka);
    }
  
    public static void nuorenna(Henkilo henkilo) {
        henkilo.setSyntymavuosi(henkilo.getSyntymavuosi() + 1);
    }
}
Eka (1970)
Eka (1971)
Eka (1972)

Ohjelman suoritus alkaa main-metodin ensimmäiseltä riviltä. Main-metodin ensimmäisellä rivillä esitellään Henkilo-tyyppinen muuttuja eka, johon kopioidaan Henkilo-luokan konstruktorin palauttama arvo. Konstruktorissa luodaan olio, jonka syntymävuodeksi asetetaan 1970 ja jonka nimeksi asetetaan parametrina saatu arvo. Konstruktori palauttaa viitteen. Rivin suorituksen jälkeen ohjelman tilanne on seuraava -- ohjelman muistiin on luotu Henkilo-olio, johon on viittaus main-metodissa määritellystä eka-muuttujasta.

Main-metodin kolmannella rivillä tulostetaan muuttujan eka arvo. Metodikutsu System.out.println etsii sille parametrina annetulta viittaustyyppiseltä muuttujalta toString-metodia. Henkilo-luokalla on metodi toString, joten metodia kutsutaan eka-muuttujan osoittamalle oliolle. Oliossa olevan muuttujan nimi arvo on "Eka" ja syntymävuoden arvo on 1970. Tulostukseksi tulee "Eka (1970)".

Neljännellä rivillä kutsutaan nuorenna-metodia, jolle annetaan parametriksi muuttuja eka. Metodia kutsuttaessa parametriksi annetun muuttujan arvo kopioituu metodin parametriksi. Koska muuttuja eka on viittaustyyppinen, kopioituu metodin käyttöön aiemmin luotu viite. Metodin suorituksen lopussa tilanne on seuraava -- metodi kasvattaa parametrina saamansa olion syntymävuotta yhdellä.

Kun metodin nuorenna suoritus loppuu, palataan takaisin main-metodiin. Nuorenna-metodin suoritukseen liittyvät tiedot katoavat kutsupinosta.

Metodikutsusta palaamisen jälkeen suoritetaan taas muuttujan eka arvon tulostaminen. Tällä kertaa muuttujan eka osoittama olio on muuttunut hieman -- edellisen metodikutsun yhteydessä viitatun olion syntymäpäivä-muuttujaa kasvatettiin yhdellä. Tulostukseksi tulee lopulta "Eka (1971)".

Tämän jälkeen ohjelmassa esitellään uusi Henkilo-tyyppinen muuttuja toka. Muuttujaan toka kopioidaan muuttujan eka arvo, eli muuttujan toka arvoksi tulee viite jo olemassaolevaan Henkilo-olioon.

Tämän jälkeen kutsutaan metodia nuorenna, jolle annetaan parametriksi muuttuja toka. Metodia kutsuttaessa parametriksi annetun muuttujan arvo kopioituu metodin arvoksi. Metodi saa siis käyttöönsä muuttujan toka sisältämän viitteen. Metodin suorituksen lopuksi metodin viittaaman olion syntymävuosi on kasvanut yhdellä.

Lopulta metodin suoritus päättyy, ja ohjelman suoritus palaa takaisin main-metodiin. Main-metodissa tulostetaan vielä kerran muuttujan eka arvo. Tulostukseksi tulee lopulta "Eka (1972)".

Listat ja oliot

Edellisessä osassa tutuksi tulleet listat ovat olioita, joihin pystyy lisäämään arvoja. Listalle lisättyjä arvoja voidaan tarkastella indeksin perusteella, ja listalla olevia arvoja voidaan etsiä ja poistaa. Kaikkia listan tarjoamia toimintoja käytetään listan tarjoamien metodien kautta.

Listalle lisättävien muuttujien tyyppi määrätään listan luomisen yhteydessä annettavan tyyppiparametrin avulla. Esimerkiksi ArrayList<String> sisältää merkkijonoja, ArrayList<Integer> sisältää kokonaislukuja, ja ArrayList<Double> sisältää liukulukuja.

Alla olevassa esimerkissä lisätään ensin merkkijonoja listalle, jonka jälkeen listalla olevat merkkijonot tulostetaan yksitellen.

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

// merkkijono voidaan ensin muuttujaan
String nimi = "Betty Jennings";
// ja sitten lisätä se listalle
nimet.add(nimi);

// merkkijono voidaan myös lisätä suoraan listalle:
nimet.add("Betty Snyder");
nimet.add("Frances Spence");
nimet.add("Kay McNulty");
nimet.add("Marlyn Wescoff");
nimet.add("Ruth Lichterman");

// listan alkioiden läpikäynti onnistuu toistolauseen avulla
int indeksi = 0;
while (indeksi < nimet.size()) {
    System.out.println(nimet.get(indeksi));
    indeksi++;
}
Betty Jennings
Betty Snyder
Frances Spence
Kay McNulty
Marlyn Wescoff
Ruth Lichterman

Olioita listalla

Edellisessä osassa lisäsimme listalle muunmuassa merkkijonoja. Merkkijonot ovat olioita, joten ei liene yllätys että listalla voi olla olioita. Tarkastellaan seuraavaksi listan ja olioiden yhteistoimintaa tarkemmin.

Oletetaan, että käytössämme on alla oleva luokka.

public class Henkilo {

    private String nimi;
    private int ika;
    private int paino;
    private int pituus;

    public Henkilo(String nimi) {
        this.nimi = nimi;
        this.ika = 0;
        this.paino = 0;
        this.pituus = 0;
    }

    // muita konstruktoreja ja metodeja

    public String getNimi() {
        return this.nimi;
    }

    public int getIka() {
        return this.ika;
    }

    public void vanhene() {
        this.ika++;
    }

    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);
    }

    @Override
    public String toString() {
        return this.nimi + ", ikä " + this.ika + " vuotta";
    }
}

Olioiden käsittely listalla ei oikeastaan poikkea aiemmin näkemästämme listan käytöstä millään tavalla. Oleellista on vain listalle lisättävien olioiden tyypin määrittely listan luomisen yhteydessä.

Alla olevassa esimerkissä luodaan ensin Henkilo-tyyppisille olioille tarkoitettu lista, jonka jälkeen listalle lisätään henkilöolioita. Lopulta henkilöoliot tulostetaan yksitellen.

ArrayList<Henkilo> henkilot = new ArrayList<>();

// henkilöolio voidaan ensin luoda
Henkilo juhana = new Henkilo("Juhana");
// ja sitten lisätä se listalle
henkilot.add(juhana);

// henkilöolio voidaan myös lisätä listalle "samassa lauseessa"
henkilot.add(new Henkilo("Matti"));
henkilot.add(new Henkilo("Martin"));

int indeksi = 0;
while (indeksi < henkilot.size()) {
    System.out.println(henkilot.get(indeksi));
    indeksi++;
}
Juhana, ikä 0 vuotta
Matti, ikä 0 vuotta
Martin, ikä 0 vuotta

Aiemmin käyttämämme rakenne syötteiden lukemiseen on yhä varsin käytännöllinen.

Scanner lukija = new Scanner(System.in);
ArrayList<Henkilo> henkilot = new ArrayList<>();

while (true) {
    System.out.print("Kirjoita nimi, tyhjä lopettaa: ");
    String nimi = lukija.nextLine();
    if (nimi.isEmpty()) {
        break;
    }

    henkilot.add(new Henkilo(nimi));
}

System.out.println();
System.out.println("Henkilöitä yhteensä: " + henkilot.size());
System.out.println("Henkilöt: ");

int indeksi = 0;
while (indeksi < henkilot.size()) {
    Henkilo henkilo = henkilot.get(indeksi);
    System.out.println(henkilo);
    // tai: System.out.println(henkilot.get(indeksi));

    indeksi++;
}

Listalla olevia olioita voidaan myös tarkastella listan läpikäynnin yhteydessä. Alla olevassa esimerkissä tulostetaan vain täysi-ikäiset henkilöt.

// ..
int indeksi = 0;
while (indeksi < henkilot.size()) {
    Henkilo henkilo = henkilot.get(indeksi);

    if (henkilo.getIka() >= 18) {
        System.out.println(henkilo);
    }

    indeksi++;
}

Ikärajan voi kysyä myös käyttäjältä.

// ..
System.out.print("Mikä ikäraja? ");
int ikaraja = Integer.parseInt(lukija.nextLine());

int indeksi = 0;
while (indeksi < henkilot.size()) {
    Henkilo henkilo = henkilot.get(indeksi);

    if (henkilo.getIka() >= ikaraja) {
        System.out.println(henkilo);
    }

    indeksi++;
}

Tehtäväpohjassa on valmiina televisio-ohjelmaa kuvaava luokka Ohjelma. Luokalla Ohjelma on oliomuuttujat nimi ja pituus, konstruktori, ja muutamia metodeja.

Toteuta ohjelma, joka ensin lukee käyttäjältä televisio-ohjelmia. Kun käyttäjä syöttää tyhjän ohjelman nimen, televisio-ohjelmien lukeminen lopetetaan.

Tämän jälkeen käyttäjältä kysytään ohjelman maksimipituutta. Kun käyttäjä on syöttänyt ohjelman maksimipituuden, tulostetaan kaikki ne ohjelmat, joiden pituus on pienempi tai yhtäsuuri kuin haluttu maksimipituus.

Nimi: Salatut elämät
Pituus: 30
Nimi: Miehen puolikkaat
Pituus: 30
Nimi: Remppa vai muutto
Pituus: 60
Nimi: House
Pituus: 60

Ohjelman maksimipituus? 30
Salatut elämät, 30 minuuttia
Miehen puolikkaat, 30 minuuttia

Toteuta ohjelma, joka ensin lukee kirjojen tietoja käyttäjältä. Jokaisesta kirjasta tulee lukea kirjan nimi, sivujen lukumäärä sekä kirjoitusvuosi. Kirjojen lukeminen lopetetaan kun käyttäjä syöttää tyhjän kirjan nimen.

Tämän jälkeen käyttäjältä kysytään mitä tulostetaan. Jos käyttäjä syöttää merkkijonon "kaikki", tulostetaan kirjojen nimet, sivujen lukumäärät sekä kirjoitusvuodet. Jos taas käyttäjä syöttää merkkijonon "nimi", tulostetaan vain kirjojen nimet.

Ohjelmaa varten kannattanee toteuttaa Kirjaa kuvaava luokka. Vaikka tehtävässä on todellisuudessa vain yksi osa, on se kahden osan arvoinen.

Nimi: Minä en sitten muutu
Sivuja: 201
Kirjoitusvuosi: 2010
Nimi: Nalle Puh ja elämisen taito
Sivuja: 100
Kirjoitusvuosi: 2005
Nimi: Beautiful Code
Sivuja: 593
Kirjoitusvuosi: 2007
Nimi: KonMari
Sivuja: 222
Kirjoitusvuosi: 2011

Mitä tulostetaan? kaikki
Minä en sitten muutu, 201 sivua, 2010
Nalle Puh ja elämisen taito, 100 sivua, 2005
Beautiful Code, 593 sivua, 2007
KonMari, 222 sivua, 2011
Nimi: Minä en sitten muutu
Sivuja: 201
Kirjoitusvuosi: 2010
Nimi: Nalle Puh ja elämisen taito
Sivuja: 100
Kirjoitusvuosi: 2005
Nimi: Beautiful Code
Sivuja: 593
Kirjoitusvuosi: 2007
Nimi: KonMari
Sivuja: 222
Kirjoitusvuosi: 2011

Mitä tulostetaan? nimi
Minä en sitten muutu
Nalle Puh ja elämisen taito
Beautiful Code
KonMari

Listakin sisältää viitteitä

Kun olio lisätään listalle, listalle kopioidaan viite. Kuten aiemmin, olion sisäisestä tilasta ei luoda kopiota, vaan listalle lisätään viite olemassa olevaan olioon.

Alla olevassa esimerkissä luodaan ensin olio juhana, joka lisätään listalle. Tämän jälkeen listalle lisätään kaksi muuta oliota. Seuraavaksi juhana-olion metodia vanhene kutsutaan. Lopulta jokaista listalla olevaa oliota vanhennetaan.

ArrayList<Henkilo> henkilot = new ArrayList<>();

Henkilo juhana = new Henkilo("Juhana");
henkilot.add(juhana);

henkilot.add(new Henkilo("Matti"));
henkilot.add(new Henkilo("Martin"));

// juhana vanhenee 2 vuotta
juhana.vanhene();
juhana.vanhene();

// jokainen listalla oleva henkilöolio vanhenee vuoden
int indeksi = 0;
while (indeksi < henkilot.size()) {
    Henkilo henkilo = henkilot.get(indeksi);
    henkilo.vanhene();
    indeksi++;
}

// tulostetaan henkilöt
indeksi = 0;
while (indeksi < henkilot.size()) {
    Henkilo henkilo = henkilot.get(indeksi);
    System.out.println(henkilo);

    // tai: System.out.println(henkilot.get(indeksi));

    indeksi++;
}
Juhana, ikä 3 vuotta
Matti, ikä 1 vuotta
Martin, ikä 1 vuotta

Listalle on kopioituna viitteet olioihin. Yllä olevassa esimerkissä muuttujan juhana arvona on sama viite kuin listalla, joten "Juhanan" ikä muuttuu myös jos hän vanhenee listan ulkopuolella.

 

Lista oliomuuttujana

Listat ovat olioita, joten oliomuuttujaksi voi asettaa listan. Tarkastellaan tätä seuraavaksi.

Olemme aiemmin huomanneet, että listat ovat esimerkiksi näppäriä silloin, silloin kun haluamme pitää kirjaa useammasta erillisestä asiasta. Alla olevassa esimerkissä käsitteelle soittolista on luotu luokka. Soittolista sisältää kappaleita.

// importit

public class Soittolista {
    private ArrayList<String> kappaleet;

    public Soittolista() {
        this.kappaleet = new ArrayList<>();
    }

    public void lisaaKappale(String kappale) {
        this.kappaleet.add(kappale);
    }

    public void poistaKappale(String kappale) {
        this.kappaleet.remove(kappale);
    }

    public void tulostaKappaleet() {
        int indeksi = 0;
        while (indeksi < this.kappaleet.size()) {
            String kappale = this.kappaleet.get(indeksi);
            System.out.println(kappale);

            indeksi++;
        }
    }
}

Soittolistojen luominen on edellisen luokan avulla helppoa.

Soittolista lista = new Soittolista();
lista.lisaaKappale("Sorateiden kuningas");
lista.lisaaKappale("Teuvo, maanteiden kuningas");
lista.tulostaKappaleet();
Sorateiden kuningas
Teuvo, maanteiden kuningas

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

    // toteuta tänne tarvittavat metodit
}

Ruokalistaoliolla on oliomuuttujana ArrayList, jonka on tarkoitus tallentaa ruokalistalla olevien ruokalajien nimet. Ruokalistan tulee tarjota seuraavat metodit:

  • public void lisaaAteria(String ateria) lisää aterian ruokalistalle.
  • public void tulostaAteriat() tulostaa ateriat.
  • public void tyhjennaRuokalista() tyhjentää ruokalistan.

Aterian lisääminen

Toteuta metodi public void lisaaAteria(String ateria), joka lisää uuden aterian listalle ateriat. Jos lisättävä ateria on jo listalla, sitä ei tule lisätä uudelleen. Jos et muista miten listalla olemassaoloa tarkastellaan, lue edellisestä osasta kohta "Onko listalla".

Aterioiden tulostaminen

Toteuta metodi public void tulostaAteriat(), joka tulostaa ateriat. 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.

Oliomuuttujana oleva lista voi sisältää myös muunlaisia olioita. Laajennetaan neljännessä osassa hahmoteltua luokkaa PainonvartijaYhdistys siten, että yhdistys lisää kaikki jäsenensä listalle. Laajennetussa versiossa konstruktorille annetaan alimman painoindeksin lisäksi myös 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<>();
    }

    //..
}

Tehdään metodi jolla henkilö liitetään yhdistykseen. Metodi ei liitä yhdistykseen kuin tarpeeksi suuren painoindeksin 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) {
        // sama kuin hyvaksytaanJaseneksi(henkilo) == false
        if (!hyvaksytaanJaseneksi(henkilo)) {
            // void-tyyppisistä metodeista voi palata
            // return-kutsulla
            return;
        }

        this.jasenet.add(henkilo);
    }

    public String toString() {
        String jasenetMerkkijonona = "";

        int indeksi = 0;
        while (indeksi < this.jasenet.size()) {
            Henkilo jasen = this.jasenet.get(indeksi);
            jasenetMerkkijonona += "  " + jasen.getNimi() + "\n";
            indeksi++;
        }

        return "Painonvartijayhdistys " + this.nimi + " jäsenet: \n" + jasenetMerkkijonona;
    }
}

Metodi lisaaJaseneksi käyttää aiemmin tehtyä metodia hyvaksytaanJaseneksi.

Kokeillaan laajentunutta painonvartijayhdistystä:

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

Tehdään vielä lopuksi 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);

        int indeksi = 0;
        while (indeksi < this.jasenet.size()) {
            Henkilo henkilo = this.jasenet.get(indeksi);
            if (henkilo.painoIndeksi() > painavinTahanAsti.painoIndeksi()) {
                painavinTahanAsti = henkilo;
            }

            indeksi++;
        }

        return painavinTahanAsti;
    }
}

Logiikaltaan edeltävä 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.

PainonvartijaYhdistys painonVartija = new PainonvartijaYhdistys("Kumpulan paino", 25);

// .. lisätään listalle ..

Henkilo painavin = painonVartija.suurinPainoindeksinen();
System.out.print("suurin painoindeksi on jäsenellä " + painavin.getNimi());
suurin painoindeksi on jäsenellä Petri

Tehtävässä tehdään puhelinmuistio.

Henkilö

Tee ensin luokka Henkilo. Luokan tulee toimia seuraavan esimerkin osoittamalla tavalla:

Henkilo pekka = new Henkilo("Pekka Mikkola", "040-123123");

System.out.println(pekka.getNimi());
System.out.println(pekka.getNumero());

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

  • metodi public String toString(), joka palauttaa henkilön merkkijonoesityksen (yo. esimerkin tapaan muotoiltuna)
  • konstruktori, jolla asetetaan henkilölle nimi ja puhelinnumero
  • public String getNimi(), joka palauttaa nimen
  • public String getNumero(), joka palauttaa puhelinnumeron
  • metodi public void vaihdaNumeroa(String uusiNumero), joka muuttaa henkilön puhelinnumeroa

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:

  • public void lisaa(String nimi, String numero) luo Henkilo-olion ja lisää sen puhelinmuistion ArrayListiin.
  • public void tulostaKaikki(), tulostaa puhelinmuistion sisällön

Esimerkki muistion toiminnasta:

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:

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

Tässä tehtävässä tehdään joukkueiden ja joukkueen pelaajien ylläpitoon tarkoitettuun ohjelmistoon tarvittavat ydinluokat.

Joukkue

Tee luokka Joukkue, johon tallennetaan joukkueen nimi (String). Tee luokkaan seuraavat metodit:

  • konstruktori, jolle annetaan joukkueen nimi
  • getNimi, joka palauttaa joukkueen nimen

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:

  • getNimi, joka palauttaa pelaajan nimen
  • getMaalit, joka palauttaa tehtyjen maalien määrän
  • toString, joka palauttaa pelaajan merkkijonoesityksen
public class Main {
    public static void main(String[] args) {
        Joukkue tapiiri = new Joukkue("FC Tapiiri");
        System.out.println("Joukkue: " + tapiiri.getNimi());

        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:

  • lisaaPelaaja, joka lisää pelaajan joukkueeseen
  • tulostaPelaajat, joka tulostaa joukkueessa olevat pelaajat

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:

  • setMaksimikoko(int maksimikoko), joka asettaa joukkueen maksimikoon (eli maksimimäärän pelaajia)
  • getPelaajienLukumaara, joka palauttaa pelaajien määrän (int)

Joukkueen suurin sallittu pelaajamäärä on oletusarvoisesti 16 (lisää tämä luokan oliomuuttujaksi ja alusta muuttujan arvo konstruktorissa). Metodin setMaksimikoko avulla tätä rajaa voi muuttaa. Muuta edellisessä osassa tehtyä 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.setMaksimikoko(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.getPelaajienLukumaara());
    }
}
Pelaajia yhteensä: 1

Joukkueen maalit

Lisää luokkaan Joukkue metodi:

  • yhteismaalit, joka palauttaa joukkueen pelaajien tekemien maalien yhteismäärän.

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.yhteismaalit());
    }
}
Maaleja yhteensä: 40

Tässä tehtävässä harjoitellaan lahjojen pakkaamista. Tehdään luokat Lahja ja Pakkaus. Lahjalla on nimi ja paino, ja Pakkaus sisältää lahjoja.

Lahja-luokka

Tee luokka Lahja, josta muodostetut oliot kuvaavat erilaisia lahjoja. Tallennettavat tiedot ovat tavaran nimi ja paino (kg).

Lisää luokkaan seuraavat metodit:

  • Konstruktori, jolle annetaan parametrina lahjan nimi ja paino
  • Metodi public String getNimi(), joka palauttaa lahjan nimen
  • Metodi public int getPaino(), joka palauttaa lahjan painon
  • Metodi public String toString(), joka palauttaa merkkijonon muotoa "nimi (paino kg)"

Seuraavassa on luokan käyttöesimerkki:

public class Main {
    public static void main(String[] args) {
        Lahja kirja = new Lahja("Aapiskukko", 2);

        System.out.println("Lahjan nimi: " + kirja.getNimi());
        System.out.println("Lahjan paino: " + kirja.getPaino());

        System.out.println("Lahja: " + kirja);
    }
}

Ohjelman tulostuksen tulisi olla seuraava:

Lahjan nimi: Aapiskukko
Lahjan paino: 2
Lahja: Aapiskukko (2 kg)

Pakkaus-luokka

Tee luokka Pakkaus. Pakkaus voi sisältää äärettömän määrän lahjoja, jonka lisäksi se tarjoaa metodin lahjojen yhteispainon laskemiseen.

Lisää luokkaan seuraavat metodit:

  • Parametriton konstruktori
  • Metodi public void lisaaLahja(Lahja lahja), joka lisää parametrina annettavan lahjan pakkaukseen. Metodi ei palauta mitään arvoa.

Tavarat kannattaa tallentaa ArrayList-olioon:

ArrayList<Lahja> lahjat = new ArrayList<>();

Seuraavassa on luokan käyttöesimerkki:

public class Main {
    public static void main(String[] args) {
        Lahja kirja = new Lahja("Aapiskukko", 2);

        Pakkaus paketti = new Pakkaus();
        paketti.lisaaLahja(kirja);
        System.out.println(paketti.getPaino());
    }
}

Ohjelman tulostuksen tulisi olla seuraava:

2

Tässä tehtäväsarjassa tehdään luokat Tavara, Matkalaukku ja Lastiruuma, joiden avulla harjoitellaan lisää olioita, jotka sisältävät toisia olioita.

Tavara-luokka

Tee luokka Tavara, josta muodostetut oliot vastaavat erilaisia tavaroita. Tallennettavat tiedot ovat tavaran nimi ja paino (kg).

Lisää luokkaan seuraavat metodit:

  • Konstruktori, jolle annetaan parametrina tavaran nimi ja paino
  • Metodi public String getNimi(), joka palauttaa tavaran nimen
  • Metodi public int getPaino(), joka palauttaa tavaran painon
  • Metodi public String toString(), joka palauttaa merkkijonon muotoa "nimi (paino kg)"

Seuraavassa on luokan käyttöesimerkki:

public class Main {
    public static void main(String[] args) {
        Tavara kirja = new Tavara("Aapiskukko", 2);
        Tavara puhelin = new Tavara("Nokia 3210", 1);

        System.out.println("Kirjan nimi: " + kirja.getNimi());
        System.out.println("Kirjan paino: " + kirja.getPaino());

        System.out.println("Kirja: " + kirja);
        System.out.println("Puhelin: " + puhelin);
    }
}

Ohjelman tulostuksen tulisi olla seuraava:

Kirjan nimi: Aapiskukko
Kirjan paino: 2
Kirja: Aapiskukko (2 kg)
Puhelin: Nokia 3210 (1 kg)

Matkalaukku-luokka

Tee luokka Matkalaukku. Matkalaukkuun liittyy tavaroita ja maksimipaino, joka määrittelee tavaroiden suurimman mahdollisen yhteispainon.

Lisää luokkaan seuraavat metodit:

  • Konstruktori, jolle annetaan maksimipaino
  • Metodi public void lisaaTavara(Tavara tavara), joka lisää parametrina annettavan tavaran matkalaukkuun. Metodi ei palauta mitään arvoa.
  • Metodi public String toString(), joka palauttaa merkkijonon muotoa "x tavaraa (y kg)"

Tavarat kannattaa tallentaa ArrayList-olioon:

ArrayList<Tavara> tavarat = new ArrayList<>();

Luokan Matkalaukku tulee valvoa, että sen sisältämien tavaroiden yhteispaino ei ylitä maksimipainoa. Jos maksimipaino ylittyisi lisättävän tavaran vuoksi, metodi lisaaTavara ei saa lisätä uutta tavaraa laukkuun.

Seuraavassa on luokan käyttöesimerkki:

public class Main {
    public static void main(String[] args) {
        Tavara kirja = new Tavara("Aapiskukko", 2);
        Tavara puhelin = new Tavara("Nokia 3210", 1);
        Tavara tiiliskivi = new Tavara("tiiliskivi", 4);

        Matkalaukku matkalaukku = new Matkalaukku(5);
        System.out.println(matkalaukku);

        matkalaukku.lisaaTavara(kirja);
        System.out.println(matkalaukku);

        matkalaukku.lisaaTavara(puhelin);
        System.out.println(matkalaukku);

        matkalaukku.lisaaTavara(tiiliskivi);
        System.out.println(matkalaukku);
    }
}

Ohjelman tulostuksen tulisi olla seuraava:

0 tavaraa (0 kg)
1 tavaraa (2 kg)
2 tavaraa (3 kg)
2 tavaraa (3 kg)

Kielenhuoltoa

Ilmoitukset "0 tavaraa" ja "1 tavaraa" eivät ole kovin hyvää suomea – paremmat muodot olisivat "ei tavaroita" ja "1 tavara". Tee tämä muutos luokassa Matkalaukku sijaitsevaan toString-metodiin.

Nyt edellisen ohjelman tulostuksen tulisi olla seuraava:

ei tavaroita (0 kg)
1 tavara (2 kg)
2 tavaraa (3 kg)
2 tavaraa (3 kg)

Kaikki tavarat

Lisää luokkaan Matkalaukku seuraavat metodit:

  • metodi tulostaTavarat, joka tulostaa kaikki matkalaukussa olevat tavarat
  • metodi yhteispaino, joka palauttaa tavaroiden yhteispainon

Seuraavassa on luokan käyttöesimerkki:

public class Main {
    public static void main(String[] args) {
        Tavara kirja = new Tavara("Aapiskukko", 2);
        Tavara puhelin = new Tavara("Nokia 3210", 1);
        Tavara tiiliskivi = new Tavara("tiiliskivi", 4);

        Matkalaukku matkalaukku = new Matkalaukku(10);
        matkalaukku.lisaaTavara(kirja);
        matkalaukku.lisaaTavara(puhelin);
        matkalaukku.lisaaTavara(tiiliskivi);

        System.out.println("Matkalaukussa on seuraavat tavarat:");
        matkalaukku.tulostaTavarat();
        System.out.println("Yhteispaino: " + matkalaukku.yhteispaino() + " kg");
    }
}

Ohjelman tulostuksen tulisi olla seuraava:

Matkalaukussa on seuraavat tavarat:
Aapiskukko (2 kg)
Nokia 3210 (1 kg)
Tiiliskivi (4 kg)
Yhteispaino: 7 kg

Muokkaa myös luokkaasi siten, että käytät vain kahta oliomuuttujaa. Toinen sisältää maksimipainon, toinen on lista laukussa olevista tavaroista.

Raskain tavara

Lisää vielä luokkaan Matkalaukku metodi raskainTavara, joka palauttaa painoltaan suurimman tavaran. Jos yhtä raskaita tavaroita on useita, metodi voi palauttaa minkä tahansa niistä. Metodin tulee palauttaa olioviite. Jos laukku on tyhjä, palauta arvo null.

Seuraavassa on luokan käyttöesimerkki:

public class Main {
    public static void main(String[] args) {
        Tavara kirja = new Tavara("Aapiskukko", 2);
        Tavara puhelin = new Tavara("Nokia 3210", 1);
        Tavara tiiliskivi = new Tavara("Tiiliskivi", 4);

        Matkalaukku matkalaukku = new Matkalaukku(10);
        matkalaukku.lisaaTavara(kirja);
        matkalaukku.lisaaTavara(puhelin);
        matkalaukku.lisaaTavara(tiiliskivi);

        Tavara raskain = matkalaukku.raskainTavara();
        System.out.println("Raskain tavara: " + raskain);
    }
}

Ohjelman tulostuksen tulisi olla seuraava:

Raskain tavara: Tiiliskivi (4 kg)

Lastiruuma-luokka

Tee luokka Lastiruuma, johon liittyvät seuraavat metodit:

  • konstruktori, jolle annetaan maksimipaino
  • metodi public void lisaaMatkalaukku(Matkalaukku laukku), joka lisää parametrina annetun matkalaukun lastiruumaan
  • metodi public String toString(), joka palauttaa merkkijonon muotoa "x matkalaukkua (y kg)"

Tallenna matkalaukut sopivaan ArrayList-rakenteeseen.

Luokan Lastiruuma tulee valvoa, että sen sisältämien matkalaukkujen yhteispaino ei ylitä maksimipainoa. Jos maksimipaino ylittyisi uuden matkalaukun vuoksi, metodi lisaaMatkalaukku ei saa lisätä uutta matkalaukkua.

Seuraavassa on luokan käyttöesimerkki:

public class Main {
    public static void main(String[] args) {
        Tavara kirja = new Tavara("Aapiskukko", 2);
        Tavara puhelin = new Tavara("Nokia 3210", 1);
        Tavara tiiliskivi = new Tavara("tiiliskivi", 4);

        Matkalaukku adanLaukku = new Matkalaukku(10);
        adanLaukku.lisaaTavara(kirja);
        adanLaukku.lisaaTavara(puhelin);

        Matkalaukku pekanLaukku = new Matkalaukku(10);
        pekanLaukku.lisaaTavara(tiiliskivi);

        Lastiruuma lastiruuma = new Lastiruuma(1000);
        lastiruuma.lisaaMatkalaukku(adanLaukku);
        lastiruuma.lisaaMatkalaukku(pekanLaukku);

        System.out.println(lastiruuma);
    }
}

Ohjelman tulostuksen tulisi olla seuraava:

2 matkalaukkua (7 kg)

Lastiruuman sisältö

Lisää luokkaan Lastiruuma metodi public void tulostaTavarat(), joka tulostaa kaikki lastiruuman matkalaukuissa olevat tavarat.

Seuraavassa on luokan käyttöesimerkki:

public class Main {
    public static void main(String[] args) {
        Tavara kirja = new Tavara("Aapiskukko", 2);
        Tavara puhelin = new Tavara("Nokia 3210", 1);
        Tavara tiiliskivi = new Tavara("tiiliskivi", 4);

        Matkalaukku adanLaukku = new Matkalaukku(10);
        adanLaukku.lisaaTavara(kirja);
        adanLaukku.lisaaTavara(puhelin);

        Matkalaukku pekanLaukku = new Matkalaukku(10);
        pekanLaukku.lisaaTavara(tiiliskivi);

        Lastiruuma lastiruuma = new Lastiruuma(1000);
        lastiruuma.lisaaMatkalaukku(adanLaukku);
        lastiruuma.lisaaMatkalaukku(pekanLaukku);

        System.out.println("Ruuman matkalaukuissa on seuraavat tavarat:");
        lastiruuma.tulostaTavarat();
    }
}

Ohjelman tulostuksen tulisi olla seuraava:

Ruuman matkalaukuissa on seuraavat tavarat:
Aapiskukko (2 kg)
Nokia 3210 (1 kg)
tiiliskivi (4 kg)

Ensiaskeleet automaattiseen testaamiseen

Otetaan seuraavaksi ensiaskeleet ohjelmien testaamiseen.

Virhetilanteet ja ongelman ratkaiseminen askel kerrallaan

Ohjelmia luodessa niihin päätyy virheitä. Joskus virheet eivät ole niin vakavia, ja aiheuttavat päänvaivaa lähinnä ohjelman käyttäjälle. Joskus toisaalta virheet voivat johtaa hyvinkin vakaviin seurauksiin. Varmaa on joka tapauksessa se, että ohjelmoimaan opetteleva ihminen tekee paljon virheitä.

Virheitä ei kannata missään nimessä pelätä tai välttää, sillä virheitä tekemällä oppii parhaiten. Pyri siis myös välillä rikkomaan työstämääsi ohjelmaa, jolloin pääset tutkimaan virheilmoitusta ja tarkastelemaan kertooko virheilmoitus jotain tekemästäsi virheestä.

Ohjelmistovirhe

Osoitteessa http://sunnyday.mit.edu/accidents/MCO_report.pdf oleva raportti kuvaa erään hieman vakavamman ohjelmistovirheestä johtuneen tapaturman sekä ohjelmistovirheen.

Ohjelmistovirhe liittyi siihen, että käytetty ohjelma odotti, että ohjelmoija käyttäisi kansainvälistä yksikköjärjestelmää laskuissa (metrit, kilogrammat, ...). Ohjelmoija oli kuitenkin käyttänyt amerikkalaista mittajärjestelmää erään järjestelmän osan laskuissa, jolloin satelliitin navigointiin liittynyt automaattinen korjausjärjestelmä ei toiminut oikein.

Satelliitti tuhoutui.

Ohjelmien muuttuessa monimutkaisemmiksi, tulee virheiden löytämisestäkin haastavampaa. NetBeansiin integroitu debuggeri voi olla avuksi virheiden löytämisessä. Debuggerin käyttöä on esitelty kurssimateriaaliin upotetuilla videoilla; alla oleva video esittelee myös miten projekteja voidaan luoda, avata ja sulkea sekä miten ohjelmia voidaan suorittaa NetBeansin ulkopuolella. Screencastissa on myös asioita, joita kurssilla ei vielä ole tullut -- älä huoli, nämä tulevat vastaan opintojen edetessä.

Stack trace

Kun ohjelmassa tapahtuu virhe, ohjelma tyypillisesti tulostaa ns. stack tracen, eli niiden metodikutsujen listan, joiden seurauksena virhetilanne syntyi. Stack trace voi näyttää esimerkiksi seuraavalta:

Exception in thread "main" ...
    at Ohjelma.main(Ohjelma.java:15)

Listan alussa kerrotaan minkälainen virhe tapahtui (tässä ...), ja seuraavalla rivillä kerrotaan missä virhe tapahtui. Rivi "at Ohjelma.main(Ohjelma.java:15)" sanoo, että virhe tapahtui Ohjelma.java-tiedoston rivillä 15.

at Ohjelma.main(Ohjelma.java:15)

Muistilista virheenselvitykseen

Jos koodisi ei toimi etkä tiedä missä on virhe, näillä askeleilla pääset alkuun.

  1. Sisennä koodisi oikein ja selvitä, puuttuuko sulkuja.
  2. Tarkista ovatko käytetyt muuttujat oikean nimisiä.
  3. Testaa ohjelman kulkua erilaisilla syötteillä, ja selvitä minkälaisella syötteellä ohjelma ei toimi halutusti. Jos sait virheen testeistä, testit saattavat myös kertoa käytetyn syötteen.
  4. Lisää ohjelmaan tulostuskomentoja, joissa tulostat käytettävien muuttujien arvoja ohjelman suorituksen eri vaiheissa.
  5. Tarkista, että kaikki käyttämäsi muuttujat on alustettu. Jos tätä ei ole tehty, seuraa virhe NullPointerException.
  6. Jos ohjelmasi aiheuttaa poikkeuksen, kannattaa ehdottomasti kiinnittää huomiota poikkeuksen yhteydessä olevaan stack traceen, eli niiden metodikutsujen listaan, minkä seurauksena poikkeuksen aiheuttanut tilanne syntyi.
  7. Opettele käyttämään debuggeria, aiemmin nähdyllä videolla pääsee alkuun.

Testisyötteen antaminen Scannerille

Ohjelman testaaminen käsin on usein työlästä. Syötteen antaminen on mahdollista automatisoida esimerkiksi syöttämällä Scanner-oliolle luettava merkkijono. Alla on annettu esimerkki siitä, miten ohjelmaa voi testata automaattisesti. Ohjelmassa syötetään ensin viisi merkkijonoa, jonka jälkeen syötetään aiemmin nähty merkkijono. Tämän jälkeen yritetään syöttää vielä uusi merkkijono. Merkkijonoa "kuusi" ei pitäisi esiintyä sanajoukossa.

Testisyötteen voi antaa merkkijonona Scanner-oliolle konstruktorissa. Jokainen testisyötteessä annettava rivinvaihto merkitään merkkijonoon kenoviivan ja n-merkin yhdistelmänä "\n".

String syote = "yksi\n" + "kaksi\n"  +
    "kolme\n" + "nelja\n" +
    "viisi\n" + "yksi\n"  +
    "kuusi\n";

Scanner lukija = new Scanner(syote);

ArrayList<String> luetut = new ArrayList<>();
while (true) {
    System.out.println("Anna syote: ");
    String rivi = lukija.nextLine();
    if (luetut.contains(rivi)) {
        break;
    }

    luettu.add(rivi);
}

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

if (luetut.sisaltaa("kuusi")) {
    System.out.println("Joukkoon lisättiin arvo, jota sinne ei olisi pitänyt lisätä.");
}

Ohjelma tulostus näyttää vain ohjelman antaman tulostuksen, ei käyttäjän tekemiä komentoja.

Anna syote:
Anna syote:
Anna syote:
Anna syote:
Anna syote:
Anna syote:
Kiitos!

Merkkijonon antaminen Scanner-luokan konstruktorille korvaa näppäimistöltä luettavan syötteen. Merkkijonomuuttujan syote sisältö siis "simuloi" käyttäjän antamaa syötettä. Rivinvaihto syötteeseen merkitään \n:llä. Jokainen yksittäinen rivinvaihtomerkkiin loppuva osa syote-merkkijonossa siis vastaa yhtä nextLine()-komentoon annettua syötettä.

Kun haluat testata ohjelmasi toimintaa jälleen käsin, vaihda Scanner-olion konstruktorin parametriksi System.in, eli järjestelmän syötevirta. Voit toisaalta halutessasi myös vaihtaa testisyötettä, sillä kyse on merkkijonosta.

Ohjelman toiminnan oikeellisuus tulee edelleen tarkastaa ruudulta. Tulostus voi olla aluksi hieman hämmentävää, sillä automatisoitu syöte ei näy ruudulla ollenkaan. Lopullinen tavoite on automatisoida myös ohjelman tulostuksen oikeellisuden tarkastaminen niin hyvin, että ohjelman testaus ja testituloksen analysointi onnistuu "nappia painamalla". Palaamme aiheeseen myöhemmissä osissa.

Ohjelmassa on yritetty luoda sovellus, joka kysyy käyttäjältä merkkijonoa ja lukua. Sovelluksen pitäisi toimia esimerkiksi seuraavasti:

Sana:
testi
Luku:
3
t
 e
  s
t
 i

Esimerkki 2:

Sana:
esim
Luku:
2
e
 s
i
 m

Tällä hetkellä ohjelma ei kuitenkaan toimi halutusti. Ota selvää miksi ja korjaa ohjelma. Huomaat myös, että ohjelmassa käytetään paljon omituisia asioita, jotka eivät ole tulleet vielä kurssilla tutuksi. Kannattaakin tehdä niin, että kokeilee myös näitä uusia asioita erikseen.

Yksikkötestaus

Edellä esitetty menetelmä automaattiseen testaamiseen missä ohjelmalle syötetyt syötteet muutetaan on varsin kätevä, mutta kuitenkin melko rajoittunut. Isompien ohjelmien testaaminen edellä kuvatulla tavalla on haastavaa. Eräs ratkaisu tähän on yksikkötestaus, missä ohjelman pieniä osia testataan erikseen.

Yksikkötestauksella tarkoitetaan lähdekoodiin kuuluvien yksittäisten osien kuten luokkien ja niiden tarjoamien metodien testaamista. Luokkien ja metodien rakenteen suunnittelussa käytettävän ohjesäännön -- jokaisella metodilla ja luokalla tulee olla yksi selkeä vastuu -- noudattamisen tai siitä poikkeamisen huomaa testejä kirjoittaessa. Mitä useampi vastuu metodilla on, sitä monimutkaisempi testi on. Jos laaja sovellus on kirjoitettu yksittäiseen metodiin, on testien kirjoittaminen sitä varten erittäin haastavaa ellei jopa mahdotonta. Vastaavasti, jos sovellus on pilkottu selkeisiin luokkiin ja metodeihin, on testienkin kirjoittaminen suoraviivaista.

Testien kirjoittamisessa hyödynnetään tyypillisesti valmiita yksikkötestauskirjastoja, jotka tarjoavat metodeja ja apuluokkia testien kirjoittamiseen. Javassa käytetyin yksikkötestauskirjasto on JUnit, johon löytyy myös tuki lähes kaikista ohjelmointiympäristöistä. Esimerkiksi NetBeans osaa automaattisesti etsiä JUnit-testejä projektista -- jos testejä löytyy, ne näytetään projektin alla Test Packages -kansiossa.

Tarkastellaan yksikkötestien kirjoittamista esimerkin kautta. Oletetaan, että käytössämme on seuraava luokka Laskin, ja haluamme kirjoittaa sitä varten automaattisia testejä.

public class Laskin {

    private int arvo;

    public Laskin() {
        this.arvo = 0;
    }

    public void summa(int luku) {
        this.arvo += luku;
    }

    public void erotus(int luku) {
        this.arvo += luku;
    }

    public int getArvo() {
        return this.arvo;
    }
}

Laskimen toiminta perustuu siihen, että se muistaa aina edellisen laskuoperaation tuottaman tuloksen. Seuraavat laskuoperaatiot lisätään aina edelliseen lopputulokseen. Yllä olevaan laskimeen on jäänyt myös pieni copy-paste -ohjelmoinnista johtuva virhe. Metodin erotus pitäisi vähentää arvosta, mutta nyt se lisää arvoon.

Yksikkötestien kirjoittaminen aloitetaan testiluokan luomisella. Testiluokka luodaan Test Packages -kansion alle. Kun testaamme luokkaa Laskin, testiluokan nimeksi tulee LaskinTest. Nimen lopussa oleva merkkijono Test kertoo ohjelmointiympäristölle, että kyseessä on testiluokka. Ilman merkkijonoa Test luokassa olevia testejä ei suoriteta. (Huom! Testit luodaan NetBeansissa Test Packages -kansion alle.)

Testiluokka LaskinTest on aluksi tyhjä.

public class LaskinTest {

}

Testit ovat testiluokassa olevia metodeja ja jokainen testi testaa yksittäistä asiaa. Aloitetaan luokan Laskin testaaminen -- luodaan ensin testimetodi, jossa varmistetaan, että juuri luodun laskimen sisältämä arvo on 0.

import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class LaskinTest {

    @Test
    public void laskimenArvoAlussaNolla() {
        Laskin laskin = new Laskin();
        assertEquals(0, laskin.getArvo());
    }
}

Yllä olevassa metodissa laskimenArvoAlussaNolla luodaan ensin laskinolio. Tämän jälkeen käytetään JUnit-testikehyksen tarjoamaa assertEquals-metodia arvon tarkistamiseen. Metodi tuodaan luokasta Assert komennolla import static, ja sille annetaan parametrina odotettu arvo -- tässä 0 -- sekä laskimen palauttama arvo. Jos metodin assertEquals arvot poikkeavat toisistaan, testin suoritus ei pääty hyväksytysti. Jokaisella testimetodilla tulee olla annotaatio @Test -- tämä kertoo JUnit-testikehykselle, että kyseessä on suoritettava testimetodi.

Testien suorittaminen onnistuu valitsemalla projekti oikealla hiirennapilla ja klikkaamalla vaihtoehtoa Test.

Testien suorittaminen luo output-välilehdelle (tyypillisesti NetBeansin alalaidassa) tulosteen, jossa on testiluokkakohtaiset tilastot. Alla olevassa esimerkissä on suoritettu pakkauksessa laskin olevan testiluokan LaskinTest testit. Testejä suoritettiin 1, joista yksikään ei epäonnistunut -- epäonnistuminen tarkoittaa tässä sitä, että testin testaama toiminnallisuus ei toiminut oletetusti.

Testsuite: LaskinTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.054 sec

test-report:
test:
BUILD SUCCESSFUL (total time: 0 seconds)

Lisätään testiluokkaan summaa ja erotusta lisäävää toiminnallisuutta.

package laskin;

import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class LaskinTest {

    @Test
    public void laskimenArvoAlussaNolla() {
        Laskin laskin = new Laskin();
        assertEquals(0, laskin.getArvo());
    }

    @Test
    public void arvoViisiKunSummataanViisi() {
        Laskin laskin = new Laskin();
        laskin.summa(5);
        assertEquals(5, laskin.getArvo());
    }

    @Test
    public void arvoMiinusKaksiKunErotetaanKaksi() {
        Laskin laskin = new Laskin();
        laskin.erotus(2);
        assertEquals(-2, laskin.getArvo());
    }
}

Testien suorittaminen antaa seuraavanlaisen tulostuksen.

Testsuite: LaskinTest
Tests run: 3, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.059 sec

Testcase: arvoMiinusKaksiKunErotetaanKaksi(LaskinTest):	FAILED
expected:<-2> but was:<2>
junit.framework.AssertionFailedError: expected:<-2> but was:<2>
    at LaskinTest.arvoMiinusKaksiKunErotetaanKaksi(LaskinTest.java:25)


Test LaskinTest FAILED
test-report:
test:
BUILD SUCCESSFUL (total time: 0 seconds)

Tulostus kertoo, että kolme testiä suoritettiin. Yksi niistä päätyi virheeseen. Testitulostuksessa on tieto myös testin rivistä, jossa virhe tapahtui (25) sekä tieto odotetusta (-2) ja saadusta arvosta (2). Kun testien suoritus päättyy virheeseen, NetBeans näyttää testien suoritukseen liitttyvän virhetilanteen myös visuaalisena.

Edellisillä testeillä kaksi testeistä menee läpi, mutta yhdessä on tapahtunut virhe. Korjataan luokkaan Laskin jäänyt virhe.

// ...
public void erotus(int luku) {
    this.arvo -= luku;
}
// ...

Kun testit suoritetaan uudestaan, testit menevät läpi.

Testsuite: LaskinTest
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.056 sec

test-report:
test:
BUILD SUCCESSFUL (total time: 0 seconds)

Tehtäväpohjassa tulee edellisen esimerkin alkutilanne. Seuraa edellistä esimerkkiä, ja luo laskimelle esimerkissä näytetyt testit. Kokeile testien toimintaa, ja palauta lopulta tehtävä Test My Code-palvelimelle.

Breakout on Atarin vuonna 1976 julkaisema videopeli. Pelin ideana käyttää pelin alalaidassa olevaa mailaa pelissä liikkuvan pallon lyömiseen siten, että pallolla saadaan rikottua ylälaidassa olevia palasia.

Termi "Breakout" tulee tilanteesta, missä pelaaja saa pallon ylälaidassa olevien palasten yläpuolelle, missä pallo tekee tuhoa useammalle palalle samaan aikaan.

Tässä tehtävässä fiilistellään Breakout-pelin tekemistä.

Tehtäväpohjaan on toteutettuna ensimmäisiä palasia Breakout peliin. Tehtävänäsi on täydentää pelin toimintaa. Alla lista täydennysehdotuksista:

  1. Tällä hetkellä pelin ylälaidassa on vain muutama vaivainen palikka. Muokkaa peliä siten, että ylälaidassa on useita erivärisiä palikoita. Hae inspiraatiota Googlen kuvahausta avainsanalla "Breakout".
  2. Pelin pelattavuus on tällä hetkellä melko heikko. Mailalla ei osuta palloon, vaikka kuinka yritettäisiin. Lisää peliin mahdollisuus osua mailalla palloon -- palloon osumisen pitäisi muuttaa pallon suuntaa. Osoitteesta http://www.edu4java.com/en/game/game6.html olevasta oppaasta saattaa olla hyötyä. Hifistelyä kaipaavat voivat lähteä liikenteeseen kysymyksestä A ball hits the corner, where will it deflect?
  3. Kun pallon osuminen mailaan on hanskassa, lisää samanlainen osumistoiminnallisuus kaikkiin paloihin. Tässä kohtaa paloja ei vielä tarvitse poistaa.
  4. Kun pallo osuu palaan, pala pitäisi poistaa. Sehän on melkein jo peli!
  5. Mieti minkälaisia luokkia pelissä kannattaisi olla. Nyt piirtämiseen käytetty paikka sisältää varmaankin jo hyvin paljon koodia.. Siistimisen paikka!

Tehtävässä ei ole automaattisia testejä ja se on yhden pisteen arvoinen. Voit palauttaa tehtävän jo kun saat ensimmäisen parannusehdotuksen tehtyä mutta peliä saa toki viilata enemmänkin.

Sisällysluettelo