Tehtävät
Yhdeksännen osan tavoitteet

Tuntee luokkaakaavioiden merkintätavan ja luo luokkia luokkakaavioiden perusteella. Ymmärtää käsitteen pakkaus ja osaa hyödyntää pakkauksia ohjelman jäsentelyssä. Tuntee Javan erityyppiset poikkeukset ja luo ohjelmia, jotka varautuvat poikkeuksiin. Tuntee käsitteen perintähierarkia ja kertaa arvojen ryhmittelyä hajautustaulun avulla. Tallentaa olioita useampaan hajautustauluun.

Luokkakaaviot

Luokkakaavio on ohjelmistojen mallinnuksessa käytettävä kaaviotyyppi, jonka avulla kuvataan olio-ohjelmoinnin luokkia. Luokkaaviossa kuvattavat luokat vastaavat ohjelmakoodin luokkia. Kaavioissa kuvataan luokkien nimet, attribuutit, luokkien väliset yhteydet sekä mahdollisesti myös metodit.

Luokka ja attribuutit

Luodaan luokka nimeltä Henkilo, jolla on oliomuuttujat nimi ja ikä.

public class Henkilo {
    private String nimi;
    private int ika;
}

Yllä kuvattua luokkaa kuvaa seuraava luokkakaavio. Luokkakaavioissa luokka kuvataan suorakulmiona, jonka ylälaidassa on luokan nimi, ja keskellä on oliomuuttujien nimet ja tyypit.

Luokkaakaaviossa luokkaan liittyvät muuttujat määritellään muodossa "muuttujanNimi: muuttujanTyyppi". Miinusmerkki ennen muuttujan nimeä kertoo, että muuttujalla on avainsana private.

[Henkilo|-nimi:String;-ika:int]

Olemme nyt määritelleet rakennuspiirustukset -- luokan -- henkilöoliolle. Jokaisella uudella henkilöolioilla on muuttujat nimi ja ika, joissa voi olla oliokohtainen arvo. Henkilöiden "tila" koostuu niiden nimeen ja ikään asetetuista arvoista.

Määrittellään seuraavaksi luokalle konstruktori, joka saa parametrina nimen.

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

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

Luokkakaaviossa konstruktori (ja metodit) merkitään oliomuuttujien alapuolelle. Konstruktori saa näkyvyysmääreen public takia eteen plussan, jonka lisäksi siitä merkitään nimi sekä parametrien nimet ja niiden tyypit (tässä + Henkilo(nimi: String)).

[Henkilo|-nimi:String;-ika:int|+Henkilo(nimi:String)]

Lisätään luokalle metodi, jonka palautustyyppi on void.

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

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

    public void tulostaHenkilo() {
        System.out.println(this.nimi + ", ikä " + this.ika + " vuotta");
    }
}

Luokkakaaviossa metodi merkitään kuten konstruktori, mutta tämän lisäksi metodista kirjoitetaan myös sen palautustyyppi.

[Henkilo|-nimi:String;-ika:int|+Henkilo(nimi:String);+tulostaHenkilo():void]

Metodi tulostaHenkilo hyödyntää oliomuuttujia nimi ja ika, mutta luokkakaaviossa tätä ei kerrota. Tarkemmin katsoen huomaamme, että luokkakaavio ei kerro mitään konstruktorien ja metodien sisäisestä toteutuksesta. Luokkakaaviolla kerrotaan siis olioiden rakenteesta, mutta luokkakaaviot eivät itsessään määrittele toiminnallisuutta.

Lisätään luokalle vielä nimen palauttava metodi getNimi.

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

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

    public void tulostaHenkilo() {
        System.out.println(this.nimi + ", ikä " + this.ika + " vuotta");
    }

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

[Henkilo|-nimi:String;-ika:int|+Henkilo(nimi:String);+tulostaHenkilo():void;+getNimi():String]

Yhteyksien merkintä luokkakaavioon

Luokkakaavioissa yhteydet merkitään viivoilla, joissa nuolet kuvaavat yhteyden suuntaa. Oletetaan, että käytössämme luokka Kirja.

public class Kirja {
    private String nimi;
    private String kustantaja;

    // konstruktorit ja metodit
}

[Kirja|-nimi:String;-julkaisija:String]

Jos luokalle kirja merkitään kirjoittaja, joka on tyyppiä Henkilö, ohjelmakoodissa oliomuuttuja merkitänä muiden muuttujien seuraksi.

public class Kirja {
    private String nimi;
    private String kustantaja;
    private Henkilo kirjoittaja;

    // konstruktorit ja metodit
}

Luokkakaaviossa toisiin olioihin viittaavia muuttujia ei merkitä attribuutteihin, vaan ne merkitään yhteyksinä. Alla olevassa luokkakaaviossa on merkittynä sekä luokka Henkilo että luokka Kirja, sekä näiden välinen yhteys. Yhteys kertoo, että se luotu kirjan suunnasta (nuoli henkilöön) ja että yhteys kertoo kirjoittajasta (nuolen teksti "kirjoittaja").

[Henkilo|-nimi:String;-ika:int|+Henkilo(nimi:String);+tulostaHenkilo():void;+getNimi():String]
											 [Kirja|-nimi:String;-julkaisija:String]
											 [Kirja]-kirjoittaja->[Henkilo]

Jos kirjalla voi olla useita kirjoittajia, kirjoittajat merkitään luokkaan listana.

public class Kirja {
    private String nimi;
    private String kustantaja;
    private ArrayList<Henkilo> kirjoittajat;

    // konstruktorit ja metodit
}

Luokkakaaviossa tilanne merkitään yhteyden päätyyn asetettavalla tähdellä. Tähti kertoo, että kirjalla voi olla nollasta äärettömään kirjoittajaa. Alla olevassa esimerkissä yhteyteen ei ole merkitty yhteyttä kuvaavaa tekstiä "kirjoittajat", mutta se kannattaisi lisätä kaavioon.

[Henkilo|-nimi:String;-ika:int|+Henkilo(nimi:String);+tulostaHenkilo():void;+getNimi():String]
											  [Kirja|-nimi:String;-julkaisija:String]
											  [Kirja]-*>[Henkilo]

Metodit merkitään luokkakaavioon normaalisti. Alla luokkaan Kirja on lisätty metodit getKirjoittajat ja lisaaKirjoittaja.

public class Kirja {
    private String nimi;
    private String kustantaja;
    private ArrayList<Henkilo> kirjoittajat;

    // konstruktori

    public ArrayList<Henkilo> getKirjoittajat() {
        return this.kirjoittajat;
    }

    public void lisaaKirjoittaja(Henkilo kirjoittaja) {
        this.kirjoittajat.add(kirjoittaja);
    }
}

[Henkilo|-nimi:String;-ika:int|+Henkilo(nimi:String);+tulostaHenkilo():void;+getNimi():String]
												     [Kirja|-nimi:String;-julkaisija:String|+getKirjoittajat():ArrayList;+lisaaKirjoittaja(kirjoittaja:Henkilo)]
												     [Kirja]-*>[Henkilo]

Ylläolevaan kaavioon voisi lisätä vielä ArrayListin sisältämien arvojen tyypin ArrayList<Henkilo> sekä yhteyttä tarkentavan määreen "kirjoittajat".

Rajapinnat luokkakaaviossa

Rajapinnat merkitään luokkakaavioissa muodossa <<interface>> RajapintaLuokanNimi. Tarkastellaan esimerkkinä rajapintaa Luettava.

public interface Luettava {

}

[<<interface>> Luettava]

Metodit voidaan merkitä alle kuten luokkakaavioissa.

Rajapinnan toteuttaminen merkitään katkoviivalla ja kolmiolla. Alla on kuvattu tilanne, missä luokka Kirja toteuttaa rajapinnan Luettava.

[<<interface>> Luettava]
									   [Kirja]-.-^[<<interface>> Luettava]

Laajempi esimerkki: Maatilasimulaattori

Eräs edellisen osan ohjelmointitehtävä oli maatilasimulaattori. Maatilalla on lehmiä, jotka ovat eleleviä ja lypsäviä. Navetassa on lypsyrobotti ja maitosäiliö. Maatilalla on navetta ja lehmiä. Tämän lisäksi myös Maatila on elelevä. Eräs mahdollinen tehtävän loppuratkaisua kuvaava luokkakaavio on seuraavanlainen.

[Maitosailio|-tilavuus:double;-saldo:double]
								     [Lehma]
								     [<<interface>> Eleleva]
								     [<<interface>> Lypsava]
								     [Lypsyrobotti]
								     [Maatila|-omistaja:String]
								     [Navetta]
								     [Navetta]->[Maitosailio]
								     [Navetta]->[Lypsyrobotti]
								     [Maatila]->[Navetta]
								     [Maatila]->*[Lehma]
								     [Maatila]-.-^[<<interface>> Eleleva]
								     [Lehma]-.-^[<<interface>> Eleleva]
								     [Lehma]-.-^[<<interface>> Lypsava]

Luokkakaavioiden käytöstä

Luokkakaavioita kannattaa käyttää laajempien tehtävien (ja ongelmien) käsitteiden sekä niiden yhteyksien hahmottamisessa. Kurssin tehtäviä tehdessä hyvä lähtökohta on piirtää luokat ja niiden yhteydet ilman oliomuuttujia tai metodeja.

Ohjelman rakenne ja pakkaukset

Ohjelmaa varten toteutettujen luokkien määrän kasvaessa niiden toiminnallisuuksien ja metodien muistaminen vaikeutuu. Muistamista helpottaa luokkien järkevä nimentä sekä luokkien suunnittelu siten, että jokaisella luokalla on yksi selkeä vastuu. Tämän lisäksi luokat kannattaa jakaa toiminnallisuutta, käyttötarkoitusta tai jotain muuta loogista kokonaisuutta kuvaaviin pakkauksiin.

Pakkaukset (package) ovat käytännössä hakemistoja (directory, puhekielessä myös kansio), joihin lähdekooditiedostot organisoidaan.

Ohjelmointiympäristöt tarjoavat valmiit työkalut pakkausten hallintaan. Olemme tähän mennessä luoneet luokkia ja rajapintoja vain projektiin liittyvän lähdekoodipakkaukset-osion (Source Packages) oletuspakkaukseen (default package). Uuden pakkauksen voi luoda NetBeansissa projektin pakkauksiin liittyvässä Source Packages -osiossa oikeaa hiirennappia painamalla ja valitsemalla New -> Java Package....

Pakkauksen sisälle voidaan luoda luokkia aivan kuten oletuspakkaukseenkin (default package). Alla luodaan juuri luotuun pakkaukseen kirjasto luokka Sovellus.

Luokan pakkaus -- eli pakkaus, jossa luokka sijaitsee -- ilmaistaan lähdekooditiedoston alussa lauseella package pakkaus;. Alla oleva luokka Sovellus sijaitsee pakkauksessa kirjasto.

package kirjasto;

public class Sovellus {

    public static void main(String[] args) {
        System.out.println("Hello packageworld!");
    }
}

