Ohjelmoinnin jatkokurssi

Matti Paksula, Arto Vihavainen, Matti Luukkainen

Metodien nimeäminen

Javassa puhutaan usein gettereistä ja settereistä, eli metodeista jotka joko palauttavat tai asettavat arvoja. Kirjoitetaan tästä eteenpäin arvon asettavat metodit muodossa setMuuttujanNimi(), ja arvon palauttavat metodit muodossa getMuuttujanNimi(). Alla olevassa luokassa Elain on oikein nimetyt getterit ja setterit.

public class Elain {
  private String nimi;
  private double pituus;

  public Elain(String nimi, double pituus) {
    this.nimi = nimi;
    this.pituus = pituus;
  }
  
  public double getPituus() {
    return this.pituus;
  }
  
  public void setPituus(double pituus) {
    this.pituus = pituus;
  }
  
  public String getNimi() {
    return this.nimi;
  }
  
  public void setNimi(String nimi) {
    this.nimi = nimi;
  }
}

Getterit ja Setterit voidaan luoda automaattisesti useimmissa IDEissä, esimerkiksi NetBeanssissa niiden luominen tapahtuu valitsemalla Source -> Insert Code -> Getter and Setter... ja valitsemalla muuttujat joille getterit ja setterit tehdään.

Periytyminen

Periytymisellä tarkoitetaan sitä kun luokka saa, eli perii, jo olemassa olevan luokan ominaisuudet. Periytymisen hyötynä on ohjelmointityön väheneminen, kun jo valmiiksi olemassaolevia toteutuksia voidaan käyttää pohjana uusille toteutuksille. Luodaan luokka Tehdas, jolla on metodi soitaPillia().

public class Tehdas {
  public void soitaPillia() {
    System.out.println("Piip!");
  }
}

Periytyminen tapahtuu avainsanalla extends, jota seuraa perittävän luokan nimi. Luokka voi periä aina vain yhden luokan. Luodaan seuraavaksi luokka OhjelmistoTehdas, joka perii luokan Tehdas toiminnot. Ohjelmistotehtaalla on myös metodi tuotaKoodia(), joka palauttaa lähdekoodia merkkijonona.

public class OhjelmistoTehdas extends Tehdas {
  public String tuotaKoodia() {
    return "System.out.println(\"HelloWorld!\");";
  }
}

Koska luokka OhjelmistoTehdas perii luokan Tehdas, on sillä käytössä myös Tehtaan metodi soitaPillia(). Voimme siis käyttää yllä olevaa ohjelmistotehdasta seuraavasti.

OhjelmistoTehdas t = new OhjelmistoTehdas();
t.soitaPillia();
System.out.println(t.tuotaKoodia());

Konstruktorikutsun new OhjelmistoTehdas() voi tehdä koska luokka Tehdas sisältää parametrittoman oletuskonstruktorin. Perivän luokan konstruktoria kutsuttaessa kutsutaan automaattisesti perityn luokan parametritonta konstruktoria. Saamma ylläolevasta esimerkistä seuraavan tulosteen.

Piip!
System.out.println("HelloWorld!");

Perittyä luokkaa kutsutaan yläluokaksi, ja perivää luokkaa aliluokaksi. Kukin luokka voi periä vain yhden luokan, mutta moni luokka voi periä saman luokan. Yllä olevassa esimerkissä luokka Tehdas on yläluokka, luokka OhjelmistoTehdas aliluokka.

Luodaan vielä toinen tehdas, luokka KarkkiTehdas.

public class KarkkiTehdas extends Tehdas {
  private String karkkimerkki;

  public KarkkiTehdas(String karkkimerkki) {
    this.karkkimerkki = karkkimerkki;
  }
  
  public String getKarkkimerkki() {
    return this.karkkimerkki;
  }
}

Myös luokalla KarkkiTehdas on käytössä metodi soitaPillia(). Karkkitehtaan voi ottaa käyttöön seuraavasti.

KarkkiTehdas tehdas = new KarkkiTehdas("Kater");
tehdas.soitaPillia();

Karkkitehtaan soitaPillia() metodikutsu tuottaa myös tulosteen Piip!.

Yläluokan konstruktorit - super

Jos yläluokalla on määritelty parametrillinen konstruktori, voi sitä kutsua määreen super-avulla. Määre super on kuin this, mutta viittaa perityn luokan ominaisuuksiin, kun taas this viittaa kyseiseen olioon. Luodaan parametrillisen konstruktorin omaava luokka Elain, jolla on nimi ja pituus.

