Tämä ei ole uusin versio kurssista. Katso myös Ohjelmoinnin MOOC 2019: https://ohjelmointi-19.mooc.fi.
Tehtävät
Kymmenennen osan tavoitteet

Osaa luoda luokkia, jotka periytyvät toisesta luokasta. Ymmärtää perinnän hyödyt ja osaa toisaalta esittää tapauksia, joihin perintä ei sovi. Tietää miten abstraktit luokat toimivat. Tietää miten perintä merkitään luokkakaavioon. Osaa käsitellä tietokokoelmia arvojen virtana ja osaa käyttää lambda-lauseketta Javassa.

Luokan periminen

Luokkia käytetään olio-ohjelmoinnissa ongelma-alueeseen liittyvien käsitteiden selkeyttämiseen. Jokainen luomamme luokka lisää ohjelmointikieleen toiminnallisuutta. Tätä toiminnallisuutta tarvitaan kohtaamiemme ongelmien ratkomiseen, ratkaisut syntyvät luokista luotujen olioiden välisen interaktion avulla. Olio-ohjelmoinnissa olio on itsenäinen kokonaisuus, jolla on olion tarjoamien metodien avulla muutettava tila. Olioita käytetään yhteistyössä; jokaisella oliolla on oma vastuualue. Esimerkiksi käyttöliittymäluokkamme ovat tähän mennessä hyödyntäneet Scanner-olioita.

Jokainen Javan luokka perii luokan Object, eli jokainen luomamme luokka saa käyttöönsä kaikki Object-luokassa määritellyt metodit. Jos haluamme muuttaa Object-luokassa määriteltyjen metodien toiminnallisuutta tulee ne korvata (Override) määrittelemällä niille uusi toteutus luodussa luokassa.

Luokan Object perimisen lisäksi myös muiden luokkien periminen on mahdollista. Javan ArrayList-luokan APIa tarkasteltaessa huomaamme että ArrayList perii luokan AbstractList. Luokka AbstractList perii luokan AbstractCollection, joka perii luokan Object.

  java.lang.Object
  java.util.AbstractCollection<E>
    java.util.AbstractList<E>
      java.util.ArrayList<E>

Kukin luokka voi periä suoranaisesti yhden luokan. Välillisesti luokka kuitenkin perii kaikki perimänsä luokan ominaisuudet. Luokka ArrayList perii luokan AbstractList, ja välillisesti luokat AbstractCollection ja Object. Luokalla ArrayList on siis käytössään luokkien AbstractList, AbstractCollection ja Object muuttujat, metodit ja rajapinnat.

Luokan ominaisuudet peritään avainsanalla extends. Luokan perivää luokkaa kutsutaan aliluokaksi (subclass), perittävää luokkaa yliluokaksi (superclass).

Tutustutaan erään autonvalmistajan järjestelmään, joka hallinnoi auton osia. Osien hallinan peruskomponentti on luokka Osa, joka määrittelee tunnuksen, valmistajan ja kuvauksen.

public class Osa {

    private String tunnus;
    private String valmistaja;
    private String kuvaus;

    public Osa(String tunnus, String valmistaja, String kuvaus) {
        this.tunnus = tunnus;
        this.valmistaja = valmistaja;
        this.kuvaus = kuvaus;
    }

    public String getTunnus() {
        return tunnus;
    }

    public String getKuvaus() {
        return kuvaus;
    }

    public String getValmistaja() {
        return valmistaja;
    }
}

Yksi osa autoa on moottori. Kuten kaikilla osilla, myös moottorilla on valmistaja, tunnus ja kuvaus. Näiden lisäksi moottoriin liittyy moottorityyppi: esimerkiksi polttomoottori, sähkömoottori tai hybridi.

Perinteinen, ei perintää hyödyntävä tapa olisi toteuttaa luokka Moottori seuraavasti.

public class Moottori {

    private String moottorityyppi;
    private String tunnus;
    private String valmistaja;
    private String kuvaus;

    public Moottori(String moottorityyppi, String tunnus, String valmistaja, String kuvaus) {
        this.moottorityyppi = moottorityyppi;
        this.tunnus = tunnus;
        this.valmistaja = valmistaja;
        this.kuvaus = kuvaus;
    }
  
    public String getMoottorityyppi() {
        return moottorityyppi;
    }
  
    public String getTunnus() {
        return tunnus;
    }

    public String getKuvaus() {
        return kuvaus;
    }

    public String getValmistaja() {
        return valmistaja;
    }
}

Huomaamme luokassa Moottori merkittävän määrän yhtäläisyyksiä luokan Osa kanssa. Voidaankin sanoa, että Moottori on luokan Osa erikoistapaus. Moottori on Osa, mutta sillä on myös joitakin omia ominaisuuksia eli tässä moottorin tyyppi.

Tehdään sama luokka Moottori, ja toteutetaan luokka perintää hyödyntämällä. Luodaan luokan Osa perivä luokka Moottori: moottori on osan erikoistapaus.

public class Moottori extends Osa {

    private String moottorityyppi;

    public Moottori(String moottorityyppi, String tunnus, String valmistaja, String kuvaus) {
        super(tunnus, valmistaja, kuvaus);
        this.moottorityyppi = moottorityyppi;
    }

    public String getMoottorityyppi() {
        return moottorityyppi;
    }
}

Luokkamäärittely public class Moottori extends Osa kertoo että luokka Moottori perii luokan Osa toiminnallisuuden. Luokassa Moottori määritellään oliomuuttuja moottorityyppi.