Jokainen pakkaus -- myös oletuspakkaus eli default package -- voi sisältää useampia pakkauksia. Esimerkiksi pakkausmäärittelyssä package kirjasto.domain pakkaus domain on pakkauksen kirjasto sisällä. Edellä käytettyä nimeä domain käytetään usein kuvaamaan sovellusalueen käsitteisiin liittyvien luokkien säilytyspaikkaa. Esimerkiksi luokka Kirja voisi hyvin olla pakkauksen kirjasto.domain sisällä, sillä se kuvaa kirjastosovellukseen liittyvää käsitettä.

package kirjasto.domain;

public class Kirja {
    private String nimi;

    public Kirja(String nimi) {
        this.nimi = nimi;
    }

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

Pakkauksissa olevia luokkia tuodaan luokan käyttöön import-lauseen avulla. Pakkauksessa kirjasto.domain oleva luokka Kirja tuodaan käyttöön puolipisteeseen päättyvällä lauseella import kirjasto.domain.Kirja. Luokkien tuomiseen käytetyt import-lauseet asetetaan lähdekooditiedostoon pakkausmäärittelyn jälkeen.

package kirjasto;

import kirjasto.domain.Kirja;

public class Sovellus {

    public static void main(String[] args) {
        Kirja kirja = new Kirja("pakkausten ABC!");
        System.out.println("Hello packageworld: " + kirja.getNimi());
    }
}
Hello packageworld: pakkausten ABC!

Jatkossa kaikissa tehtävissämme käytetään pakkauksia. Luodaan seuraavaksi ensimmäiset pakkaukset itse.

Käyttöliittymä-rajapinta

Tehtäväpohjassa on valmiina pakkaus mooc. Rakennetaan tämän pakkauksen sisälle sovelluksen toiminta. Lisää pakkaukseen mooc pakkaus ui (tämän jälkeen käytössä pitäisi olla pakkaus mooc.ui), ja lisää sinne rajapinta Kayttoliittyma.

Rajapinnan Kayttoliittyma tulee määritellä metodi void paivita().

Tekstikäyttöliittymä

Luo samaan pakkaukseen luokka Tekstikayttoliittyma, joka toteuttaa rajapinnan Kayttoliittyma. Toteuta luokassa Tekstikayttoliittyma rajapinnan Kayttoliittyma vaatima metodi public void paivita() siten, että sen ainut tehtävä on merkkijonon "Päivitetään käyttöliittymää"-tulostaminen System.out.println-metodikutsulla.

Sovelluslogiikka

Luo tämän jälkeen pakkaus mooc.logiikka, ja lisää sinne luokka Sovelluslogiikka. Sovelluslogiikan tarjoaman toiminnallisuuden tulee olla seuraavanlainen.

  • public Sovelluslogiikka(Kayttoliittyma kayttoliittyma)
    Sovelluslogiikka-luokan konstruktori. Saa parametrina Kayttoliittyma-rajapinnan toteuttavan luokan. Huom: jotta sovelluslogiikka näkisi rajapinnan, on sen "importoitava" se, eli tarvitset tiedoston alkuun rivin import mooc.ui.Kayttoliittyma;
  • public void suorita(int montaKertaa)
    Tulostaa montaKertaa-muuttujan määrittelemän määrän merkkijonoa "Sovelluslogiikka toimii". Jokaisen "Sovelluslogiikka toimii"-tulostuksen jälkeen tulee kutsua konstruktorin parametrina saadun rajapinnan Kayttoliittyma-toteuttaman olion määrittelemää paivita()-metodia.

Voit testata sovelluksen toimintaa seuraavalla pääohjelmaluokalla.

import mooc.logiikka.Sovelluslogiikka;
import mooc.ui.Kayttoliittyma;
import mooc.ui.Tekstikayttoliittyma;

public class Main {

    public static void main(String[] args) {
        Kayttoliittyma kayttoliittyma = new Tekstikayttoliittyma();
        new Sovelluslogiikka(kayttoliittyma).suorita(3);
    }
}
Sovelluslogiikka toimii
Päivitetään käyttöliittymää
Sovelluslogiikka toimii
Päivitetään käyttöliittymää
Sovelluslogiikka toimii
Päivitetään käyttöliittymää

Hakemistorakenne tiedostojärjestelmässä

Kaikki NetBeansissa näkyvät projektit ovat tietokoneesi tiedostojärjestelmässä tai jollain keskitetyllä levypalvelimella. Jokaiselle projektille on olemassa oma hakemisto, jonka sisällä on projektiin liittyvät tiedostot ja hakemistot.

Projektin hakemistossa src on ohjelmaan liittyvät lähdekoodit. Jos luokan pakkauksena on kirjasto, sijaitsee se projektin lähdekoodihakemiston src sisällä olevassa hakemistossa kirjasto. NetBeansissa voi käydä katsomassa projektien konkreettista rakennetta Files-välilehdeltä joka on normaalisti Projects-välilehden vieressä. Jos et näe välilehteä Files, saa sen näkyville valitsemalla vaihtoehdon Files valikosta Window.

Sovelluskehitystä tehdään normaalisti Projects-välilehdeltä, jossa NetBeans on piilottanut projektiin liittyviä tiedostoja joista ohjelmoijan ei tarvitse välittää.

Pakkaukset ja näkyvyysmääreet

Olemme tähän mennessä käyttäneet kahta näkyvyysmäärettä. Näkyvyysmääreellä private määritellään muuttujia (ja metodeja), jotka ovat näkyvissä vain sen luokan sisällä joka määrittelee ne. Niitä ei voi käyttää luokan ulkopuolelta. Näkyvyysmääreellä public varustetut metodit ja muuttujat ovat taas kaikkien käytettävissä.

package kirjasto.ui;

public class Kayttoliittyma {
    private Scanner lukija;

    public Kayttoliittyma(Scanner lukija) {
        this.lukija = lukija;
    }

    public void kaynnista() {
        tulostaOtsikko();

        // muu toiminnallisuus
    }

    private void tulostaOtsikko() {
        System.out.println("************");
        System.out.println("* KIRJASTO *");
        System.out.println("************");
    }
}

Yllä olevasta Kayttoliittyma-luokasta tehdyn olion konstruktori ja kaynnista-metodi on kutsuttavissa mistä tahansa ohjelmasta. Metodi tulostaOtsikko ja lukija-muuttuja on käytössä vain luokan sisällä.

Jos näkyvyysmäärettä ei määritellä, metodit ja muuttujat ovat näkyvillä saman pakkauksen sisällä. Tätä kutsutaan oletus- tai pakkausnäkyvyydeksi. Muutetaan yllä olevaa esimerkkiä siten, että metodilla tulostaOtsikko on pakkausnäkyvyys.

package kirjasto.ui;

public class Kayttoliittyma {
    private Scanner lukija;

    public Kayttoliittyma(Scanner lukija) {
        this.lukija = lukija;
    }

    public void kaynnista() {
        tulostaOtsikko();

        // muu toiminnallisuus
    }

    void tulostaOtsikko() {
        System.out.println("************");
        System.out.println("* KIRJASTO *");
        System.out.println("************");
    }
}

Nyt saman pakkauksen sisällä olevat luokat -- eli luokat, jotka sijaitsevat pakkauksessa kirjasto.ui voivat käyttää metodia tulostaOtsikko.

package kirjasto.ui;

import java.util.Scanner;

public class Main {

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

        kayttoliittyma.tulostaOtsikko(); // onnistuu!
    }
}

Jos luokka on eri pakkauksessa, ei metodia tulostaOtsikko pysty käyttämään. Alla olevassa esimerkissä luokka Main on pakkauksessa kirjasto, jolloin pakkauksessa kirjasto.ui pakkausnäkyvyydellä määriteltyyn metodiin tulostaOtsikko ei pääse käsiksi.

package kirjasto;

import java.util.Scanner;
import kirjasto.ui.Kayttoliittyma;

public class Main {

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

        kayttoliittyma.tulostaOtsikko(); // ei onnistu!
    }
}

Laajempi esimerkki: lentokentän hallinta

Tarkastellaan ohjelmaa, joka tarjoaa tekstikäyttöliittymän lentokoneiden ja lentojen lisäämiseen sekä näiden tarkasteluun. Ohjelman tekstikäyttöliittymä on seuraava.

Lentokentän hallinta
--------------------

Valitse toiminto:
[1] Lisää lentokone
[2] Lisää lento
[x] Poistu hallintamoodista
> 1
Anna lentokoneen tunnus: HA-LOL
Anna lentokoneen kapasiteetti: 42
Valitse toiminto:
[1] Lisää lentokone
[2] Lisää lento
[x] Poistu hallintamoodista
> 1
Anna lentokoneen tunnus: G-OWAC
Anna lentokoneen kapasiteetti: 101
Valitse toiminto:
[1] Lisää lentokone
[2] Lisää lento
[x] Poistu hallintamoodista
> 2
Anna lentokoneen tunnus: HA-LOL
Anna lähtöpaikan tunnus: HEL
Anna kohdepaikan tunnus: BAL
Valitse toiminto:
[1] Lisää lentokone
[2] Lisää lento
[x] Poistu hallintamoodista
> 2
Anna lentokoneen tunnus: G-OWAC
Anna lähtöpaikan tunnus: JFK
Anna kohdepaikan tunnus: BAL
Valitse toiminto:
[1] Lisää lentokone
[2] Lisää lento
[x] Poistu hallintamoodista
> 2
Anna lentokoneen tunnus: HA-LOL
Anna lähtöpaikan tunnus: BAL
Anna kohdepaikan tunnus: HEL
Valitse toiminto:
[1] Lisää lentokone
[2] Lisää lento
[x] Poistu hallintamoodista
> x

Lentopalvelu
------------

Valitse toiminto:
[1] Tulosta lentokoneet
[2] Tulosta lennot
[3] Tulosta lentokoneen tiedot
[x] Lopeta
> 1
G-OWAC (101 henkilöä)
HA-LOL (42 henkilöä)
Valitse toiminto:
[1] Tulosta lentokoneet
[2] Tulosta lennot
[3] Tulosta lentokoneen tiedot
[x] Lopeta
> 2
HA-LOL (42 henkilöä) (HEL-BAL)
HA-LOL (42 henkilöä) (BAL-HEL)
G-OWAC (101 henkilöä) (JFK-BAL)

Valitse toiminto:
[1] Tulosta lentokoneet
[2] Tulosta lennot
[3] Tulosta lentokoneen tiedot
[x] Lopeta
> 3
Mikä kone: G-OWAC
G-OWAC (101 henkilöä)

Valitse toiminto:
[1] Tulosta lentokoneet
[2] Tulosta lennot
[3] Tulosta lentokoneen tiedot
[x] Lopeta
> x

Ohjelmasta löytyy useita aihealueen käsitteitä, joista oleellisia ovat Lentokone ja Lento. Kuhunkin lentoon liittyy lisäksi Paikka (lähtöpaikka ja kohdepaikka). Aihealuetta kuvaavien käsitteiden lisäksi ohjelmaan kuuluu tekstikäyttöliittymä sekä luokka, jonka kautta tekstikäyttöliittymä hallinnoi käsitteitä.

Ohjelman pakkausrakenne voi olla -- esimerkiksi -- seuraava:

  • lentokentta - sisältää ohjelman käynnistämiseen tarvittavan pääohjelmaluokan.
  • lentokentta.domain - sisältää aihealueen käsitteitä kuvaavat luokat Lentokone, Lento, ja Paikka.
  • lentokentta.logiikka - sisältää toiminnallisuuden, jonka avulla sovellusta hallinnoidaan
  • lentokentta.ui - sisältää tekstikäyttöliittymän