public class Elain {
  private String nimi;
  private double pituus;
  
  public Elain(String nimi, double pituus) {
    this.nimi = nimi;
    this.pituus = pituus;
  }
  
  public double getPituus() {
    return this.pituus;
  }
  
  public String getNimi() {
    return this.nimi;
  }
}

Seuraavaksi luodaan luokka Vesieläin (Vesielain), joka laajentaa luokkaa Elain ja osaa uida. Luokan Elain konstruktorissa viitataan yläluokkaan avainsanalla super. Jos kutsua super() käytetään konstruktorissa, täytyy sen olla konstruktorin ensimmäinen komento.

public class Vesielain extends Elain {
  // vesieläimillä lienee myös kidukset, mutta unohdetaan ne hetkeksi
  public double nopeus;
  
  public Vesielain(String nimi, double pituus, double nopeus) {
    super(nimi, pituus);
    this.nopeus = nopeus;
  }
  
  public void ui() {
    System.out.println("Viuh!");
  }
}

Koska luokalla Elain on ohjelmoijan kirjoittama konstruktori, ei luokalla ole enää Javan automaattisesti generoimaa parametrotonta oletuskonstruktoria. Aliluokan konstruktorista on pakko kutsua yliluokan konstruktoria. Sen takia seuraava konstruktori ei kelpaisi luokalle Vesieläin:

public Vesielain(String nimi, double pituus, double nopeus) {
	this.nimi = nimi;
	this.pituus = pituus;
	this.nopeus = nopeus;
}

Jos taas luokalle Elain lisättäisiin parametriton konstruktori, edelleinen kävisi luokan Vesieläin konstruktoriksi, Java nimittäin lisää aliluokaan kutsun yliluokan parametrittomaan konstruktoriin jos konstruktori ei sisällä super-kutsua yliluokan konstruktoriin.

Tehdään vielä luokat Riikinkukkoahven ja Siipisimppu, jotka perivät luokan Vesielain ominaisuudet.

public class Riikinkukkoahven extends Vesielain {
  public Riikinkukkoahven(String nimi, double pituus, double nopeus) {
    super(nimi, pituus, nopeus);
  }
}
public class Siipisimppu extends Vesielain {
  public Siipisimppu(String nimi, double pituus, double nopeus) {
    super(nimi, pituus, nopeus);
  }
}

Koska Riikinkukkoahven ja Siipisimppu perivät kummatkin luokan Vesielain, voivat ne käyttää Vesieläin luokassa toteutettuja valmiita toimintoja. Seuraavassa esimerkissä luodaan "Matti" - niminen siipisimppu, ja kutsutaan sen metodia ui(). Huomaa että metodi ui() on toteutettu luokassa Vesielain. Koska Siipisimppu perii luokan Vesielain, saa se käyttää sen ja sen yläluokkien metodeja.

Siipisimppu mattiL = new Siipisimppu("Matti", 187, 22.5);
mattiL.ui();

Esimerkki luo mattiL-nimisellä viitteellä varustetun Siipisimppu-tyyppisen olion ja kutsuu sen ui()-metodia. Metodi ui() tulostaa merkkijonon Viuh!.

Yläluokan metodit

Yläluokan metodit ovat suoraan alaluokan käytettävissä jos niille ei ole asetettu näkyvyysmäärettä private.. Lisätään luokalle Siipisimppu toString()-metodi, joka kutsuu luokassa Elain määriteltyä getNimi()-metodia.

public class Siipisimppu extends Vesielain {
  public Siipisimppu(String nimi, double pituus, double nopeus) {
    super(nimi, pituus, nopeus);
  }
  
  public String toString() {
    return "Hei, olen Siipisimppu, ja nimeni on " + getNimi();
  }
}

Luodaan vielä Siipisimppu-olio ja tulostetaan sen tila.

Siipisimppu mattiL = new Siipisimppu("Matti", 187, 22.5);
System.out.println(mattiL);

Esimerkki tulostaa merkkijonon Hei, olen Siipisimppu, ja nimeni on Matti.

Metodien ylikirjoitus

Metodien ylikirjoituksella tarkoitetaan sitä, että yläluokassa oleva metodi toteutetaan uudestaan aliluokassa. Tällöin kun aliluokasta luodulle oliolle kutsutaan kyseistä metodia, kutsutaan aliluokan toteutusta. Yläluokasta luodulle oliolle kutsutaan yläluokan metoditoteutusta.

