Tehtävät

Kahdeksas osa aloittaa Ohjelmoinnin jatkokurssin. Jos tulet tässä kohtaa mukaan, kannattaa käydä läpi edellisten osien materiaali. Jos teet kurssia Helsingin yliopistolla, joudut vaihtamaan kurssia TMC:stä. Katso ohjeet materiaalin Johdantosivulta.

Kahdeksannen osan tavoitteet

Tuntee käsitteen object ja ymmärtää miksi jokaisella oliolla on metodi toString ja hashCode. Tietää joitakin käyttökohteita em. metodeille. Tuntee käsitteen rajapinta ja osaa luoda luokan, joka toteuttaa annetun rajapinnan. Osaa määritellä rajapinnan ja tietää, että eri luokista luodut oliot voivat toteuttaa saman rajapinnan. Ymmärtää käsitteen pakkaus ja osaa hyödyntää pakkauksia ohjelman rakenteen pilkkomisessa.

Tehtäväpohjien rakenne ja Maven

Ohjelmoinnin jatkokurssista lähtien kurssin tehtäväpohjat käyttävät Maven-nimistä projektinhallintatyövälinettä. Kyseinen työväline helpottaa kurssin tehtävien mukana tuotavien kirjastojen hallintaa. Tehtäviin tämä vaikuttaa siten, että niiden kansiorakenne muuttuu hieman.

Jatkossa tehtävän juurikansiossa on tiedosto pom.xml, joka kuvaa tehtäväpohjan rakenteen. Kansio src sisältää kansiot main ja test, jotka sisältävät lähdekooditiedostot sekä testitiedostot.

$/Osa08_01.SamaPaivays$ tree
.
├── pom.xml
└── src
    ├── main
    │   └── java
    │       ├── Paaohjelma.java
    │       └── Paivays.java
    └── test
        └── java
            └── PaivaysTest.java

Ohjelmien toiminta ei käytännössä juurikaan muutu. Toisin kuin ennen, ohjelmien tarvitsemat kirjastot eivät kuitenkaan tule tehtäväpohjan mukana, vaan Mavenilta tulee pyytää tarvittaessa niiden lataamista. Tämä onnistuu klikkaamalla tehtäväpohjan Dependencies-kuvaketta Projects-välilehdellä ja valitsemalla "Download declared dependencies."


Riippuen tietokoneesi käyttöjärjestelmästä, on mahdollista, että joudut lisäämään Maven-ohjelmaan ajo-oikeudet kun sitä käytetään ensimmäistä kertaa. Windowsille ohjeita löytyy yleisesti ottaen googlettamalla ja mm. täältä: https://www.online-tech-tips.com/computer-tips/set-file-folder-permissions-windows/ -- myös pajassa neuvotaan tähän liittyen.

Ongelmia ja ratkaisuja

Mavenin käyttöönottoon on liittynyt kurssilla ongelmia. Tässä lyhyt ongelmanratkaisuopas.

Linux ja Mac

Ongelma: Maven-binäärin suoritusoikeudet puuttuvat. Mavenin virhe on (esimerkiksi) muotoa.

Cannot run program
  /Applications/tmcbeans.app/Contents/Resources/tmcbeans/java/maven/bin/mvn"
  (in directory "/Users/[nimi]/NetBeansProjects/hy-[kurssi]/[tehtava]"):
  error=13,
  Permission denied

Ratkaisu: lisää maven-binäärille suoritusoikeudet. Suorita terminaalissa (pääte) komento.

chmod +x /Applications/tmcbeans.app/Contents/Resources/tmcbeans/java/maven/bin/mvn

Huomaa, että edellä polku on sama kuin virheviestin "Cannot run program"-polku.

Windows

Ongelma: Ympäristömuuttuja JAVA_HOME ei ole asetettu. Käytännössä Maven yrittää etsiä Javaa, mutta ei löydä sitä.

Ratkaisu: Lisää Windowsiin JAVA_HOME ympäristömuuttuja (esim.) osoitteessa https://confluence.atlassian.com/doc/setting-the-java_home-variable-in-windows-8895.html olevia ohjeita seuraamalla. Huomaa, että kansion tulee olla jdk, ei jre.

Tietojenkäsittelytieteen laitoksen koneet

Ongelma: Riippuvuuksien lataaminen ei onnistu tai ne eivät toimi. Tässä syynä on joko loppunut levytila tai verkkoyhteysongelma. Levytilan tilanteen saa selvitettyä komennolla "quota".

Ratkaisu:

  1. Poista vanhat ohjelmoinnin perusteiden (ja mahdollisesti muiden TMCtä käyttävien kurssien) tehtäväpohjat. Nämä löytyvät kotikansiosta kansion NetBeansProjects alta. Kannattanee harkita myös muiden turhien tiedostojen poistamista sekä esimerkiksi selaimen välimuistin tyhjentämistä ajoittain.
  2. Poista kotikansiossa olevan .m2-kansion sisältö. Tämä tehdään sillä Maven luo ladattavista riippuvuuksista (eli kirjastoista) vahingossa tyhjät tiedostot mikäli levytila on loppunut.
  3. (Jatkuva) Kun saat tehtäviä lähetettyä TMC:lle, klikkaa tehtäväpohjaa ja valitse "clean". Tämä poistaa tehtäväpohjasta käännetyt tiedostot.

Object

Olemme useampaan otteeseen käyttäneet metodia public String toString() olion merkkijonoesityksen muodostamiseen. Emme ole kuitenkaan saaneet selvyyttä miksi Java osaa käyttää kyseistä metodia. Olemattoman metodin kutsuminenhan tuottaa normaalisti virheen.

Tutkitaan seuraavaa luokkaa Kirja, jolla ei ole metodia public String toString(), ja ohjelmaa joka yrittää tulostaa Kirja-luokasta luodun olion System.out.println()-komennolla.

public class Kirja {
    private String nimi;
    private int julkaisuvuosi;

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

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

    public int getJulkaisuvuosi() {
        return this.julkaisuvuosi;
    }
}
Kirja olioKirja = new Kirja("Oliokirja", 2000);
System.out.println(olioKirja);
System.out.println(olioKirja.toString());

Ohjelma ei tulosta virheilmoitusta tai kaadu kun annamme Kirja-luokasta tehdyn olion parametrina System.out.println-komennolle tai kutsumme oliolle metodia toString. Näemme virheilmoituksen tai kaatumisen sijaan mielenkiintoisen tulosteen. Tuloste sisältää luokan Kirja nimen ja epämääräisen @-merkkiä seuraavan merkkijonon. Huomaa että kutsussa System.out.println(olioKirja) Java tekee oikeasti kutsun System.out.println(olioKirja.toString())

Selitys liittyy Javan luokkien rakenteeseen. Jokainen Javan luokka perii automaattisesti luokan Object, joka sisältää joukon jokaiselle Javan luokalle hyödyllisiä perusmetodeja. Perintä tarkoittaa että oma luokkamme saa käyttöön perittävän luokan määrittelemiä toiminnallisuuksia ja ominaisuuksia. Luokka Object sisältää muun muassa metodin toString, joka periytyy luokkiimme. Tämän takia metodi toString on jokaisen luomamme luokan käytössä, riippumatta siitä lisäämmekö metodille toteutuksen luokkaamme vai emme.

Object-luokassa määritelty toString-metodi ei yleensä ole toivomamme, minkä takia se tyypillisesti korvataan omalla toteutuksellamme. Tämä tapahtuu luomalla omaan luokkaamme public String toString()-metodi, jossa on toivomamme toiminnallisuus.

Lisätään luokkaan Kirja metodi public String toString(), joka korvaa perityssä Object luokassa olevan metodin toString.

public class Kirja {
    private String nimi;
    private int julkaisuvuosi;

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

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

    public int getJulkaisuvuosi() {
        return this.julkaisuvuosi;
    }

    @Override
    public String toString() {
        return this.nimi + " (" + this.julkaisuvuosi + ")";
    }
}

Nyt kun teemme oliosta ilmentymän ja annamme sen tulostusmetodille, näemme luokassa Kirja olevan toString-metodin tuottaman merkkijonon.

Kirja olioKirja = new Kirja("Oliokirja", 2000);
System.out.println(olioKirja);
Oliokirja (2000)

Luokassa Kirja olevan metodin toString yläpuolella on annotaatio @Override. Annotaatioilla annetaan vinkkejä siitä, miten metodeihin tulisi suhtautua. Annotaatio @Override kertoo lukijalle että annotaatiota seuraava metodi korvaa perityssä luokassa määritellyn metodin. Jos korvattavaan metodiin ei liitetä annotaatiota, antaa kääntäjä tilanteessa varoituksen, overriden kirjottamatta jättäminen ei kuitenkaan ole virhe.

Luokasta Object peritään muitakin hyödyllisiä metodeja. Tutustutaan seuraavaksi metodeihin equals ja hashCode.

Samanarvoisuudesta kertova metodi "equals"

Metodia equals käytetään kahden olion yhtäsuuruusvertailuun. Metodia on jo käytetty muun muassa String-olioiden yhteydessä.

Scanner lukija = new Scanner(System.in);

System.out.print("Kirjoita salasana: ");
String salasana = lukija.nextLine();

if (salasana.equals("salasana")) {
    System.out.println("Oikein meni!");
} else {
    System.out.println("Pieleen meni!");
}
Kirjoita salasana: mahtiporkkana
Pieleen meni!

Luokassa Object määritelty metodi equals tarkastaa onko parametrina annetulla oliolla sama viite kuin oliolla johon verrataan, eli toisinsanoen oletusarvoisesti vertaillaan onko kyse kahdesta samasta oliosta. Jos viite on sama, palauttaa metodi arvon true, muuten false. Tämä selvenee seuraavalla esimerkillä. Luokassa Kirja ei ole omaa equals-metodin toteutusta, joten se käyttää Object-luokassa olevaa toteutusta.

Kirja olioKirja = new Kirja("Oliokirja", 2000);
Kirja toinenOlioKirja = olioKirja;

if (olioKirja.equals(toinenOlioKirja)) {
    System.out.println("Kirjat olivat samat");
} else {
    System.out.println("Kirjat eivät olleet samat");
}

// nyt luodaan saman sisältöinen olio joka kuitenkin on oma erillinen olionsa
toinenOlioKirja = new Kirja("Oliokirja", 2000);

if (olioKirja.equals(toinenOlioKirja)) {
    System.out.println("Kirjat olivat samat");
} else {
    System.out.println("Kirjat eivät olleet samat");
}
Kirjat olivat samat
Kirjat eivät olleet samat

Vaikka edellisessä esimerkissä olevien kirjaolioiden sisäinen rakenne (eli oliomuuttujien arvot) on täsmälleen sama, vain ensimmäinen vertailu tulostaa merkkijonon "Kirjat olivat samat". Tämä johtuu siitä että vain ensimmäisessä tapauksessa viitteet ovat samat, eli olioa vertaillaan itseensä. Toisessa vertailussa kyse on kahdesta eri oliosta, vaikka muuttujilla onkin samat arvot.

Merkkijonojen eli Stringien yhteydessä equals toimii odotetulla tavalla, eli se ilmoittaa kaksi samansisältöistä merkkijonoa "equalseiksi" vaikka kyseessä olisikin kaksi erillistä olioa. String-luokassa onkin korvattu oletusarvoinen equals omalla toteutuksella.