Alla olevissa aliluvuissa on kuvattu sovelluksen käyttämät luokat (poislukien pääohjelmaluokka).

Aihealueen käsitteitä kuvaavat luokat

package lentokentta.domain;

public class Paikka {

    private String tunnus;

    public Paikka(String tunnus) {
        this.tunnus = tunnus;
    }

    @Override
    public String toString() {
        return this.tunnus;
    }
}
package lentokentta.domain;

public class Lentokone {
  
    private String tunnus;
    private int kapasiteetti;
  
    public Lentokone(String tunnus, int kapasiteetti) {
        this.tunnus = tunnus;
        this.kapasiteetti = kapasiteetti;
    }
  
    public String getTunnus() {
        return this.tunnus;
    }
  
    public int getKapasiteetti() {
        return this.kapasiteetti;
    }
  
    @Override
    public String toString() {
        return this.tunnus + " (" + this.kapasiteetti + " henkilöä)";
    }
}
package lentokentta.domain;

public class Lento {

    private Lentokone lentokone;
    private Paikka lahtopaikka;
    private Paikka kohdepaikka;

    public Lento(Lentokone lentokone, Paikka lahtopaikka, Paikka kohdepaikka) {
        this.lentokone = lentokone;
        this.lahtopaikka = lahtopaikka;
        this.kohdepaikka = kohdepaikka;
    }

    public Lentokone getLentokone() {
        return this.lentokone;
    }

    public Paikka getLahtopaikka() {
        return lahtopaikka;
    }

    public Paikka getKohdepaikka() {
        return kohdepaikka;
    }

    @Override
    public String toString() {
        return this.lentokone + " (" + this.lahtopaikka + "-" + this.kohdepaikka + ")";
    }
}

Sovelluslogiikka

package lentokentta.logiikka;

import java.util.Collection;
import lentokentta.domain.Lento;
import lentokentta.domain.Lentokone;
import java.util.HashMap;
import java.util.Map;
import lentokentta.domain.Paikka;

public class Lentohallinta {

    private Map<String, Lentokone> lentokoneet;
    private Map<String, Lento> lennot;
    private Map<String, Paikka> paikat;

    public Lentohallinta() {
        this.lennot = new HashMap<>();
        this.lentokoneet = new HashMap<>();
        this.paikat = new HashMap<>();
    }

    public void lisaaLentokone(String tunnus, int kapasiteetti) {
        Lentokone lentokone = new Lentokone(tunnus, kapasiteetti);
        this.lentokoneet.put(tunnus, lentokone);
    }

    public void lisaaLento(Lentokone lentokone, String lahtotunnus, String kohdetunnus) {
        this.paikat.putIfAbsent(lahtotunnus, new Paikka(lahtotunnus));
        this.paikat.putIfAbsent(kohdetunnus, new Paikka(kohdetunnus));

        Lento lento = new Lento(lentokone, this.paikat.get(lahtotunnus), this.paikat.get(kohdetunnus));
        this.lennot.put(lento.toString(), lento);
    }

    public Collection<Lentokone> getLentokoneet() {
        return this.lentokoneet.values();
    }

    public Collection<Lento> getLennot() {
        return this.lennot.values();
    }

    public Lentokone haeLentokone(String tunnus) {
        return this.lentokoneet.get(tunnus);
    }
}

Tekstikäyttöliittymä

package lentokentta.ui;

import lentokentta.domain.Lento;
import lentokentta.domain.Lentokone;
import java.util.Scanner;
import lentokentta.logiikka.Lentohallinta;

public class Tekstikayttoliittyma {

    private Lentohallinta lentohallinta;
    private Scanner lukija;

    public Tekstikayttoliittyma(Lentohallinta lentohallinta, Scanner lukija) {
        this.lentohallinta = lentohallinta;
        this.lukija = lukija;
    }

    public void kaynnista() {
        // tehdään käynnistys kahdessa osassa -- ensin käynnistetään hallinta,
        // sitten lentopalvelu
        kaynnistaLentokentanHallinta();
        System.out.println();
        kaynnistaLentoPalvelu();
        System.out.println();
    }

    private void kaynnistaLentokentanHallinta() {
        System.out.println("Lentokentän hallinta");
        System.out.println("--------------------");
        System.out.println();

        while (true) {
            System.out.println("Valitse toiminto:");
            System.out.println("[1] Lisää lentokone");
            System.out.println("[2] Lisää lento");
            System.out.println("[x] Poistu hallintamoodista");

            System.out.print("> ");
            String vastaus = lukija.nextLine();

            if (vastaus.equals("1")) {
                lisaaLentokone();
            } else if (vastaus.equals("2")) {
                lisaaLento();
            } else if (vastaus.equals("x")) {
                break;
            }
        }
    }

    private void lisaaLentokone() {
        System.out.print("Anna lentokoneen tunnus: ");
        String tunnus = lukija.nextLine();
        System.out.print("Anna lentokoneen kapasiteetti: ");
        int kapasiteetti = Integer.parseInt(lukija.nextLine());

        this.lentohallinta.lisaaLentokone(tunnus, kapasiteetti);
    }

    private void lisaaLento() {
        System.out.print("Anna lentokoneen tunnus: ");
        Lentokone lentokone = kysyLentokone();
        System.out.print("Anna lähtöpaikan tunnus: ");
        String lahtotunnus = lukija.nextLine();
        System.out.print("Anna kohdepaikan tunnus: ");
        String kohdetunnus = lukija.nextLine();

        this.lentohallinta.lisaaLento(lentokone, lahtotunnus, kohdetunnus);
    }

    private void kaynnistaLentoPalvelu() {
        System.out.println("Lentopalvelu");
        System.out.println("------------");
        System.out.println();

        while (true) {
            System.out.println("Valitse toiminto:");
            System.out.println("[1] Tulosta lentokoneet");
            System.out.println("[2] Tulosta lennot");
            System.out.println("[3] Tulosta lentokoneen tiedot");
            System.out.println("[x] Lopeta");

            System.out.print("> ");
            String vastaus = lukija.nextLine();
            if (vastaus.equals("1")) {
                tulostaLentokoneet();
            } else if (vastaus.equals("2")) {
                tulostaLennot();
            } else if (vastaus.equals("3")) {
                tulostaLentokone();
            } else if (vastaus.equals("x")) {
                break;
            }
        }
    }

    private void tulostaLentokoneet() {
        for (Lentokone lentokone : lentohallinta.getLentokoneet()) {
            System.out.println(lentokone);
        }
    }

    private void tulostaLennot() {
        for (Lento lento : lentohallinta.getLennot()) {
            System.out.println(lento);
            System.out.println("");
        }
    }

    private void tulostaLentokone() {
        System.out.print("Mikä kone: ");
        Lentokone kone = kysyLentokone();
        System.out.println(kone);
        System.out.println();
    }

    private Lentokone kysyLentokone() {
        Lentokone lentokone = null;
        while (lentokone == null) {
            String tunnus = lukija.nextLine();
            lentokone = lentohallinta.haeLentokone(tunnus);

            if (lentokone == null) {
                System.out.println("Tunnuksella " + tunnus + " ei ole lentokonetta.");
            }
        }

        return lentokone;
    }
}

Tässä tehtävässä toteutat edellä kuvattuun aiheeseen sovelluksen. Saat suunnitella rakenteen vapaasti, tai voit noudattaa edellä kuvattua rakennetta. Käyttöliittymän ulkomuoto sekä vaaditut komennot on määritelty ennalta. Tehtävä on kahden yksittäisen tehtäväpisteen arvoinen.

Huom: jotta testit toimisivat, saat luoda ohjelmassasi vain yhden Scanner-olion käyttäjän syötteen lukemiseen.

Lentokenttä-tehtävässä toteutetaan lentokentän hallintasovellus. Lentokentän hallintasovelluksessa hallinnoidaan lentokoneita ja lentoja. Lentokoneista tiedetään aina tunnus ja kapasiteetti. Lennoista tiedetään lennon lentokone, lähtöpaikan tunnus (esim. HEL) ja kohdepaikan tunnus (esim. BAL).

Sekä lentokoneita että lentoja voi olla useita. Samalla lentokoneella voidaan myös lentää useita eri lentoja.

Sovelluksen tulee toimia kahdessa vaiheessa: ensin syötetään lentokoneiden ja lentojen tietoja hallintakäyttöliittymässä, jonka jälkeen siirrytään lentopalvelun käyttöön. Lentopalvelussa on kolme toimintoa; lentokoneiden tulostaminen, lentojen tulostaminen, ja lentokoneen tietojen tulostaminen. Tämän lisäksi käyttäjä voi poistua ohjelmasta valitsemalla vaihtoehdon x. Jos käyttäjä syöttää epäkelvon komennon, kysytään komentoa uudestaan.

Lentokentän hallinta
--------------------

Valitse toiminto:
[1] Lisää lentokone
[2] Lisää lento
[x] Poistu hallintamoodista
> 1
Anna lentokoneen tunnus: HA-LOL
Anna lentokoneen kapasiteetti: 42
Valitse toiminto:
[1] Lisää lentokone
[2] Lisää lento
[x] Poistu hallintamoodista
> 1
Anna lentokoneen tunnus: G-OWAC
Anna lentokoneen kapasiteetti: 101
Valitse toiminto:
[1] Lisää lentokone
[2] Lisää lento
[x] Poistu hallintamoodista
> 2
Anna lentokoneen tunnus: HA-LOL
Anna lähtöpaikan tunnus: HEL
Anna kohdepaikan tunnus: BAL
Valitse toiminto:
[1] Lisää lentokone
[2] Lisää lento
[x] Poistu hallintamoodista
> 2
Anna lentokoneen tunnus: G-OWAC
Anna lähtöpaikan tunnus: JFK
Anna kohdepaikan tunnus: BAL
Valitse toiminto:
[1] Lisää lentokone
[2] Lisää lento
[x] Poistu hallintamoodista
> 2
Anna lentokoneen tunnus: HA-LOL
Anna lähtöpaikan tunnus: BAL
Anna kohdepaikan tunnus: HEL
Valitse toiminto:
[1] Lisää lentokone
[2] Lisää lento
[x] Poistu hallintamoodista
> x

Lentopalvelu
------------

Valitse toiminto:
[1] Tulosta lentokoneet
[2] Tulosta lennot
[3] Tulosta lentokoneen tiedot
[x] Lopeta
> 1
G-OWAC (101 henkilöä)
HA-LOL (42 henkilöä)
Valitse toiminto:
[1] Tulosta lentokoneet
[2] Tulosta lennot
[3] Tulosta lentokoneen tiedot
[x] Lopeta
> 2
HA-LOL (42 henkilöä) (HEL-BAL)
HA-LOL (42 henkilöä) (BAL-HEL)
G-OWAC (101 henkilöä) (JFK-BAL)

Valitse toiminto:
[1] Tulosta lentokoneet
[2] Tulosta lennot
[3] Tulosta lentokoneen tiedot
[x] Lopeta
> 3
Mikä kone: G-OWAC
G-OWAC (101 henkilöä)