Moottori-luokan konstruktori on mielenkiintoinen. Konstruktorin ensimmäisellä rivillä on avainsana super, jolla kutsutaan yliluokan konstruktoria. Kutsu super(tunnus, valmistaja, kuvaus) kutsuu luokassa Osa määriteltyä konstruktoria public Osa(String tunnus, String valmistaja, String kuvaus, jolloin yliluokassa määritellyt oliomuuttujat saavat arvonsa. Tämän jälkeen oliomuuttujalle moottorityyppi asetetaan siihen liittyvä arvo.

Mikäli konstruktorissa käytetään yliluokan konstruktoria, eli konstruktorissa on super-kutsu, tulee super-kutsun olla konstruktorin ensimmäisellä rivillä.

Kun luokka Moottori perii luokan Osa, saa se käyttöönsä kaikki luokan Osa tarjoamat metodit. Luokasta Moottori voi tehdä ilmentymän aivan kuten mistä tahansa muustakin luokasta.

Moottori moottori = new Moottori("polttomoottori", "hz", "volkswagen", "VW GOLF 1L 86-91");
System.out.println(moottori.getMoottorityyppi());
System.out.println(moottori.getValmistaja());
polttomoottori
volkswagen

Kuten huomaat, luokalla Moottori on käytössä luokassa Osa määritellyt metodit.

Näkyvyysmääreet private, protected ja public

Jos metodilla tai muuttujalla on näkyvyysmääre private, ei se näy aliluokille eikä aliluokalla ole mitään suoraa tapaa päästä käsiksi siihen. Moottori-luokasta ei siis pääse suoraan käsiksi yliluokassa Osa määriteltyihin muuttujiin tunnus, valmistaja, kuvaus. Tällä tarkoitetaan sitä, että Moottori-luokassa ohjelmoija ei voi suoraan käsitellä niitä yliluokan muuttujia, joilla on näkyvyysmääre private.

Aliluokka näkee kaiken yliluokan julkisen eli public-määreellä varustetun kaluston. Jos halutaan määritellä yliluokkaan joitain muuttujia tai metodeja joiden näkeminen halutaan sallia aliluokille, mutta estää muilta voidaan käyttää näkyvyysmäärettä protected.

Yliluokan konstruktorin ja metodien kutsuminen

Yliluokan konstruktoria kutsutaan avainsanalla super. Kutsulle annetaan parametrina yliluokan konstruktorin vaatiman tyyppiset arvot.

Konstruktoria kutsuttaessa yliluokassa määritellyt muuttujat alustetaan. Konstruktorikutsussa tapahtuu käytännössä täysin samat asiat kuin normaalissa konstruktorikutsussa. Jos yliluokassa ei ole määritelty parametritonta konstruktoria, tulee aliluokan konstruktorikutsuissa olla aina mukana yliluokan konstruktorikutsu.

Huom! Kutsun super tulee olla aina konstruktorin ensimmäisellä rivillä!

Yliluokan metodin kutsuminen

Yliluokassa määriteltyjä metodeja voi kutsua super-etuliitteen avulla, aivan kuten tässä luokassa määriteltyjä metodeja voi kutsua this-etuliitteellä. Esimerkiksi yliluokassa määriteltyä toString-metodia voi hyödyntää sen korvaavassa metodissa seuraavasti:

@Override
public String toString() {
    return super.toString() + "\n  Ja oma viestini vielä!";
}

Henkilo

Tee pakkaus henkilot ja sinne luokka Henkilo. Luokan tulee toimia seuraavan esimerkin mukaisesti.

Henkilo ada = new Henkilo("Ada Lovelace", "Korsontie 1 03100 Vantaa");
Henkilo esko = new Henkilo("Esko Ukkonen", "Mannerheimintie 15 00100 Helsinki");
System.out.println(ada);
System.out.println(esko);
Ada Lovelace
  Korsontie 1 03100 Vantaa
Esko Ukkonen
  Mannerheimintie 15 00100 Helsinki

Opiskelija

Tee pakkaukseen henkilot luokka Opiskelija joka perii luokan Henkilo.

Opiskelijalla on aluksi 0 opintopistettä. Aina kun opiskelija opiskelee, opintopistemäärä kasvaa. Luokan tulee toimia seuraavan esimerkin mukaisesti.

Opiskelija olli = new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki");
System.out.println(olli);
System.out.println("opintopisteitä " + olli.opintopisteita());
olli.opiskele();
System.out.println("opintopisteitä "+ olli.opintopisteita());
Olli
  Ida Albergintie 1 00400 Helsinki
opintopisteitä 0
opintopisteitä 1

Opiskelijalle toString

Edellisessä tehtävässä Opiskelija perii toString-metodin luokalta Henkilo. Perityn metodin voi myös ylikirjoittaa, eli korvata omalla versiolla. Tee luokalle Opiskelija oma versio toString-metodista. Metodin tulee toimia seuraavan esimerkin mukaisesti.

Opiskelija olli = new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki");
System.out.println(olli);
olli.opiskele();
System.out.println(olli);
Olli
  Ida Albergintie 1 00400 Helsinki
  opintopisteitä 0
Olli
  Ida Albergintie 1 00400 Helsinki
  opintopisteitä 1

Opettaja

Tee luokan Henkilo perivä luokka Opettaja. Opettajalla on palkka joka tulostuu opettajan merkkijonoesityksessä.

Luokan tulee toimia seuraavan esimerkin mukaisesti.

Opettaja ada = new Opettaja("Ada Lovelace", "Korsontie 1 03100 Vantaa", 1200);
Opettaja esko = new Opettaja("Esko Ukkonen", "Mannerheimintie 15 00100 Helsinki", 5400);
System.out.println(ada);
System.out.println(esko);

Opiskelija olli = new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki");
for (int i = 0; i < 25; i++) {
    olli.opiskele();
}
System.out.println(olli);
Ada Lovelace
  Korsontie 1 03100 Vantaa
  palkka 1200 euroa/kk
Esko Ukkonen
  Mannerheimintie 15 00100 Helsinki
  palkka 5400 euroa/kk
Olli
  Ida Albergintie 1 00400 Helsinki
  opintopisteitä 25

Kaikki Henkilot listalle

Toteuta luokkaan HenkiloTulostus metodi public void tulostaLaitoksenHenkilot(List<Henkilo> henkilot), joka tulostaa kaikki metodille parametrina annetussa listassa olevat henkilöt. Metodin tulee toimia seuraavasti main-metodista kutsuttaessa.

public static void main(String[] args) {
    List<Henkilo> henkilot = new ArrayList<Henkilo>();
    henkilot.add(new Opettaja("Ada Lovelace", "Korsontie 1 03100 Vantaa", 1200));
    henkilot.add(new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki"));

    new HenkiloTulostus().tulostaLaitoksenHenkilot(henkilot);
}
Ada Lovelace
  Korsontie 1 03100 Vantaa
  palkka 1200 euroa/kk
Olli
  Ida Albergintie 1 00400 Helsinki
  opintopisteitä 0

Todellinen tyyppi määrää suoritettavan metodin

Olion kutsuttavissa olevat metodit määrittyvät muuttujan tyypin kautta. Esimerkiksi jos edellä toteutetun Opiskelija-tyyppisen olion viite on talletettu Henkilo-tyyppiseen muuttujaan, on oliosta käytössä vain Henkilo-luokassa määritellyt metodit (sekä Henkilo-luokan yliluokan ja rajapintojen metodit):

Henkilo olli = new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki");
olli.opintopisteita();        // EI TOIMI!
olli.opiskele();              // EI TOIMI!
String.out.println(olli);   // olli.toString() TOIMII

Oliolla on siis käytössä jokainen sen tyyppiin sekä sen yliluokkiin ja rajapintoihin liittyvä metodi. Esimerkiksi Opiskelija-tyyppisellä oliolla on käytössä Henkilo-luokassa määritellyt metodit sekä Object-luokassa määritellyt metodit.

Edellisessä tehtävässä korvasimme Opiskelijan luokalta Henkilö perimän toString uudella versiolla. Myös luokka Henkilö oli jo korvannut Object-luokalta perimänsä toStringin. Jos käsittelemme olioa jonkun muun kuin sen todellisen tyypin kautta, mitä versiota olion metodista kutsutaan?

Seuraavassa esimerkissä kahta opiskelijaa käsitellään erityyppisten muuttujien kautta. Mikä versio metodista toString suoritetaan, luokassa Object, Henkilo vai Opiskelija määritelty?

Opiskelija olli = new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki");
String.out.println(olli);
Henkilo olliHenkilo = new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki")
System.out.println(olliHenkilo);
Object olliObject = new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki")
System.out.println(olliObject);

Object liisa = new Opiskelija("Liisa", "Väinö Auerin katu 20 00500 Helsinki");
String.out.println(liisa);
Olli
  Ida Albergintie 1 00400 Helsinki
  opintopisteitä 0
Olli
  Ida Albergintie 1 00400 Helsinki
  opintopisteitä 0
Olli
  Ida Albergintie 1 00400 Helsinki
  opintopisteitä 0
Liisa
  Väinö Auerin katu 20 00500 Helsinki
  opintopisteitä 0

Suoritettava metodi valitaan olion todellisen tyypin perusteella, eli sen luokan perusteella, jonka konstruktoria kutsutaan kun olio luodaan. Jos kutsuttua metodia ei ole määritelty luokassa, suoritetaan perintähierarkiassa olion todellista tyyppiä lähinnä oleva metodin toteutus.

Polymorfismi

Suoritettava metodi valitaan aina olion todellisen tyypin perusteella riippumatta käytetyn muuttujan tyypistä. Oliot ovat monimuotoisia, eli olioita voi käyttää usean eri muuttujatyypin kautta. Suoritettava metodi liittyy aina olion todelliseen tyyppiin. Tätä monimuotoisuutta kutsutaan polymorfismiksi.

Tarkastellaan Polymorfismia toisen esimerkin avulla.

Kaksiulotteisessa koordinaatiostossa sijaitsevaa pistettä voisi kuvata seuraavan luokan avulla:

public class Piste {

    private int x;
    private int y;

    public Piste(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int manhattanEtaisyysOrigosta() {
        return Math.abs(x) + Math.abs(y);
    }

    protected String sijainti(){
        return x + ", " + y;
    }

    @Override
    public String toString() {
        return "(" + this.sijainti() + ") etäisyys " + this.manhattanEtaisyysOrigosta();
    }
}

Metodi sijainti ei ole tarkoitettu ulkoiseen käyttöön, joten se on näkyvyysmääreeltään protected, eli aliluokat pääsevät siihen käsiksi. Esimerkiksi reitinhakualgoritmien hyödyntämällä Manhattan-etäisyydellä tarkoitetaan pisteiden etäisyyttä, jos niiden välin voi kulkea ainoastaan koordinaattiakselien suuntaisesti.

Värillinen piste on muuten samanlainen kuin piste, mutta se sisältää merkkijonona ilmaistavan värin. Luokka voidaan siis tehdä perimällä Piste.

public class VariPiste extends Piste {

    private String vari;

    public VariPiste(int x, int y, String vari) {
        super(x, y);
        this.vari = vari;
    }

    @Override
    public String toString() {
        return super.toString() + " väri: " + vari;
    }
}

Luokka määrittelee oliomuuttujan värin talletusta varten. Koordinaatit on valmiiksi määriteltynä yliluokassa. Merkkijonoesityksestä halutaan muuten samanlainen kuin pisteellä, mutta väri tulee myös ilmaista. Ylikirjoitettu metodi toString kutsuu yliluokan toString-metodia ja lisää sen tulokseen pisteen värin.

Seuraavassa on esimerkki, jossa listalle laitetaan muutama piste. Osa pisteistä on "normaaleja" ja osa väripisteitä. Lopulta tulostetaan listalla olevat pisteet. Jokaisen pisteen metodi toString suoritetaan pisteen todellisen tyypin perusteella, vaikka lista tuntee kaikki pisteet Piste-tyyppisinä.

public class Main {
    public static void main(String[] args) {
        List<Piste> pisteet = new ArrayList<>();
        pisteet.add(new Piste(4, 8));
        pisteet.add(new VariPiste(1, 1, "vihreä"));
        pisteet.add(new VariPiste(2, 5, "sininen"));
        pisteet.add(new Piste(0, 0));

        pisteet.stream().forEach(p -> System.out.println(p));
    }
}
(4, 8) etäisyys 12
(1, 1) etäisyys 2 väri: vihreä
(2, 5) etäisyys 7 väri: sininen
(0, 0) etäisyys 0

Haluamme ohjelmaamme myös kolmiulotteisen pisteen. Koska kyseessä ei ole värillinen versio, periytetään se luokasta piste.

public class Piste3D extends Piste {

    private int z;

    public Piste3D(int x, int y, int z) {
        super(x, y);
        this.z = z;
    }

    @Override
    protected String sijainti() {
        return super.sijainti() + ", " + z;    // tulos merkkijono muotoa "x, y, z"
    }

    @Override
    public int manhattanEtaisyysOrigosta() {
        // kysytään ensin yliluokalta x:n ja y:n perusteella laskettua etäisyyttä
        // ja lisätään tulokseen z-koordinaatin vaikutus
        return super.manhattanEtaisyysOrigosta() + Math.abs(z);
    }

    @Override
    public String toString() {
        return "(" + this.sijainti() + ") etäisyys " + this.manhattanEtaisyysOrigosta();
    }
}

Kolmiulotteinen piste siis määrittelee kolmatta koordinaattia vastaavan oliomuuttujan ja ylikirjoittaa metodit sijainti, manhattanEtaisyysOrigosta ja toString siten, että ne huomioivat kolmannen ulottuvuuden. Voimme nyt laajentaa edellistä esimerkkiä ja lisätä listalle myös kolmiulotteisia pisteitä.

public class Main {

    public static void main(String[] args) {
        List<Piste> pisteet = new ArrayList<>();
        pisteet.add(new Piste(4, 8));
        pisteet.add(new VariPiste(1, 1, "vihreä"));
        pisteet.add(new VariPiste(2, 5, "sininen"));
        pisteet.add(new Piste3D(5, 2, 8));
        pisteet.add(new Piste(0, 0));

        pisteet.stream().forEach(p -> System.out.println(p));
    }
  }
(4, 8) etäisyys 12
(1, 1) etäisyys 2 väri: vihreä
(2, 5) etäisyys 7 väri: sininen
(5, 2, 8) etäisyys 15
(0, 0) etäisyys 0

Huomamme, että kolmiulotteisen pisteen metodi toString on täsmälleen sama kuin pisteen toString. Voisimmeko jättää toStringin ylikirjoittamatta? Vastaus on kyllä! Kolmiulotteinen piste pelkistyy seuraavanlaiseksi.

public class Piste3D extends Piste {

    private int z;

    public Piste3D(int x, int y, int z) {
        super(x, y);
        this.z = z;
    }

    @Override
    protected String sijainti() {
        return super.sijainti() + ", " + z;
    }

    @Override
    public int manhattanEtaisyysOrigosta() {
        return super.manhattanEtaisyysOrigosta() + Math.abs(z);
    }
}

Mitä tarkalleenottaen tapahtuu kuin kolmiulotteiselle pisteelle kutsutaan toString-metodia? Suoritus etenee seuraavasti.

  1. etsitään toString:in määrittelyä luokasta Piste3D, sitä ei löydy joten mennään yliluokkaan
  2. etsitään toString:in määrittelyä yliluokasta Piste, metodi löytyy, joten suoritetaan sen koodi
    • suoritettava koodi siis on return "("+this.sijainti()+") etäisyys "+this.manhattanEtaisyysOrigosta();
    • esimmäisenä suoritetaan metodi sijainti
    • etsitään metodin sijainti määrittelyä luokasta Piste3D, metodi löytyy ja suoritetaan sen koodi
    • metodin sijainti laskee oman tuloksensa kutsumalla yliluokassa olevaa metodia sijainti
    • seuraavaksi etsitään metodin manhattanEtaisyysOrigosta määrittelyä luokasta Piste3D, metodi löytyy ja suoritetaan sen koodi
    • jälleen metodi laskee tuloksensa kutsuen ensin yliluokassa olevaa samannimistä metodia

Metodikutsun aikaansaama toimintoketju siis on monivaiheinen. Periaate on kuitenkin selkeä: suoritettavan metodin määrittelyä etsitään ensin olion todellisen tyypin määrittelystä ja jos sitä ei löydy edetään yliluokkaan. Ja jos yliluokastakaan ei löydy metodin toteutusta siirrytään etsimään yliluokan yliluokasta jne...

Milloin perintää kannattaa käyttää?

Perintä on väline käsitehierarkioiden rakentamiseen ja erikoistamiseen; aliluokka on aina yliluokan erikoistapaus. Jos luotava luokka on olemassaolevan luokan erikoistapaus, voidaan uusi luokka luoda perimällä olemassaoleva luokka. Esimerkiksi auton osiin liittyvässä esimerkissä moottori on osa, mutta moottoriin liittyy lisätoiminnallisuutta mitä jokaisella osalla ei ole.

Perittäessä aliluokka saa käyttöönsä yliluokan toiminnallisuudet. Jos aliluokka ei tarvitse tai käytä perittyä toiminnallisuutta, ei perintä ole perusteltua. Perityt luokat perivät yliluokkiensa metodit ja rajapinnat, eli aliluokkia voidaan käyttää missä tahansa missä yliluokkaa on käytetty. Perintähierarkia kannattaa pitää matalana, sillä hierarkian ylläpito ja jatkokehitys vaikeutuu perintöhierarkian kasvaessa. Yleisesti ottaen, jos perintähierarkian korkeus on yli 2 tai 3, ohjelman rakenteessa on todennäköisesti parannettavaa.

Perinnän käyttöä tulee miettiä. Esimerkiksi luokan Auto periminen luokasta Osa (tai Moottori) on väärin. Auto sisältää moottorin ja osia, mutta auto ei ole moottori tai osa. Voimme yleisemmin ajatella että jos olio omistaa tai koostuu toisista olioista, ei perintää tule käyttää.

Perintää käytettäessä tulee varmistaa että Single Responsibility Principle pätee myös perittäessä. Jokaisella luokalla tulee olla vain yksi syy muuttua. Jos huomaat että perintä lisää luokan vastuita, tulee luokka pilkkoa useammaksi luokaksi.

Esimerkki perinnän väärinkäytöstä

Pohditaan postituspalveluun liittyviä luokkia Asiakas, joka sisältää asiakkaan tiedot, ja Tilaus, joka perii asiakkaan tiedot ja sisältää tilattavan tavaran tiedot. Luokassa Tilaus on myös metodi postitusOsoite, joka kertoo tilauksen postitusosoitteen.

public class Asiakas {

    private String nimi;
    private String osoite;

    public Asiakas(String nimi, String osoite) {
        this.nimi = nimi;
        this.osoite = osoite;
    }

    public String getNimi() {
        return nimi;
    }

    public String getOsoite() {
        return osoite;
    }

    public void setOsoite(String osoite) {
        this.osoite = osoite;
    }
}
public class Tilaus extends Asiakas {

    private String tuote;
    private String lukumaara;

    public Tilaus(String tuote, String lukumaara, String nimi, String osoite) {
        super(nimi, osoite);
        this.tuote = tuote;
        this.lukumaara = lukumaara;
    }

    public String getTuote() {
        return tuote;
    }

    public String getLukumaara() {
        return lukumaara;
    }

    public String postitusOsoite() {
        return this.getNimi() + "\n" + this.getOsoite();
    }
}

Yllä perintää on käytetty väärin. Luokkaa perittäessä aliluokan tulee olla yliluokan erikoistapaus; tilaus ei ole asiakkaan erikoistapaus. Väärinkäyttö ilmenee single responsibility principlen rikkomisena: luokalla Tilaus on vastuu sekä asiakkaan tietojen ylläpidosta, että tilauksen tietojen ylläpidosta.

Ratkaisussa piilevä ongelma tulee esiin kun mietimme mitä käy asiakkaan osoitteen muuttuessa.

Osoitteen muuttuessa joutuisimme muuttamaan jokaista kyseiseen asiakkaaseen liittyvää tilausoliota, mikä ei missään nimessä ole toivottua. Parempi ratkaisu olisi kapseloida Asiakas Tilaus-luokan oliomuuttujaksi. Jos ajattelemme tarkemmin tilauksen semantiikkaa, tämä on selvää. Tilauksella on asiakas.

Muutetaan luokkaa Tilaus siten, että se sisältää Asiakas-viitteen.

public class Tilaus {

    private Asiakas asiakas;
    private String tuote;
    private String lukumaara;

    public Tilaus(Asiakas asiakas, String tuote, String lukumaara) {
        this.asiakas = asiakas;
        this.tuote = tuote;
        this.lukumaara = lukumaara;
    }

    public String getTuote() {
        return tuote;
    }

    public String getLukumaara() {
        return lukumaara;
    }

    public String postitusOsoite() {
        return this.asiakas.getNimi() + "\n" + this.asiakas.getOsoite();
    }
}

Yllä oleva luokka Tilaus on nyt parempi. Metodi postitusosoite käyttää asiakas-viitettä postitusosoitteen saamiseen sen sijaan että luokka perisi luokan Asiakas. Tämä helpottaa sekä ohjelman ylläpitoa, että sen konkreettista toiminnallisuutta.

Nyt asiakkaan muuttaessa tarvitsee muuttaa vain asiakkaan tietoja, tilauksiin ei tarvitse tehdä muutoksia.

Tehtäväpohjassa tulee mukana luokka Varasto, jonka tarjoamat konstruktorit ja metodit ovat seuraavat:

  • public Varasto(double tilavuus)
    Luo tyhjän varaston, jonka vetoisuus eli tilavuus annetaan parametrina; sopimaton tilavuus (<=0) luo käyttökelvottoman varaston, jonka tilavuus on 0.
  • public double getSaldo()
    Palauttaa arvonaan varaston saldon, eli varastossa olevan tavaran tilavuuden.
  • public double getTilavuus()
    Palauttaa arvonaan varaston kokonaistilavuuden (eli sen, joka annettiin konstruktorille).
  • public double paljonkoMahtuu()
    Palauttaa arvonaan tiedon, paljonko varastoon vielä mahtuu.
  • public void lisaaVarastoon(double maara)
    Lisää varastoon pyydetyn määrän; jos määrä on negatiivinen, mikään ei muutu, jos kaikki pyydetty ei enää mahdu, varasto laitetaan täydeksi ja loput määrästä "heitetään menemään", "vuotaa yli".
  • public double otaVarastosta(double maara)
    Otetaan varastosta pyydetty määrä, metodi palauttaa paljonko saadaan. Jos pyydetty määrä on negatiivinen, mikään ei muutu ja palautetaan nolla. Jos pyydetään enemmän kuin varastossa on, annetaan mitä voidaan ja varasto tyhjenee.
  • public String toString()
    Palauttaa olion tilan merkkijonoesityksenä tyyliin saldo = 64.5, tilaa 123.5

Tehtävässä rakennetaan Varasto-luokasta useampia erilaisia varastoja. Huom! Toteuta kaikki luokat pakkaukseen varastot.

Tuotevarasto, vaihe 1

Luokka Varasto hallitsee tuotteen määrään liittyvät toiminnot. Nyt tuotteelle halutaan lisäksi tuotenimi ja nimen käsittelyvälineet. Ohjelmoidaan Tuotevarasto Varaston aliluokaksi! Toteutetaan ensin pelkkä yksityinen oliomuuttuja tuotenimelle, konstruktori ja getteri nimikentälle:

  • public Tuotevarasto(String tuotenimi, double tilavuus)
    Luo tyhjän tuotevaraston. Tuotenimi ja vetoisuus annetaan parametrina.
  • public String getNimi()
    Palauttaa arvonaan tuotteen nimen.

Muista millä tavoin konstruktori voi ensi toimenaan suorittaa yliluokan konstruktorin!

Käyttöesimerkki:

Tuotevarasto mehu = new Tuotevarasto("Juice", 1000.0);
mehu.lisaaVarastoon(1000.0);
mehu.otaVarastosta(11.3);
System.out.println(mehu.getNimi()); // Juice
System.out.println(mehu);           // saldo = 988.7, tilaa 11.3
Juice
saldo = 988.7, vielä tilaa 11.3

Tuotevarasto, vaihe 2

Kuten edellisestä esimerkistä näkee, Tuotevarasto-olion perimä toString() ei tiedä (tietenkään!) mitään tuotteen nimestä. Asialle on tehtävä jotain! Lisätään samalla myös setteri tuotenimelle:

  • public void setNimi(String uusiNimi) asettaa tuotteelle uuden nimen.
  • public String toString() palauttaa olion tilan merkkijonoesityksenä tyyliin Juice: saldo = 64.5, tilaa 123.5

Uuden toString()-metodin voisi toki ohjelmoida käyttäen yliluokalta perittyjä gettereitä, joilla perittyjen, mutta piilossa pidettyjen kenttien arvoja saa käyttöönsä. Koska yliluokkaan on kuitenkin jo ohjelmoitu tarvittava taito varastotilanteen merkkiesityksen tuottamiseen, miksi nähdä vaivaa sen uudelleen ohjelmointiin. Käytä siis hyväksesi perittyä toStringiä.

Muista miten korvattua metodia voi kutsua aliluokassa!

Käyttöesimerkki:

Tuotevarasto mehu = new Tuotevarasto("Juice", 1000.0);
mehu.lisaaVarastoon(1000.0);
mehu.otaVarastosta(11.3);
System.out.println(mehu.getNimi()); // Juice
mehu.lisaaVarastoon(1.0);
System.out.println(mehu);           // Juice: saldo = 989.7, tilaa 10.299999999999955
Juice
Juice: saldo = 989.7, tilaa 10.299999999999955

Muutoshistoria

Toisinaan saattaa olla kiinnostavaa tietää, millä tavoin jonkin tuotteen varastotilanne muuttuu: onko varasto usein hyvin vajaa, ollaanko usein ylärajalla, onko vaihelu suurta vai pientä, jne. Varustetaan siksi Tuotevarasto-luokka taidolla muistaa tuotteen määrän muutoshistoriaa.

Aloitetaan apuvälineen laadinnalla.

Muutoshistorian muistamisen voisi toki toteuttaa suoraankin ArrayList<Double>-oliona luokassa Tuotevarasto, mutta nyt laaditaan kuitenkin oma erikoistettu väline tähän tarkoitukseen. Väline toteutetaan kapseloimalla ArrayList<Double>-olio.

Muutoshistoria-luokan julkiset konstruktorit ja metodit:

  • public Muutoshistoria() luo tyhjän Muutoshistoria-olion.
  • public void lisaa(double tilanne) lisää muutoshistorian viimeisimmäksi muistettavaksi määräksi parametrina annetun tilanteen.
  • public void nollaa() tyhjää muistin.
  • public String toString() palauttaa muutoshistorian merkkijonoesityksen. ArrayList-luokan antama merkkijonoesitys kelpaa sellaisenaan.

Muutoshistoria, vaihe 2

Täydennä Muutoshistoria-luokkaa analyysimetodein:

  • public double maxArvo() palauttaa muutoshistorian suurimman arvon. Jos historia on tyhjä, metodi palauttaa nollan.
  • public double minArvo() palauttaa muutoshistorian pienimmän arvon. Jos historia on tyhjä, metodi palauttaa nollan.
  • public double keskiarvo() palauttaa muutoshistorian arvojen keskiarvon. Jos historia on tyhjä, metodi palauttaa nollan.

Muutoshistoria, vaihe 3

Täydennä Muutoshistoria-luokkaa analyysimetodein:

  • public double suurinMuutos() palauttaa muutoshistorian isoimman (huom: -5:n kokoinen muutos on isompi kuin 4:n kokoinen muutos) yksittäisen muutoksen itseisarvon. Jos historia on tyhjä tai yhden arvon mittainen, metodi palauttaa nollan. Itseisarvo on luvun etäisyys nollasta. Esimerkiksi luvun -5.5 itseisarvo on 5.5, luvun 3.2 itseisarvo on 3.2.
  • public double varianssi() palauttaa muutoshistorian arvojen varianssin (käytetään otosvarianssin kaavaa). Jos historia on tyhjä tai yhden arvon mittainen, metodi palauttaa nollan.

Ohjeen varianssin laskemiseksi voit katsoa esimerkiksi Wikipediasta kohdasta populaatio- ja otosvarianssi. Esimerkiksi lukujen 3, 2, 7, 2 keskiarvo on 3.5, joten otosvarianssi on ((3 - 3.5)² + (2 - 3.5)² + (7 - 3.5)² + (2 - 3.5)²)/(4 - 1) ≈ 5,666667.)

Muistava tuotevarasto, vaihe 1

Toteuta luokan Tuotevarasto aliluokkana MuistavaTuotevarasto. Uusi versio tarjoaa vanhojen lisäksi varastotilanteen muutoshistoriaan liittyviä palveluita. Historiaa hallitaan Muutoshistoria-oliolla.

Julkiset konstruktorit ja metodit:

  • public MuistavaTuotevarasto(String tuotenimi, double tilavuus, double alkuSaldo) luo tuotevaraston. Tuotenimi, vetoisuus ja alkusaldo annetaan parametrina. Aseta alkusaldo sekä varaston alkusaldoksi että muutoshistorian ensimmäiseksi arvoksi.
  • public String historia() palauttaa tuotehistorian tyyliin [0.0, 119.2, 21.2]. Käytä Muutoshistoria-olion merkkiesitystä sellaisenaan.

Huomaa että tässä esiversiossa historia ei vielä toimi kunnolla; nyt vasta vain aloitussaldo muistetaan.

Käyttöesimerkki:

// tuttuun tapaan:
MuistavaTuotevarasto mehu = new MuistavaTuotevarasto("Juice", 1000.0, 1000.0);
mehu.otaVarastosta(11.3);
System.out.println(mehu.getNimi()); // Juice
mehu.lisaaVarastoon(1.0);
System.out.println(mehu);           // Juice: saldo = 989.7, vielä tilaa 10.3
...
    // mutta vielä historia() ei toimi kunnolla:
System.out.println(mehu.historia()); // [1000.0]
    // saadaan siis vasta konstruktorin asettama historian alkupiste...
...
Juice
Juice: saldo = 989.7, vielä tilaa 10.299999999999955
[1000.0]

Muistava tuotevarasto, vaihe 2

On aika aloittaa historia! Ensimmäinen versio ei historiasta tiennyt kuin alkupisteen. Täydennä luokkaa metodein

  • public void lisaaVarastoon(double maara) toimii kuin Varasto-luokan metodi, mutta muuttunut tilanne kirjataan historiaan. Huom: historiaan tulee kirjata lisäyksen jälkeinen varastosaldo, ei lisättävää määrää!
  • public double otaVarastosta(double maara) toimii kuin Varasto-luokan metodi, mutta muuttunut tilanne kirjataan historiaan. Huom: historiaan tulee kirjata poiston jälkeinen varastosaldo, ei poistettavaa määrää!

Käyttöesimerkki:

// tuttuun tapaan:
MuistavaTuotevarasto mehu = new MuistavaTuotevarasto("Juice", 1000.0, 1000.0);
mehu.otaVarastosta(11.3);
System.out.println(mehu.getNimi()); // Juice
mehu.lisaaVarastoon(1.0);
System.out.println(mehu);           // Juice: saldo = 989.7, vielä tilaa 10.3
...
// mutta nyt on historiaakin:
System.out.println(mehu.historia()); // [1000.0, 988.7, 989.7]
...
Juice
Juice: saldo = 989.7, vielä tilaa 10.299999999999955
[1000.0, 988.7, 989.7]

Muista miten korvaava metodi voi käyttää hyväkseen korvattua metodia!

Muistava tuotevarasto, vaihe 3

Täydennä luokkaa metodilla

  • public void tulostaAnalyysi(), joka tulostaa tuotteeseen liittyviä historiatietoja esimerkin esittämään tapaan.

Käyttöesimerkki:

MuistavaTuotevarasto mehu = new MuistavaTuotevarasto("Juice", 1000.0, 1000.0);
mehu.otaVarastosta(11.3);
mehu.lisaaVarastoon(1.0);
//System.out.println(mehu.historia()); // [1000.0, 988.7, 989.7]

mehu.tulostaAnalyysi();
Tuote: Juice
Historia: [1000.0, 988.7, 989.7]
Suurin tuotemäärä: 1000.0
Pienin tuotemäärä: 988.7
Keskiarvo: 992.8

Muistava tuotevarasto, vaihe 4

Täydennä analyysin tulostus sellaiseksi, että mukana ovat myös muutoshistorian suurin muutos ja historian varianssi.

Perintä, rajapinnat, kumpikin, vai eikö kumpaakaan?

Perintä ei sulje pois rajapintojen käyttöä, eikä rajapintojen käyttö sulje pois perinnän käyttöä. Rajapinnat toimivat sopimuksena luokan tarjoamasta toteutuksesta, ja mahdollistavat konkreettisen toteutuksen abstrahoinnin. Rajapinnan toteuttavan luokan vaihto on hyvin helppoa.

Aivan kuten rajapintaa toteuttaessa, sitoudumme perittäessä siihen, että aliluokkamme tarjoaa kaikki yliluokan metodit. Monimuotoisuuden ja polymorfismin takia perintäkin toimii kuin rajapinnat. Voimme antaa yliluokkaa käyttävälle metodille sen aliluokan ilmentymän.

Abstraktit luokat

Perintähierarkiaa pohtiessa tulee joskus esille tilanteita, missä on olemassa selkeä käsite, mutta käsite ei sellaisenaan ole hyvä kandidaatti olioksi. Hyötyisimme käsitteestä perinnän kannalta, sillä se sisältää muuttujia ja toiminnallisuuksia, jotka ovat kaikille käsitteen periville luokille samoja, mutta toisaalta käsitteestä itsestään ei pitäisi pystyä tekemään olioita.

Abstrakti luokka yhdistää rajapintoja ja perintää. Niistä ei voi tehdä ilmentymiä, vaan ilmentymät tehdään tehdään abstraktin luokan aliluokista. Abstrakti luokka voi sisältää sekä normaaleja metodeja, joissa on metodirunko, että abstrakteja metodeja, jotka sisältävät ainoastaan metodimäärittelyn. Abstraktien metodien toteutus jätetään perivän luokan vastuulle. Yleisesti ajatellen abstrakteja luokkia käytetään esimerkiksi kun abstraktin luokan kuvaama käsite ei ole selkeä itsenäinen käsite. Tällöin siitä ei tule pystyä tekemään ilmentymiä.

Sekä abstraktin luokan että abstraktien metodien määrittelyssä käytetään avainsanaa abstract. Abstrakti luokka määritellään lauseella public abstract class LuokanNimi, abstrakti metodi taas lauseella public abstract palautustyyppi metodinNimi. Pohditaan seuraavaa abstraktia luokkaa Toiminto, joka tarjoaa rungon toiminnoille ja niiden suorittamiselle.

public abstract class Toiminto {

    private String nimi;

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

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

    public abstract void suorita(Scanner lukija);
}

Abstrakti luokka Toiminto toimii runkona erilaisten toimintojen toteuttamiseen. Esimerkiksi pluslaskun voi toteuttaa perimällä luokka Toiminto seuraavasti.

public class Pluslasku extends Toiminto {

    public Pluslasku() {
        super("Pluslasku");
    }

    @Override
    public void suorita(Scanner lukija) {
        System.out.print("Anna ensimmäinen luku: ");
        int eka = Integer.parseInt(lukija.nextLine());
        System.out.print("Anna toinen luku: ");
        int toka = Integer.parseInt(lukija.nextLine());

        System.out.println("Lukujen summa on " + (eka + toka));
    }
}

Koska kaikki Toiminto-luokan perivät luokat ovat myös tyyppiä toiminto, voimme rakentaa käyttöliittymän Toiminto-tyyppisten muuttujien varaan. Seuraava luokka Kayttoliittyma sisaltaa listan toimintoja ja lukijan. Toimintoja voi lisätä käyttöliittymään dynaamisesti.

public class Kayttoliittyma {

    private Scanner lukija;
    private List<Toiminto> toiminnot;

    public Kayttoliittyma(Scanner lukija) {
        this.lukija = lukija;
        this.toiminnot = new ArrayList<>();
    }

    public void lisaaToiminto(Toiminto toiminto) {
        this.toiminnot.add(toiminto);
    }

    public void kaynnista() {
        while (true) {
            tulostaToiminnot();
            System.out.println("Valinta: ");

            String valinta = this.lukija.nextLine();
            if (valinta.equals("0")) {
                break;
            }

            suoritaToiminto(valinta);
            System.out.println();
        }
    }

    private void tulostaToiminnot() {
        System.out.println("\t0: Lopeta");
        for (int i = 0; i < this.toiminnot.size(); i++) {
            String toiminnonNimi = this.toiminnot.get(i).getNimi();
            System.out.println("\t" + (i + 1) + ": " + toiminnonNimi);
        }
    }

    private void suoritaToiminto(String valinta) {
        int toiminto = Integer.parseInt(valinta);

        Toiminto valittu = this.toiminnot.get(toiminto - 1);
        valittu.suorita(lukija);
    }
}

Käyttöliittymä toimii seuraavasti:

Kayttoliittyma kayttolittyma = new Kayttoliittyma(new Scanner(System.in));
kayttolittyma.lisaaToiminto(new Pluslasku());

kayttolittyma.kaynnista();
Toiminnot:
        0: Lopeta
        1: Pluslasku
Valinta: 1
Anna ensimmäinen luku: 8
Anna toinen luku: 12
Lukujen summa on 20

Toiminnot:
        0: Lopeta
        1: Pluslasku
Valinta: 0

Rajapintojen ja abstraktien luokkien suurin ero on siinä, että abstrakteissa luokissa voidaan määritellä metodien lisäksi myös oliomuuttujia sekä konstruktoreja. Koska abstrakteihin luokkiin voidaan määritellä toiminnallisuutta, voidaan niitä käyttää esimerkiksi oletustoiminnallisuuden määrittelyyn. Yllä käyttöliittymä käytti abstraktissa luokassa määriteltyä toiminnan nimen tallentamista.

Tehtäväpohjan mukana tulee luokat Tavara ja Laatikko. Luokka Laatikko on abstrakti luokka, jossa useamman tavaran lisääminen on toteutettu siten, että kutsutaan aina lisaa-metodia. Yhden tavaran lisäämiseen tarkoitettu metodi lisaa on abstrakti, joten jokaisen Laatikko-luokan perivän laatikon tulee toteuttaa se. Tehtävänäsi on muokata luokkaa Tavara ja toteuttaa muutamia erilaisia laatikoita luokan Laatikko pohjalta.

Lisää kaikki uudet luokat pakkaukseen laatikot.

package laatikot;

import java.util.Collection;

public abstract class Laatikko {

    public abstract void lisaa(Tavara tavara);

    public void lisaa(Collection<Tavara> tavarat) {
        for (Tavara t: tavarat) {
            lisaa(t);
        }
    }

    public abstract boolean onkoLaatikossa(Tavara tavara);
}

Tavaran muokkaus

Lisää Tavara-luokan konstruktoriin tarkistus, jossa tarkistetaan että tavaran paino ei ole koskaan negatiivinen (paino 0 hyväksytään). Jos paino on negatiivinen, tulee konstruktorin heittää IllegalArgumentException-poikkeus. Toteuta Tavara-luokalle myös metodit equals ja hashCode, joiden avulla pääset hyödyntämään erilaisten listojen ja kokoelmien contains-metodia. Toteuta metodit siten, että Tavara-luokan oliomuuttujan paino arvolla ei ole väliä. Voit hyvin hyödyntää NetBeansin tarjoamaa toiminnallisuutta equalsin ja hashCoden toteuttamiseen.

Maksimipainollinen laatikko

Toteuta pakkaukseen laatikot luokka MaksimipainollinenLaatikko, joka perii luokan Laatikko. Maksimipainollisella laatikolla on konstruktori public MaksimipainollinenLaatikko(int maksimipaino), joka määrittelee laatikon maksimipainon. Maksimipainolliseen laatikkoon voi lisätä tavaraa jos ja vain jos tavaran lisääminen ei ylitä laatikon maksimipainoa.

MaksimipainollinenLaatikko kahviLaatikko = new MaksimipainollinenLaatikko(10);
kahviLaatikko.lisaa(new Tavara("Saludo", 5));
kahviLaatikko.lisaa(new Tavara("Pirkka", 5));
kahviLaatikko.lisaa(new Tavara("Kopi Luwak", 5));

System.out.println(kahviLaatikko.onkoLaatikossa(new Tavara("Saludo")));
System.out.println(kahviLaatikko.onkoLaatikossa(new Tavara("Pirkka")));
System.out.println(kahviLaatikko.onkoLaatikossa(new Tavara("Kopi Luwak")));
true
true
false

Yhden tavaran laatikko ja Hukkaava laatikko

Toteuta seuraavaksi pakkaukseen laatikot luokka YhdenTavaranLaatikko, joka perii luokan Laatikko. Yhden tavaran laatikolla on konstruktori public YhdenTavaranLaatikko(), ja siihen mahtuu tasan yksi tavara. Jos tavara on jo laatikossa sitä ei tule vaihtaa. Laatikkoon lisättävän tavaran painolla ei ole väliä.

YhdenTavaranLaatikko laatikko = new YhdenTavaranLaatikko();
laatikko.lisaa(new Tavara("Saludo", 5));
laatikko.lisaa(new Tavara("Pirkka", 5));

System.out.println(laatikko.onkoLaatikossa(new Tavara("Saludo")));
System.out.println(laatikko.onkoLaatikossa(new Tavara("Pirkka")));
true
false

Toteuta seuraavaksi pakkaukseen laatikot luokka HukkaavaLaatikko, joka perii luokan Laatikko. Hukkaavalla laatikolla on konstruktori public HukkaavaLaatikko(). Hukkaavaan laatikkoon voi lisätä kaikki tavarat, mutta tavaroita ei löydy niitä etsittäessä. Laatikkoon lisäämisen tulee siis aina onnistua, mutta metodin onkoLaatikossa kutsumisen tulee aina palauttaa false.

HukkaavaLaatikko laatikko = new HukkaavaLaatikko();
laatikko.lisaa(new Tavara("Saludo", 5));
laatikko.lisaa(new Tavara("Pirkka", 5));

System.out.println(laatikko.onkoLaatikossa(new Tavara("Saludo")));
System.out.println(laatikko.onkoLaatikossa(new Tavara("Pirkka")));
false
false

Tässä tehtävässä demonstroit perinnän ja rajapintojen käyttöä. Toteuta kaikki luokat ja rajapinnat pakkaukseen perintaa.

Eläin

Toteuta ensin abstrakti luokka Elain. Luokalla Elain on konstruktori, jolle annetaan parametrina eläimen nimi. Luokalla Elain on lisäksi parametrittomat metodit syo ja nuku, jotka eivät palauta arvoa (void), sekä parametriton metodi getNimi, joka palauttaa eläimen nimen.

Metodin nuku tulee tulostaa "(nimi) nukkuu" ja metodin syo tulee tulostaa "(nimi) syo". Tässä (nimi) on eläimelle annettu nimi.

Koira

Toteuta luokan Elain perivä luokka Koira. Luokalla Koira tulee olla parametrillinen konstruktori, jolla luotavalle koiraoliolle voi antaa nimen. Tämän lisäksi koiralla tulee olla parametriton konstruktori, jolla koiran nimeksi tulee "Koira" sekä parametriton metodi hauku, joka ei palauta arvoa (void). Koiralla tulee olla myös metodit syo ja nuku kuten eläimillä yleensä ottaen.

Alla on esimerkki luokan Koira odotetusta toiminnasta:

Koira koira = new Koira();
koira.hauku();
koira.syo();
    
Koira vuffe = new Koira("Vuffe");
vuffe.hauku();
Koira haukkuu
Koira syo
Vuffe haukkuu

Kissa

Toteuta seuraavaksi luokka Kissa, joka perii luokan Elain. Luokalla Kissa tulee olla parametrillinen konstruktori, jolla luotavalle kissaoliolle voi antaa nimen. Tämän lisäksi kissalla tulee olla parametriton konstruktori, jolla kissan nimeksi tulee "Kissa" sekä parametriton metodi mourua, joka ei palauta arvoa (void). Kissalla tulee olla myös metodit syo ja nuku kuten ensimmäisessä osassa.

Alla on esimerkki luokan Kissa odotetusta toiminnasta:

Kissa kissa = new Kissa();
kissa.mourua();
kissa.syo();
    
Kissa karvinen = new Kissa("Karvinen");
karvinen.mourua();
Kissa mouruaa
Kissa syo
Karvinen mouruaa

Ääntelevä

Luo lopulta rajapinta Aanteleva, joka maarittelee parametrittoman metodin aantele, joka ei palauta arvoa (void). Toteuta rajapinta luokissa Koira että Kissa. Rajapinnan tulee hyödyntää aiemmin määriteltyjä hauku ja mourua -metodeja.

Alla on esimerkki odotetusta toiminnasta:

Aanteleva koira = new Koira();
koira.aantele();
    
Aanteleva kissa = new Kissa("Karvinen");
kissa.aantele();
Kissa k = (Kissa) kissa;
k.mourua();
Koira haukkuu
Karvinen mouruaa
Karvinen mouruaa    

Perintä ja abstraktit luokat luokkakaaviossa

Perintä merkitään luokkakaavioon kolmion muotoisella nuolella. Kolmio on perittävän luokan päädyssä. Alla olevassa esimerkissä luokka Moottori perii luokan Osa.

[Osa|-tunnus:String;-valmistaja:String;-kuvaus:String]
								   [Moottori|-moottorityyppi:String]
								   [Osa]^-[Moottori]

 

Alla olevaan esimerkkiin on kirjoitettu auki muistavaa tuotevarastoa käsittelevän tehtävän luokkakaavio. Muistava tuotevarasto perii tuotevaraston, joka taas perii varaston. Muutoshistoria on erillinen luokka, jonka muistava tuotevarasto sisältää. Muistava tuotevarasto tietää muutoshistorian, mutta muutoshistoria ei tiedä muistavasta tuotevarastosta.

[Varasto|-tilavuus:double;-saldo:double|+Varasto(tilavuus:double);+getSaldo():double;+getTilavuus():double;+paljonkoMahtuu():double;+lisaaVarastoon(maara:double):void;+otaVarastosta(maara:double):double;+toString():String]
								     [Tuotevarasto|-nimi:String|+Tuotevarasto(nimi:String، tilavuus:double);+getNimi():String;+setNimi(nimi:String):String;+toString():String]
								     [Muutoshistoria|-tilanteet:ArrayList|+Muutoshistoria();+lisaa(tilanne:double);+nollaa():void;...]
								     [MuistavaTuotevarasto||+MuistavaTuotevarasto(nimi:String، tilavuus:double،alkusaldo:double);+historia():String;+tulostaAnalyysi():void;+lisaaVarastoon(maara:double);+otaVarastosta(maara:double):double]

								     [Varasto]^-[Tuotevarasto]
								     [Tuotevarasto]^-[MuistavaTuotevarasto]
								     [Muutoshistoria]<-[MuistavaTuotevarasto]

 

Abstraktien luokkien perintä toimii lähes samalla tavalla. Abstraktit luokat kuitenkin merkitään luokkakaavioon siten, että luokan nimen yläpuolella lukee <<abstract>>. Tämän lisäksi luokan nimi ja luokassa määritellyt abstraktit metodit kuvataan kursiivilla.

Alla olevassa esimerkissä on tehtävän erilaisia laatikoita ensimmäistä kahta osaa kuvaava luokkaakaavio.

Miten näitä kannattaa piirtää?

Luokkakaaviot ovat erinomainen tapa kuvata ongelma-aluetta ja ongelman muotoa muille. Niiden käyttö on erittäin hyödyllistä myös silloin, kun ohjelmoija suunnittelee useammasta luokasta koostuvan ohjelman rakennetta.

Luokkakaavioita piirretään suunnitteluvaiheessa usein esimerkiksi valkotaulua tai isompaa paperiarkkia käyttäen. Luokkakaaviot kannattaa ajatella poisheitettävinä tuotoksina, jotka auttavat ohjelman rakennuksessa. Kaavion piirtämiseen -- eli tyylin oikeellisuuteen ja yksityiskohtiin -- ei kannata käyttää liian pitkään aikaa. Vastaavasti kaavio kannattaa piirtää sopivalla abstraktiotasolla. Esimerkiksi kymmeniä luokkia sisältävään luokkakaavioon ei todennäköisesti kannata merkitä jokaisen luokan jokaista metodia ja muuttujaa.

Materiaalissa käytetyt luokkakaaviot on piirretty sekä yUML että Createlyn avulla. Myös NetBeansiin löytyy välineitä luokkakaavioiden luomiseen -- esimerkiksi easyUML mahdollistaa luokkakaavioiden luomisen suoraan projektin koodista.

Kokoelmien käsittely arvojen virtana

Tutustutaan kokoelmien kuten listojen läpikäyntiin arvojen virtana (stream). Virta on menetelmä tietoa sisältävän kokoelman läpikäyntiin siten, että ohjelmoija määrittelee kullekin arvolle suoritettavan toiminnallisuuden. Indeksistä tai kullakin hetkellä käsiteltävästä muuttujasta ei pidetä kirjaa.

Virran avulla ohjelmoija määrittelee käytännössä funktioketjun, joita kutsutaan tietokokoelman arvoille. Virran avulla voi muuntaa tietoa muodosta toiseen, mutta virta ei muuta alkuperäisen tietokokoelman arvoja.

Tutustutaan virran käyttöön konkreettisen esimerkin kautta. Tarkastellaan seuraavaa ongelmaa:

Kirjoita ohjelma, joka lukee käyttäjältä syötteitä ja tulostaa niihin liittyen tilastoja. Kun käyttäjä syöttää merkkijonon "loppu", lukeminen lopetetaan. Muut syötteet ovat lukuja. Kun syötteiden lukeminen lopetetaan, ohjelma tulostaa kolmella jaollisten positiivisten lukujen lukumäärän sekä kaikkien lukujen keskiarvon.

// alustetaan lukija ja lista, johon syotteet luetaan
Scanner lukija = new Scanner(System.in);
List<String> syotteet = new ArrayList<>()

// luetaan syotteet
while (true) {
    String rivi = lukija.nextLine();
    if (rivi.equals("loppu")) {
        break;
    }
  
    syotteet.add(rivi);
}

// selvitetään kolmella jaollisten lukumaara
long kolmellaJaollistenLukumaara = syotteet.stream()
    .mapToInt(s -> Integer.parseInt(s))
    .filter(luku -> luku % 3 == 0)
    .count();

// selvitetään keskiarvo
double keskiarvo = syotteet.stream()
    .mapToInt(s -> Integer.parseInt(s))
    .average()
    .getAsDouble();

// tulostetaan tilastot
System.out.println("Kolmella jaollisia: " + kolmellaJaollistenLukumaara);
System.out.println("Lukujen keskiarvo: " + keskiarvo);

Tarkastellaan tarkemmin yllä kuvatun ohjelman osaa, missä luettuja syötteitä käsitellään virtana.

// selvitetään kolmella jaollisten lukumaara
long kolmellaJaollistenLukumaara = syotteet.stream()
    .mapToInt(s -> Integer.parseInt(s))
    .filter(luku -> luku % 3 == 0)
    .count();

Virta luodaan mistä tahansa Collection-rajapinnan toteuttavasta oliosta (esim. ArrayList, HashSet, HashMap, ...) metodilla stream(). Tämän jälkeen merkkijonomuotoiset arvot muunnetaan ("map") kokonaislukumuotoon virran metodilla mapToInt(arvo -> muunnos) -- muunto toteutetaan Integer-luokan tarjoamalla parseInt-metodilla, jota olemme käyttäneet aiemminkin. Seuraavaksi rajaamme metodilla filter(arvo -> rajausehto) käsiteltäväksi vain ne luvut, jotka ovat kolmella jaollisia. Lopulta kutsumme virran metodia count(), joka laskee virran alkioiden lukumäärän ja palauttaa sen long-tyyppisenä muuttujana.

Tarkastellaan tämän jälkeen listan alkioiden keskiarvon laskemiseen tarkoitettua ohjelmaa.

// selvitetään keskiarvo
double keskiarvo = syotteet.stream()
    .mapToInt(s -> Integer.parseInt(s))
    .average()
    .getAsDouble();

Keskiarvon laskeminen onnistuu virrasta, jolle on kutsuttu mapToInt-metodia. Kokonaislukuja sisältävällä virralla on metodi average(), joka palauttaa OptionalDouble-tyyppisen olion. Oliolla on metodi getAsDouble(), joka palauttaa listan arvojen keskiarvon double-tyyppisenä muuttujana.

Lyhyt yhteenveto tähän mennessä tutuiksi tulleista virtaan liittyvistä metodeista.

Tarkoitus ja metodi Oletukset
Virran luominen: stream() Metodia kutsutaan Collection-rajapinnan toteuttavalle kokoelmalle kuten ArrayList-oliolle. Luotavalle virralle tehdään jotain.
Virran muuntaminen kokonaislukuvirraksi: mapToInt(arvo -> toinen) Virta muuntuu kokonaislukuja sisältäväksi virraksi. Merkkijonoja sisältävä muunnos voidaan tehdä esimerkiksi Integer-luokan parseInt-metodin avulla. Kokonaislukuja sisältävälle virralle tehdään jotain.
Arvojen rajaaminen: filter(arvo -> hyvaksymisehto) Virrasta rajataan pois ne arvot, jotka eivät täytä hyväksymisehtoa. "Nuolen" oikealla puolella on lauseke, joka palauttaa totuusarvon. Jos totuusarvo on true, arvo hyväksytään virtaan. Jos totuusarvo on false, arvoa ei hyväksytä virtaan. Rajatuille arvoille tehdään jotain.
Keskiarvon laskeminen: average() Palauttaa OptionalDouble-tyyppisen olion, jolla on double tyyppisen arvon palauttava metodi getAsDouble(). Metodin average() kutsuminen onnistuu kokonaislukuja sisältävälle virralle (luominen onnistuu mapToInt-metodilla.
Virrassa olevien alkioiden lukumaara: count() Palauttaa virrassa olevien alkioiden lukumäärän long-tyyppisenä arvona.

Harjoitellaan lukujen lukemista listalle sekä listan arvojen keskiarvon laskemista virran avulla.

Toteuta ohjelma, joka lukee käyttäjältä syötteitä. Jos käyttäjä syöttää merkkijonon "loppu", lukeminen lopetetaan. Muut syötteet ovat lukuja. Kun käyttäjä syöttää merkkijonon "loppu", ohjelman tulee tulostaa syötettyjen lukujen keskiarvo.

Kirjoita syötteitä, "loppu" lopettaa.
2
4
6
loppu
Lukujen keskiarvo: 4.0
Kirjoita syötteitä, "loppu" lopettaa.
-1
1
2
loppu
Lukujen keskiarvo: 0.6666666666666666

Harjoitellaan lukujen lukemista listalle sekä listan arvojen rajaamista virran avulla.

Toteuta ohjelma, joka lukee käyttäjältä syötteitä. Jos käyttäjä syöttää merkkijonon "loppu", lukeminen lopetetaan. Muut syötteet ovat lukuja. Kun käyttäjä syöttää merkkijonon "loppu", syötteiden lukeminen lopetetaan.

Tämän jälkeen käyttäjältä kysytään tulostetaanko negatiivisten vai positiivisten lukujen keskiarvo (n vai p). Jos käyttäjä syöttää merkkijonon "n", tulostetaan negatiivisten lukujen keskiarvo, muulloin tulostetaan positiivisten lukujen keskiarvo.

Kirjoita syötteitä, "loppu" lopettaa.
-1
1
2
loppu
    
Tulostetaanko negatiivisten vai positiivisten lukujen keskiarvo? (n/p)
n
Negatiivisten lukujen keskiarvo: -1.0
Kirjoita syötteitä, "loppu" lopettaa.
-1
1
2
loppu
    
Tulostetaanko negatiivisten vai positiivisten lukujen keskiarvo? (n/p)
p
Positiivisten lukujen keskiarvo: 1.5

Lambda-lauseke

Virran arvoja käsitellään virtaan liittyvillä metodeilla. Arvoja käsittelevät metodit saavat parametrinaan funktion, joka kertoo mitä kullekin arvolle tulee tehdä. Funktion toiminnallisuus on metodikohtaista: rajaamiseen käytetylle metodille filter annetaan funktio, joka palauttaa totuusarvoisen muuttujan arvon true tai false, riippuen halutaanko arvo säilyttää virrassa; muuntamiseen käytetylle metodille mapToInt annetaan funktio, joka muuntaa arvon kokonaisluvuksi, jne.

Miksi funktiot kirjoitetaan muodossa luku -> luku > 5?

Kyseinen kirjoitusmuoto, lambda-lauseke, on Javan tarjoama lyhenne ns. anonyymeille metodeille, joilla ei ole "omistajaa" eli ne eivät ole osa luokkaa tai rajapintaa. Funktio sisältää sekä parametrien määrittelyn että funktion rungon. Saman funktion voi kirjoittaa useammalla eri tavalla, kts. alla.

// alkuperäinen
virta.filter(luku -> luku > 5).jatkokäsittely
  
// on sama kuin
virta.filter((Integer luku) -> 
    if (luku > 5) {
        return true;
    }
    
    return false;
}).jatkokäsittely

Saman voi kirjoittaa myös eksplisiittisesti niin, että ohjelmaan määrittelee staattisen metodin, jota hyödynnetään virralle parametrina annetussa funktiossa.

public class Rajaajat {
    public static boolean vitostaSuurempi(int luku) {
        return luku > 5;
    }
}
// alkuperäinen
virta.filter(luku -> luku > 5).jatkokäsittely

// on sama kuin
virta.filter(luku -> Rajaajat.vitostaSuurempi(luku)).jatkokäsittely

Funktion voi antaa myös suoraan parametrina. Alla oleva syntaksi Rajaajat::vitostaSuurempi tarkoittaa "hyödynnä tässä Rajaajat-luokassa olevaa staattista metodia vitostaSuurempi".

// on sama kuin
virta.filter(Rajaajat::vitostaSuurempi).jatkokäsittely

Virran arvoja käsittelevät funktiot eivät voi muuttaa funktion ulkopuolisten muuttujien arvoja. Kyse on käytännössä staattisten metodien käyttäytymisestä -- metodia kutsuttaessa metodin ulkopuolisiin muuttujiin ei pääse käsiksi. Funktioiden tilanteessa funktion ulkopuolisten muuttujien arvoja voi lukea olettaen, että luettavien muuttujien arvot eivät muutu lainkaan ohjelmassa.

Alla oleva ohjelma demonstroi tilannetta, missä funktiossa yritetään hyödyntää funktion ulkopuolista muuttujaa. Tämä ei toimi.

// alustetaan lukija ja lista, johon syotteet luetaan
Scanner lukija = new Scanner(System.in);
List<String> syotteet = new ArrayList<>()

// luetaan syotteet
while (true) {
    String rivi = lukija.nextLine();
    if (rivi.equals("loppu")) {
        break;
    }
  
    syotteet.add(rivi);
}

int muunnettujaYhteensa = 0;

// selvitetään kolmella jaollisten lukumaara
long kolmellaJaollistenLukumaara = syotteet.stream()
    .mapToInt(s -> {
        // anonyymissä funktiossa ei voi käsitellä (tai tarkemmin muuttaa) funktion
        // ulkopuolella määriteltyä muuttujaa, joten tämä ei toimi
        muunnettujaYhteensa++;
        return Integer.parseInt(s);
    }).filter(luku -> luku % 3 == 0)
    .count();

Virran metodit

Virran metodit voi jakaa karkeasti kahteen eri ryhmään: virran (1) arvojen käsittelyyn tarkoitettuihin välioperaatioihin sekä (2) käsittelyn lopettaviin pääteoperaatiohin. Edellisessä esimerkissä nähdyt metodit filter ja mapToInt ovat välioperaatioita. Välioperaatiot palauttavat arvonaan virran, jonka käsittelyä voi jatkaa -- käytännössä välioperaatioita voi olla käytännössä ääretön määrä ketjutettuna peräkkäin (pisteellä eroteltuna). Toisaalta edellisessä esimerkissä nähty metodi average on pääteoperaatio. Pääteoperaatio palauttaa käsiteltävän arvon, joka luodaan esimerkiksi virran arvoista.

Alla olevassa kuvassa on kuvattu virran toimintaa. Lähtötilanteena (1) on lista, jossa on arvoja. Kun listalle kutsutaan stream()-metodia, (2) luodaan virta listan arvoista. Arvoja käsitellään tämän jälkeen yksitellen. Virran arvoja voidaan (3) rajata metodilla filter. Tämä poistaa virrasta ne arvot, jotka ovat rajauksen ulkopuolella. Virran metodilla map voidaan (4) muuntaa virrassa olevia arvoja muodosta toiseen. Metodi collect (5) kerää virrassa olevat arvot arvot sille annettuun kokoelmaan, esim. listalle.

Yllä tekstuaalisesti kuvattu virran toiminta kuvana.

 

Alla vielä yllä olevan kuvan kuvaama esimerkki ohjelmakoodina.

List<Integer> lista = new ArrayList<>();
lista.add(3);
lista.add(7);
lista.add(4);
lista.add(2);
lista.add(6);

ArrayList<Integer> luvut = lista.stream()
    .filter(luku -> luku > 5)
    .map(luku -> luku * 2)
    .collect(Collectors.toCollection(ArrayList::new));

Pääteoperaatiot

Tarkastellaan tässä neljää pääteoperaatiota: listan arvojen lukumäärän selvittämistä count-metodin avulla, listan arvojen läpikäyntiä forEach-metodin avulla sekä listan arvojen keräämistä tietorakenteeseen collect-metodin avulla, sekä listan alkioiden yhdistämistä reduce-metodin avulla.

Metodi count kertoo virran alkioiden lukumäärän long-tyyppisenä muuttujana.

List<Integer> luvut = new ArrayList<>();
luvut.add(3);
luvut.add(2);
luvut.add(17);
luvut.add(6);
luvut.add(8);

System.out.println("Lukuja: " + luvut.stream().count());
Lukuja: 5

Metodi forEach kertoo mitä kullekin listan arvolle tulee tehdä ja samalla päättää virran käsittelyn. Alla olevassa esimerkissä luodaan ensin numeroita sisältävä lista, jonka jälkeen tulostetaan vain kahdella jaolliset luvut.

List<Integer> luvut = new ArrayList<>();
luvut.add(3);
luvut.add(2);
luvut.add(17);
luvut.add(6);
luvut.add(8);

luvut.stream()
    .filter(luku -> luku % 2 == 0)
    .forEach(luku -> System.out.println(luku));
2
6
8

Virran arvojen kerääminen toiseen kokoelmaan onnistuu metodin collect avulla. Alla olevassa esimerkissä luodaan uusi lista annetun positiivisista arvoista. Metodille collect annetaan parametrina Collectors-luokan avulla luotu olio, johon virran arvot kerätään -- esimerkiksi kutsu Collectors.toCollection(ArrayList::new) luo uuden ArrayList-olion, johon arvot kerätään.

List<Integer> luvut = new ArrayList<>();
luvut.add(3);
luvut.add(2);
luvut.add(-17);
luvut.add(-6);
luvut.add(8);

ArrayList<Integer> positiiviset = luvut.stream()
    .filter(luku -> luku > 0)
    .collect(Collectors.toCollection(ArrayList::new));

positiiviset.stream()
    .forEach(luku -> System.out.println(luku));
3
2
8

Tehtävässä harjoitellaan virran filter ja collect-metodien käyttöä.

Tehtäväpohjassa on annettuna metodirunko public static ArrayList<Integer> jaolliset(ArrayList<Integer> luvut). Toteuta metodirunkoon toiminnallisuus, kerää parametrina saadulta listalta kahdella, kolmella tai viidellä jaolliset luvut, ja palauttaa ne uudessa listassa. Metodille parametrina annetun listan ei tule muuttua.

ArrayList<Integer> luvut = new ArrayList<>();
luvut.add(3);
luvut.add(2);
luvut.add(-17);
luvut.add(-5);
luvut.add(7);
    
ArrayList<Integer> jaolliset = jaolliset(luvut);

jaolliset.stream()
    .forEach(luku -> System.out.println(luku));
3
2
-5

Metodi reduce 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

Välioperaatiot

Virran välioperaatiot ovat metodeja, jotka palauttavat arvonaan virran. Koska palautettava arvo on virta, voidaan välioperaatioita kutsua peräkkäin. Tyypillisiä välioperaatioita ovat arvon muuntaminen muodosta toiseen map sekä sen erityistapaus mapToInt, arvojen rajaaminen filter, uniikkien arvojen tunnistaminen distinct sekä arvojen järjestäminen sorted (mikäli mahdollista).

Tarkastellaan näitä metodeja muutaman ongelman avulla. Oletetaan, että käytössämme on seuraava luokka Henkilo.

public class Henkilo {
    private String etunimi;
    private String sukunimi;
    private int syntymavuosi;

    public Henkilo(String etunimi, String sukunimi, int syntymavuosi) {
        this.etunimi = etunimi;
        this.sukunimi = sukunimi;
        this.syntymavuosi = syntymavuosi;
    }

    public String getEtunimi() {
        return this.etunimi;
    }

    public String getSukunimi() {
        return this.sukunimi;
    }

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

Ongelma 1: Saat käyttöösi listan henkilöitä. Tulosta ennen vuotta 1970 syntyneiden henkilöiden lukumäärä.

Käytetään filter-metodia henkilöiden rajaamiseen niihin, jotka ovat syntyneet ennen vuotta 1970. Lasketaan tämän jälkeen henkilöiden lukumäärä metodilla count.

// oletetaan, että käytössämme on lista henkiloita
// ArrayList<Henkilo> henkilot = new ArrayList<>();

long lkm = henkilot.stream()
    .filter(henkilo -> henkilo.getSyntymavuosi() < 1970)
    .count();
System.out.println("Lukumäärä: " + lkm);

Ongelma 2: Saat käyttöösi listan henkilöitä. Kuinka monen henkilön etunimi alkaa kirjaimella "A"?

Käytetään filter-metodia henkilöiden rajaamiseen niihin, joiden etunimi alkaa kirjaimella "A". Lasketaan tämän jälkeen henkilöiden lukumäärä metodilla count.

// oletetaan, että käytössämme on lista henkiloita
// ArrayList<Henkilo> henkilot = new ArrayList<>();

long lkm = henkilot.stream()
    .filter(henkilo -> henkilo.getEtunimi().startsWith("A"))
    .count();
System.out.println("Lukumäärä: " + lkm);

Ongelma 3: Saat käyttöösi listan henkilöitä. Tulosta henkilöiden uniikit etunimet aakkosjärjestyksessä.

Käytetään ensin map-metodia, jonka avulla henkilö-olioita sisältävä virta muunnetaan etunimiä sisältäväksi virraksi. Tämän jälkeen kutsutaan metodia distinct, joka palauttaa virran, jossa on uniikit arvot. Seuraavaksi kutsutaan metodia sorted, joka järjestää merkkijonot. Lopulta kutsutaan metodia forEach, jonka avulla tulostetaan merkkijonot.

// oletetaan, että käytössämme on lista henkiloita
// ArrayList<Henkilo> henkilot = new ArrayList<>();

henkilot.stream()
    .map(henkilo -> henkilo.getEtunimi())
    .distinct()
    .sorted()
    .forEach(nimi -> System.out.println(nimi));

Yllä kuvattu distinct-metodi hyödyntää olioiden equals-metodia yhtäsuuruuden tarkasteluun. Metodi sorted taas osaa järjestää olioita, joilla on tieto siitä, miten olio tulee järjestää -- näitä ovat esimerkiksi luvut ja merkkijonot.

Kirjoita ohjelma, joka lukee käyttäjältä merkkijonoja. Lukeminen tulee lopettaa kun käyttäjä syöttää tyhjän merkkijonon. Tulosta tämän jälkeen käyttäjän syöttämät merkkijonot.

eka
toka
kolmas
eka
toka
kolmas

Kirjoita ohjelma, joka lukee käyttäjältä lukuja. Kun käyttäjä syöttää negatiivisen luvun, lukeminen lopetetaan. Tulosta tämän jälkeen ne luvut, jotka ovat välillä 1-5.

7
14
4
5
4
-1
4
5
4

Tehtäväpohjaan on hahmoteltu ohjelmaa, joka lukee käyttäjältä syötteenä henkilötietoja. Täydennä ohjelmaa siten, että tietojen lukemisen jälkeen ohjelma tulostaa henkilöiden uniikit sukunimet aakkosjärjestyksessä.

Syötetäänkö henkilöiden tietoja, "loppu" lopettaa: 
Syötä etunimi: Ada
Syötä sukunimi: Lovelace
Syötä syntymävuosi: 1815

Syötetäänkö henkilöiden tietoja, "loppu" lopettaa: 
Syötä etunimi: Grace
Syötä sukunimi: Hopper
Syötä syntymävuosi: 1906

Syötetäänkö henkilöiden tietoja, "loppu" lopettaa: 
Syötä etunimi: Alan
Syötä sukunimi: Turing
Syötä syntymävuosi: 1912
    
Syötetäänkö henkilöiden tietoja, "loppu" lopettaa: loppu
    
Uniikit sukunimet aakkosjärjestyksessä:
Hopper
Lovelace
Turing

Ohjelmassa ei ole valmiita automaattisia testejä. Voit kirjoittaa automaattisia testejä testiluokkaan UniikitSukunimetTest -- tässä tapauksessa olisi näppärää tehdä esimerkiksi erillinen listan palauttava metodi uniikkien sukunimien tunnistamiseen sille parametrina annetusta henkilölistasta.

Oliot ja virta

Olioiden käsittely virran metodien avulla on luontevaa. Kukin virran metodi, missä käsitellään virran arvoja, mahdollistaa myös arvoihin liittyvän metodin kutsumisen. Tarkastellaan esimerkkiä, missä käytössämme on Kirjoja, joilla on kirjailijoita. Luokat Henkilo ja Kirja on annettu alla.

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

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

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

    public String toString() {
        return this.nimi + " (" + this.syntymavuosi + ")";
    }
}
public class Kirja {
    private Henkilo kirjailija;
    private String nimi;
    private int sivujenLukumaara;
  
    public Kirja(Henkilo kirjailija, String nimi, int sivuja) {
        this.kirjailija = kirjailija;
        this.nimi = nimi;
        this.sivujenLukumaara = sivuja;
    }

    public Henkilo getKirjailija() {
        return this.kirjailija;
    }

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

    public int getSivujenLukumaara() {
        return this.sivujenLukumaara;
    }
}

Oletetaan, että käytössämme on lista kirjoja. Virran metodien avulla esimerkiksi kirjailijoiden syntymävuosien keskiarvon selvittäminen onnistuu luontevasti. Ensin muunnamme kirjoja sisältävän virran henkilöitä sisältäväksi virraksi ja tämän jälkeen muunnamme henkilöitä sisältävän virran syntymävuosia sisältäväksi virraksi. Lopulta pyydämme (kokonaislukuja sisältävältä) virralta keskiarvoa.

// oletetaan, että käytössämme on lista kirjoja
// List<Kirja> kirjat = new ArrayList<>();

double keskiarvo = kirjat.stream()
    .map(kirja -> kirja.getKirjailija())
    .mapToInt(kirjailija -> kirjailija.getSyntymavuosi())
    .average()
    .getAsDouble();

System.out.println("Kirjailijoiden syntymävuosien keskiarvo: " + keskiarvo);

// muunnoksen kirjasta kirjailijan syntymävuoteen pystyisi tekemään myös yhdellä map-kutsulla
// double keskiarvo = kirjat.stream()
//     .mapToInt(kirja -> kirja.getKirjailija().getSyntymavuosi())
//     ...

Vastaavasti kirjojen, joiden nimessä esiintyy sana "Potter", kirjailijoiden nimet saa selville seuraavasti.

// oletetaan, että käytössämme on lista kirjoja
// List<Kirja> kirjat = new ArrayList<>();

kirjat.stream()
    .filter(kirja -> kirja.getNimi().contains("Potter"))
    .map(kirja -> kirja.getKirjailija())
    .forEach(kirjailija -> System.out.println(kirjailija));

Myös monimutkaisempien merkkijonoesitysten rakentaminen on virran avulla mahdollista. Alla olevassa esimerkissä tulostamme "Kirjailijan nimi: Kirja" -parit aakkosjärjestyksessä.

// oletetaan, että käytössämme on lista kirjoja
// ArrayList<Kirja> kirjat = new ArrayList<>();

kirjat.stream()
    .map(kirja -> kirja.getKirjailija().getNimi() + ": " + kirja.getNimi())
    .sorted()
    .forEach(nimi -> System.out.println(nimi));

Tehtäväpohjassa on tutuhko tehtävä "Tavara, Matkalaukku ja Lastiruuma". Tässä tehtävässä tarkoituksenasi on muuttaa toistolausetta käyttävät metodit virtaa käyttäviksi metodeiksi. Lopputuloksessa ei tule esiintyä while (...) tai for (...)-toistolauseita.

Tiedostot ja virta

Virta on myös erittäin näppärä tiedostojen käsittelyssä. Tiedoston lukeminen virtamuotoisena tapahtuu Javan valmiin Files-luokan avulla. Files-luokan metodin lines avulla tiedostosta voidaan luoda syötevirta, jonka avulla tiedoston rivit voidaan käsitellä yksi kerrallaan. Metodi lines saa patametrikseen polun, joka luodaan luokan Paths tarjoamalla metodilla get, jolle annetaan parametrina tiedostopolkua kuvaava merkkijono.

Alla olevassa esimerkissä luetaan tiedoston "tiedosto.txt" kaikki rivit ja lisätään ne listaan.

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

// tee jotain luetuilla riveillä

Jos tiedosto löytyy ja sen lukeminen onnistuu, tulee ohjelman suorituksen lopussa tiedoston "tiedosto.txt" rivit olemaan listamuuttujassa rivit. Jos taas tiedostoa ei löydy, tai sen lukeminen epäonnistuu, ilmoitetaan tästä virheviestillä. Alla eräs mahdollisuus:

Virhe: tiedosto.txt (No such file or directory)

Virran metodit tekevät määritellyn muotoisten tiedostojen lukemisesta melko suoraviivaista. Tarkastellaan tilannetta, missä tiedosto sisältää henkilöiden tietoja. Kukin henkilö on omalla rivillään, ensin tulee henkilön nimi, sitten puolipiste, sitten henkilön syntymävuosi. Tiedoston muoto on seuraava.

Kaarlo Juho Ståhlberg; 1865
Lauri Kristian Relander; 1883
Pehr Evind Svinhufvud; 1861
Kyösti Kallio; 1873
Risto Heikki Ryti; 1889
Carl Gustaf Emil Mannerheim; 1867
Juho Kusti Paasikivi; 1870
Urho Kaleva Kekkonen; 1900
Mauno Henrik Koivisto; 1923
Martti Oiva Kalevi Ahtisaari; 1937
Tarja Kaarina Halonen; 1943
Sauli Väinämö Niinistö; 1948

Oletetaan, että tiedoston nimi on presidentit.txt. Henkilöiden lukeminen onnistuu seuraavasti.

List<Henkilo> presidentit = new ArrayList<>();
try {
    // luetaan tiedosto "presidentit.txt" riveittäin
    Files.lines(Paths.get("presidentit.txt"))
        // pilkotaan rivi osiin ";"-merkin kohdalta 
        .map(rivi -> rivi.split(";"))
        // poistetaan ne pilkotut rivit, joissa alle 2 osaa
        // (haluamme että rivillä on aina nimi ja syntymävuosi)
        .filter(palat -> palat.length >= 2)
        // luodaan palojen perusteella henkilöitä
        .map(palat -> new Henkilo(palat[0], Integer.parseInt(palat[1])))
        // ja lisätään henkilöt lopulta listalle
        .forEach(henkilo -> presidentit.add(henkilo));
} catch (Exception e) {
    System.out.println("Virhe: " + e.getMessage());
}

// nyt presidentit ovat listalla henkilöolioina

Esimerkki: tekstin luominen

Materiaalin osassa 3 esiintynyt "Ajatustenlukija" sekä materiaalin osassa 6 esiintynyt "Kivi, Paperi, Sakset" perustuivat ajatukselle siitä, että pelaajan aiempia siirtoja voidaan hyödyntää tulevaisuuden ennustamisessa. Sama periaate -- eli aiemman tiedon hyödyntyminen tulevan määrittelyssä -- toimii myös tekstin luomisessa. Voimme "oppia" tekstiä ja käyttää sitä uuden tekstin luomiseen.

Tarkastellaan seuraavaa katkelmaa 1990-luvun hittikappaleesta Scatman (Ski-Ba-Bop-Ba-Dop-Bop).

ski-bi dibby dib yo da dub dub
yo da dub dub
ski-bi dibby dib yo da dub dub
yo da dub dub

Tässä esiteltävässä tekstin luomismenetelmässä periaatteena on tarkastella kutakin tekstin sanaparia laskea sanapareja seuraavien sanojen lukumäärät. Aloitetaan. Ensimmäinen sanapari on ski-bi dibby ja sitä seuraa sana dib.

ski-bi dibby dib yo da dub dub
yo da dub dub
ski-bi dibby dib yo da dub dub
yo da dub dub

Pidämme kirjaa esiintymien lukumääristä, yllä huomaamme esiintymän "ski-bi dibby -> dib". Lukumäärien ylläpitoon sopisi esimerkiksi kaksiulotteinen hajautustaulu.

ski-bi dibby -> dib: 1

Seuraavaksi tarkastellaan sanaparia dibby dib. Tätä seuraa sana yo.

ski-bi dibby dib yo da dub dub
yo da dub dub
ski-bi dibby dib yo da dub dub
yo da dub dub

Esiintymien lukumäärät päivittyvät taas.

ski-bi dibby -> dib: 1
dibby dib -> yo: 1

Sanapari siirtyy yhdellä, ja esiintymien lukumäärät päivittyvät.

ski-bi dibby dib yo da dub dub
yo da dub dub
ski-bi dibby dib yo da dub dub
yo da dub dub
ski-bi dibby -> dib: 1
dibby dib -> yo: 1
dib yo > da: 1

Tätä jatketaan kunnes koko tekstidokumentti on käyty läpi. Tekstin läpikäynnin jälkeen esiintymien lukumäärät (opittu tekstimalli) ovat seuraavat.

ski-bi dibby -> dib: 2
dibby dib -> yo: 2
dib yo > da: 2
yo da > dub: 4
da dub > dub: 4
dub dub > yo: 2
dub yo > da: 2
dub dub > ski-bi: 1
dub ski-bi > dibby: 1

Sanaparia ski-bi dibby seuraa aina sana dib, sanaparia dibby dib seuraa aina sana yo, sanaparia dib yo seuraa aina sana da, sanaparia yo da seuraa aina sana dub, sanaparia da dub seuraa aina sana dub. Eli tähän mennessä tekstimalli on aika yksinkertainen.

Mutta! Sanaparia dub dub seuraa sana yo kaksi kertaa kolmesta, ja sana ski-bi kerran kolmesta. Voimme siis ajatella niin, että sanaparin dub dub kohdalla noin 33% todennäköisyydellä seuraava sana on ski-bi, ja noin 67% todennäköisyydellä seuraava sana on yo.

Yllä kuvattua tekstimallia voidaan tarkastella myös verkkona, missä jokainen solmu (pallo) on sanapari, ja solmujen välillä kulkevat kaaret kuvaavat sanojen välisiä yhteyksiä. Kaarien kohdalle merkitään todennäköisyydet sille, että sanaparin jälkeen valitaan tietty toinen sanapari. Alla kaikissa muissa tilanteissa päätös on selvä, mutta sanaparin dub dub jälkeen on kaksi vaihtoehtoa.

Lyriikat verkkona kuvattuna

Tekstin tuottaminen yllä kuvatulla mallilla on suoraviivaista. Oletetaan, että aloitamme sanaparista yo da.

yo da

Sanaparia yo da seuraa aina sanapari da dub.

yo da dub

Ja sanaparia da dub seuraa aina sanapari dub dub.

yo da dub dub

Sanaparia dub dub seuraa 66.6% todennäköisyydellä sanapari dub yo, ja 33.3% todennäköisyydellä sanapari dub ski-bi. Tässä kohtaa hyödyntäisimme esimerkiksi Javan luokka Random ja valitsisimme sanan edellä kuvatun vaihtoehdon väliltä satunnaisesti. Mikäli Random-luokan tuottama satunnaisluku lukujen 0 ja 1 välillä on pienempi tai yhtäsuuri kuin 0.666%, valitsemme dub yo, muulloin dub ski-bi.

Ja tekstin generointi jatkuu..

Edellisessä esimerkissä lähtökohtana käytetty tekstidokumentti on melko suppea, eikä vaihtelua juurikaan tapahdu. Tarkastellaan tekstin luomista, mutta käytetään opittavana tekstinä Ylen kuntavaalidataa. Tavoitteena tässä tehtävässä on luoda vaalipuhegeneraattori, joka pyrkii luomaan perustelun kysymykselle "Miksi juuri sinut kannattaisi valita kunnanvaltuustoon?".

Tehtäväpohjassa on mukana dokumentti vaalidata.txt, joka sisältää osajoukon Ylen tarjoamasta vaalidatasta. Tekstidokumentti on jaettu sarakkeisiin puolipisteiden perusteella. Ensimmäisessä sarakkeessa on tieto siitä, tuliko ehdokas valituksi (0 tai 1), toisessa sarakkeessa on perustelu kysymykseen "Miksi juuri sinut kannattaisi valita kunnanvaltuustoon?", ja kolmannessa sarakkeessa on perustelu kysymykseen "Mitä asioita haluat edistää tai ajaa tulevalla vaalikaudella?". Tässä tehtävässä käytetty vaalidatatiedosto on lisensoitu Creative Commons CC BY-SA-lisenssillä alkuperäisen tiedoston tavoin.

Tiedoston ensimmäiset kolme riviä ovat seuraavat:

1;olen aikaansaava ja aktiivinen luottamushenkilö joka pitää kuntalaisten puolia kuuntelen selvitän ja vien asioita eteenpäin hoidan koko sydämelläni kotiseutuni asioita ;edistää elinvointia kunnan tehtävä on huolehtia asukkaidensa hyvinvoinnista haluan tehdä edelleen aloitteita joissa on vahva terveyttä edistävä näkökulma haluan olla edelleen kaikkien kuntalaisten käytettävissä tiedän että kykenen vaikuttamaan asioihin oma alotteisesti sekä viemään kuntalaisten viestiä eteenpäin aina päätöksentekoon saakka tulevalla vaalikaudella meidän on panostettava ympäristönhoitoon jotta kunnassamme viihtyisi ja se houkuttelisi myös uusia asukkaita
0;olen ratkaisukeskeinen yhteistyökykyinen ja avoin henkilö teen aina päätöksiin vaadittavan taustatyön huolellisesti arvostan tasa arvoa ja vastuullisuutta yhteisten asioiden hoidossa ;päätösten pitkäjänteisyyttä helsinkiläisten aitoa kuuntelua kulttuuritarjonnan monipuolisuutta ja saatavuutta tietoisuutta jo olemassaolevasta tapahtumakirjosta koko kaupungin pitää olla turvallinen paikka asua syrjäytyneet nuoret tarvitsevat pikaisesti apua meillä ei ole varaa menettää yhtään sukupolvea kaikki ovat arvokkaita helsinki on koko suomen käyntikortti hyvä talous koulutus kulttuuri ja joukkoliikenne näiden turvaaminen ja säilyminen ovat kaikille tärkeitä
0;uuden salon aikaiset kaksi valtuustokautta ovat tuoneet hyvän perehtyneisyyden ja hyvät verkostot kaupungin asioiden hoitoon nykyinen elämäntilanne antaa hyvin aikaa yhteisten asioiden hoitoon ;yrittäjänä näen kaupungin elinvoimapolitiikan edistämisen ensisijaiseksi toinen tärkeä asia tulevaisuuden kunnassa on sivistys siinä perusasioita ovat tietysti hyvä varhaiskasvatus ja perusopetus ja muu koulutus sivistyksen sektorilla erityisesti lähinnä sydäntäni on kulttuuri ja kirjasto säilytetään kattavat kirjastopalvelut kaupungin resurssit ovat rajalliset ja vastuun kulttuuriasioista hoitaa pääasiassa kolmas sektori kaupungin tuki sellaiselle tasolle että tekijöiden työn ilo säilyy
  

Luodaan vaalipuhegeneraattori osissa. Teemme ensin luokan Sanajakauma, joka pitää kirjaa yksittäisten sanojen esiintymisisistä ja tarjoaa satunnaisia sanoja niiden esiintymistodennäköisyyksiin perustuen. Tämän jälkeen luomme luokan Tekstimalli, joka pitää kirjaa jokaiseen sanapariin liittyvistä sanajakaumista. Lopulta teemme luokan Puhegeneraattori, joka luo tekstiä annetun tekstimallin perusteella.

Edellisestä esimerkistä poiketen tekstimallia rakennettaessa otetaan huomioon vain peräkkäiset sanat, ei sanapareja. Esimerkiksi merkkijono "olen ratkaisukeskeinen yhteistyökykyinen ja avoin henkilö teen aina päätöksiin vaadittavan taustatyön huolellisesti arvostan tasa arvoa ja vastuullisuutta yhteisten asioiden hoidossa" tuottaa seuraavanlaisen tekstimallin.

olen -> ratkaisukeskeinen: 1
ratkaisukeskeinen -> yhteistyökykyinen: 1
yhteistyökykyinen -> ja: 1
ja -> avoin: 1
avoin -> henkilö: 1
henkilö -> teen: 1
teen -> aina: 1
aina -> päätöksiin: 1
päätöksiin -> vaadittavan: 1
vaadittavan -> taustatyön: 1
taustatyön -> huolellisesti: 1
huolellisesti -> arvostan: 1
arvostan -> tasa: 1
tasa -> arvoa: 1
arvoa -> ja: 1
ja -> vastuullisuutta: 1
vastuullisuutta -> yhteisten: 1
yhteisten -> asioiden: 1
asioiden -> hoidossa: 1
  

Koko vaalidata-aineiston hyödyntäminen mahdollistaa esimerkiksi seuraavanlaisten puheiden luomisen: tuijottamatta puoluerajoja olen kyvykäs ottamaan asioista selvää ja ensimmäistä kautta kunnallisesta päätöksenteosta olen aktiivinen eläkeläisten seurassa.

Sanajakauma, osa 1

Tehtäväpohjassa tulee valmiina seuraavanlainen luokka Sanajakauma. Sanajakauman tehtävänä on pitää kirjaa sanojen esiintymisistä ja tarjota sanoja niiden esiintymisten perusteella.

package vaalit;

public class Sanajakauma {
    
    public void lisaaSana(String sana) {
    }
    
    public int esiintymiskertoja(String sana) {
        return 0;
    }

    public String annaSana() {
        return null;
    }
}

Täydennä luokan toimintaa. Luokan tulee toimia seuraavasti.

Sanajakauma jakauma = new Sanajakauma();
jakauma.lisaaSana("yo");
jakauma.lisaaSana("yo");
jakauma.lisaaSana("ski-bi");

System.out.println(jakauma.esiintymiskertoja("yo"));
System.out.println(jakauma.esiintymiskertoja("ski-bi"));
System.out.println(jakauma.esiintymiskertoja("dub"));

System.out.println(jakauma.annaSana());
2
1
0
yo

Metodin annaSana palauttama arvo tulee valita satunnaisesti kaikkien sanojen niin, että kunkin sanan valinta on yhtä todennäköistä. Yllä kumpikin sanoista "yo" ja "ski-bi" tulee siis palauttaa metodikutsusta annaSana 50% todennäköisyydellä.

Mikäli olioon ei ole lisätty vielä yhtäkään sanaa, tulee metodin annaSana palauttaa null-viite.

Sanajakauma, osa 2

Tässä osassa kehität metodin annaSana-toiminnallisuutta siten, että metodikutsun palauttaman merkkijonon todennäköisyys perustuu merkkijonon esiintymien todennäköisyyteen.

Oletetaan, että sanajakaumaan lisätään kolme sanaa, "yo", "yo", ja "ski-bi". Nyt sanajakauman tulee tietää, että sana "yo" on esiintynyt kahdesti ja sana "ski-bi" on esiintynyt kerran. Mikäli sanajakaumalta kysytään nyt sanaa, tulee sen palauttaa 2/3 kyselyistä eli 66.66..% todennäköisyydellä sana yo, ja 1/3 kyselyistä eli 33.333...% todennäköisyydellä sana ski-bi.

Eräs tapa, millä satunnaisesti valitun sanan valintaa voi suoraviivaistaa, on hyödyntää kaikkien sanojen esiintymiskertoja satunnaisen sanan valitsemisessa. Oletetaan, että sanat ja niiden esiintymiskerrat ovat seuraavat:

yo: 4
ski-bi: 2
heh: 3
  

Sanojen esiintymiskertoja on yhteensä 9. Javan Random-luokalta voi pyytää satunnaista lukua nollan ja yhdeksän välillä (yhdeksän poislukien): int luku = new Random().nextInt(9);. Oletetaan, että luvuksi tulee 8. Nyt sanoja voidaan käydä läpi yksitellen siten, että pidämme kirjaa esiintymiskertojen summasta ja etsimme sen avulla arvottua lukua vastaavan sanan:

    Sana yo, esiintymiskertoja 4, esiintymiskertojen summa 4
    Sana ski-bi, esiintymiskertoja 2, esiintymiskertojen summa 6
    Sana heh, esiintymiskertoja 3, esiintymiskertojen summa 9 --> palauta heh.
  

Toisaalta, mikäli satunnaisesti valituksi luvuksi tulee 4, toimitaan seuraavasti:

    Sana yo, esiintymiskertoja 4, esiintymiskertojen summa 4
    Sana ski-bi, esiintymiskertoja 2, esiintymiskertojen summa 6 --> palauta ski-bi
  

Tekstimalli, osa 1

Tekstimallin tulee sanajakaumaa sopivasti hyödyntäen pitää kirjaa jokaista sanaa seuraavista sanoista sekä niiden esiintymiskerroista. Tehtäväpohjassa tulee valmiina seuraavanlainen luokka Tekstimalli.

package vaalit;

public class Tekstimalli {

    public void lisaaAineisto(String aineisto) {
    }

    public String annaSana(String edeltava) {
        return null;
    }

    public List<String> sanat() {
        return null;
    }
}

Toteuta tässä metodit public void lisaaAineisto(String aineisto), joka saa parametrinaan tekstiaineistoa kuvaavan merkkijonon, sekä public List<String> sanat(), joka palauttaa uniikit sanat listana.

Ohjelman tulee toimia seuraavasti:

Tekstimalli malli = new Tekstimalli();
malli.lisaaAineisto("olen kyvykäs ottamaan asioista selvää");
malli.lisaaAineisto("olen aktiivinen ja urheilullinen");
malli.lisaaAineisto("olen rauhallinen ja iloinen");

for (String sana: malli.sanat()) {
    System.out.println(sana);
}
olen
kyvykäs
ottamaan
asioista
selvää
aktiivinen
ja
urheilullinen
rauhallinen
iloinen

Metodin lisaaAineisto tulee siis lisätä yksittäiset sanat tekstimalliin, ja metodin sanat tulee palauttaa uniikit sanat tekstimallista. Sanojen järjestyksellä ei metodin sanat palauttamassa listassa ole väliä. Voit olettaa, että sanat ovat eroteltu toisistaan välilyönneillä.

Hyödynnä metodissa public void lisaaAineisto(String aineisto) String-luokan tarjoamaa metodia split. Metodi toimii seuraavasti:

String merkkijono = "eka toka kolmas neljäs";
String[] palat = merkkijono.split("\\s+");
System.out.println(palat[0]);
System.out.println(palat[1]);
System.out.println(palat[2]);
System.out.println(palat[3]);
eka
toka
kolmas
neljäs

Mikäli sanoja ei ole lainkaan, metodin sanat tulee palauttaa tyhjä lista.

Tekstimalli, osa 2

Muokkaa luokan Tekstimalli toimintaa siten, että tekstimallia rakennettaessa hyödynnetään aineiston peräkkäisiä sanoja. Esimerkiksi merkkijono "olen ratkaisukeskeinen yhteistyökykyinen ja avoin henkilö teen aina päätöksiin vaadittavan taustatyön huolellisesti arvostan tasa arvoa ja vastuullisuutta yhteisten asioiden hoidossa" tuottaa seuraavanlaisen tekstimallin.

olen -> ratkaisukeskeinen: 1
ratkaisukeskeinen -> yhteistyökykyinen: 1
yhteistyökykyinen -> ja: 1
ja -> avoin: 1
avoin -> henkilö: 1
henkilö -> teen: 1
teen -> aina: 1
aina -> päätöksiin: 1
päätöksiin -> vaadittavan: 1
vaadittavan -> taustatyön: 1
taustatyön -> huolellisesti: 1
huolellisesti -> arvostan: 1
arvostan -> tasa: 1
tasa -> arvoa: 1
arvoa -> ja: 1
ja -> vastuullisuutta: 1
vastuullisuutta -> yhteisten: 1
yhteisten -> asioiden: 1
asioiden -> hoidossa: 1
  

Tekstimalliin tulee pystyä lisäämään useita tekstiaineistoja. Alla on annettuna esimerkki ohjelman toiminnasta.

Tekstimalli malli = new Tekstimalli();
malli.lisaaAineisto("olen kyvykäs ottamaan asioista selvää");
malli.lisaaAineisto("olen aktiivinen ja urheilullinen");

System.out.println(malli.annaSana("ottamaan"));
System.out.println(malli.annaSana("aktiivinen"));
System.out.println(malli.annaSana("ja"));
System.out.println(malli.annaSana("olen"));
System.out.println(malli.annaSana("olen"));
System.out.println(malli.annaSana("olen"));
System.out.println(malli.annaSana("kumiankka"));
System.out.println(malli.annaSana("selvää"));
asioista
ja
urheilullinen
aktiivinen
aktiivinen
kyvykäs
null
null

Huomaa, että tekstimallissa sanaa "olen" seuraa sekä sana "kyvykäs" että sana "aktiivinen". Yllä kummankin sanan todennäköisyys sanan "olen" seuraajana on 50%. Syötettävät aineistot tulee myös käsitellä erillisinä -- esimerkiksi yllä olevassa esimerkissä sanan "selvää" seuraaja ei ole sana "olen".

Puhegeneraattori

Puhegeneraattorin tulee tekstimallia sopivasti hyödyntäen pitää tarjota mahdollisuus aineiston lukemiseen sekä puheen tuottamiseen. Tehtäväpohjassa on valmiina seuraavanlainen luokka Puhegeneraattori.

package vaalit;

public class Puhegeneraattori {

    public void lue(String tiedosto) {
    }

    public String tuotaPuhetta(int sanojaEnintaan) {
        return null;
    }
}

Toteuta tässä metodit public void lue(String tiedosto), joka saa parametrinaan luettavan tiedoston nimen ja luo tiedostossa olevasta datasta käytettävän tekstimallin, sekä public String tuotaPuhetta(int sanojaEnintaan), joka tuottaa tekstimallista puhetta, jossa on korkeintaan annettu määrä sanoja.

Metodien tulee tarkemmin ottaen toimia seuraavasti:

Metodi public void lue(String tiedosto) lukee paramatetrina annetun tiedoston, joka on tehtävänannon alussa annettua muotoa. Jokaiselta riviltä tulee ottaa puolipisteillä eroteltu alue, missä ehdokas vastaa kysymykseen "Miksi juuri sinut kannattaisi valita kunnanvaltuustoon?" -- kun rivi pilkotaan puolipisteillä osiksi, kysymys löytyy indeksistä 1. Kukin rivi tulee lisätä tekstimalliin.

Metodi public String tuotaPuhetta(int sanoja) valitsee tekstimallin tarjoamasta sanalistasta satunnaisen sanan alkusanaksi. Tämän jälkeen tekstimalli tuottaa puhetta tekstimallia hyödyntäen -- alkusanalla saadaan seuraava sana, seuraavalla sanalla sitä seuraava ymym. Alla on esimerkki satunnaisen sanan valitsemiseksi listalta.

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

Collections.shuffle(lista);

String satunnainen = lista.get(0);

Mikäli puhetta tuotettaessa päädytään sanaan, jolle ei löydy seuraajaa, puheen tuottaminen loppuu ja puhe palautetaan. Muulloin puhe palautetaan kun puheessa on metodille parametrina annettu määrä sanoja. Esimerkiksi, jos tekstimalli on rakennettu tekstistä "olipa kerran ihminen", ei tekstimallin perusteella luodussa puheessa tule sanan "ihminen" jälkeen enää sanoja.

Kun olet saanut Puhegeneraattorin luotua, seuraava ohjelma tuottaa satunnaista puhetta.

Puhegeneraattori generaattori = new Puhegeneraattori();
generaattori.lue("vaalidata.csv");
System.out.println(generaattori.tuotaPuhetta(10));
esiille mielipiteeni ja eri näkökulmat kiista tilanteissa kuntalaistemme hyvinvoinnin parantamiseksi

Tässä tehtävässä kannattaa tarkastella myös tehtäväpohjan yksikkötestejä.

Tekoälyä

Tehtävät, joissa opitaan historiasta ja sovelletaan opittua tulevaan, ovat oikeastaan esimerkkejä tekoälystä. Tekoälyalgoritmeja on toki useita muitakin. Näihinkin tutustutaan tietojenkäsittelytieteen perusopinnoissa.

Sisällysluettelo