Haluamme että kirjojen vertailu onnistuu myös nimen ja vuoden perusteella. Korvataan Object-luokassa oleva metodi equals määrittelemällä sille toteutus luokkaan Kirja. Metodin equals tehtävänä on selvittää onko olio sama kuin metodin parametrina saatu olio. Metodi saa parametrina Object-tyyppisen viitteen olion. Määritellään ensin metodi, jonka mielestä kaikki oliot ovat samoja.

public boolean equals(Object olio) {
    return true;
}

Metodimme on varsin optimistinen, joten muutetaan sen toimintaa hieman. Määritellään että oliot eivät ole samoja jos parametrina saatu olio on null tai jos olioiden tyypit eivät ole samat. Olion tyypin saa (Object-luokassa määritellyllä) metodilla getClass(). Muussa tapauksessa oletetaan että oliot ovat samat.

public boolean equals(Object olio) {
    if (olio == null) {
        return false;
    }

    if (this.getClass() != olio.getClass()) {
        return false;
    }

    return true;
}

Metodi equals huomaa eron erityyppisten olioiden välillä, mutta ei vielä osaa erottaa samanlaisia olioita toisistaan. Jotta voisimme verrata nykyistä oliota ja parametrina saatua Object-tyyppisellä parametrilla viitattua olioa, tulee Object-viitteen tyyppiä muuttaa. Viitteen tyyppiä voidaan muuttaa tyyppimuunnoksella jos ja vain jos olion tyyppi on oikeasti sellainen, mihin sitä yritetään muuttaa. Tyyppimuunnos tapahtuu antamalla asetuslauseen oikealla puolella haluttu luokka suluissa, esimerkiksi:

HaluttuTyyppi muuttuja = (HaluttuTyyppi) vanhaMuuttuja;

Voimme tehdä tyyppimuunnoksen koska tiedämme olioiden olevan samantyyppisiä, jos ne ovat erityyppisiä yllä oleva metodi getClass palauttaa arvon false. Muunnetaan metodissa equals saatu Object-tyyppinen parametri Kirja-tyyppiseksi, ja todetaan kirjojen olevan eri jos niiden julkaisuvuodet ovat eri. Muuten kirjat ovat vielä samat.

public boolean equals(Object olio) {
    if (olio == null) {
        return false;
    }

    if (getClass() != olio.getClass()) {
        return false;
    }

    Kirja verrattava = (Kirja) olio;

    if(this.julkaisuvuosi != verrattava.getJulkaisuvuosi()) {
        return false;
    }

    return true;
}

Nyt vertailumetodimme osaa erottaa eri vuosina julkaistut kirjat. Lisätään vielä tarkistus, että kirjojemme nimet ovat samat ja että oman kirjamme nimi ei ole null.

public boolean equals(Object olio) {
    if (olio == null) {
        return false;
    }

    if (getClass() != olio.getClass()) {
        return false;
    }

    Kirja verrattava = (Kirja) olio;

    if (this.julkaisuvuosi != verrattava.getJulkaisuvuosi()) {
        return false;
    }

    if (this.nimi == null || !this.nimi.equals(verrattava.getNimi())) {
        return false;
    }

    return true;
}

Mahtavaa, viimeinkin toimiva vertailumetodi! Alla vielä tämänhetkinen Kirja-luokkamme.

public class Kirja {
    private String nimi;
    private int julkaisuvuosi;

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

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

    public int getJulkaisuvuosi() {
        return this.julkaisuvuosi;
    }

    @Override
    public String toString() {
        return this.nimi + " (" + this.julkaisuvuosi + ")";
    }

    @Override
    public boolean equals(Object olio) {
        if (olio == null) {
            return false;
        }

        if (getClass() != olio.getClass()) {
            return false;
        }

        Kirja verrattava = (Kirja) olio;

        if (this.julkaisuvuosi != verrattava.getJulkaisuvuosi()) {
            return false;
        }

        if (this.nimi == null || !this.nimi.equals(verrattava.getNimi())) {
            return false;
        }

        return true;
    }
}

Nyt kirjojen vertailu palauttaa true jos kirjojen sisällöt ovat samat.

Kirja olioKirja = new Kirja("Oliokirja", 2000);
Kirja toinenOlioKirja = new Kirja("Oliokirja", 2000);

if (olioKirja.equals(toinenOlioKirja)) {
    System.out.println("Kirjat olivat samat");
} else {
    System.out.println("Kirjat eivät olleet samat");
}
Kirjat olivat samat

Laajennetaan alkukurssin tehtävässä "Päivämäärien erotus" toteutettua Paivays-luokkaa siten, että se osaa myös sanoa ovatko päivämäärät täsmälleen samat.

Lisää Paivays-luokkaan metodi public boolean equals(Object object), joka kertoo onko metodille parametrina annettu olio päiväys ja onko parametrina annetun olion päiväys sama kuin käytetyn olion päiväys.

Metodin tulee toimia seuraavasti:

Paivays p = new Paivays(1, 2, 2000);
System.out.println(p.equals("heh"));
System.out.println(p.equals(new Paivays(5, 2, 2012)));
System.out.println(p.equals(new Paivays(1, 2, 2000)));
false
false
true

Equals ja ArrayList

Useat Javan valmiit tietorakenteet käyttävät equals-metodia osana sisäistä hakumekanismiaan. Esimerkiksi luokan ArrayList contains-metodi vertailee olioiden yhtäsuuruutta equals-metodin avulla. Jatketaan aiemmin määrittelemämme Kirja-luokan käyttöä seuraavassa esimerkissä. Jos emme toteuta omissa olioissamme equals-metodia, ei contains-metodi toimi oikein, sillä se käyttää omassa toteutuksessaan equals-metodia olioiden vertailemiseen. Kokeile alla olevaa koodia kahdella erilaisella Kirja-luokalla. Toisessa on equals-metodi, ja toisessa sitä ei ole.

ArrayList<Kirja> kirjat = new ArrayList<>();
Kirja olioKirja = new Kirja("Oliokirja", 2000);
kirjat.add(olioKirja);

if (kirjat.contains(olioKirja)) {
    System.out.println("Oliokirja löytyi.");
}

olioKirja = new Kirja("Oliokirja", 2000);

if (!kirjat.contains(olioKirja)) {
    System.out.println("Oliokirjaa ei löytynyt.");
}

Tämä oletusmetodeihin kuten equals tukeutuminen on oikeastaan syy sille, miksi Java haluaa, että ArrayListiin lisättävät muuttujat ovat viittaustyyppisiä. Koska jokaisella luokalla on Object-luokasta periytyvä equals-metodi, ei luokan ArrayList sisäistä toteutusta tarvitse muuttaa lainkaan erilaisia muuttujia lisättäessä. Alkeistyyppisillä muuttujilla tällaisia metodeja ei ole, jolloin ArrayList ei löydä niihin liittyvää equals-metodia.

Hajautusarvo "hashCode"

Object-luokasta periytyvää metodia hashCode käytetään oliota kuvaavan hajautusarvon luomiseen. Hajautusarvoa käytetään suurpiirteiseen vertailuun. Jos kahdella oliolla on sama hajautusarvo, ne saattavat olla samanarvoiset. Jos taas kahdella oliolla on eri hajautusarvot, ne ovat varmasti eriarvoiset.

Hajautusarvoa tarvitaan muunmuassa HashMapissa. HashMapin sisäinen toiminta perustuu siihen, että avain-arvo -parit on tallennettu avaimen hajautusarvon perusteella listoja sisältävään taulukkoon. Jokainen taulukon indeksi viittaa listaan. Hajautusarvon perusteella tunnistetaan taulukon indeksi, jonka jälkeen taulukon indeksistä löytyvä lista käydään läpi. Avaimeen liittyvä arvo palautetaan jos ja vain jos listasta löytyy täsmälleen sama arvo (samansuuruisuuden vertailu tapahtuu equals-metodilla). Näin etsinnässä tarvitsee tarkastella vain murto-osaa hajautustauluun tallennetuista avaimista.

Olemme tähän mennessä käyttäneet HashMapin avaimina ainoastaan String- ja Integer-tyyppisiä olioita, joilla on ollut valmiina sopivasti toteutetut hashCode-metodit. Luodaan esimerkki jossa näin ei ole: jatketaan kirjojen parissa ja pidetään kirjaa lainassa olevista kirjoista. Päätetään ratkaista kirjanpito HashMapin avulla. Avaimena toimii kirja ja kirjaan liitetty arvo on merkkijono, joka keroo lainaajan nimen:

HashMap<Kirja, String> lainaajat = new HashMap<>();

Kirja oliokirja = new Kirja("Oliokirja", 2000);
lainaajat.put(oliokirja, "Pekka");
lainaajat.put(new Kirja("Test Driven Development", 1999), "Arto");