Valitse toiminto:
[1] Tulosta lentokoneet
[2] Tulosta lennot
[3] Tulosta lentokoneen tiedot
[x] Lopeta
> x

Huom1: Testien kannalta on oleellista että käyttöliittymä toimii täsmälleen kuten yllä kuvattu. Ohjelman tulostamat vaihtoehdot kannattanee copypasteta tästä ohjelmakoodiin. Testit eivät oleta, että ohjelmasi on varautunut epäkelpoihin syötteisiin.

Huom2: älä käytä luokkein nimissä skandeja, ne saattavat aiheuttaa ongelmia testeihin!

Ohjelman tulee käynnistyä kun pakkauksessa lentokentta olevan luokan Main metodi main suoritetaan.

Poikkeukset

Poikkeukset ovat tilanteita, joissa ohjelman suoritus päättyy virheeseen. Ohjelmassa on esimerkiksi kutsuttu null-viitteeseen liittyvää metodia, jolloin ohjelmassa tapahtuu poikkeus NullPointerException. Vastaavasti taulukon ulkopuolella olevan indeksin hakeminen johtaa poikkeukseen IndexOutOfBoundsException ym.

Osa Javassa esiintyvistä poikkeuksista on sellaisia, että niihin tulee aina varautua. Näitä ovat esimerkiksi tiedoston lukemisessa tapahtuvaan virheeseen tai verkkoyhteyden katkeamiseen liittyvät poikkeukset. Osa poikkeuksista taas on ajonaikaisia poikkeuksia -- kuten vaikkapa NullPointerException --, joihin ei erikseen tarvitse varautua. Java ilmoittaa aina jos ohjelmassa on lause tai lauseke, jossa mahdollisesti tapahtuvaan poikkeukseen tulee varautua.

Poikkeusten käsittely

Poikkeukset käsitellään try { } catch (Exception e) { } -lohkorakenteella. Avainsanan try aloittaman lohkon sisällä on lähdekoodi, jonka suorituksessa tapahtuu mahdollisesti poikkeus. Avainsanan catch aloittaman lohkon sisällä taas määritellään poikkeustilanteessa tapahtuva käsittely, eli mitä tehdään kun try-lohkossa tapahtuu poikkeus. Avainsanaa catch seuraa myös käsiteltävän poikkeuksen tyyppi, esimerkiksi "kaikki poikkeukset" eli Exception (catch (Exception e)).

try {
    // poikkeuksen mahdollisesti heittävä ohjelmakoodi
} catch (Exception e) {
    // lohko johon päädytään poikkeustilanteessa
}

Avainsana catch eli ota kiinni tulee siitä, että poikkeukset heitetään (throw).

Kuten edellä todettiin, ajonaikaisiin poikkeuksiin kuten NullPointerException ei tarvitse erikseen varautua. Tällaiset poikkeukset voidaan jättää käsittelemättä, jolloin ohjelman suoritus päättyy virheeseen poikkeustilanteen tapahtuessa. Tarkastellaan erästä poikkeustilannetta nyt jo tutun merkkijonon kokonaisluvuksi muuntamisen kautta.

Olemme käyttäneet luokan Integer metodia parseInt merkkijonon kokonaisluvuksi muuntamiseen. Metodi heittää poikkeuksen NumberFormatException, jos sille parametrina annettu merkkijono ei ole muunnettavissa kokonaisluvuksi.

Scanner lukija = new Scanner(System.in);
System.out.print("Syötä numero: ");

int numero = Integer.parseInt(lukija.nextLine());
Syötä numero: tatti
  Exception in thread "..." java.lang.NumberFormatException: For input string: "tatti"

Yllä ohjelma heittää poikkeuksen, kun käyttäjä syöttää virheellisen numeron. Ohjelman suoritus päättyy tällöin virhetilanteeseen.

Lisätään esimerkkiin poikkeuksen käsittely. Kutsu, joka saattaa heittää poikkeuksen asetetaan try-lohkon sisään, ja virhetilanteessa tapahtuva toiminta catch-lohkon sisään.

Scanner lukija = new Scanner(System.in);

System.out.print("Syötä numero: ");
int numero = -1;

try {
    numero = Integer.parseInt(lukija.nextLine());
} catch (Exception e) {
    System.out.println("Et syöttänyt kunnollista numeroa.");
}
Syötä numero: 5
Syötä numero: enpäs!
Et syöttänyt kunnollista numeroa.

Avainsanan try määrittelemän lohkon sisältä siirrytään catch-lohkoon heti poikkeuksen tapahtuessa. Havainnollistetaan tätä lisäämällä tulostuslause try-lohkossa metodia Integer.parseInt kutsuvan rivin jälkeen.

Scanner lukija = new Scanner(System.in);

System.out.print("Syötä numero: ");
int numero = -1;

try {
    numero = Integer.parseInt(lukija.nextLine());
    System.out.println("Hienosti syötetty!");
} catch (Exception e) {
    System.out.println("Et syöttänyt kunnollista numeroa.");
}
Syötä numero: 5
Hienosti syötetty!
Syötä numero: enpäs!
Et syöttänyt kunnollista numeroa.

Ohjelmalle syötetty merkkijono enpäs! annetaan parametrina Integer.parseInt-metodille, joka heittää poikkeuksen, jos parametrina saadun merkkijonon muuntaminen luvuksi epäonnistuu. Huomaa, että catch-lohkossa oleva koodi suoritetaan vain poikkeustapauksissa.

Tehdään yllä olevasta luvun muuntajasta hieman hyödyllisempi. Tehdään siitä metodi, joka kysyy numeroa yhä uudestaan, kunnes käyttäjä syöttää oikean numeron. Metodin suoritus loppuu vasta silloin, kun käyttäjä syöttää kokonaisluvun.

public int lueLuku(Scanner lukija) {
    while (true) {
        System.out.print("Syötä numero: ");

        try {
            int numero = Integer.parseInt(lukija.nextLine());
            return numero;
        } catch (Exception e) {
            System.out.println("Et syöttänyt kunnollista numeroa.");
        }
    }
}
Syötä numero: enpäs!
Et syöttänyt kunnollista numeroa.
Syötä numero: Matilla on ovessa tatti.
Et syöttänyt kunnollista numeroa.
Syötä numero: 43

Poikkeukset ja resurssit

Erilaisten käyttöjärjestelmäresurssien kuten tiedostojen lukemiseen on toteutettu erillinen versio poikkeustenhallinnasta. ns. try-with-resources -tyyppisessä poikkeustenhallinnassa avattava resurssi lisätään try-osaan määriteltävään ei-pakolliseen suluilla rajattavaan osaan.

Alla olevassa esimerkissä luetaan tiedoston "tiedosto.txt" kaikki rivit, jotka lisätään ArrayList-listaan. Tiedostoja lukiessa voidaan kohdata virhetilanne, joten tiedoston lukeminen vaatii erillisen "yrittämisen" (try) sekä mahdollisen virheen kiinnioton (catch).

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

// luodaan lukija tiedoston lukemista varten
try (Scanner lukija = new Scanner(new File("tiedosto.txt"))) {

    // luetaan kaikki tiedoston rivit
    while (lukija.hasNextLine()) {
        rivit.add(lukija.nextLine());
    }
} catch (Exception e) {
    System.out.println("Virhe: " + e.getMessage());
}

// tee jotain luetuilla riveillä

Yllä kuvattu try-with-resources -lähestymistapa on hyödyllinen resurssien käsittelyssä, sillä tässä tapauksessa ohjelma sulkee käytetyt resurssit automaattisesti. Tällöin esimerkiksi tiedostoihin liittyvät viitteet saavat luvan "kadota", koska niille ei ole enää käyttöä. Mikäli taas resursseja ei suljeta, ovat tiedostot käyttöjärjestelmän näkökulmasta käytössä kunnes ohjelma sammutetaan.

Käsittelyvastuun siirtäminen

Metodit ja konstruktorit voivat heittää poikkeuksia. Heitettäviä poikkeuksia on karkeasti ottaen kahdenlaisia. On poikkeuksia jotka on pakko käsitellä, ja on poikkeuksia joita ei ole pakko käsitellä. Poikkeukset käsitellään joko try-catch -lohkossa, tai heittämällä ne ulos metodista.

Alla olevassa esimerkissä luetaan parametrina annetun tiedoston rivit yksitellen. Tiedoston lukeminen saattaa heittää poikkeuksen -- voi olla, ettei tiedostoa esimerkiksi löydy, tai voi olla ettei siihen ole lukuoikeuksia. Tällainen poikkeus tulee käsitellä. Poikkeuksen käsittely tapahtuu try-catch -lauseella. Seuraavassa esimerkissä emme juurikaan välitä poikkeustilanteesta, mutta tulostamme kuitenkin poikkeukseen liittyvän viestin.

public List<String> lue(String tiedosto) {
    List<String> rivit = new ArrayList<>();

    try {
        Files.lines(Paths.get("tiedosto.txt")).forEach(rivi -> rivit.add(rivi));
    } catch (Exception e) {
        System.out.println("Virhe: " + e.getMessage());
    }

    return rivit;
}

Ohjelmoija voi myös jättää poikkeuksen käsittelemättä ja siirtää vastuun poikkeuksen käsittelystä metodin kutsujalle. Vastuun siirto tapahtuu heittämällä poikkeus metodista eteenpäin lisäämällä tästä tieto metodin määrittelyyn. Tieto poikkeuksen heitosta -- throws PoikkeusTyyppi, missä poikkeustyyppi esimerkiksi Exception -- lisätään ennen metodirungon avaavaa aaltosulkua.

public List<String> lue(String tiedosto) throws Exception {
    ArrayList<String> rivit = new ArrayList<>();
    Files.lines(Paths.get(tiedosto)).forEach(rivi -> rivit.add(rivi));
    return rivit;
}

Nyt metodia lue kutsuvan metodin tulee joko käsitellä poikkeus try-catch -lohkossa tai siirtää poikkeuksen käsittelyn vastuuta eteenpäin. Joskus poikkeuksen käsittelyä vältetään viimeiseen asti, ja main-metodikin heittää poikkeuksen käsiteltäväksi eteenpäin:

public class Paaohjelma {
   public static void main(String[] args) throws Exception {
       // ...
   }
}

Tällöin mahdollinen poikkeus päätyy ohjelman suorittajalle eli Javan virtuaalikoneelle, joka keskeyttää ohjelman suorituksen poikkeukseen johtavan virheen tapahtuessa.

Poikkeusten heittäminen

Voimme heittää poikkeuksen throw-komennolla. Esimerkiksi NumberFormatException-luokasta luodun poikkeuksen heittäminen tapahtuisi komennolla throw new NumberFormatException(). Seuraava ohjelma päätyy aina poikkeustilaan.

public class Ohjelma {

    public static void main(String[] args) throws Exception {
        throw new NumberFormatException(); // Ohjelmassa heitetään poikkeus
    }
}

Eräs poikkeus, johon käyttäjän ei ole pakko varautua on IllegalArgumentException. Poikkeuksella IllegalArgumentException kerrotaan että metodille tai konstruktorille annettujen parametrien arvot ovat vääränlaiset. IllegalArgumentException-poikkeusta käytetään esimerkiksi silloin, kun halutaan varmistaa, että parametreilla on tietyt arvot.