Toteutetaan Vesielain-luokan periva luokka Hummeri. Hummerit eivät osaa uida, joten niitä varten Vesielain-luokassa oleva metodi ui() täytyy ylikirjoittaa.

public class Hummeri extends Vesielain {
  public Hummeri(String nimi, double pituus, double nopeus) {
    super(nimi, pituus, nopeus);
  }
  
  public void ui() {
    System.out.println("Uisin jos osaisin :,(");
  }
}

Luodaan vielä kaksi vesieläintä, Hummeri ja Siipisimppu, ja kutsutaan kummallekin metodia ui().

Siipisimppu mattiL = new Siipisimppu("Matti", 187, 22.5);
System.out.println("Siipisimppu ui:");
mattiL.ui();

Hummeri mattiV = new Hummeri("Matti", 180, 3.5);
System.out.println("Hummeri ui:");
mattiV.ui();

Esimerkin tulostus on seuraavanlainen

Siipisimppu ui:
Viuh!
Hummeri ui:
Uisin jos osaisin :,(

Yllä olevan esimerkin kautta huomaamme suunnittelemassamme Eläin->Vesieläin->Hummeri periytymishierarkiassamme piilevän ongelman. Olemme luoneet Vesielain-luokan, jolla on ui()-metodi, vaikka kaikki vesieläimet eivät osaa uida. Parempi ratkaisu olisikin luoda luokasta Vesielain luokat Kala ja Äyriäinen. Hummeri laajentaisi, eli perisi, luokkaa Äyriäinen, kun taas Riikinkukkoahven ja Siipisimppu perisivät luokan Kala.

Yliluokan ylikirjoitettujen metodien kutsuminen - super

Jos aliluokka ylikirjoittaa yliluokan metodin, on aliluokan sisältä mahdollista tarvittaessa kutsua yliluokan metodia viittaamalla siihen super.metodinNimi(); Seuraavassa luokan Muikku hyödyntää omassa ui()-metodin toteutuksessa yliluokan ui()-metodia, johon siis viitataan super.ui().

public class Muikku extends Vesielain {
  public Muikku(String nimi, double pituus, double nopeus) {
    super(nimi, pituus, nopeus);
  }
  
  public void ui() {
    for (int i = 0; i < 10; i++) {
      super.ui();    
    }
  }
}

Vielä käyttöesimerkki:

Muikku mattiP = new Muikku("Matti", 12, 7.5);
System.out.println("Muikku ui:");
mattiP.ui();

Esimerkin tulostus on seuraavanlainen

Muikku ui:
Viuh!Viuh!Viuh!Viuh!Viuh!Viuh!Viuh!Viuh!Viuh!Viuh!

Näkyvyys - Protected

Olemme tähän mennessä nähneet kaksi erilaista näkyvyysmäärettä. Määre public asettaa metodit ja muuttujat kaikille näkyviksi, kun taas määrettä private käytetään luokan ominaisuuksien kapselointiin. Periytymisessä voidaan käyttää kolmatta määrettä protected, joka tarkoittaa sitä, että metodi tai muuttuja on aliluokille näkyvissä. Määrettä protected käytetään esimerkiksi silloin, kun tarvitaan luokan sisäisiä apuvälineitä, joita ei kuitenkaan haluta kaikkien näkyville. Esimerkiksi seuraava Kassa-toteutus sisältää Laskuri-tyyppiä olevan olion, joka näkyy kaikille luokan periville luokille.

public class Kassa {
  protected Laskuri laskuri;
  
  public Kassa() {
    this.laskuri = new Laskuri();
  }
  
  public void lisaaKassaan(int montako) {
    for(int i = 0; i < montako; i++) {
      laskuri.kasvataArvoa();
    }
  }
  
  // muita metodeja
}

Toinen luokka, MatinKassa tekee oman toteutuksen lisaaKassaan()-metodista.

public class MatinKassa extends Kassa {
  private Laskuri jemma;
  
  public MatinKassa() {
    jemma = new Laskuri();
  }
  
  @Override
  public void lisaaKassaan(int montako) {
    for(int i = 0; i < montako; i++) {
      if(i % 5 == 0) {
        jemma.kasvataArvoa();
      } else {
        laskuri.kasvataArvoa();
      }
    }
  }
}

Luokka MatinKassa jemmaa siis aina joka viidennen asian, eikä lisää sitä alkuperäisen kassan laskuriin. Alkuperäisen kassan laskuri-attribuutti on käytettävissä, koska sille on annettu määre protected. Javassa on public, protected ja private-määreiden lisäksi käsite pakkausnäkyvyys, jolla tarkoitetaan metodien ja attribuuttien näkymistä saman pakkauksen sisällä. Palataan pakkausmääreeseen ensi viikolla.

Yläluokka Object

Javan kaikki luokat periytyvät luokasta Object, jota voidaan ajatella kaikkien luokkien peruspalikkana. Luokka Object määrittelee muunmuassa metodin toString(), joka tulostaa olion sisäisen tilan. Jos oma luokkamme ei toteuta metodia toString(), kutsumme oliota tulostettaessa luokan Object määrittelemää toString()-metodia. Perimistä voidaan ajatella seuraavanlaisena puuna, missä jokainen solmu, eli laatikko, perii yläpuolellaan olevan solmun.

Rajapinnat ja niiden periytyminen

Rajapinnat käyttäytyvät kuin luokat periytymisessä, eli niitä voi periä kuten luokkia. Katsotaan kahta erilaista periytymistilannetta.

Rajapintojen periytyminen

Jos yläluokka toteuttaa rajapinnan, on sen toteutus olemassa myös rajapinnalle. Tällöin myös alaluokkaa voidaan käyttää esimerkiksi parametrina metodille, joka ottaa parametrikseen yläluokan toteuttaman rajapinnan tyyppiä olevan olion. Esimerkiksi luokka KahviLaskuri, joka toteuttaa rajapinnan Laskuri.

public class KahviLaskuri implements Laskuri {
  protected int arvo;

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

  public void kasvataArvoa() {
    arvo = arvo + 1;
  }

  public void vahennaArvoa() {
    arvo = arvo - 1;
  }

  public int getArvo() {
    return arvo;
  }
  
  public void lisaaArvoon(Laskuri laskuri) {
    this.arvo += laskuri.getArvo();
  }
}

Luokka KahviLaskuri toteuttaa rajapinnan Laskuri. Kahvilaskurilla on lisäksi metodi lisaaArvoon(), joka lisää arvoon parametrina annetun arvon. Toteutetaan luokka EspressoLaskuri, joka perii luokan KahviLaskuri.

public class EspressoLaskuri extends KahviLaskuri {
    // ei mitään tällä hetkellä
}

Luokan EspressoLaskuri ilmentymää voi käyttää KahviLaskuri-luokan metodin lisaaArvoon() parametrina koska sen yläluokka toteuttaa rajapinnan Laskuri.

EspressoLaskuri espressoLaskuri = new EspressoLaskuri();

// juodaan espressoa
espressoLaskuri.kasvataArvoa();
espressoLaskuri.kasvataArvoa();
espressoLaskuri.kasvataArvoa();
espressoLaskuri.kasvataArvoa();

// siirretään saldot kahviin
KahviLaskuri kahviLaskuri = new KahviLaskuri();
kahviLaskuri.lisaaArvoon(espressoLaskuri);
System.out.println("Kahvilaskurissa arvona " + kahviLaskuri.getArvo());

Esimerkin tulostus tulostaa merkkijonon Kahvilaskurissa arvona 4.

Rajapinnan periminen

Rajapinnan voi periä samalla tavalla kuin luokan. Tällöin alirajapinta saa ylärajapinnan kaikki metodit ja määrittelyt käyttöönsä. Esimerkiksi rajapinta Laskuri ja sen periva rajapinta AsettavaLaskuri.

public interface Laskuri {
  public void kasvataArvoa();
  public void vahennaArvoa();
  public int getArvo();
}
public interface AsettavaLaskuri extends Laskuri {
  public void setArvo(int arvo);
}

Rajapinnan AsettavaLaskuri toteuttavan luokan pitää toteuttaa neljä metodia, kasvataArvoa(), vahennaArvoa(), getArvo() ja asetaArvo(). Toteutetaan vielä luokka MokkaLaskuri, joka toteuttaa rajapinnan AsettavaLaskuri. Koska olemme oppineet perimään luokkien metodeja ja attribuutteja, laajennamme luokkaa KahviLaskuri.

public class MokkaLaskuri extends KahviLaskuri implements AsettavaLaskuri {
  public void setArvo(int uusiArvo) {
    arvo = uusiArvo;
  }
}

Huomaa että arvon asetus toimii vain, koska luokan KahviLaskuri toteutuksessa on määritelty muuttuja arvo aliluokille näkyviksi.

Tiedosto

Javaan kuuluu myös valmis tiedostoa kuvaava luokka File, jonka sisältö voidaan lukea kurssilla jo tutuksi tulleen Scanner-luokan avulla. Tiedosto saa konstruktorissaan parametriksi tiedostopolun, joka kuvaa tiedoston sijaintia tietokoneen levyjärjestelmässä.

Tiedostot NetBeanssissa

NetBeans-ohjelmassa tiedostoille on oma välilehti nimeltä Files. Files-välilehdellä on määritelty kaikki projektiin liittyvät tiedostot. Jos projektin juureen, eli ei yhdenkään kansion sisälle, lisätään tiedosto, voidaan siihen viitata projektin sisältä suoraan tiedoston nimellä. Tiedosto-olion luominen tapahtuu antamalla sille parametrina polku tiedostoon, esimerkiksi seuraavasti

File tiedosto = new File("tiedoston-nimi.txt");

Tiedoston lukeminen

Koska Scanner-luokan konstruktori on kuormitettu, voi lukemislähde olla näppäimistön lisäksi myös tiedosto. Käytössämme on siis samat metodit tietoston lukemiseen kuin käyttäjän syötteen lukemiseen. Seuraavassa esimerkissä avataan tiedosto, tarkistetaan onko siellä tekstiriviä, ja luetaan se jos on. Lopuksi luettu rivi tulostetaan ja lukijan avaama tiedosto suljetaan. Huomaa että olemme lisänneet määreen throws Exception main()-metodiin. Tutustumme poikkeuksiin ja niiden hallintaan paremmin myöhemmin tällä kurssilla.

import java.io.File;
import java.util.Scanner;

public class TiedostonLuku {

  public static void main(String[] komentoriviParametrit) throws Exception {
    // tiedosto mistä luetaan
    File tiedosto = new File("tiedosto.txt");
    
    Scanner lukija = new Scanner(tiedosto);
    if(lukija.hasNextLine()) {
      String rivi = lukija.nextLine();
      System.out.println(rivi);  
    }
    
    lukija.close();
  }
}

Yllä oleva esimerkki avaa tiedoston tiedosto.txt, joka sijaitsee samassa sijainnissa ohjelman kanssa, ja lukee sen ensimmäisen rivin. Lopuksi lukija suljetaan, jolloin tiedosto myös suljetaan. Määre throws Exception kertoo että metodi saattaa heittää poikkeuksen. Samanlaisen määreen voi laittaa kaikkiin metodeihin jotka käsittelevät tiedostoja.

Useampia rivejä voi lukea esimerkiksi seuraavanlaisen toistorakenteen avulla.

while(lukija.hasNextLine()) {
  String rivi = lukija.nextLine();
  System.out.println(rivi);
}

Luokan Scanner metodi hasNextLine() palauttaa totuusarvon true jos tiedostossa on luettava rivi.

Koska käytämme luokkaa Scanner tiedoston lukemiseen, voimme käyttää myös sen muita metodeja. Seuraavassa esimerkissä luetaan tiedosto sana kerrallaan. Metodi hasNext() palauttaa totuusarvon true, jos tiedostossa on vielä luettava sana, ja metodi next() lukee sen String-olioon, jonka se palauttaa.

Esimerkkitiedostomme "tiedosto.txt" sisältää seuraavan tekstin: (voit copy-pasteta seuraavan tekstin oman tiedoston sisään kokeillessasi esimerkkiä itse!)

Poikkeukset (exceptions) ovat "poikkeuksellisia tilanteita" kesken normaalin ohjelmansuorituksen:
tiedosto loppuu, merkkijono ei kelpaa kokonaisluvuksi, odotetun olion tilalla onkin null-arvo, 
taulukon indeksi menee ohjelmointivirheen takia sopimattomaksi, ... 

Seuraava ohjelma luo Scanner-olion, joka avaa tiedoston tiedosto.txt. Sen jälkeen se tulostaa joka viidennen sanan tiedostosta.

    
File tiedosto = new File("tiedosto.txt");
Scanner lukija = new Scanner(tiedosto);
int monesko = 0;
while (lukija.hasNext()) {
  monesko++;
  String sana = lukija.next();
  if (monesko % 5 == 0) {
    System.out.println(sana);
  }
}   

Ohjelman tulostus on seuraavanlainen

tilanteita"
loppuu,
odotetun
taulukon
sopimattomaksi,