System.out.println(lainaajat.get(oliokirja));
System.out.println(lainaajat.get(new Kirja("Oliokirja", 2000));
System.out.println(lainaajat.get(new Kirja("Test Driven Development", 1999));
Pekka
null
null

Löydämme lainaajan hakiessamme samalla oliolla, joka annettiin hajautustaulun put-metodille avaimeksi. Täsmälleen samanlaisella kirjalla mutta eri oliolla haettaessa lainaajaa ei kuitenkaan löydy ja saamme null-viitteen. Syynä on Object-luokassa oleva hashCode-metodin oletustoteutus. Oletustoteutus luo hashCode-arvon olion viitteen perusteella, eli samansisältöiset mutta eri oliot saavat eri tuloksen hashCode-metodista. Tämän takia olioa ei osata etsiä oikeasta paikasta.

Jotta HashMap toimisi haluamallamme tavalla, eli palauttaisi lainaajan kun avaimeksi annetaan oikean sisältöinen olio (ei välttämässä siis sama olio kuin alkuperäinen avain), on avaimena toimivan luokan ylikirjoitettava metodin equals lisäksi metodi hashCode. Metodi on ylikirjoitettava siten, että se antaa saman numeerisen tuloksen kaikille samansisältöisille olioille. Myös jotkut erisisältöiset oliot saavat saada saman tuloksen hashCode-metodista. On kuitenkin HashMapin tehokkuuden kannalta oleellista, että erisisältöiset oliot saavat mahdollisimman harvoin saman hajautusarvon.

Olemme aiemmin käyttäneet String-olioita menestyksekkäästi HashMapin avaimena, joten voimme päätellä että String-luokassa on oma järkevästi toimiva hashCode-toteutus. Delegoidaan, eli siirretään laskemisvastuu String-oliolle.

public int hashCode() {
    return this.nimi.hashCode();
}

Yllä oleva ratkaisu on melko hyvä, mutta jos nimi on null, näemme NullPointerException-virheen. Korjataan tämä vielä määrittelemällä ehto: jos nimi-muuttujan arvo on null, palautetaan hajautusarvoksi julkaisuvuosi.

public int hashCode() {
    if (this.nimi == null) {
        return this.julkaisuvuosi;
    }

    return this.nimi.hashCode();
}

Nyt ylläolevassa ratkaisussa kaikki saman nimiset kirjat niputetaan samaan joukkoon. Parannetaan toteutusta vielä siten, että kirjan julkaisuvuosi huomioidaan myös nimeen perustuvassa hajautusarvon laskennassa.

public int hashCode() {
    if (this.nimi == null) {
        return this.julkaisuvuosi;
    }

    return this.julkaisuvuosi + this.nimi.hashCode();
}

Laajennetaan vielä edellisessä tehtävässä nähtyä Paivays-luokkaa siten, että sillä on myös oma hashCode-metodi.

Lisää Paivays-luokkaan metodi public int hashCode(), joka laskee päiväys-oliolle hajautusarvon. Toteuta hajautusarvon laskeminen siten, että vuosien 1900 ja 2100 välillä löytyy mahdollisimman vähän samankaltaisia hajautusarvoja.

Luokka Kirja nyt kokonaisuudessaan.

public class Kirja {

    private String nimi;
    private int julkaisuvuosi;

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

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

    public int getJulkaisuvuosi() {
        return this.julkaisuvuosi;
    }

    @Override
    public String toString() {
        return this.nimi + " (" + this.julkaisuvuosi + ")";
    }

    @Override
    public boolean equals(Object olio) {
        if (olio == null) {
            return false;
        }

        if (getClass() != olio.getClass()) {
            return false;
        }

        Kirja verrattava = (Kirja) olio;

        if (this.julkaisuvuosi != verrattava.getJulkaisuvuosi()) {
            return false;
        }

        if (this.nimi == null || !this.nimi.equals(verrattava.getNimi())) {
            return false;
        }

        return true;
    }

    public int hashCode() {
        if (this.nimi == null) {
            return this.julkaisuvuosi;
        }

        return this.julkaisuvuosi + this.nimi.hashCode();
    }
}

Kerrataan vielä: jotta luokkaa voidaan käyttää HashMap:in avaimena, tulee sille määritellä

  • metodi equals siten, että kaikki samansuuruisena (tai saman sisältöisinä) ajatellut oliot tuottavat vertailussa tuloksen true ja muut false
  • metodi hashCode siten, että mahdollisimman harvalla erisuuruisella oliolla on sama hajautusarvo

Luokalle Kirja määrittelemämme equals ja hashCode selvästi täyttävät nämä ehdot. Nyt myös aiemmin kohtaamamme ongelma ratkeaa ja kirjojen lainaajat löytyvät:

HashMap<Kirja, String> lainaajat = new HashMap<>();

Kirja oliokirja = new Kirja("Oliokirja", 2000);
lainaajat.put(oliokirja, "Pekka");
lainaajat.put(new Kirja("Test Driven Development",1999), "Arto");

System.out.println(lainaajat.get(oliokirja));
System.out.println(lainaajat.get(new Kirja("Oliokirja", 2000));
System.out.println(lainaajat.get(new Kirja("Test Driven Development", 1999));

Tulostuu:

Pekka
Pekka
Arto
Metodien equals ja hashCode automaattinen luominen

NetBeans tarjoaa metodien equals ja hashCode automaattisen luonnin. Voit valita valikosta Source -> Insert Code, ja valita aukeavasta listasta equals() and hashCode(). Tämän jälkeen NetBeans kysyy oliomuuttujat joita metodeissa käytetään. Nämä NetBeansin generoimat metodit ovat tyypillisesti "tarpeeksi hyviä" omiin tarpeisiimme.

Rekisterinumeron equals ja hashCode

Eurooppalaiset rekisteritunnukset koostuvat kahdesta osasta: yksi tai kaksikirjaimisesta maatunnuksesta ja maakohtaisesti määrittyvästä rekisterinumerosta, joka taas koostuu numeroista ja merkeistä. Rekisterinumeroita esitetään seuraavanlaisen luokan avulla:

public class Rekisterinumero {
    // tässä määre final tarkoittaa sitä, että arvoa ei voi muuttaa asetuksen jälkeen
    private final String rekNro;
    private final String maa;

    public Rekisterinumero(String rekNro, String maa) {
       this.rekNro = rekNro;
       this.maa = maa;
    }

    public String toString(){
        return maa+ " "+rekNro;
    }
}

Rekisterinumeroja halutaan tallettaa esim. ArrayList:eille ja käyttää HashMap:in avaimina, eli kuten yllä mainittu, tulee niille toteuttaa metodit equals ja hashCode, muuten ne eivät toimi halutulla tavalla. Toteuta luokalle rekisterinumero metodit equals ja hashCode.

Esimerkkiohjelma:

public static void main(String[] args) {
    Rekisterinumero rek1 = new Rekisterinumero("FI", "ABC-123");
    Rekisterinumero rek2 = new Rekisterinumero("FI", "UXE-465");
    Rekisterinumero rek3 = new Rekisterinumero("D", "B WQ-431");

    ArrayList<Rekisterinumero> suomalaiset = new ArrayList<>();
    suomalaiset.add(rek1);
    suomalaiset.add(rek2);

    Rekisterinumero uusi = new Rekisterinumero("FI", "ABC-123");
    if (!suomalaiset.contains(uusi)) {
        suomalaiset.add(uusi);
    }
    System.out.println("suomalaiset: " + suomalaiset);
    // jos equals-metodia ei ole ylikirjoitettu, menee sama rekisterinumero toistamiseen listalle

    HashMap<Rekisterinumero, String> omistajat = new HashMap<>();
    omistajat.put(rek1, "Arto");
    omistajat.put(rek3, "Jürgen");

    System.out.println("omistajat:");
    System.out.println(omistajat.get(new Rekisterinumero("FI", "ABC-123")));
    System.out.println(omistajat.get(new Rekisterinumero("D", "B WQ-431")));
    // jos hashCode ei ole ylikirjoitettu, eivät omistajat löydy
}

Jos equals ja hashCode on toteutettu oikein, tulostus on seuraavanlainen.

suomalaiset: [FI ABC-123, FI UXE-465]
omistajat:
Arto
Jürgen

Omistaja rekisterinumeron perusteella

Toteuta luokka Ajoneuvorekisteri jolla on seuraavat metodit:

  • public boolean lisaa(Rekisterinumero rekkari, String omistaja) lisää parametrina olevaa rekisterinumeroa vastaavalle autolle parametrina olevan omistajan, metodi palauttaa true jos omistajaa ei ollut ennestään, jos rekisterinumeroa vastaavalla autolla oli jo omistaja, metodi palauttaa false ja ei tee mitään
  • public String hae(Rekisterinumero rekkari) palauttaa parametrina olevaa rekisterinumeroa vastaavan auton omistajan. Jos auto ei ole rekisterissä, palautetaan null
  • public boolean poista(Rekisterinumero rekkari) poistaa parametrina olevaa rekisterinumeroa vastaavat tiedot, metodi palauttaa true jos tiedot poistetiin, ja false jos parametria vastaavia tietoja ei ollut rekisterissä

Huom: Ajoneuvorekisterin täytyy tallettaa omistajatiedot HashMap<Rekisterinumero, String> omistajat -tyyppiseen oliomuuttujaan!

Ajoneuvorekisteri laajenee

Lisää Ajoneuvorekisteriin vielä seuraavat metodit:

  • public void tulostaRekisterinumerot() tulostaa rekisterissä olevat rekisterinumerot
  • public void tulostaOmistajat() tulostaa rekisterissä olevien autojen omistajat, yhden omistajan nimeä ei saa tulostaa kuin kertaalleen vaikka omistajalla olisikin useampi auto

Rajapinta

Rajapinnan (engl. interface) avulla määritellään luokalta vaadittu käyttäytyminen, eli sen metodit. Rajapinnat määritellään kuten normaalit Javan luokat, mutta luokan alussa olevan määrittelyn "public class ..." sijaan käytetään määrittelyä "public interface ...". Rajapinnat määrittelevät käyttäytymisen metodien niminä ja palautusarvoina, mutta ne eivät aina sisällä metodien konkreettista toteutusta. Näkyvyysmäärettä rajapintoihin ei erikseen merkitä, sillä se on aina public. Tutkitaan luettavuutta kuvaavaa rajapintaa Luettava.

public interface Luettava {
    String lue();
}

Rajapinta Luettava määrittelee metodin lue(), joka palauttaa String-tyyppisen olion. Luettava kuvaa käyttäytymistä: esimerkiksi tekstiviesti tai sähköpostiviesti voi olla luettava.

Rajapinnan toteuttavat luokat päättävät miten rajapinnassa määritellyt metodit toteutetaan. Luokka toteuttaa rajapinnan lisäämällä luokan nimen jälkeen avainsanan implements, jota seuraa rajapinnan nimi. Luodaan luokka Tekstiviesti, joka toteuttaa rajapinnan Luettava.

public class Tekstiviesti implements Luettava {
    private String lahettaja;
    private String sisalto;

    public Tekstiviesti(String lahettaja, String sisalto) {
        this.lahettaja = lahettaja;
        this.sisalto = sisalto;
    }

    public String getLahettaja() {
        return this.lahettaja;
    }

    public String lue() {
        return this.sisalto;
    }
}

Koska luokka Tekstiviesti toteuttaa rajapinnan Luettava (public class Tekstiviesti implements Luettava), on luokassa Tekstiviesti pakko olla metodin public String lue() toteutus. Rajapinnassa määriteltyjen metodien toteutuksilla tulee aina olla näkyvyysmääre public.

Rajapinta on sopimus käyttäytymisestä

Kun luokka toteuttaa rajapinnan, se allekirjoittaa sopimuksen. Sopimuksessa luvataan, että luokka toteuttaa rajapinnan määrittelemät metodit. Jos metodeja ei ole luokassa toteutettu, ei ohjelma toimi.

Rajapinta määrittelee vain vaadittujen metodien nimet, parametrit, ja paluuarvot. Rajapinta ei kuitenkaan ota kantaa metodien sisäiseen toteutukseen. Ohjelmoijan vastuulla on määritellä metodien sisäinen toiminnallisuus.

Toteutetaan luokan Tekstiviesti lisäksi toinen Luettava rajapinnan toteuttava luokka. Luokka Sahkokirja on sähköinen toteutus kirjasta, joka sisältää kirjan nimen ja sivut. Sähkökirjaa luetaan sivu kerrallaan, metodin public String lue() kutsuminen palauttaa aina seuraavan sivun merkkijonona.

public class Sahkokirja implements Luettava {
    private String nimi;
    private ArrayList<String> sivut;
    private int sivunumero;

    public Sahkokirja(String nimi, ArrayList<String> sivut) {
        this.nimi = nimi;
        this.sivut = sivut;
        this.sivunumero = 0;
    }

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

    public int sivuja() {
        return this.sivut.size();
    }

    public String lue() {
        String sivu = this.sivut.get(this.sivunumero);
        seuraavaSivu();
        return sivu;
    }

    private void seuraavaSivu() {
        this.sivunumero = this.sivunumero + 1;
        if(this.sivunumero % this.sivut.size() == 0) {
            this.sivunumero = 0;
        }
    }
}

Rajapinnan toteuttavasta luokasta voi tehdä olioita aivan kuten normaaleistakin luokista, ja niitä voidaan käyttää myös esimerkiksi ArrayList-listojen tyyppinä.

Tekstiviesti viesti = new Tekstiviesti("ope", "Huikeaa menoa!");
System.out.println(viesti.lue());

ArrayList<Tekstiviesti> tekstiviestit = new ArrayList<>();
tekstiviestit.add(new Tekstiviesti("tuntematon numero", "I hid the body.");
Huikeaa menoa!
ArrayList<String> sivut = new ArrayList<>();
sivut.add("Pilko metodisi lyhyiksi luettaviksi kokonaisuuksiksi.");
sivut.add("Erota käyttöliittymälogiikka sovelluksen logiikasta.");
sivut.add("Ohjelmoi aina ensin pieni osa, jolla ratkaiset osan ongelmasta.");
sivut.add("Harjoittelu tekee mestarin. Keksi ja tee omia kokeiluja ja projekteja.");

Sahkokirja kirja = new Sahkokirja("Vinkkejä ohjelmointiin.", sivut);
for (int sivu = 0; sivu < kirja.sivuja(); sivu++) {
    System.out.println(kirja.lue());
}
Pilko metodisi lyhyiksi luettaviksi kokonaisuuksiksi.
Erota käyttöliittymälogiikka sovelluksen logiikasta.
Ohjelmoi aina ensin pieni osa, jolla ratkaiset osan ongelmasta.
Harjoittelu tekee mestarin. Keksi ja tee omia kokeiluja ja projekteja.
Mikä ihmeen for (int i = 0; ...?

Yllä olevassa esimerkissä käytettiin toisenlaista toistolausetta alkioiden läpikäyntiin. Olemme tottuneet seuraavanlaiseen toistolauseeseen while-toistolauseeseen.

ArrayList<String> sivut = new ArrayList<>();
sivut.add("Pilko metodisi lyhyiksi luettaviksi kokonaisuuksiksi.");
sivut.add("Erota käyttöliittymälogiikka sovelluksen logiikasta.");
sivut.add("Ohjelmoi aina ensin pieni osa, jolla ratkaiset osan ongelmasta.");
sivut.add("Harjoittelu tekee mestarin. Keksi ja tee omia kokeiluja ja projekteja.");

Sahkokirja kirja = new Sahkokirja("Vinkkejä ohjelmointiin.", sivut);

int sivu = 0;
while (sivu < kirja.sivuja()) {
    System.out.println(kirja.lue());
    sivu++;
}

Saman voi kirjoittaa myös for-toistolauseella seuraavasti.

ArrayList<String> sivut = new ArrayList<>();
sivut.add("Pilko metodisi lyhyiksi luettaviksi kokonaisuuksiksi.");
sivut.add("Erota käyttöliittymälogiikka sovelluksen logiikasta.");
sivut.add("Ohjelmoi aina ensin pieni osa, jolla ratkaiset osan ongelmasta.");
sivut.add("Harjoittelu tekee mestarin. Keksi ja tee omia kokeiluja ja projekteja.");

Sahkokirja kirja = new Sahkokirja("Vinkkejä ohjelmointiin.", sivut);
for (int sivu = 0; sivu < kirja.sivuja(); sivu++) {
    System.out.println(kirja.lue());
}

Tehtäväpohjassa on valmiina rajapinta Palvelusvelvollinen, jossa on seuraavat toiminnot:

  • metodi int paiviaJaljella() palauttaa jäljellä olevien palveluspäivien määrän
  • metodi void palvele() vähentää yhden palveluspäivän. Palveluspäivien määrä ei saa mennä negatiiviseksi.
public interface Palvelusvelvollinen {
    int paiviaJaljella();
    void palvele();
}

Sivari

Tee Palvelusvelvollinen-rajapinnan toteuttava luokka Sivari, jolla parametriton konstruktori. Luokalla on oliomuuttuja paivia, joka alustetaan konstruktorikutsun yhteydessä arvoon 362.

Asevelvollinen

Tee Palvelusvelvollinen-rajapinnan toteuttava luokka Asevelvollinen, jolla on parametrillinen konstruktori, jolla määritellään palvelusaika (int paivia).

Rajapinta muuttujan tyyppinä

Uutta muuttujaa esitellessä kerrotaan aina muuttujan tyyppi. Tyyppejä on kahdenlaisia, alkeistyyppiset muuttujat (int, double, ...) ja viittaustyyppiset muuttujat (kaikki oliot). Olemme tähän mennessä käyttäneet viittaustyyppisten muuttujien tyyppinä olion luokkaa.

String merkkijono = "merkkijono-olio";
Tekstiviesti viesti = new Tekstiviesti("ope", "samalla oliolla monta tyyppiä");

Olion tyyppi voi olla muutakin kuin sen luokka. Esimerkiksi rajapinnan Luettava toteuttavan luokan Sahkokirja tyyppi on sekä Sahkokirja että Luettava. Samalla tavalla myös tekstiviestillä on monta tyyppiä. Koska luokka Tekstiviesti toteuttaa rajapinnan Luettava, on sillä tyypin Tekstiviesti lisäksi myös tyyppi Luettava.

Tekstiviesti viesti = new Tekstiviesti("ope", "Kohta tapahtuu huikeita");
Luettava luettava = new Tekstiviesti("ope", "Tekstiviesti on Luettava!");
ArrayList<String> sivut = new ArrayList<>();
sivut.add("Metodi voi kutsua itse itseään.");

Luettava kirja = new Sahkokirja("Rekursion alkeet.", sivut);
for (int sivu = 0; sivu < kirja.sivuja(); sivu++) {
    System.out.println(kirja.lue());
}

Koska rajapintaa voidaan käyttää tyyppinä, on mahdollista luoda rajapintaluokan tyyppisiä olioita sisältävä lista.

ArrayList<Luettava> lukulista = new ArrayList<>();

lukulista.add(new Tekstiviesti("ope", "never been programming before..."));
lukulista.add(new Tekstiviesti("ope", "gonna love it i think!"));
lukulista.add(new Tekstiviesti("ope", "give me something more challenging! :)"));
lukulista.add(new Tekstiviesti("ope", "you think i can do it?"));
lukulista.add(new Tekstiviesti("ope", "up here we send several messages each day"));


ArrayList<String> sivut = new ArrayList<>();
sivut.add("Metodi voi kutsua itse itseään.");

lukulista.add(new Sahkokirja("Rekursion alkeet.", sivut));

lukulista.stream().forEach(l -> System.out.println(l.lue()));

Huomaa että vaikka rajapinnan Luettava toteuttava luokka Sahkokirja on aina rajapinnan tyyppinen, eivät kaikki Luettava-rajapinnan toteuttavat luokat ole tyyppiä Sahkokirja. Luokasta Sahkokirja tehdyn olion asettaminen Luettava-tyyppiseen muuttujaan onnistuu, mutta toiseen suuntaan asetus ei ole sallittua ilman erillistä tyyppimuunnosta.

Luettava luettava = new Tekstiviesti("ope", "Tekstiviesti on Luettava!"); // toimii
Tekstiviesti viesti = luettava; // ei toimi

Tekstiviesti muunnettuViesti = (Tekstiviesti) luettava; // toimii jos ja vain jos
                                                        // luettava on tyyppiä Tekstiviesti

Tyyppimuunnos onnistuu jos ja vain jos muuttuja on oikeastikin sitä tyyppiä johon sitä yritetään muuntaa. Tyyppimuunnoksen käyttöä ei yleisesti suositella, ja lähes ainut sallittu paikka sen käyttöön on equals-metodin toteutuksessa.

Rajapinta metodin parametrina

Rajapintojen todelliset hyödyt tulevat esille kun niitä käytetään metodille annettavan parametrin tyyppinä. Koska rajapintaa voidaan käyttää muuttujan tyyppinä, voidaan sitä käyttää metodikutsuissa parametrin tyyppinä. Esimerkiksi seuraavan luokan Tulostin metodi tulosta saa parametrina Luettava-tyyppisen muuttujan.

public class Tulostin {
    public void tulosta(Luettava luettava) {
        System.out.println(luettava.lue());
    }
}

Luokan Tulostin tarjoaman metodin tulosta huikeus piilee siinä, että sille voi antaa parametrina minkä tahansa Luettava-rajapinnan toteuttavan luokan ilmentymän. Kutsummepa metodia millä tahansa Luettava-luokan toteuttaneen luokan oliolla, metodi osaa toimia oikein.

Tekstiviesti viesti = new Tekstiviesti("ope", "Huhhuh, tää tulostinkin osaa tulostaa näitä!");

ArrayList<String> sivut = new ArrayList<>();
sivut.add("Lukujen {1, 3, 5} ja {2, 3, 4, 5} yhteisiä lukuja ovat {3, 5}.");
Sahkokirja kirja = new Sahkokirja("Yliopistomatematiikan perusteet.", sivut);

Tulostin tulostin = new Tulostin();
tulostin.tulosta(viesti);
tulostin.tulosta(kirja);
Huhhuh, tää tulostinkin osaa tulostaa näitä!
Lukujen {1, 3, 5} ja {2, 3, 4, 5} yhteisiä lukuja ovat {3, 5}.

Toteutetaan toinen luokka Lukulista, johon voidaan lisätä mielenkiintoisia luettavia asioita. Luokalla on oliomuuttujana ArrayList-luokan ilmentymä, johon luettavia asioita tallennetaan. Lukulistaan lisääminen tapahtuu lisaa-metodilla, joka saa parametrikseen Luettava-tyyppisen olion.

public class Lukulista {
    private ArrayList<Luettava> luettavat;

    public Lukulista() {
        this.luettavat = new ArrayList<>();
    }

    public void lisaa(Luettava luettava) {
        this.luettavat.add(luettava);
    }

    public int luettavia() {
        return this.luettavat.size();
    }
}

Lukulistat ovat yleensä luettavia, joten toteutetaan luokalle Lukulista rajapinta Luettava. Lukulistan lue-metodi lukee kaikki luettavat-listalla olevat oliot läpi, ja lisää yksitellen niiden lue()-metodin palauttaman merkkijonoon.

public class Lukulista implements Luettava {
    private ArrayList<Luettava> luettavat;

    public Lukulista() {
        this.luettavat = new ArrayList<>();
    }

    public void lisaa(Luettava luettava) {
        this.luettavat.add(luettava);
    }

    public int luettavia() {
        return this.luettavat.size();
    }

    public String lue() {
        String luettu = this.luettavat.stream()
            .reduce("", (a, luettava) -> a + luettava.lue() + "\n");

        // yllä oleva on sama kuin
        /*
        String luettu = "";
        for (int i = 0; i < this.luettavat.size(); i++) {
            luettu += this.luettavat.get(i).lue() + "\n";
        }
        */

        // kun lukulista on luettu, tyhjennetään se
        this.luettavat.clear();
        return luettu;
    }
}
Lukulista joninLista = new Lukulista();
joninLista.lisaa(new Tekstiviesti("arto", "teitkö jo testit?"));
joninLista.lisaa(new Tekstiviesti("arto", "katsoitko jo palautukset?"));

System.out.println("Jonilla luettavia: " + joninLista.luettavia());
Jonilla luettavia: 2

Koska Lukulista on tyyppiä Luettava, voi lukulistalle lisätä Lukulista-olioita. Alla olevassa esimerkissä Jonilla on paljon luettavaa. Onneksi Verna tulee hätiin ja lukee viestit Jonin puolesta.

Lukulista joninLista = new Lukulista();
for (int i = 0; i < 1000; i++) {
    joninLista.lisaa(new Tekstiviesti("arto", "teitkö jo testit?"));
}

System.out.println("Jonilla luettavia: " + joninLista.luettavia());
System.out.println("Delegoidaan lukeminen Vernalle");

Lukulista vernanLista = new Lukulista();
vernanLista.lisaa(joninLista);
vernanLista.lue();

System.out.println();
System.out.println("Jonilla luettavia: " + joninLista.luettavia());
Jonilla luettavia: 1000
Delegoidaan lukeminen Vernalle

Jonilla luettavia: 0

Ohjelmassa Vernan listalle kutsuttu lue-metodi käy kaikki sen sisältämät Luettava-oliot läpi, ja kutsuu niiden lue-metodia. Kutsuttaessa lue-metodia Vernan listalle käydään myös Vernan lukulistalla oleva Jonin lukulista läpi. Jonin lukulista käydään läpi kutsumalla sen lue-metodia. Jokaisen lue-metodin kutsun lopussa tyhjennetään juuri luettu lista. Eli Jonin lukulista tyhjenee kun Verna lukee sen.

Kuten huomaat, ohjelmassa on jo hyvin paljon viitteitä. Kannattaa piirtää ohjelman tilaa askeleittain paperille, ja hahmotella miten vernanLista-oliolle tapahtuva metodikutsu lue etenee!

Mikä ihmeen reduce?

Edellisessä esimerkissä käytettiin virtaan liittyvää reduce-metodia. Reduce-metodi on hyödyllinen kun virrassa olevat alkiot halutaan yhdistää jonkinlaiseen toiseen muotoon. Metodin saamat parametrit ovat seuraavaa muotoa: reduce(alkutila, (edellinen, olio) -> mitä oliolla tehdään).

Esimerkiksi kokonaislukuja sisältävän listan summan saa luotua reduce-metodin avulla seuraavasti.

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

int summa = luvut.stream()
    .reduce(0, (edellinenSumma, luku) -> edellinenSumma + luku);
System.out.println(summa);
13

Vastaavasti merkkijonoista koostuvasta listasta saa luotua rivitetyn merkkijonon seuraavasti.

ArrayList<String> sanat = new ArrayList<>();
sanat.add("Eka");
sanat.add("Toka");
sanat.add("Kolmas");
sanat.add("Neljäs");

String yhdistetty = sanat.stream()
    .reduce("", (edellinenMjono, sana) -> edellinenMjono + sana + "\n");
System.out.println(yhdistetty);
Eka
Toka
Kolmas
Neljäs

Talletettavia

Muuton yhteydessa tarvitaan muuttolaatikoita. Laatikoihin talletetaan erilaisia esineitä. Kaikkien laatikoihin talletettavien esineiden on toteutettava seuraava rajapinta:

public interface Talletettava {
    double paino();
}

Lisää rajapinta ohjelmaasi. Rajapinta lisätään melkein samalla tavalla kuin luokka, new Java class sijaan valitaan new Java interface.

Tee rajapinnan toteuttavat luokat Kirja ja CDLevy. Kirja saa konstruktorin parametreina kirjan kirjoittajan (String), kirjan nimen (String), ja kirjan painon (double). CD-Levyn konstruktorin parametreina annetaan artisti (String), levyn nimi (String), ja julkaisuvuosi (int). Kaikkien CD-levyjen paino on 0.1 kg.

Muista toteuttaa luokilla myös rajapinta Talletettava. Luokkien tulee toimia seuraavasti:

public static void main(String[] args) {
    Kirja kirja1 = new Kirja("Fedor Dostojevski", "Rikos ja Rangaistus", 2);
    Kirja kirja2 = new Kirja("Robert Martin", "Clean Code", 1);
    Kirja kirja3 = new Kirja("Kent Beck", "Test Driven Development", 0.5);

    CDLevy cd1 = new CDLevy("Pink Floyd", "Dark Side of the Moon", 1973);
    CDLevy cd2 = new CDLevy("Wigwam", "Nuclear Nightclub", 1975);
    CDLevy cd3 = new CDLevy("Rendezvous Park", "Closer to Being Here", 2012);

    System.out.println(kirja1);
    System.out.println(kirja2);
    System.out.println(kirja3);
    System.out.println(cd1);
    System.out.println(cd2);
    System.out.println(cd3);
}

Tulostus:

Fedor Dostojevski: Rikos ja Rangaistus
Robert Martin: Clean Code
Kent Beck: Test Driven Development
Pink Floyd: Dark Side of the Moon (1973)
Wigwam: Nuclear Nightclub (1975)
Rendezvous Park: Closer to Being Here (2012)

Huom! Painoa ei ilmoiteta tulostuksessa.

Laatikko

Tee luokka laatikko, jonka sisälle voidaan tallettaa Talletettava-rajapinnan toteuttavia tavaroita. Laatikko saa konstruktorissaan parametrina laatikon maksimikapasiteetin kiloina. Laatikkoon ei saa lisätä enempää tavaraa kuin sen maksimikapasiteetti määrää. Laatikon sisältämien tavaroiden paino ei siis koskaan saa olla yli laatikon maksimikapasiteetin.

Seuraavassa esimerkki laatikon käytöstä:

public static void main(String[] args) {
    Laatikko laatikko = new Laatikko(10);

    laatikko.lisaa(new Kirja("Fedor Dostojevski", "Rikos ja Rangaistus", 2)) ;
    laatikko.lisaa(new Kirja("Robert Martin", "Clean Code", 1));
    laatikko.lisaa(new Kirja("Kent Beck", "Test Driven Development", 0.7));

    laatikko.lisaa(new CDLevy("Pink Floyd", "Dark Side of the Moon", 1973));
    laatikko.lisaa(new CDLevy("Wigwam", "Nuclear Nightclub", 1975));
    laatikko.lisaa(new CDLevy("Rendezvous Park", "Closer to Being Here", 2012));

    System.out.println(laatikko);
}

Tulostuu

Laatikko: 6 esinettä, paino yhteensä 4.0 kiloa

Huom: koska painot esitetään doubleina, saattaa laskutoimituksissa tulla pieniä pyöristysvirheitä. Tehtävässä ei tarvitse välittää niistä.

Laatikon paino

Jos teit laatikon sisälle oliomuuttujan double paino, joka muistaa laatikossa olevien esineiden painon, korvaa se metodilla, joka laskee painon:

public class Laatikko {
    //...

    public double paino() {
        double paino = 0;
        // laske laatikkoon talletettujen tavaroiden yhteispaino
        return paino;
    }
}

Kun tarvitset laatikon sisällä painoa esim. uuden tavaran lisäyksen yhteydessä, riittää siis kutsua laatikon painon laskevaa metodia.

Metodi voisi palauttaa myös oliomuuttujan arvon. Harjoittelemme tässä kuitenkin tilannetta, jossa oliomuuttujaa ei tarvitse eksplisiittisesti ylläpitää vaan se voidaan tarpeentullen laskea. Seuraavan tehtävän jälkeen laatikossa olevaan oliomuuttujaan talletettu painotieto ei kuitenkaan välttämättä enää toimisi. Pohdi tehtävän tekemisen jälkeen miksi näin on.

Laatikkokin on talletettava!

Rajapinnan Talletettava toteuttaminen siis edellyttää että luokalla on metodi double paino(). Laatikollehan lisättiin juuri tämä metodi. Laatikosta voidaan siis tehdä talletettava!

Laatikot ovat olioita joihin voidaan laittaa Talletettava-rajapinnan toteuttavia olioita. Laatikot toteuttavat itsekin rajapinnan. Eli laatikon sisällä voi olla myös laatikoita!

Kokeile että näin varmasti on, eli tee ohjelmassasi muutama laatikko, laita laatikoihin tavaroita ja laita pienempiä laatikoita isompien laatikoiden sisään. Kokeile myös mitä tapahtuu kun laitat laatikon itsensä sisälle. Miksi näin käy?

Rajapinta metodin paluuarvona

Kuten mitä tahansa muuttujan tyyppiä, myös rajapintaa voi käyttää metodin paluuarvona. Seuraavassa Tehdas, jota voi pyytää valmistamaan erilaisia Talletettava-rajapinnan toteuttavia oliota. Tehdas valmistaa aluksi satunnaisesti kirjoja ja levyjä.

import java.util.Random;

public class Tehdas {

    public Tehdas() {
        // HUOM: parametritonta tyhjää konstruktoria ei ole pakko kirjoittaa,
        // jos luokalla ei ole muita konstruktoreja
        // Java tekee automaattisesti tälläisissä tilanteissa luokalle oletuskonstruktorin
        // eli parametrittoman tyhjän konstruktorin
    }

    public Talletettava valmistaUusi() {
        // Tässä käytettyä Random-oliota voi käyttää satunnaisten lukujen arpomiseen
        Random arpa = new Random();
        // arpoo luvun väliltä [0, 4[. Luvuksi tulee 0, 1, 2 tai 3.
        int luku = arpa.nextInt(4);

        if (luku == 0) {
            return new CDLevy("Pink Floyd", "Dark Side of the Moon", 1973);
        } else if (luku == 1) {
            return new CDLevy("Wigwam", "Nuclear Nightclub", 1975);
        } else if (luku == 2) {
            return new Kirja("Robert Martin", "Clean Code", 1);
        } else {
            return new Kirja("Kent Beck", "Test Driven Development", 0.7);
        }
    }
}

Tehdasta on mahdollista käyttää tuntematta tarkalleen mitä erityyppisiä Talletettava-rajapinnan luokkia on olemassa. Seuraavassa luokka Pakkaaja, jolta voi pyytää laatikollisen esineitä. Pakkaaja tuntee tehtaan, jota se pyytää luomaan esineet:

public class Pakkaaja {
    private Tehdas tehdas;

    public Pakkaaja() {
        this.tehdas = new Tehdas();
    }

    public Laatikko annaLaatikollinen() {
         Laatikko laatikko = new Laatikko(100);

         for (int i = 0; i < 10; i++) {
             Talletettava uusiTavara = tehdas.valmistaUusi();
             laatikko.lisaa(uusiTavara);
         }

         return laatikko;
    }
}

Koska pakkaaja ei tunne rajapinnan Talletettava toteuttavia luokkia, on ohjelmaan mahdollisuus lisätä uusia luokkia jotka toteuttavat rajapinnan ilman tarvetta muuttaa pakkaajaa. Seuraavassa on luotu uusi Talletettava-rajapinnan toteuttava luokka, Suklaalevy. Tehdasta on muutettu siten, että se luo kirjojen ja cd-levyjen lisäksi suklaalevyjä. Luokka Pakkaaja toimii muuttamatta tehtaan laajennetun version kanssa.

public class Suklaalevy implements Talletettava {
    // koska Javan generoima oletuskonstruktori riittää, emme tarvitse konstruktoria!

    public double paino() {
        return 0.2;
    }
}
import java.util.Random;

public class Tehdas {
    // koska Javan generoima oletuskonstruktori riittää, emme tarvitse konstruktoria!

    public Talletettava valmistaUusi() {

        Random arpa = new Random();
        int luku = arpa.nextInt(5);

        if (luku == 0) {
            return new CDLevy("Pink Floyd", "Dark Side of the Moon", 1973);
        } else if (luku == 1) {
            return new CDLevy("Wigwam", "Nuclear Nightclub", 1975);
        } else if (luku == 2) {
            return new Kirja("Robert Martin", "Clean Code", 1 );
        } else if (luku == 3) {
            return new Kirja("Kent Beck", "Test Driven Development", 0.7);
        } else {
            return new Suklaalevy();
        }
    }
}
Luokkien välisten riippuvuuksien vähentäminen

Rajapintojen käyttö ohjelmoinnissa mahdollistaa luokkien välisten riippuvaisuuksien vähentämisen. Esimerkissämme Pakkaaja ei ole riippuvainen rajapinnan Talletettava-toteuttavista luokista vaan ainoastaan rajapinnasta. Tämä mahdollistaa rajapinnan toteuttavien luokkien lisäämisen ohjelmaan ilman tarvetta muuttaa luokkaa Pakkaaja. Myöskään pakkaaja-luokkaa käyttäviin luokkiin uusien Talletettava-rajapinnan toteuttavien luokkien lisääminen ei vaikuta.

Vähäisemmät riippuvuudet helpottavat ohjelman laajennettavuutta.

Valmiit rajapinnat

Javan API tarjoaa huomattavan määrän valmiita rajapintoja. Tutustutaan tässä neljään usein käytettyyn rajapintaan: List, Map, Set ja Collection.

List-rajapinta

Rajapinta List määrittelee listoihin liittyvän peruskäyttäytymisen. Koska ArrayList-luokka toteuttaa List-rajapinnan, voi sitä käyttää myös List-rajapinnan kautta.

List<String> merkkijonot = new ArrayList<>();
merkkijonot.add("merkkijono-olio arraylist-oliossa!");

Kuten huomaamme List-rajapinnan Java API:sta, rajapinnan List toteuttavia luokkia on useita. Eräs tietojenkäsittelijöille tuttu listarakenne on linkitetty lista (linked list). Linkitettyä listaa voi käyttää rajapinnan List-kautta täysin samoin kuin ArrayLististä luotua oliota.

List<String> merkkijonot = new LinkedList<>();
merkkijonot.add("merkkijono-olio linkedlist-oliossa!");

Molemmat rajapinnan List toteutukset toimivat käyttäjän näkökulmasta samoin. Rajapinta siis abstrahoi niiden sisäisen toiminnallisuuden. ArrayListin ja LinkedListin sisäinen rakenne on kuitenkin huomattavan erilainen. ArrayList tallentaa alkioita taulukkoon, josta tietyllä indeksillä hakeminen on nopeaa. LinkedList taas rakentaa listan, jossa jokaisessa listan alkiossa on viite seuraavan listan alkioon. Kun linkitetyssä listassa haetaan alkiota tietyllä indeksillä, tulee listaa käydä läpi alusta indeksiin asti.

Isoilla listoille voimme nähdä huomattaviakin suorituskykyeroja. Linkitetyn listan vahvuutena on se, että listaan lisääminen on aina nopeaa. ArrayListillä taas taustalla on taulukko, jota täytyy kasvattaa aina kun se täyttyy. Taulukon kasvattaminen vaatii uuden taulukon luonnin ja vanhan taulukon tietojen kopioinnin uuteen taulukkoon. Toisaalta, indeksin perusteella hakeminen on Arraylististä erittäin nopeaa, kun taas linkitetyssä listassa joudutaan käymään listan alkioita yksitellen läpi tiettyyn indeksiin pääsemiseksi.

Tällä ohjelmointikurssilla eteen tulevissa tilanteissa kannattanee käytännössä valita aina ArrayList. "Rajapintoihin ohjelmointi" kuitenkin kannattaa: toteuta ohjelmasi siten, että käytät tietorakenteita rajapintojen kautta.

Toteuta luokkaan ListanTarkistin metodi palautaKoko, joka saa parametrina List-olion ja palauttaa sen koon kokonaislukuna.

Metodin tulee toimia esimerkiksi seuraavasti:

List<String> nimet = new ArrayList<>();
nimet.add("eka");
nimet.add("toka");
nimet.add("kolmas");

System.out.println(new ListanTarkistin().palautaKoko(nimet));
3

Map-rajapinta

Rajapinta Map määrittelee hajautustauluihin liittyvän peruskäyttäytymisen. Koska HashMap-luokka toteuttaa Map-rajapinnan, voi sitä käyttää myös Map-rajapinnan kautta.

Map<String, String> kaannokset = new HashMap<>();
kaannokset.put("gambatte", "tsemppiä");
kaannokset.put("hai", "kyllä");

Hajautustaulun avaimet saa hajautustaulusta keySet-metodin avulla.

Map<String, String> kaannokset = new HashMap<>();
kaannokset.put("gambatte", "tsemppiä");
kaannokset.put("hai", "kyllä");

kaannokset.keySet().stream()
    .forEach(avain -> System.out.println(avain + ": " + kaannokset.get(avain)));
gambatte: tsemppiä
hai: kyllä

Metodi keySet palauttaa Set-rajapinnan toteuttavan joukon alkioita. Set-rajapinnan toteuttavan joukon voi käydä läpi virtana. Hajautustaulusta saa talletetut arvot metodin values-avulla. Metodi values palauttaa Collection rajapinnan toteuttavan joukon alkioita. Tutustutaan vielä pikaisesti Set- ja Collection-rajapintoihin.

Toteuta luokkaan HajautustaulunTarkistin metodi palautaKoko, joka saa parametrina Map-olion ja palauttaa sen koon kokonaislukuna.

Metodin tulee toimia esimerkiksi seuraavasti:

Map<String, String> nimet = new HashMap<>();
nimet.put("eka", "first");
nimet.put("toka", "second");

System.out.println(new HajautustaulunTarkistin().palautaKoko(nimet));
2

Set-rajapinta

Rajapinta Set kuvaa joukkoihin liittyvää toiminnallisuutta. Javassa joukot sisältävät aina joko 0 tai 1 kappaletta tiettyä oliota. Set-rajapinnan toteuttaa muun muassa HashSet. Joukon alkioita pystyy käymään läpi seuraavasti.

Set<String> joukko = new HashSet<>();
joukko.add("yksi");
joukko.add("yksi");
joukko.add("kaksi");

joukko.stream().forEach(alkio -> System.out.println(alkio));
yksi
kaksi

Huomaa että HashSet ei ota millään tavalla kantaa joukon alkioiden järjestykseen.

Toteuta luokkaan JoukonTarkistin metodi palautaKoko, joka saa parametrina Set-olion ja palauttaa sen koon kokonaislukuna.

Metodin tulee toimia esimerkiksi seuraavasti:

Set<String> nimet = new HashSet<>();
nimet.add("eka");
nimet.add("eka");
nimet.add("toka");
nimet.add("toka");
nimet.add("toka");

System.out.println(new JoukonTarkistin().palautaKoko(nimet));

Tulostaa:

2

Collection-rajapinta

Rajapinta Collection kuvaa kokoelmiin liittyvää toiminnallisuutta. Javassa muun muassa listat ja joukot ovat kokoelmia -- rajapinnat List ja Set toteuttavat rajapinnan Collection. Kokoelmarajapinta tarjoaa metodit muun muassa alkioiden olemassaolon tarkistamiseen (metodi contains) ja kokoelman koon tarkistamiseen (metodi size).

Collection-rajapinta määrää myös virtatoteutuksesta. Jokaisella luokalla, joka toteuttaa Collection-rajapinnan joko välillisesti tai suoraan, tulee olla virran luomiseen käytettävä metodi stream.

Luodaan vielä hajautustaulu ja käydään erikseen läpi siihen liittyvät avaimet ja arvot.

Map<String, String> kaannokset = new HashMap<>();
kaannokset.put("gambatte", "tsemppiä");
kaannokset.put("hai", "kyllä");

Set<String> avaimet = kaannokset.keySet();
Collection<String> avainKokoelma = avaimet;

System.out.println("Avaimet:");
avainKokoelma.stream().forEach(avain -> System.out.println(avain));

System.out.println();
System.out.println("Arvot:");
Collection<String> arvot = kaannokset.values();
arvot.stream().forEach(arvo -> System.out.println(arvo));
Avaimet:
gambatte
hai

Arvot:
kyllä
tsemppiä

Seuraavassa tehtävässä rakennetaan verkkokauppaan liittyvää toiminnallisuutta ja harjoitellaan luokkien käyttämistä niiden tarjoamien rajapintojen kautta.

Teemme tehtävässä muutamia verkkokaupan hallinnointiin soveltuvia ohjelmakomponentteja.

Varasto

Tee luokka Varasto jolla on seuraavat metodit:

  • public void lisaaTuote(String tuote, int hinta, int saldo) lisää varastoon tuotteen jonka hinta ja varastosaldo ovat parametrina annetut luvut
  • public int hinta(String tuote) palauttaa parametrina olevan tuotteen hinnan, jos tuotetta ei ole varastossa, palauttaa metodi -99

Varaston sisällä tuotteiden hinnat (ja seuraavassa kohdassa saldot) tulee tallettaa Map<String, Integer>-tyyppiseksi määriteltyyn muuttujaan! Luotava olio voi olla tyypiltään HashMap, muuttujan tyyppinä on käytettävä Map-rajapintaa.

Seuraavassa esimerkki varaston käytöstä:

Varasto varasto = new Varasto();
varasto.lisaaTuote("maito", 3, 10);
varasto.lisaaTuote("kahvi", 5, 7);

System.out.println("hinnat:");
System.out.println("maito: " + varasto.hinta("maito"));
System.out.println("kahvi: " + varasto.hinta("kahvi"));
System.out.println("sokeri: " + varasto.hinta("sokeri"));

Tulostuu:

hinnat:
maito: 3
kahvi: 5
sokeri: -99

Tuotteen varastosaldo

Aseta tuotteiden varastosaldot samaan tapaan Map<String, Integer>-tyyppiseen muuttujaan kuin hinnat. Täydennä varastoa seuraavilla metodeilla:

  • public int saldo(String tuote) palauttaa parametrina olevan tuotteen varastosaldon. Jos tuotetta ei ole varastossa lainkaan, tulee palauttaa 0.
  • public boolean ota(String tuote) vähentää parametrina olevan tuotteen saldoa yhdellä ja palauuttaa true jos tuotetta oli varastossa. Jos tuotetta ei ole varastossa, palauttaa metodi false, tuotteen saldo ei saa laskea alle nollan.

Esimerkki varaston käytöstä:

Varasto varasto = new Varasto();
varasto.lisaaTuote("kahvi", 5, 1);

System.out.println("saldot:");
System.out.println("kahvi:  " + varasto.saldo("kahvi"));
System.out.println("sokeri: " + varasto.saldo("sokeri"));

System.out.println("otetaan kahvi " + varasto.ota("kahvi"));
System.out.println("otetaan kahvi " + varasto.ota("kahvi"));
System.out.println("otetaan sokeri " + varasto.ota("sokeri"));

System.out.println("saldot:");
System.out.println("kahvi:  " + varasto.saldo("kahvi"));
System.out.println("sokeri: " + varasto.saldo("sokeri"));

Tulostuu:

saldot:
kahvi:  1
sokeri: 0
otetaan kahvi true
otetaan kahvi false
otetaan sokeri false
saldot:
kahvi:  0
sokeri: 0

Tuotteiden listaus

Listätään varastolle vielä yksi metodi:

  • public Set<String> tuotteet() palauttaa joukkona varastossa olevien tuotteiden nimet.

Metodi on helppo toteuttaa HashMapin avulla. Saat tietoon varastossa olevat tuotteet kysymällä ne joko hinnat tai saldot muistavalta Map:iltä metodin keySet avulla.

Esimerkki varaston käytöstä:

Varasto varasto = new Varasto();
varasto.lisaaTuote("maito", 3, 10);
varasto.lisaaTuote("kahvi", 5, 6);
varasto.lisaaTuote("piimä", 2, 20);
varasto.lisaaTuote("jugurtti", 2, 20);

System.out.println("tuotteet:");
varasto.tuotteet().stream().forEach(t -> System.out.println(t));
tuotteet:
piimä
jugurtti
kahvi
maito

Ostos

Ostoskoriin lisätään ostoksia. Ostoksella tarkoitetaan tiettyä määrää tiettyjä tuotteita. Koriin voidaan laittaa esim. ostos joka vastaa yhtä leipää tai ostos joka vastaa 24:ää kahvia.

Tee luokka Ostos jolla on seuraavat toiminnot:

  • public Ostos(String tuote, int kpl, int yksikkohinta) konstruktori joka luo ostoksen joka vastaa parametrina annettua tuotetta. Tuotteita ostoksessa on kpl kappaletta ja yhden tuotteen hinta on kolmantena parametrina annettu yksikkohinta
  • public int hinta() palauttaa ostoksen hinnan. Hinta saadaan kertomalla kappalemäärä yksikköhinnalla
  • public void kasvataMaaraa() kasvattaa ostoksen kappalemäärää yhdellä
  • public String toString() palauttaa ostoksen merkkijonomuodossa, joka on alla olevan esimerkin mukainen

Esimerkki ostos-luokan käytöstä:

Ostos ostos = new Ostos("maito", 4, 2);
System.out.println("ostoksen joka sisältää 4 maitoa yhteishinta on " + ostos.hinta());
System.out.println(ostos);
ostos.kasvataMaaraa();
System.out.println(ostos);
ostoksen joka sisältää 4 maitoa yhteishinta on 8
maito: 4
maito: 5

Huom: toString on siis muotoa tuote: kpl hintaa ei merkkijonoesitykseen tule!

Ostoskori

Vihdoin pääsemme toteuttamaan luokan ostoskori!

Ostoskori tallettaa sisäisesti koriin lisätyt tuotteet Ostos-olioina. Ostoskorilla tulee olla oliomuuttuja jonka tyyppi on joko Map<String, Ostos> tai List<Ostos>. Älä laita mitään muita oliomuuttujia ostoskorille kuin ostosten talletukseen tarvittava Map tai List.

Huom: jos talletat Ostos-oliot Map-tyyppiseen apumuuttujaan, on tässä ja seuraavassa tehtävässä hyötyä Map:in metodista values(), jonka avulla on helppo käydä läpi kaikki talletetut ostos-oliot.

Tehdään aluksi ostoskorille parametriton konstruktori ja metodit:

  • public void lisaa(String tuote, int hinta) lisää ostoskoriin ostoksen joka vastaa parametrina olevaa tuotetta ja jolla on parametrina annettu hinta.
  • public int hinta() palauttaa ostoskorin kokonaishinnan

Esimerkki ostoskorin käytöstä:

Ostoskori kori = new Ostoskori();
kori.lisaa("maito", 3);
kori.lisaa("piimä", 2);
kori.lisaa("juusto", 5);
System.out.println("korin hinta: " + kori.hinta());
kori.lisaa("tietokone", 899);
System.out.println("korin hinta: " + kori.hinta());
korin hinta: 10
korin hinta: 909

Ostoskorin tulostus

Tehdään ostoskorille metodi public void tulosta() joka tulostaa korin sisältämät Ostos-oliot. Tulostusjärjestyksessä ei ole merkitystä. Edellisen esimerkin ostoskori tulostetuna olisi:

piimä: 1
juusto: 1
tietokone: 1
maito: 1

Huomaa, että tulostuva numero on siis tuotteen korissa oleva kappalemäärä, ei hinta!

Yksi ostos tuotetta kohti

Täydennetään Ostoskoria siten, että jos korissa on jo tuote joka sinne lisätään, ei koriin luoda uutta Ostos-olioa vaan päivitetään jo korissa olevaa tuotetta vastaavaa ostosolioa kutsumalla sen metodia kasvataMaaraa().

Esimerkki:

Ostoskori kori = new Ostoskori();
kori.lisaa("maito", 3);
kori.tulosta();
System.out.println("korin hinta: " + kori.hinta() + "\n");

kori.lisaa("piimä", 2);
kori.tulosta();
System.out.println("korin hinta: " + kori.hinta() + "\n");

kori.lisaa("maito", 3);
kori.tulosta();
System.out.println("korin hinta: " + kori.hinta() + "\n");

kori.lisaa("maito", 3);
kori.tulosta();
System.out.println("korin hinta: " + kori.hinta() + "\n");
maito: 1
korin hinta: 3

piimä: 1
maito: 1
korin hinta: 5

piimä: 1
maito: 2
korin hinta: 8

piimä: 1
maito: 3
korin hinta: 11

Eli ensin koriin lisätään maito ja piimä ja niille omat ostos-oliot. Kun koriin lisätään lisää maitoa, ei luoda uusille maidoille omaa ostosolioa, vaan päivitetään jo korissa olevan maitoa kuvaavan ostosolion kappalemäärää.

Kauppa

Nyt meillä on valmiina kaikki osat "verkkokauppaa" varten. Verkkokaupassa on varasto joka sisältää kaikki tuotteet. Jokaista asiakkaan asiointia varten on oma ostoskori. Aina kun asiakas valitsee ostoksen, lisätään se asiakkaan ostoskoriin jos tuotetta on varastossa. Samalla varastosaldoa pienennetään yhdellä.

Seuraavassa on valmiina verkkokaupan tekstikäyttöliittymän runko. Tee projektiin luokka Kauppa ja kopioi alla oleva koodi luokkaan.

import java.util.Scanner;

public class Kauppa {

    private Varasto varasto;
    private Scanner lukija;

    public Kauppa(Varasto varasto, Scanner lukija) {
        this.varasto = varasto;
        this.lukija = lukija;
    }

    // metodi jolla hoidetaan yhden asiakkaan asiointi kaupassa
    public void asioi(String asiakas) {
        Ostoskori kori = new Ostoskori();
        System.out.println("Tervetuloa kauppaan " + asiakas);
        System.out.println("valikoimamme:");

        varasto.tuotteet().stream().forEach(t -> System.out.println(t));

        while (true) {
            System.out.print("mitä laitetaan ostoskoriin (pelkkä enter vie kassalle):");
            String tuote = lukija.nextLine();
            if (tuote.isEmpty()) {
                break;
            }

            // tee tänne koodi joka lisää tuotteen ostoskoriin jos sitä on varastossa
            // ja vähentää varastosaldoa
            // älä koske muuhun koodiin!

        }

        System.out.println("ostoskorissasi on:");
        kori.tulosta();
        System.out.println("korin hinta: " + kori.hinta());
    }
}

Seuraavassa pääohjelma joka täyttää kaupan varaston ja laittaa Pekan asioimaan kaupassa:

Varasto varasto = new Varasto();
varasto.lisaaTuote("kahvi", 5, 10);
varasto.lisaaTuote("maito", 3, 20);
varasto.lisaaTuote("piimä", 2, 55);
varasto.lisaaTuote("leipä", 7, 8);

Kauppa kauppa = new Kauppa(varasto, new Scanner(System.in));
kauppa.asioi("Pekka");

Kauppa on melkein valmiina. Yhden asiakkaan asioinnin hoitavan metodin public void asioi(String asiakas) on kommenteilla merkitty kohta jonka joudut täydentämään. Lisää kohtaan koodi joka tarkastaa onko asiakkaan haluamaa tuotetta varastossa. Jos on, vähennä tuotteen varastosaldoa ja lisää tuote ostoskoriin.

Todellisuudessa verkkokauppa toteutettaisiin hieman eri tavalla. Verkkosovelluksia tehtäessä käyttöliittymä toteutetaan HTML-sivuna, ja sivuilla tapahtuvat klikkaukset ohjataan palvelinohjelmistolle. Teemaan liittyen löytyy useampia kursseja Helsingin yliopistolta.

Maatiloilla on lypsäviä eläimiä, jotka tuottavat maitoa. Maatilat eivät itse käsittele maitoa, vaan se kuljetetaan Maitoautoilla meijereille. Meijerit ovat yleisiä maitotuotteita tuottavia rakennuksia. Jokainen meijeri erikoistuu yhteen tuotetyyppiin, esimerkiksi Juustomeijeri tuottaa Juustoa, Voimeijeri tuottaa voita ja Maitomeijeri tuottaa maitoa.

Rakennetaan maidon elämää kuvaava simulaattori.

Maitosäiliö

Jotta maito pysyisi tuoreena, täytyy se säilöä sille tarkoitettuun säiliöön. Säiliöitä valmistetaan sekä oletustilavuudella 2000 litraa, että asiakkaalle räätälöidyllä tilavuudella. Toteuta luokka Maitosailio jolla on seuraavat konstruktorit ja metodit.

  • public Maitosailio()
  • public Maitosailio(double tilavuus)
  • public double getTilavuus()
  • public double getSaldo()
  • public double paljonkoTilaaJaljella()
  • public void lisaaSailioon(double maara) lisää säiliöön vain niin paljon maitoa kuin sinne mahtuu, ylimääräiset jäävät lisäämättä, maitosäiliön ei siis tarvitse huolehtia tilanteesta jossa maitoa valuu yli
  • public double otaSailiosta(double maara) ottaa säiliöstä pyydetyn määrän, tai niin paljon kuin siellä on jäljellä

Huomaa, että teet kaksi konstruktoria. Kutsuttava konstruktori määräytyy sille annettujen parametrien perusteella. Jos kutsut new Maitosailio(), suoritetaan ensimmäisen konstruktorin lähdekoodi. Toista konstruktoria taas kutsutaan antamalla konstruktorille parametrina tilavuus, esim. new Maitosailio(300.0).

Toteuta Maitosailio-luokalle myös toString()-metodi, jolla kuvaat sen tilaa. Ilmaistessasi säiliön tilaa toString()-metodissa, pyöristä litramäärät ylöspäin käyttäen Math-luokan tarjoamaa ceil()-metodia.

Testaa maitosailiötä seuraavalla ohjelmapätkällä:

Maitosailio sailio = new Maitosailio();
sailio.otaSailiosta(100);
sailio.lisaaSailioon(25);
sailio.otaSailiosta(5);
System.out.println(sailio);

sailio = new Maitosailio(50);
sailio.lisaaSailioon(100);
System.out.println(sailio);
20.0/2000.0
50.0/50.0

Lehmä

Saadaksemme maitoa tarvitsemme myös lehmiä. Lehmällä on nimi ja utareet. Utareiden tilavuus on satunnainen luku väliltä 15 ja 40, luokkaa Random voi käyttäää satunnaislukujen arpomiseen, esimerkiksi int luku = 15 + new Random().nextInt(26);. Luokalla Lehma on seuraavat toiminnot:

  • public Lehma() luo uuden lehmän satunnaisesti valitulla nimellä
  • public Lehma(String nimi) luo uuden lehmän annetulla nimellä
  • public String getNimi() palauttaa lehmän nimen
  • public double getTilavuus() palauttaa utareiden tilavuuden
  • public double getMaara() palauttaa utareissa olevan maidon määrän
  • public String toString() palauttaa lehmää kuvaavan merkkijonon (ks. esimerkki alla)

Lehma toteuttaa myös rajapinnat: Lypsava, joka kuvaa lypsämiskäyttäytymistä, ja Eleleva, joka kuvaa elelemiskäyttäytymistä.

public interface Lypsava {
    public double lypsa();
}

public interface Eleleva {
    public void eleleTunti();
}

Lehmää lypsettäessä sen koko maitovarasto tyhjennetään jatkokäsittelyä varten. Lehmän elellessä sen maitovarasto täyttyy hiljalleen. Suomessa maidontuotannossa käytetyt lehmät tuottavat keskimäärin noin 25-30 litraa maitoa päivässä. Simuloidaan tätä tuotantoa tuottamalla noin 0.7 - 2 litraa tunnissa.

Simuloi tuotantoa tuottamalla noin 0.7 - 2 litraa tunnissa. Random-luokan metodista nextDouble, joka palauttaa satunnaisluvun 0 ja 1 välillä lienee tässä hyötyä.

Lisäksi, jos lehmälle ei anneta nimeä, valitse sille nimi satunnaisesti seuraavasta taulukosta. Tässä on hyötyä Random-luokan metodista nextInt, jolle annetaan parametrina yläraja. Kannattaa tutustua Random-luokan toimintaan erikseen ennen kuin lisää sen osaksi tätä ohjelmaa.

private static final String[] NIMIA = new String[]{
    "Anu", "Arpa", "Essi", "Heluna", "Hely",
    "Hento", "Hilke", "Hilsu", "Hymy", "Matti", "Ilme", "Ilo",
    "Jaana", "Jami", "Jatta", "Laku", "Liekki",
    "Mainikki", "Mella", "Mimmi", "Naatti",
    "Nina", "Nyytti", "Papu", "Pullukka", "Pulu",
    "Rima", "Soma", "Sylkki", "Valpu", "Virpi"};

Toteuta luokka Lehma ja testaa sen toimintaa seuraavan ohjelmapätkän avulla.

Lehma lehma = new Lehma();
System.out.println(lehma);


Eleleva elelevaLehma = lehma;
elelevaLehma.eleleTunti();
elelevaLehma.eleleTunti();
elelevaLehma.eleleTunti();
elelevaLehma.eleleTunti();

System.out.println(lehma);

Lypsava lypsavaLehma = lehma;
lypsavaLehma.lypsa();

System.out.println(lehma);
System.out.println("");

lehma = new Lehma("Ammu");
System.out.println(lehma);
lehma.eleleTunti();
lehma.eleleTunti();
System.out.println(lehma);
lehma.lypsa();
System.out.println(lehma);

Ohjelman tulostus on erimerkiksi seuraavanlainen.

Liekki 0.0/23.0
Liekki 7.0/23.0
Liekki 0.0/23.0
Ammu 0.0/35.0
Ammu 9.0/35.0
Ammu 0.0/35.0

Lypsyrobotti

Nykyaikaisilla maatiloilla lypsyrobotit hoitavat lypsämisen. Jotta lypsyrobotti voi lypsää lypsävää otusta, tulee lypsyrobotin olla kiinnitetty maitosäiliöön:

  • public Lypsyrobotti() luo uuden lypsyrobotin
  • public Maitosailio getMaitosailio() palauttaa kiinnitetyn maitosäiliö tai null-viitteen, jos säiliötä ei ole vielä kiinnitetty
  • public void setMaitosailio(Maitosailio maitosailio) kiinnittää annetun säiliön lypsyrobottiin
  • public void lypsa(Lypsava lypsava) lypsää lehmän robottiin kiinnitettyyn maitosäiliöön. Jos robottiin ei ole kiinnitetty maitosäiliötä, ohjelma ilmoittaa että maito menee hukkaan.

Toteuta luokka Lypsyrobotti ja testaa sitä seuraavien ohjelmanpätkien avulla. Varmista että lypsyrobotti voi lypsää kaikkia Lypsava-rajapinnan toteuttavia olioita!

Lypsyrobotti lypsyrobotti = new Lypsyrobotti();
Lehma lehma = new Lehma();
lypsyrobotti.lypsa(lehma);
Maidot menevät hukkaan!
Lypsyrobotti lypsyrobotti = new Lypsyrobotti();
Lehma lehma = new Lehma();
System.out.println("");

Maitosailio sailio = new Maitosailio();
lypsyrobotti.setMaitosailio(sailio);
System.out.println("Säiliö: " + sailio);

for (int i = 0; i < 2; i++) {
    System.out.println(lehma);
    System.out.println("Elellään..");
    for (int j = 0; j < 5; j++) {
        lehma.eleleTunti();
    }
    System.out.println(lehma);

    System.out.println("Lypsetään...");
    lypsyrobotti.lypsa(lehma);
    System.out.println("Säiliö: " + sailio);
    System.out.println("");
}

Ohjelman tulostus on esimerkiksi seuraavanlainen.

Säiliö: 0.0/2000.0
Mella 0.0/23.0
Elellään..
Mella 6.2/23.0
Lypsetään...
Säiliö: 6.2/2000.0

Mella 0.0/23.0
Elellään..
Mella 7.8/23.0
Lypsetään...
Säiliö: 14.0/2000.0

Navetta

Lehmät hoidetaan (eli tässä tapauksessa lypsetään) navetassa. Alkukantaisissa navetoissa on maitosäiliö ja tilaa yhdelle lypsyrobotille. Huomaa että lypsyrobottia asennettaessa se kytketään juuri kyseisen navetan maitosäiliöön. Jos navetassa ei ole lypsyrobottia, ei siellä voida myöskään hoitaa lehmiä. Toteuta luokka Navetta jolla on seuraavat konstruktorit ja metodit:

  • public Navetta(Maitosailio maitosailio)
  • public Maitosailio getMaitosailio() palauttaa navetan maitosailion
  • public void asennaLypsyrobotti(Lypsyrobotti lypsyrobotti) asentaa lypsyrobotin ja kiinnittää sen navetan maitosäiliöön
  • public void hoida(Lehma lehma) lypsää parametrina annetun lehmän lypsyrobotin avulla, metodi heittää poikkeuksen IllegalStateException, jos lypsyrobottia ei ole asennettu
  • public void hoida(Collection<Lehma> lehmat) lypsää parametrina annetut lehmät lypsyrobotin avulla, metodi heittää poikkeuksen IllegalStateException, jos lypsyrobottia ei ole asennettu
  • public String toString() palauttaa navetan sisältämän maitosäiliön tilan

Testaa luokkaa Navetta seuraavan ohjelmapätkän avulla.

Navetta navetta = new Navetta(new Maitosailio());
System.out.println("Navetta: " + navetta);

Lypsyrobotti robo = new Lypsyrobotti();
navetta.asennaLypsyrobotti(robo);

Lehma ammu = new Lehma();
ammu.eleleTunti();
ammu.eleleTunti();

navetta.hoida(ammu);
System.out.println("Navetta: " + navetta);

List<Lehma> lehmaLista = new ArrayList<>();
lehmaLista.add(ammu);
lehmaLista.add(new Lehma());

lehmaLista.stream().forEach(lehma -> {
    lehma.eleleTunti();
    lehma.eleleTunti();
});

navetta.hoida(lehmaLista);
System.out.println("Navetta: " + navetta);

Tulostuksen tulee olla esimerkiksi seuraavanlainen:

Navetta: 0.0/2000.0
Navetta: 2.8/2000.0
Navetta: 9.6/2000.0

Maatila

Maatilalla on omistaja ja siihen kuuluu navetta sekä joukko lehmiä. Maatila toteuttaa myös aiemmin nähdyn rajapinnan Eleleva, jonka metodia eleleTunti()-kutsumalla kaikki maatilaan liittyvät lehmät elelevät tunnin. Toteuta luokka maatila siten, että se toimii seuraavien esimerkkiohjelmien mukaisesti.

Maitosailio sailio = new Maitosailio();
Navetta navetta = new Navetta(sailio);

Maatila maatila = new Maatila("Esko", navetta);
System.out.println(maatila);

System.out.println(maatila.getOmistaja() + " on ahkera mies!");

Odotettu tulostus:

Maatilan omistaja: Esko
Navetan maitosäiliö: 0.0/2000.0
Ei lehmiä.
Esko on ahkera mies!
Maatila maatila = new Maatila("Esko", new Navetta(new Maitosailio()));
maatila.lisaaLehma(new Lehma());
maatila.lisaaLehma(new Lehma());
maatila.lisaaLehma(new Lehma());
System.out.println(maatila);

Odotettu tulostus:

Maatilan omistaja: Esko
Navetan maitosäiliö: 0.0/2000.0
Lehmät:
    Naatti 0.0/19.0
    Hilke 0.0/30.0
    Sylkki 0.0/29.0
Maatila maatila = new Maatila("Esko", new Navetta(new Maitosailio()));

maatila.lisaaLehma(new Lehma());
maatila.lisaaLehma(new Lehma());
maatila.lisaaLehma(new Lehma());

maatila.eleleTunti();
maatila.eleleTunti();

System.out.println(maatila);

Odotettu tulostus:

Maatilan omistaja: Esko
Navetan maitosäiliö: 0.0/2000.0
Lehmät:
    Heluna 2.0/17.0
    Rima 3.0/32.0
    Ilo 3.0/25.0
Maatila maatila = new Maatila("Esko", new Navetta(new Maitosailio()));
Lypsyrobotti robo = new Lypsyrobotti();
maatila.asennaNavettaanLypsyrobotti(robo);

maatila.lisaaLehma(new Lehma());
maatila.lisaaLehma(new Lehma());
maatila.lisaaLehma(new Lehma());

maatila.eleleTunti();
maatila.eleleTunti();

maatila.hoidaLehmat();

System.out.println(maatila);

Odotettu tulostus:

Maatilan omistaja: Esko
Navetan maitosäiliö: 18.0/2000.0
Lehmät:
    Hilke 0.0/30.0
    Sylkki 0.0/35.0
    Hento 0.0/34.0

Edellä otettiin ensiaskeleet simulaattorin tekemiseen. Ohjelmaa voisi jatkaa vaikkapa lisäämällä maitoauton sekä luomalla useampia navettoja. Maitoautot voisivat kulkea tehtaalle, jossa tehtäisiin juustoa, jnejne..

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

Luo projektipohjaan 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!
    }
}

Tässä tehtävässä pääset suunnittelemaan vapaasti ohjelman rakenteen. Käyttöliittymän ulkomuoto sekä vaaditut komennot on määritelty ennalta. Tehtävä on kolmen 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.

Sisällysluettelo