Luodaan luokka Arvosana, joka saa konstruktorin parametrina kokonaislukutyyppisen arvosanan.

public class Arvosana {
    private int arvosana;

    public Arvosana(int arvosana) {
        this.arvosana = arvosana;
    }

    public int getArvosana() {
        return this.arvosana;
    }
}

Haluamme seuraavaksi varmistaa, että Arvosana-luokan konstruktorin parametrina saatu arvo täyttää tietyt kriteerit. Arvosanan tulee olla aina välillä 0-5. Jos arvosana on jotain muuta, haluamme heittää poikkeuksen. Lisätään Arvosana-luokan konstruktoriin ehtolause, joka tarkistaa onko arvosana arvovälin 0-5 ulkopuolella. Jos on, heitetään poikkeus IllegalArgumentException sanomalla throw new IllegalArgumentException("Arvosanan tulee olla välillä 0-5");.

public class Arvosana {
    private int arvosana;

    public Arvosana(int arvosana) {
        if (arvosana < 0 || arvosana > 5) {
            throw new IllegalArgumentException("Arvosanan tulee olla välillä 0-5");
        }

        this.arvosana = arvosana;
    }

    public int getArvosana() {
        return this.arvosana;
    }
}
Arvosana arvosana = new Arvosana(3);
System.out.println(arvosana.getArvosana());

Arvosana virheellinenArvo = new Arvosana(22);
// tapahtuu poikkeus, tästä ei jatketa eteenpäin
3
Exception in thread "..." java.lang.IllegalArgumentException: Arvosanan tulee olla välillä 0-5

Jos poikkeus on esimerkiksi tyyppiä IllegalArgumentException, tai yleisemmin ajonaikainen poikkeus, ei sen heittämisestä tarvitse kirjoittaa erikseen metodin määrittelyyn.

Harjoitellaan hieman parametrien validointia IllegalArgumentException-poikkeuksen avulla. Tehtäväpohjassa tulee kaksi luokkaa, Henkilo ja Laskin. Muuta luokkia seuraavasti:

Henkilön validointi

Luokan Henkilo konstruktorin tulee varmistaa että parametrina annettu nimi ei ole null, tyhjä tai yli 40 merkkiä pitkä. Myös iän tulee olla väliltä 0-120. Jos joku edelläolevista ehdoista ei päde, tulee konstruktorin heittää IllegalArgumentException-poikkeus.

Laskimen validointi

Luokan Laskin metodeja tulee muuttaa seuraavasti: Metodin kertoma tulee toimia vain jos parametrina annetaan ei-negatiivinen luku (0 tai suurempi). Metodin binomikerroin tulee toimia vain jos parametrit ovat ei-negatiivisia ja osajoukon koko on pienempi kuin joukon koko. Jos jompikumpi metodeista saa epäkelpoja arvoja metodikutsujen yhteydessä, tulee metodien heittää poikkeus IllegalArgumentException.

Poikkeukset ja rajapinnat

Rajapintaluokilla ei ole metodirunkoa, mutta metodimäärittely on vapaasti rajapinnan suunnittelijan toteutettavissa. Rajapintaluokissa voidaan määritellä metodeja, jotka saattavat heittää poikkeuksen. Esimerkiksi seuraavan rajapinnan Tiedostopalvelin toteuttavat luokat heittävät mahdollisesti poikkeuksen lataa- ja tallenna-metodissa.

public interface Tiedostopalvelin {
    String lataa(String tiedosto) throws Exception;
    void tallenna(String tiedosto, String merkkijono) throws Exception;
}

Jos rajapinta määrittelee metodeille throws Exception-määreet, eli että metodit heittävät mahdollisesti poikkeuksen, tulee samat määreet olla myös rajapinnan toteuttavassa luokassa. Luokan ei kuitenkaan ole pakko heittää poikkeusta kuten alla olevasta esimerkistä näkee.

public class Tekstipalvelin implements Tiedostopalvelin {

    private Map<String, String> data;

    public Tekstipalvelin() {
        this.data = new HashMap<>();
    }

    @Override
    public String lataa(String tiedosto) throws Exception {
        return this.data.get(tiedosto);
    }

    @Override
    public void tallenna(String tiedosto, String merkkijono) throws Exception {
        this.data.put(tiedosto, merkkijono);
    }
}

Poikkeuksen tiedot

Poikkeusten käsittelytoiminnallisuuden sisältämä catch-lohko määrittelee catch-osion sisällä poikkeuksen johon varaudutaan catch (Exception e). Poikkeuksen tiedot tallennetaan e-muuttujaan.

try {
    // ohjelmakoodi, joka saattaa heittää poikkeuksen
} catch (Exception e) {
    // poikkeuksen tiedot ovat tallessa muuttujassa e
}

Luokka Exception tarjoaa hyödyllisiä metodeja. Esimerkiksi metodi printStackTrace() tulostaa stack tracen, joka kertoo miten poikkeukseen päädyttiin. Tutkitaan seuraavaa metodin printStackTrace() tulostamaa virhettä.

Exception in thread "main" java.lang.NullPointerException
  at pakkaus.Luokka.tulosta(Luokka.java:43)
  at pakkaus.Luokka.main(Luokka.java:29)

Stack tracen lukeminen tapahtuu alhaalta ylöspäin. Alimpana on ensimmäinen kutsu, eli ohjelman suoritus on alkanut luokan Luokka metodista main(). Luokan Luokka main-metodin rivillä 29 on kutsuttu metodia tulosta(). Metodin tulosta rivillä 43 on tapahtunut poikkeus NullPointerException. Poikkeuksen tiedot ovatkin hyvin hyödyllisiä virhekohdan selvittämisessä.

Kaikki luotavat luokat tulee sijoittaa pakkaukseen sovellus.

Käytössämme on seuraava rajapinta:

public interface Sensori {
    boolean onPaalla();  // palauttaa true jos sensori on päällä
    void paalle();       // käynnistä sensorin
    void poisPaalta();   // sulkee sensorin
    int mittaa();        // palauttaa sensorin lukeman jos sensori on päällä
                         // jos sensori ei ole päällä heittää poikkeuksen
                         // IllegalStateException
}

Vakiosensori

Tee luokka Vakiosensori joka toteuttaa rajapinnan Sensori.

Vakiosensori on koko ajan päällä. Metodien paalle ja poisPaalta kutsuminen ei tee mitään. Vakiosensorilla tulee olla konstruktori, jonka parametrina on kokonaisluku. Metodikutsu mittaa palauttaa aina konstruktorille parametrina annetun luvun.

Esimerkki:

public static void main(String[] args) {
    Vakiosensori kymppi = new Vakiosensori(10);
    Vakiosensori miinusViis = new Vakiosensori(-5);

    System.out.println(kymppi.mittaa());
    System.out.println(miinusViis.mittaa());

    System.out.println(kymppi.onPaalla());
    kymppi.poisPaalta();
    System.out.println(kymppi.onPaalla());
}
10
-5
true
true

Lampomittari

Tee luokka Lampomittari, joka toteuttaa rajapinnan Sensori.

Aluksi lämpömittari on poissa päältä. Kutsuttaessa metodia mittaa kun mittari on päällä mittari arpoo luvun väliltä -30...30 ja palauttaa sen kutsujalle. Jos mittari ei ole päällä, heitetään poikkeus IllegalStateException.

Käytä Javan valmista luokkaa Random satunnaisen luvun arpomiseen. Saat luvun väliltä 0...60 kutsulla new Random().nextInt(61); -- väliltä -30...30 arvotun luvun saa vähentämällä väliltä 0...60 olevasta luvusta sopiva luku.

Keskiarvosensori

Tee luokka Keskiarvosensori, joka toteuttaa rajapinnan Sensori.

Keskiarvosensori sisältää useita sensoreita. Rajapinnan Sensori määrittelemien metodien lisäksi keskiarvosensorilla on metodi public void lisaaSensori(Sensori lisattava) jonka avulla keskiarvosensorin hallintaan lisätään uusi sensori.

Keskiarvosensori on päällä silloin kuin kaikki sen sisältävät sensorit ovat päällä. Kun keskiarvosensori käynnistetään, täytyy kaikkien sen sisältävien sensorien käynnistyä jos ne eivät ole käynnissä. Kun keskiarvosensori suljetaan, täytyy ainakin yhden sen sisältävän sensorin mennä pois päältä. Saa myös käydä niin että kaikki sen sisältävät sensorit menevät pois päältä.

Keskiarvosensorin metodi mittaa palauttaa sen sisältämien sensoreiden lukemien keskiarvon (koska paluuarvo on int, pyöristyy lukema alaspäin kuten kokonaisluvuilla tehdyissä jakolaskuissa). Jos keskiarvosensorin metodia mittaa kutsutaan sensorin ollessa poissa päältä, tai jos keskiarvosensorille ei vielä ole lisätty yhtään sensoria heitetään poikkeus IllegalStateException.

Seuraavassa sensoreja käyttävä esimerkkiohjelma (huomaa, että sekä Lämpömittarin että Keskiarvosensorin konstruktorit ovat parametrittomia):

public static void main(String[] args) {
    Sensori kumpula = new Lampomittari();
    kumpula.paalle();
    System.out.println("lämpötila Kumpulassa " + kumpula.mittaa() + " astetta");

    Sensori kaisaniemi = new Lampomittari();
    Sensori helsinkiVantaa = new Lampomittari();

    Keskiarvosensori paakaupunki = new Keskiarvosensori();
    paakaupunki.lisaaSensori(kumpula);
    paakaupunki.lisaaSensori(kaisaniemi);
    paakaupunki.lisaaSensori(helsinkiVantaa);

    paakaupunki.paalle();
    System.out.println("lämpötila Pääkaupunkiseudulla " + paakaupunki.mittaa() + " astetta");
}

Alla olevan esimerkin tulostukset riippuvat arvotuista lämpötiloista:

lämpötila Kumpulassa 11 astetta
lämpötila Pääkaupunkiseudulla 8 astetta

Kaikki mittaukset

Lisää luokalle Keskiarvosensori metodi public List<Integer> mittaukset(), joka palauttaa listana kaikkien keskiarvosensorin avulla suoritettujen mittausten tulokset. Seuraavassa esimerkki metodin toiminnasta:

public static void main(String[] args) {
    Sensori kumpula = new Lampomittari();
    Sensori kaisaniemi = new Lampomittari();
    Sensori helsinkiVantaa = new Lampomittari();

    Keskiarvosensori paakaupunki = new Keskiarvosensori();
    paakaupunki.lisaaSensori(kumpula);
    paakaupunki.lisaaSensori(kaisaniemi);
    paakaupunki.lisaaSensori(helsinkiVantaa);

    paakaupunki.paalle();
    System.out.println("lämpötila Pääkaupunkiseudulla " + paakaupunki.mittaa() + " astetta");
    System.out.println("lämpötila Pääkaupunkiseudulla " + paakaupunki.mittaa() + " astetta");
    System.out.println("lämpötila Pääkaupunkiseudulla " + paakaupunki.mittaa() + " astetta");

    System.out.println("mittaukset: " + paakaupunki.mittaukset());
}

Alla olevan esimerkin tulostukset riippuvat jälleen arvotuista lämpötiloista:

lämpötila Pääkaupunkiseudulla -10 astetta
lämpötila Pääkaupunkiseudulla -4 astetta
lämpötila Pääkaupunkiseudulla 5 astetta

mittaukset: [-10, -4, 5]

Olioiden monimuotoisuus

Olemme aiemmissa osissa törmänneet tilanteisiin, joissa viittaustyyppisillä muuttujilla on oman tyyppinsä lisäksi muita tyyppejä. Esimerkiksi kaikki oliot ovat tyyppiä Object, eli mikä tahansa olio voidaan oman tyyppinsä lisäksi esittää Object-tyyppisenä muuttujana.

String merkkijono = "merkkijono";
Object merkkijonoString = "toinen merkkijono";
String merkkijono = "merkkijono";
Object merkkijonoString = merkkijono;

Yllä olevissa esimerkeissä merkkijonomuuttuja esitetään sekä String-tyyppisenä että Object-tyyppisenä, jonka lisäksi String-tyyppinen muuttuja asetetaan Object-tyyppiseen muuttujaan. Asetus toiseen suuntaan, eli Object-tyyppisen muuttujan asettaminen String-tyyppiseksi ei kuitenkaan onnistu. Tämä johtuu siitä, että Object-tyyppiset muuttujat eivät ole tyyppiä String

Object merkkijonoString = "toinen merkkijono";
String merkkijono = merkkijonoString; // EI ONNISTU!

Mistä tässä oikein on kyse?

Jokainen muuttuja voidaan esittää muuttujan alkuperäisen tyypin lisäksi myös muuttujan toteuttamien rajapintojen sekä perimien luokkien tyyppisenä. Luokka String perii luokan Object, joten String-oliot ovat aina myös tyyppiä Object. Luokka Object ei peri String-luokkaa, joten Object-tyyppiset muuttujat eivät ole automaattisesti tyyppiä String. Tutustutaan tarkemmin String-luokan API-dokumentaatioon, erityisesti HTML-sivun yläosaan.

Kuvakaappaus String-luokan API-dokumentaatiosta. Kuvakaappauksessa näkyy, että String-luokka perii luokan Object.

String-luokan API-dokumentaatio alkaa yleisellä otsakkeella jota seuraa luokan pakkaus (java.lang). Pakkauksen jälkeen tulee luokan nimi (Class String), jota seuraa luokan perintähierarkia.

  java.lang.Object
  java.lang.String

Perintähierarkia listaa luokat, jotka luokka on perinyt. Perityt luokat listataan perimisjärjestyksessä, tarkasteltava luokka aina alimpana. String-luokan perintähierarkiasta näemme, että String-luokka perii luokan Object. Javassa jokainen luokka voi periä korkeintaan yhden luokan. Toisaalta, perittävä luokka on voinut periä toisen luokan, joten välillisesti luokka voi periä useampia luokkia.

Perintähierarkiaa voi ajatella myös listana tyypeistä, joita olio toteuttaa.

Tieto siitä, että oliot voivat olla montaa eri tyyppiä -- esimerkiksi tyyppiä Object -- suoraviivaistaa ohjelmointia. Jos tarvitsemme metodissa vain Object-luokassa määriteltyjä metodeja kuten toString, equals ja hashCode, voimme käyttää metodin parametrina tyyppiä Object. Tällöin metodille voi antaa parametrina minkä tahansa olion. Tarkastellaan tätä metodin tulostaMonesti avulla. Metodi saa parametrinaan Object-tyyppisen muuttujan ja tulostusten lukumäärän.

public class Tulostin {
    ...
    public void tulostaMonesti(Object object, int kertaa) {
        for (int i = 0; i < kertaa; i++) {
            System.out.println(object.toString());
            // tai System.out.println(object);
        }
    }
...
}

Metodille voi antaa parametrina minkä tahansa olion. Metodin tulostaMonesti sisällä oliolla on käytössään vain Object-luokassa määritellyt metodit, koska olio tunnetaan metodissa Object-tyyppisenä. Todellisuudessa olio voi olla myös toisen tyyppinen.

Tulostin tulostin = new Tulostin();

String merkkijono = " o ";
List<String> sanat = new ArrayList<>();
sanat.add("polymorfismi");
sanat.add("perintä");
sanat.add("kapselointi");
sanat.add("abstrahointi");

tulostin.tulostaMonesti(merkkijono, 2);
tulostin.tulostaMonesti(sanat, 3);
o
o
[polymorfismi, perintä, kapselointi, abstrahointi]
[polymorfismi, perintä, kapselointi, abstrahointi]
[polymorfismi, perintä, kapselointi, abstrahointi]

Jatketaan String-luokan API-kuvauksen tarkastelua. Kuvauksessa olevaa perintähierarkiaa seuraa listaus luokan toteuttamista rajapinnoista.

  All Implemented Interfaces:
  Serializable, CharSequence, Comparable<String>

Luokka String toteuttaa rajapinnat Serializable, CharSequence, ja Comparable<String>. Myös rajapinta on tyyppi. Luokan String API-kuvauksen mukaan String-olion tyypiksi voi asettaa seuraavat rajapinnat.

Serializable serializableString = "merkkijono";
CharSequence charSequenceString = "merkkijono";
Comparable<String> comparableString = "merkkijono";

Koska metodeille voidaan määritellä metodin parametrin tyyppi, voimme määritellä metodeja jotka vastaanottavat tietyn rajapinnan toteuttavan olion. Kun metodille määritellään parametrina rajapinta, sille voidaan antaa parametrina mikä tahansa olio, joka toteuttaa kyseisen rajapinnan.

Täydennetään Tulostin-luokkaa siten, että sillä on metodi CharSequence-rajapinnan toteuttavien olioiden merkkien tulostamiseen. Rajapinta CharSequence tarjoaa muunmuassa metodit int length(), jolla saa merkkijonon pituuden, ja char charAt(int index), jolla saa merkin tietyssä indeksissä.

public class Tulostin {
    ...
    public void tulostaMonesti(Object object, int kertaa) {
        for (int i = 0; i < kertaa; i++) {
            System.out.println(object.toString());
        }
    }

    public void tulostaMerkit(CharSequence charSequence) {
        for (int i = 0; i < charSequence.length(); i++) {
            System.out.println(charSequence.charAt(i));
        }
    }
    ...
}

Metodille tulostaMerkit voi antaa minkä tahansa CharSequence-rajapinnan toteuttavan olion. Näitä on muunmuassa String ja merkkijonojen rakentamisessa usein Stringiä tehokkaampi StringBuilder. Metodi tulostaMerkit tulostaa annetun olion jokaisen merkin omalle rivilleen.

Tulostin tulostin = new Tulostin();

String mjono = "toimii";

tulostin.tulostaMerkit(mjono);
t
o
i
m
i
i

Espanjassa ihmisillä on kaksi sukunimeä. Ennen vuoden 1999 lakimuutosta lapsen ensimmäinen sukunimi tuli isän ensimmäisestä sukunimestä ja toinen äidin ensimmäisestä sukunimestä. Vuonna 1999 lainsäädäntöä muutettiin niin, että vanhempien yhteisestä päätöksestä ensimmäiseksi sukunimeksi voidaan valita myös äidin ensimmäinen sukunimi.

Tee tämän tehtävän luokat pakkaukseen sukunimet.

Henkilo

Tarkastellaan olioiden monimuotoisuutta luomalla vuotta 1999 edeltävää Espanjalaista nimentää kuvaava luokka Henkilo.

Luo luokka Henkilo, jolla on kaksi konstruktoria:

  • public Henkilo(String etunimi, String ensimmainenSukunimi, String toinenSukunimi)
  • public Henkilo(String etunimi, Henkilo isa, Henkilo aiti)

Luo luokalle lisäksi toString-metodi, joka tulostaa henkilöä kuvaavan merkkijonoesityksen.

Luokan tulee toimia seuraavasti. Huomaa myös viimeinen rivi, missä luokasta tehtyä oliota käsitellään Object-tyyppisenä.

Henkilo dolores = new Henkilo("Dolores", "D.", "Parto");
System.out.println(dolores);

Henkilo hay = new Henkilo("Hay", "Alen", "Gualarga");
System.out.println(hay);

Henkilo nacho = new Henkilo("Nacho", "Cuesta", "Parto");
System.out.println(nacho);

Henkilo valeria = new Henkilo("Valeria", "Mogollon", "Gualarga");
System.out.println(valeria);

Henkilo enrico = new Henkilo("Enrico", dolores, hay);
System.out.println(enrico);

Henkilo luz = new Henkilo("Luz", nacho, valeria);
System.out.println(luz);

Henkilo valentina = new Henkilo("Valentina", enrico, luz);
System.out.println(valentina);

Object valObject = valentina;
System.out.println(valObject);
Dolores D. Parto
Hay Alen Gualarga
Nacho Cuesta Parto
Valeria Mogollon Gualarga
Enrico D. Alen
Luz Cuesta Mogollon
Valentina D. Cuesta
Valentina D. Cuesta

Perhe

Toteuta tämän jälkeen luokka Perhe. Luokan tulee toimia seuraavasti.

Henkilo hay = new Henkilo("Hay", "Alen", "Gualarga");
Henkilo nacho = new Henkilo("Nacho", "Cuesta", "Parto");
Henkilo dolores = new Henkilo("Dolores", "D.", "Parto");
Henkilo enrico = new Henkilo("Enrico", dolores, hay);
Henkilo luz = new Henkilo("Luz", nacho, valeria);
Henkilo valentina = new Henkilo("Valentina", enrico, luz);

Perhe perhe = new Perhe();
perhe.lisaa(dolores);
perhe.lisaa(luz);
perhe.lisaa(valentina);
System.out.println(perhe);

System.out.println();

perhe.poista(luz);
System.out.println(perhe);

System.out.println();
    
Object perheObj = perhe;
System.out.println(perheObj);

System.out.println();
    
perhe.lisaa(enrico);
System.out.println(perheObj);
Dolores D. Parto
Luz Cuesta Mogollon
Valentina D. Cuesta

Dolores D. Parto
Valentina D. Cuesta

Dolores D. Parto
Valentina D. Cuesta
Enrico D. Alen

Tee sekä metodista lisaa että metodista poista sellaiset, että ne eivät palauta mitään arvoa.

Mikä tässä oikein oli monimuotoisuutta? Kaikki luokat ovat Object-tyyppisiä. Vaikka henkilöä tarkastellaan Object-oliona, suoritetaan toString-metodin lähdekoodi olion "oikeaan" tyyppiin liittyvästä luokasta.

Tässä tehtävässä teemme eliöita ja eliöistä koostuvia laumoja jotka liikkuvat ympäriinsä. Eliöiden sijaintien ilmoittamiseen käytetään kaksiulotteista koordinaatistoa. Jokaiseen sijaintiin liittyy kaksi lukua, x- ja y-koordinaatti. Koordinaatti x kertoo, kuinka pitkällä "nollapisteestä" mitattuna sijainti on vaakasuunnassa, ja koordinaatti y vastaavasti kuinka pitkällä sijainti on pystysuunnassa. Jos koordinaatiston käsite ei ole tuttu, voit lukea siitä lisää esimerkiksi wikipediasta.

Tehtävän mukana tulee rajapinta Siirrettava, joka kuvaa asiaa jota voidaan siirtää paikasta toiseen. Rajapinta sisältää metodin void siirra(int dx, int dy). Parametri dx kertoo, paljonko asia siirtyy x-akselilla ja dy y-akselilla.

Tehtävässä toteutat luokat Elio ja Lauma, jotka molemmat ovat siirrettäviä. Toteuta kaikki toiminnallisuus pakkaukseen siirrettava.

Elio-luokan toteuttaminen

Luo pakkaukseen siirrettava luokka Elio, joka toteuttaa rajapinnan Siirrettava. Eliön tulee tietää oma sijaintinsa (x, y -koordinaatteina). Luokan Elio APIn tulee olla seuraava:

  • public Elio(int x, int y)
    Luokan konstruktori, joka saa olion aloitussijainnin x- ja y-koordinaatit parametrina
  • public String toString()
    Luo ja palauttaa oliosta merkkijonoesityksen. Eliön merkkijonoesityksen tulee olla seuraavanlainen "x: 3; y: 6". Huomaa että koordinaatit on erotettu puolipisteellä (;)
  • public void siirra(int dx, int dy)
    Siirtää oliota parametrina saatujen arvojen verran. Muuttuja dx sisältää muutoksen koordinaattiin x, muuttuja dy sisältää muutoksen koordinaattiin y. Esimerkiksi jos muuttujan dx arvo on 5, tulee oliomuuttujan x arvoa kasvattaa viidellä

Kokeile luokan Elio toimintaa seuraavalla esimerkkikoodilla.

Elio elio = new Elio(20, 30);
System.out.println(elio);
elio.siirra(-10, 5);
System.out.println(elio);
elio.siirra(50, 20);
System.out.println(elio);
x: 20; y: 30
x: 10; y: 35
x: 60; y: 55

Lauman toteutus

Luo pakkaukseen siirrettava luokka Lauma, joka toteuttaa rajapinnan Siirrettava. Lauma koostuu useasta Siirrettava-rajapinnan toteutavasta oliosta, jotka tulee tallettaa esimerkiksi listarakenteeseen.

Luokalla Lauma tulee olla seuraavanlainen API.

  • public String toString()
    Palauttaa merkkijonoesityksen lauman jäsenten sijainnista rivin vaihdolla erotettuna.
  • public void lisaaLaumaan(Siirrettava siirrettava)
    Lisää laumaan uuden Siirrettava-rajapinnan toteuttavan olion
  • public void siirra(int dx, int dy)
    Siirtää laumaa parametrina saatujen arvojen verran. Huomaa että tässä sinun tulee siirtää jokaista lauman jäsentä.

Kokeile ohjelmasi toimintaa alla olevalla esimerkkikoodilla.

Lauma lauma = new Lauma();
lauma.lisaaLaumaan(new Elio(73, 56));
lauma.lisaaLaumaan(new Elio(57, 66));
lauma.lisaaLaumaan(new Elio(46, 52));
lauma.lisaaLaumaan(new Elio(19, 107));
System.out.println(lauma);
x: 73; y: 56
x: 57; y: 66
x: 46; y: 52
x: 19; y: 107

Ryhmittely hajautustaulun avulla

Kerrataan osan lopuksi vielä hieman hajautustaulun toimintaa sekä ryhmittelyä. Hajautustaulu sisältää korkeintaan yhden arvon yhtä avainta kohti. Alla luodaan henkilöiden puhelinnumeroita hajautustauluun.

Map<String, String> puhelinnumerot = new HashMap<>();
puhelinnumerot.put("Pekka", "040-12348765");

System.out.println("Pekan numero: " + puhelinnumerot.get("Pekka"));

puhelinnumerot.put("Pekka", "09-111333");

System.out.println("Pekan numero: " + puhelinnumerot.get("Pekka"));
Pekan numero: 040-12348765
Pekan numero: 09-111333

Useampi arvo yhdelle avaimelle

Entä jos haluaisimme liittää yhteen avaimeen useita arvoja, eli esimerkiksi useampia puhelinnumeroita yhdelle henkilölle?

Koska hajautustaulun avaimet ja arvot voivat olla mitä tahansa muuttujia, listojen käyttäminen hajautustaulun arvona onnistuu. Useamman arvon lisääminen yhdelle arvolle onnistuu liittämällä avaimeen lista. Muutetaan puhelinnumeroiden talletustapaa seuraavasti:

Map<String, List<String>> puhelinnumerot = new HashMap<>();

Nyt hajautustaulussa on jokaiseen avaimeen liitettynä lista. Vaikka new-komento luo hajautustaulun, ei hajautustaulu sisällä alussa yhtäkään listaa. Ne on luotava tarvittaessa erikseen.

Map<String, List<String>> puhelinnumerot = new HashMap<>();

// liitetään Pekka-nimeen ensin tyhjä ArrayList
puhelinnumerot.put("Pekka", new ArrayList<>());

// ja lisätään Pekkaa vastaavalle listalle puhelinnumero
puhelinnumerot.get("Pekka").add("040-12348765");
// ja lisätään toinenkin puhelinnumero
puhelinnumerot.get("Pekka").add("09-111333");

System.out.println("Pekan numerot: " + puhelinnumerot.get("Pekka"));
Pekan numero: [040-12348765, 09-111333]

Määrittelimme muuttujan puhelinnumero tyypiksi Map<String, List<String>>. Tämä tarkoittaa hajautustaulua, joka käyttää avaimena merkkijonoa ja arvona merkkijonoja sisältävää listaa. Hajautustauluun lisättävät arvot ovat siis List<String>-rajapinnan toteuttavia konkreettisia olioita, eli esimerkiksi ArrayListejä.

// liitetään Pekka-nimeen ensin tyhjä ArrayList
puhelinnumerot.put("Pekka", new  ArrayList<>());

// ...

Joukoista

Rajapinta Set kuvaa joukon toiminnallisuutta. Toisin kuin listalla, joukossa kutakin alkioita on korkeintaan yksi kappale, eli yhtään samanlaista oliota ei ole kahdesti. Olioiden samankaltaisuuden tarkistaminen toteutetaan equals ja hashCode -metodeja käyttämällä.

Yksi rajapinnan Set toteuttava luokka on HashSet. Toteutetaan sen avulla luokka Tehtavakirjanpito, joka tarjoaa mahdollisuuden tehtävien kirjanpitoon ja tehtyjen tehtävien tulostamiseen. Oletetaan että tehtävät ovat aina kokonaislukuja.

public class Tehtavakirjanpito {
    private Set<Integer> tehdytTehtavat;

    public Tehtavakirjanpito() {
        this.tehdytTehtavat = new HashSet<>();
    }

    public void lisaa(int tehtava) {
        this.tehdytTehtavat.add(tehtava);
    }

    public void tulosta() {
        this.tehdytTehtavat.stream().forEach(t -> System.out.println(t));
    }
}
Tehtavakirjanpito kirjanpito = new Tehtavakirjanpito();
kirjanpito.lisaa(1);
kirjanpito.lisaa(1);
kirjanpito.lisaa(2);
kirjanpito.lisaa(3);

kirjanpito.tulosta();
1
2
3

Yllä oleva ratkaisu toimii tilanteessa, jossa emme tarvitse tietoa eri käyttäjien tekemistä tehtävistä. Muutetaan sovelluksen toiminnallisuutta siten, että tehtävät tallennetaan käyttäjäkohtaisesti hajautustaulua hyödyntäen. Käyttäjät tunnistetaan käyttäjän yksilöivällä merkkijonolla (esimerkiksi opiskelijanumero), ja jokaiselle käyttäjälle on oma joukko tehdyistä tehtävistä.

public class Tehtavakirjanpito {
    private Map<String, Set<Integer>> tehdytTehtavat;

    public Tehtavakirjanpito() {
        this.tehdytTehtavat = new HashMap<>();
    }

    public void lisaa(String kayttaja, int tehtava) {
        // uudelle käyttäjälle on lisättävä HashMapiin tyhjä tehtäväjoukko jos sitä
        // ei ole jo lisätty
        this.tehdytTehtavat.putIfAbsent(kayttaja, new HashSet<>());

        // haetaan ensin käyttäjän tehtävät sisältävä joukko ja tehdään siihen lisäys
        Set<Integer> tehdyt = this.tehdytTehtavat.get(kayttaja);
        tehdyt.add(tehtava);

        // edellinen olisi onnitunut myös ilman apumuuttujaa seuraavasti
        // this.tehdytTehtavat.get(kayttaja).add(tehtava);
    }

    public void tulosta() {
        this.tehdytTehtavat.entrySet().stream(entry -> {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        });
    }
}
Tehtavakirjanpito kirjanpito = new Tehtavakirjanpito();
kirjanpito.lisaa("Ada", 3);
kirjanpito.lisaa("Ada", 4);
kirjanpito.lisaa("Ada", 3);
kirjanpito.lisaa("Ada", 3);

kirjanpito.lisaa("Pekka", 4);
kirjanpito.lisaa("Pekka", 4);

kirjanpito.lisaa("Matti", 1);
kirjanpito.lisaa("Matti", 2);

kirjanpito.tulosta();
Matti: [1, 2]
Pekka: [4]
Ada: [3, 4]

Huomaamme että käyttäjien nimet eivät tulostu järjestyksessä. Tämä selittyy sillä että HashMap-tyyppisessä hajautustaulussa alkioiden tallennus tapahtuu hashCode-metodin palauttaman hajautusarvon perusteella. HashMap-hajautustaulu ei ota kantaa alkioiden järjestykseen.

Tehtävänäsi on toteuttaa pakkaukseen sanakirja luokka OmaUseanKaannoksenSanakirja, johon voidaan lisätä yksi tai useampi käännös jokaiselle sanalle. Luokan tulee toteuttaa tehtäväpohjassa annettu rajapinta UseanKaannoksenSanakirja, joka määrittelee seuraavat metodit:

  • public void lisaa(String sana, String kaannos) lisää käännöksen sanalle säilyttäen vanhat käännökset
  • public Set<String> kaanna(String sana) palauttaa Set-rajapinnan toteuttavan olion, jossa on kaikki käännökset sanalle. Jos sanalle ei ole yhtäkään käännöstä, metodin tulee palauttaa Set-olio, jossa ei ole yhtäkään alkiota
  • public void poista(String sana) poistaa sanan ja sen kaikki käännökset sanakirjasta.

Käännökset kannattanee lisätä Map<String, Set<String>>-tyyppiseen oliomuuttujaan.

Rajapinta UseanKaannoksenSanakirja:

package sanakirja;

import java.util.Set;

public interface UseanKaannoksenSanakirja {
    void lisaa(String sana, String kaannos);
    Set<String> kaanna(String sana);
    void poista(String sana);
}

Esimerkki:

UseanKaannoksenSanakirja sanakirja = new OmaUseanKaannoksenSanakirja();
sanakirja.lisaa("kuusi", "six");
sanakirja.lisaa("kuusi", "spruce");

sanakirja.lisaa("pii", "silicon");
sanakirja.lisaa("pii", "pi");

System.out.println(sanakirja.kaanna("kuusi"));
sanakirja.poista("pii");
System.out.println(sanakirja.kaanna("pii"));
[six, spruce]
[]

Tehtävänäsi on toteuttaa pakkaukseen tyokalut luokka OmaDuplikaattienPoistaja, joka tallettaa annetut merkkijonot siten, että annetuista merkkijonoista poistetaan samanlaiset merkkijonot (eli duplikaatit). Lisäksi luokka pitää kirjaa duplikaattien määrästä. Luokan tulee toteuttaa tehtäväpohjassa annettu rajapinta DuplikaattienPoistaja, jossa on seuraavat toiminnot:

  • public void lisaa(String merkkijono)
  • tallettaa merkkijonon, jos se ei ole duplikaatti
  • public int getHavaittujenDuplikaattienMaara()
  • palauttaa tähän mennessä havaittujen duplikaattien määrän
  • public Set<String> getUniikitMerkkijonot()
  • palauttaa Set<String>-rajapinnan toteuttavan olion, jossa on kaikki uniikit lisätyt merkkijonot (ei siis duplikaatteja!). Jos merkkijonoja ei ole, palautetaan tyhjä joukko-olio.
  • public void tyhjenna()
  • poistaa talletetut merkkijonot ja nollaa havaittujen duplikaattien määrän

Rajapinta:

package tyokalut;

import java.util.Set;

public interface DuplikaattienPoistaja {
    void lisaa(String merkkijono);
    int getHavaittujenDuplikaattienMaara();
    Set<String> getUniikitMerkkijonot();
    void tyhjenna();
}

Käyttöesimerkki:

public static void main(String[] args) {
    DuplikaattienPoistaja poistaja = new OmaDuplikaattienPoistaja();
    poistaja.lisaa("eka");
    poistaja.lisaa("toka");
    poistaja.lisaa("eka");

    System.out.println("Duplikaattien määrä nyt: " +
        poistaja.getHavaittujenDuplikaattienMaara());

    poistaja.lisaa("vika");
    poistaja.lisaa("vika");
    poistaja.lisaa("uusi");

    System.out.println("Duplikaattien määrä nyt: " +
        poistaja.getHavaittujenDuplikaattienMaara());

    System.out.println("Uniikit merkkijonot: " +
        poistaja.getUniikitMerkkijonot());

    poistaja.tyhjenna();

    System.out.println("Duplikaattien määrä nyt: " +
        poistaja.getHavaittujenDuplikaattienMaara());

    System.out.println("Uniikit merkkijonot: " +
        poistaja.getUniikitMerkkijonot());
}

Yllä oleva ohjelma tulostaisi esimerkiksi seuraavaa: (merkkijonojen järjestys saa vaihdella, sillä ei ole merkitystä)

Duplikaattien määrä nyt: 1
Duplikaattien määrä nyt: 2
Uniikit merkkijonot: [eka, toka, vika, uusi]
Duplikaattien määrä nyt: 0
Uniikit merkkijonot: []

Sama olio useammassa tietorakenteessa

Oliot ovat viittaustyyppisiä, eli muuttuja ei tallenna olioa itseään vaan viitteen. Tämä tarkoittaa myös sitä, että jos olio lisätään esimerkiksi listaan, listalle lisätään viite olioon. Mikään ei estä lisäämästä saman olion viitettä useampaan paikkaan.

Tarkastellaan esimerkkinä kirjastoa joka tallettaa kirjat hajautustauluihin sekä kirjailijan että kirjan isbn-numeron perusteella. Tämän lisäksi kirjasto pitää kirjaa lainassa olevista sekä hyllyssä olevista kirjoista erillisillä listoilla.

public class Kirja {
    private String ISBN;
    private String kirjailija;
    private String nimi;
    private int vuosi;
    // ...
}
public class Kirjasto {
    private Map<String, Kirja> kirjaIsbnNumeronPerusteella;
    private Map<String, List<Kirja>> kirjatKirjailijanPerusteella;
    private List<Kirja> lainassaOlevatKirjat;
    private List<Kirja> hyllyssaOlevatKirjat;

    public Kirjasto() {
        this.kirjaIsbnNumeronPerusteella = new HashMap<>();
        this.kirjatKirjailijanPerusteella = new HashMap<>();
        this.lainassaOlevatKirjat = new ArrayList<>();
        this.hyllyssaOlevatKirjat = new ArrayList<>();
    }

    public void lisaaKirjaKokoelmaan(Kirja uusiKirja) {
        this.kirjaIsbnNumeronPerusteella.put(uusiKirja.getIsbn(), uusiKirja);

        this.kirjatKirjailijanPerusteella.putIfAbsent(uusiKirja.getKirjailija(), new ArrayList<>());
        this.kirjatKirjailijanPerusteella.get(uusikirja.getKirjailija()).add(uusiKirja);

        this.hyllyssaOlevatKirjat.add(uusiKirja);
    }

    public Kirja haeKirjaIsbnNumeronPerusteella(String isbn){
        return kirjaIsbnNumeronPerusteella.get(isbn);
    }

    // ...
}

Jos olio on yhtäaikaa useassa kokoelmassa (listalla, joukossa tai map-rakenteessa), on kiinnitettävä erityistä huomiota, että kokoelmien tila on konsistentti. Jos esimerkiksi kirja päätetään poistaa, on se poistettava kaikista paikoista, missä kirjaan on viite.

Tässä tehtävässä kehität reseptikirjaa, josta voi hakea reseptejä sekä nimen että raaka-aineen perusteella. Tehtäväpohjassa on valmis luokka Resepti sekä tyhjät metodit sisältävä luokka Reseptikirja, jota sinun tulee täydentää. Käytä luokkaa täydentäessäsi oliomuuttujina vain Map-tyyppisiä olioita.

Reseptit nimen perusteella

Täydennä luokan Reseptikirja toimintaa siten, että luokasta tehtyyn olioon voi lisätä reseptejä ja reseptejä voi hakea reseptin nimen perusteella.

Reseptikirja kirja = new Reseptikirja();
kirja.lisaaResepti(new Resepti("Omenaleivos"));

Resepti resepti = kirja.haeNimella("Omenaleivos");
System.out.println(resepti);
    
Resepti toinen = kirja.haeNimella("leivos");
System.out.println(toinen);
Omenaleivos
null

Reseptit raaka-aineen perusteella

Täydennä luokan Reseptikirja toimintaa siten, että luokasta tehdystä oliosta voi hakea reseptejä raaka-aineen perusteella.

Reseptikirja kirja = new Reseptikirja();
Resepti omenaleivos = new Resepti("Omenaleivos");
omenaleivos.lisaaRaakaAine("omena");
omenaleivos.lisaaRaakaAine("kaurahiutale");
omenaleivos.lisaaRaakaAine("fariinisokeri");
omenaleivos.lisaaRaakaAine("voi");
    
kirja.lisaaResepti(omenaleivos);
    
List<Resepti> reseptit = kirja.haeRaakaAineella("jauheliha");
System.out.println(reseptit);
System.out.println(reseptit.size());

List<Resepti> reseptit2 = kirja.haeRaakaAineella("omena");
System.out.println(reseptit2.size());
System.out.println(reseptit2.get(0).getNimi());

List<Resepti> reseptit3 = kirja.haeRaakaAineella("mena");
System.out.println(reseptit3.size());
System.out.println(reseptit3);
[]
0
1
Omenaleivos
0
[]

Huom: jotta testit toimisivat, ohjelmasi saa luoda vain yhden syötteen lukemiseen tarkoitetun Scanner-olion.

Tehdään sovellus jonka avulla on mahdollista hallinnoida ihmisten puhelinnumeroita ja osoitteita.

Tehtävän voi suorittaa 1-5 pisteen laajuisena. Yhden pisteen laajuuteen on toteutettava seuraavat toiminnot:

  • 1 puhelinnumeron lisäys henkilölle
  • 2 henkilön puhelinnumeroiden haku

kahteen pisteeseen vaaditaan edellisten lisäksi

  • 3 numeroa vastaavan henkilön nimen haku

kolmeen pisteeseen vaaditaan edellisten lisäksi

  • 4 osoitteen lisäys henkilölle
  • 5 henkilön tietojen (osoite ja puhelinnumero) haku

neljään pisteeseen vaaditaan toiminto

  • 6 henkilön tietojen poisto

ja täysiin pisteeseen vaaditaan vielä

  • 7 hakusanalla filtteröity listaus, hakusana voi esiintyä henkilön nimessä tai osoitteessa

Esimerkki ohjelman toiminnasta:

numerotiedustelu
käytettävissä olevat komennot:
1 lisää numero
2 hae numerot
3 hae puhelinnumeroa vastaava henkilö
4 lisää osoite
5 hae henkilön tiedot
6 poista henkilön tiedot
7 filtteröity listaus
x lopeta

komento: 1
kenelle: pekka
numero: 040-123456

komento: 2
kenen: jukka
  ei löytynyt

komento: 2
kenen: pekka
    040-123456

komento: 1
kenelle: pekka
numero: 09-222333

komento: 2
kenen: pekka
  040-123456
  09-222333

komento: 3
numero: 02-444123
  ei löytynyt

komento: 3
numero: 09-222333
  pekka

komento: 5
kenen: pekka
  osoite ei tiedossa
  puhelinnumerot:
    040-123456
    09-222333

komento: 4
kenelle: pekka
katu: ida ekmanintie
kaupunki: helsinki

komento: 5
kenen: pekka
  osoite: ida ekmanintie helsinki
  puhelinnumerot:
    040-123456
    09-222333

komento: 4
kenelle: jukka
katu: korsontie
kaupunki: vantaa

komento: 5
kenen: jukka
  osoite: korsontie vantaa
  ei puhelinta

komento: 7
hakusana (jos tyhjä, listataan kaikki): kk

  jukka
    osoite: korsontie vantaa
    ei puhelinta

  pekka
    osoite: ida ekmanintie helsinki
    puhelinnumerot:
      040-123456
      09-222333

komento: 7
hakusana (jos tyhjä, listataan kaikki): vantaa

  jukka
    osoite: korsontie vantaa
    ei puhelinta

komento: 7
hakusana (jos tyhjä, listataan kaikki): seppo
  ei löytynyt

komento: 6
kenet: jukka

komento: 5
kenen: jukka
  ei löytynyt

komento: x

Huomioita:

  • Testien kannalta on oleellista että käyttöliittymä toimii kuten yllä olevassa esimerkissä. Sovellus voi itse päättää kuinka epäkelvot syötteet käsitellään. Testit sisältävät vaan kelvollisia syötteitä.
  • Ohjelman tulee käynnistyä kun tehtäväpohjassa oleva main-metodi suoritetaan, tehtävässä saa luoda vain yhden Scanner-olion.
  • Älä käytä luokkien nimissä skandeja, ne saattavat aiheuttaa ongelmia testeihin!
  • Yksinkertaisuuden vuoksi oletetaan että nimi on yksittäinen merkkijono, eli jos halutaan sukunimen mukaan järjestetyn tulostus viimeiseen toimintoon, nimi on annettava muodossa mikkola pekka.
  • Henkilöllä voi olla useita puhelinnumeroja sekä osoite. Henkilöllä ei kuitenkaan ole välttämättä yhtään puhelinnumeroa tai osoite ei ole tiedossa.
  • Jos henkilö poistetaan, ei mikään haku saa enää palauttaa henkilön tietoja.

Sisällysluettelo