Ohjelmoinnin jatkokurssi

Matti Paksula, Arto Vihavainen, Matti Luukkainen, Antti Laaksonen, Pekka Mikkola, Juhana Laurinharju, Martin Pärtel

Alkusanat

Tämä on suoraa jatkoa kevään 2011 Ohjelmoinnin perusteet -kurssin materiaaliin. Kurssi alkaa siitä mihin OhPe loppui ja oikeastaan kaikki OpPe:ssa opitut asiat oletetaan nyt osattavan. Saattaakin olla hyvä kerrata tässä vaiheessa Ohjelmoinnin perusteiden materiaalista ainakin luvut 20 ja 23-27.

Alkeistyyppi ja viittaustyyppi

Kerrataan ja täsmennetään vielä Ohjelmoinnin perusteiden luvuissa 16 ja 23.16 jo käsiteltyä asiaa.

Ehkä kannattaa lukea ensin Ohjelmoinnin perusteiden materiaalista vastaavat kohdat.

Javassa on kaksi erilaista muuttujatyyppiä, alkeistyyppiset muuttujat ja viittaustyyppiset muuttujat. Alkeistyyppisillä muuttujilla tallennetaan tietoa yksittäisiin lokeroihin, kun taas viittaustyyppiset muuttujat sisältävät viitteen, joka osoittaa tietoon.

Alkeistyyppi

Alkeistyyppiset muuttujat tallentavat arvonsa omaan lokeroon. Uutta alkeistyyppistä muuttujaa alustettaessa luodaan aina uusi lokero. Alkeistyyppiset muuttujat alustetaan sijoitusoperaatiolla =. Katsotaan esimerkkiä.

int vitonen = 5;
int kutonen = 6;

Esimerkki luo kaksi alkeistyyppistä muuttujaa, nimiltään vitonen ja kutonen. Muuttujan vitonen lokeroon asetetaan arvo 5, ja muuttujan kutonen lokeroon arvo 6. Kaikki alkeistyyppiset muuttujat, kuten javan kaikki muutkin muuttujat, ovat tietyn tyyppisiä. Muuttujat vitonen ja kutonen ovat kumpikin int-tyyppisiä, eli kokonaislukuja. Kuvana alkeistyyppiset muuttujat kannattaa ajatella laatikkoina jonka sisällä muuttujan arvo on talletettuna:

	
          -----                                   
 viitonen | 5 |                                     
          -----       

          -----                                   
 kuutonen | 6 |                                     
          -----  		  

Tarkastellaan vielä alkeistyyppisten muuttujien asettamista toisen muuttujan avulla.

int vitonen = 5;
int kutonen = 6;

vitonen = kutonen; // muuttuja vitonen sisältää nyt arvon 6, eli arvon joka oli muuttujassa kutonen
kutonen = 42; // muuttuja kutonen sisältää nyt arvon 42

// muuttuja vitonen sisältää vieläkin arvon 6

Esimerkissä alustetaan ensiksi muuttujat vitonen ja kutonen. Tämän jälkeen muuttujan vitonen lokeroon asetetaan muuttujan kutonen lokeron sisältämä arvo. Tässä vaiheessa siis muuttujan vitonen lokeroon tallentuu muuttujan kutonen sisältämä arvo. Jos muuttujan kutonen arvoa muutetaan tämän jälkeen, ei muuttujan vitonen sisältämä arvo muutu. Lopputilanne kuvana

	
          ------                                   
 viitonen |  6 |                                     
          ------       

          ------                                   
 kuutonen | 42 |                                     
          ------  		  

Alkeistyyppinen muuttuja metodin parametrina ja paluuarvona

Kun alkeistyyppinen muuttuja annetaan metodille parametrina, saa metodin parametrimuuttuja kopion annetun muuttujan arvosta. Katsotaan seuraavaa metodia lisaaLukuun(int luku, int paljonko).

public int lisaaLukuun(int luku, int paljonko) {
  return (luku + paljonko);  
}

Metodi lisaaLukuun() saa kaksi parametria, kokonaisluvut luku ja paljonko. Metodi palauttaa uuden luvun, joka on annettujen parametrien summa. Tutkitaan vielä metodin kutsumista.

int omaLuku = 10;
omaLuku = lisaaLukuun(omaLuku, 15);
// muuttuja omaLuku sisältää nyt arvon 25

Esimerkissä kutsutaan lisaaLukuun()-metodia muuttujalla omaLuku ja arvolla 15. Metodin muuttujiin luku ja paljonko kopioituvat siis arvot 10, eli muuttujan omaLuku sisältö, ja 15. Metodi palauttaa muuttujien luku ja paljonko summan, eli 10 + 15 = 25.

Minimi- ja maksimiarvot

Eri tietotyypeillä on omat minimi- ja maksimiarvonsa, eli arvot joita pienempiä tai suurempia ne eivät voi olla. Tämä johtuu Javan (ja muidenkin useimpien ohjelmointikielten) sisäisestä tiedon esitysmuodosta, jossa tietotyyppien koot on ennalta määrätty.

Alla vielä muutama Javan alkeistyyppi ja niiden minimi- ja maksimiarvot

MuuttujatyyppiSelitysMinimiarvoMaksimiarvo
intKokonaisluku-2 147 483 648 (Integer.MIN_VALUE)2 147 483 647 (Integer.MAX_VALUE)
longIso kokonaisluku-9 223 372 036 854 775 808 (Long.MIN_VALUE)9 223 372 036 854 775 807 (Long.MAX_VALUE)
booleanTotuusarvotrue tai false
doubleLiukulukuDouble.MIN_VALUEDouble.MAX_VALUE

Liukulukuja käyttäessä kannattaa muistaa että liukuluvun arvo on aina arvio oikeasta arvosta. Koska liukuluvun, kuten kaikkien muidenkin alkeistyyppien sisältämä tietomäärä on rajoitettu, voidaan huomata yllättäviäkin pyöristysvirheitä. Esimerkiksi seuraava tilanne.

double eka = 0.39;
double toka = 0.35;
System.out.println(eka - toka);

Esimerkki tulostaa arvon 0.040000000000000036. Pyöristysvirheisiin varaudutaan usein muunmuassa vertaamalla arvon kuulumista tiettyyn arvoväliin. Ohjelmointikielet tarjoavat usein työkalut vastaavien tilanteiden välttämiseen, esimerkiksi Javassa on olemassa luokka BigDecimal liukulukujen tarkempaa laskemista varten.

Viittaustyyppi

Viittaustyyppiset muuttujat tallentavat niihin liittyvän tiedon viitteen taakse eli "langan päähän", ja itse muuttuja toimii vain viitteenä tiedon sisältävään paikkaan. Toisin kuin alkeistyyppisillä muuttujilla, viittaustyyppisillä muuttujilla ei ole rajoitettua arvoaluetta, koska niiden oikea arvo tai tieto on viitteen takana.

Viittaustyyppisistä muuttujista puhutaan olioina, ja ne luodaan new-kutsulla. Muuttujan arvo asetetaan vieläkin sijoitusoperaattorilla =, mutta komento new luo olion ja palauttaa viitteen olioon. Tämä viite asetetaan muuttujan arvoksi. Katsotaan kahden viittaustyyppisen muuttujan luontia. Esimerkeissä käytetään luokkaa Laskuri:

public class Laskuri {
  int arvo;

  public Laskuri(int alkuarvo) {  // Konstruktori
    arvo = alkuarvo;
  }

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

  public int annaArvo() {
    return arvo;
  }
}

Pääohjelma:

Laskuri matinLaskuri = new Laskuri(5);
Laskuri artonLaskuri = new Laskuri(3); 

Esimerkissä luodaan ensiksi viittaustyyppinen muuttuja matinLaskuri. Komentoa new kutsuessa viitteen taakse varataan tila muuttujan tiedolle, luodaan Laskuri-tyyppinen olio, ja palautetaan viite siihen. Palautettu viite asetetaan sijoitusoperaattorilla = muuttujaan matinLaskuri. Sama tapahtuu muuttujalle nimeltä artonLaskuri. Kuvana viittaustyyppi kannattaa ajatella siten, että muuttuja sisältää "langan" tai "nuolen", jonka päässä on olio itse. Muuttuja siis ei säilytä olioa vaan tiedon eli viitteen sinne missä olio on.

             -----             --olio----
matinLaskuri | --|---------->  | arvo 5 |
             -----             ----------
	
             -----             --olio----
artonLaskuri | --|---------->  | arvo 3 |
             -----             ----------

Katsotaan seuraavaksi viittaustyyppisen muuttujan asettamista toisen muuttujan avulla.

Laskuri matinLaskuri = new Laskuri(5);
Laskuri artonLaskuri = new Laskuri(3);

matinLaskuri = artonLaskuri; // muuttuja matinLaskuri sisältää nyt muuttujan artonLaskuri sisältämän viitteen, 
                             // eli viitteen Laskuri-tyyppiseen olioon joka on saanut konstruktorissaan arvon 3
artonLaskuri = new Laskuri(10); // muuttujaan artonLaskuri asetetaan uusi viite, joka osoittaa 
                                // new Laskuri(10) - kutsulla luotuun Laskuri-olioon

// muuttuja matinLaskuri sisältää vieläkin viitteen Laskuri-olioon, joka sai konstruktorissaan arvon 3

Esimerkissä tehdään käytännössä samat operaatiot kuin alkeistyyppi-kappaleessa olevassa asetusesimerkissä. Äskeisessä esimerkissä asetimme viittaustyyppisten muuttujien viitteitä, kun taas alkeistyyppi-esimerkissä asetimme alkeistyyppien arvoja. Lopussa kukaan ei viittaa Laskuriolioon, joka sai arvokseen konstruktorissa 5. Javan roskienkeruu huolehtii tälläisistä turhista oliosta. Lopputilanne uvana:

             -----             --olio----
matinLaskuri | --|--           | arvo 5 |   tämä olio on muuttunut roskaksi, jonka Java pian hävittää
             -----  --         ----------
                      ---  
                         --    --olio----
             -----         --> | arvo 3 |
artonLaskuri | --|--           ----------
             -----  --
                      ---
                         --    --olio-----
                           --> | arvo 10 |
                               -----------

Tarkastellaan vielä kolmatta esimerkkiä, joka näyttää viite- ja alkeistyyppisten muuttujien konkreettisen eron.

Laskuri matinLaskuri = new Laskuri(5);
Laskuri artonLaskuri = new Laskuri(3);

matinLaskuri = artonLaskuri; // muuttuja matinLaskuri sisältää nyt muuttujan artonLaskuri sisältämän viitteen, 
                             // eli viitteen Laskuri-tyyppiseen olioon joka on saanut konstruktorissaan arvon 3
artonLaskuri.kasvataArvoa(); // kasvatetaan artonLaskuri-viitteen takana olevan olion arvoa yhdellä

System.out.println(matinLaskuri.annaArvo()); // koska matinLaskuri-muuttujan viite osoittaa samaan olioon, kuin 
                                             // artonLaskuri-muuttuja, on matinLaskuri.annaArvo() - kutsun palauttama arvo 4

Viittaustyyppiset muuttujat siis viittaavat aina toisaalla oleviin olioihin. Useat viittaustyyppiset muuttujat voivat sisältää saman viitteen, jolloin kaikki muuttujat osoittavat samaan olioon. Seuraavassa esimerkissä näemme kolme Laskuri-tyyppistä viitemuuttujaa, mutta vain yhden Laskuri-olion.

Laskuri mattiV = new Laskuri(5);
Laskuri mattiL = mattiV;
Laskuri mattiP = mattiV;

Esimerkissä luodaan vain yksi Laskuri-olio, mutta kaikki Laskuri-tyyppiset viitemuuttujat osoittavat lopulta siihen. Tällöin kaikki metodikutsut viitteelle mattiL, mattiP ja mattiV muokkaavat samaa viitteen takana olevaa oliota. Viittaustyyppisiä muuttujia asetettaessa toisten muuttujien avulla viitteet siis kopioituvat viitemuuttujan lokeroon. Kuvana:

            -----             
     mattiV | --|---           
            -----   ---         
                       ----       
            -----          ---->  --olio----
     mattiL | --|-------------->  | arvo 5 |
            -----          ---->  ----------
                       ----
            -----   ---           
     MattiP | --|---           
            ----- 
   

Katsotaan kopioitumista vielä esimerkillä.

Laskuri mattiV = new Laskuri(5);
Laskuri mattiP = mattiV; // muuttuja mattiP saa arvokseen mattiV-muuttujan sisältämän viitteen
Laskuri mattiL = mattiP; // muuttuja mattiL saa arvokseen mattiP-muuttujan sisältämän viitteen

mattiP = new Laskuri(3); // muuttuja mattiP saa arvokseen uuden viitteen, joka palautuu new Laskuri(3) - kutsusta

Esimerkissä muuttujan mattiL sisältö ei muutu muuttujan mattiP saadessa uuden viitteen, sillä muuttujaan mattiL on luotu asetuksessa mattiL = mattiP kopio muuttujan mattiP sisällöstä, eli viitteestä. Kun muuttujan mattiP sisältö, eli viite muuttuu, se ei vaikuta muuttujan mattiL sisältämään viitteeseen koska se on asetettu jo aiemmin. Kuvana:

            -----             
     mattiV | --|---           
            -----   ---         
                       ----       
            -----          ---->  --olio----
     mattiL | --|-------------->  | arvo 5 |
            -----                 ----------
                     
            -----                 --olio----
     MattiP | --|-------------->  | arvo 3 |
            -----                 ---------- 

Viittaustyyppinen muuttuja metodin parametrina

Kun viittaustyyppinen muuttuja annetaan metodille parametrina, saa metodin parametrimuuttuja kopion annetun muuttujan viitteestä. Katsotaan seuraavaa metodia lisaaLaskuriin(Laskuri laskuri, int paljonko).

public void lisaaLaskuriin(Laskuri laskuri, int paljonko) {
  for (int i = 0; i < paljonko; i++) {
    laskuri.kasvataArvoa();
  }
}

Metodi lisaaLaskuriin() saa kaksi parametria, viittaustyyppisen parametrin laskuri ja alkeistyyppisen (kokonaisluvun) paljonko. Metodi kutsuu Laskuri-tyyppisen parametrin metodia kasvataArvoa() paljonko-muuttujan sisältämän arvon verran. Tutkitaan vielä metodin kutsumista.

int x = 10;
Laskuri mattiL = new Laskuri(10);
lisaaLaskuriin(mattiL, x);
// muuttujan mattiL sisäinen arvo on nyt 20

Esimerkissä kutsutaan lisaaLaskuriin()-metodia muuttujalla mattiL ja muuttujalla x jonka arvo on 19. Metodin muuttujiin laskuri ja paljonko kopioituvat siis viittaustyyppisen muuttujan mattiL viite, ja arvo 10. Metodi suorittaa viitteelle laskuri paljonko muuttujan määrittelemän määrän kasvataArvoa()-metodikutsuja. Kuvana:

    main:                                            metodissa:
  
            -----            --olio-----               -----
     mattiL | --|--------->  | arvo 10 | <-------------|-- | laskuri
            -----            -----------               -----
			
            ------                                     ------
          x | 10 |                                     | 10 | paljonko
            ------                                     ------

Metodi siis näkee saman laskurin johon mattiL viittaa, eli metodin tekemä muutos vaikuttaa suoraan parametrina olevaan olioon. Alkeistyyppien suhteen tilanne on toinen, eli metodille tulee ainoastaan kopio x:n arvosta.

Viittaustyyppinen muuttuja metodin paluuarvona

Kun metodi palauttaa viittaustyyppisen muuttujan, palauttaa se viitteen muualla sijaitsevaan olioon. Metodin palauttaman viittaustyyppisen muuttujan voi asettaa muuttujalle samalla tavalla kuin normaalikin asetus tapahtuu, eli yhtäsuuruusmerkin avulla. Katsotaan metodia luoLaskuri(), joka luo uuden viittaustyyppisen muuttujan.

public Laskuri luoLaskuri(int alkuarvo) {
  Laskuri uusiLaskuri = new Laskuri(alkuarvo);
  return uusiLaskuri;
}

Metodi luoLaskuri palauttaa siis metodissa luotuun olioon viittaavan viitteen uusiLaskuri. Uusi olio luodaan aina metodia kutsuttaessa, seuraavassa esimerkissä luomme kaksi erillistä Laskuri-tyyppistä oliota.

Laskuri mattiV = luoLaskuri(10);
Laskuri mattiP = luoLaskuri(10);

Metodi luoLaskuri luo aina uuden Laskuri-tyyppisen olion. Ensimmäisessä kutsussa, eli kutsussa Laskuri mattiV = luoLaskuri(10); asetetaan metodin palauttama viite viittaustyyppiseen muuttujaan mattiV. Toisessa metodikutsussa luodaan uusi viite, joka asetetaan muuttujaan mattiP. Muuttujat mattiV ja mattiP eivät sisällä samaa viitettä, sillä metodi luo aina uuden olion ja palauttaa viitteen juuri luotuun olioon.

Static ja ei-static

Kerrataan ja täsmennetään Ohjelmoinnin perusteiden luvussa 26 jo käsiteltyä asiaa.

Staattisilla ja ei-staattisilla metodeilla ja muuttujilla erotetaan se, mihin muuttuja tai metodi liittyy. Staattiset metodit ja muuttujat liittyvät luokkaan, kun taas ei-staattiset metodit ja muuttujat ovat oliokohtaisia.

Static

Static-määreen saavat muuttujat eivät liity olioihin vaan luokkiin. Esimerkiksi Integer.MAX_VALUE, Long.MIN_VALUE ja Double.MAX_VALUE ovat kaikki staattisia muuttujia. Staattisia muuttujia ja metodeja käytetään luokan nimen kautta, esimerkiksi LuokanNimi.muuttuja tai LuokanNimi.metodi(), tietysti riippuen metodien ja muuttujien näkyvyydestä. Vain public-näkyvyydellä määritetyt muuttujat ja metodit ovat suoraan käytettävissä.

Luokkakirjasto

Luokkakirjastoksi kutsutaan luokkaa, jossa on yleiskäyttöisiä metodeja ja muuttujia. Esimerkiksi Javan Math-luokka on sellainen. Math-luokkahan tarjoaa muunmuassa Math.PI-muuttujan, jossa on piin likiarvo, sekä Math.random()-metodin, joka palauttaa satunnaisen liukuluvun väliltä 0 ja 1. Omien luokkakirjastojen toteuttaminen on usein hyödyllistä. Esimerkiksi Helsingin Seudun Liikenne (HSL) voisi pitää lippujensa hintoja luokkakirjastossa, josta ne löytyisi aina tarvittaessa.

public class HslHinnasto {
  public static double KERTALIPPU_AIKUINEN = 2.50;
  public static double RAITIOVAUNULIPPU_AIKUINEN = 2.50; 
}    

Tällöin kaikki ohjelmat, jotka käyttävät kerta- tai raitiovaunulipun hintaa voivat käyttää niitä HslHinnasto-luokan kautta. Seuraavassa esimerkissä esitellään yksinkertainen Ihminen-luokka, jolla on metodi onkoRahaaKertalippuun(), joka käyttää HslHinnasto-luokasta löytyvää lipun hintaa.

public class Ihminen {
  private double rahat; // rahat
  ...
  
  public boolean onkoRahaaKertalippuun() {
    if(rahat >= HslHinnasto.KERTALIPPU_AIKUINEN) {
      return true;
    }
    
    return false;
  }
  ...
}    

Metodi onkoRahaaKertalippuun() siis vertaa luokan Ihminen ei-staattista muuttujaa rahat HslHinnasto-luokan staattiseen muuttujaan KERTALIPPU_AIKUINEN. Metodia onkoRahaaKertalippuun() voi kutsua vain olion yhteydessä, koska se ei ole staattinen.

Huomaa nimeämiskäytäntö! Kaikki staattiset alkeistyyppiset muuttujat kirjoitetaan ISOLLA_JA_ALAVIIVOILLA. Staattiset metodit toimivat vastaavasti. Esimerkiksi Luokka HslHinnasto saattaisi kapseloida muuttujat ja antaa vain aksessorit niihin. Aksessoriksi kutsutaan metodia, jolla voi joko lukea muuttujan arvon tai sijoittaa muuttujalle uuden arvon.

public class HslHinnasto {
  private static double KERTALIPPU_AIKUINEN = 2.50;
  private static double RAITIOVAUNULIPPU_AIKUINEN = 2.50;
  
  public static double annaKertalipunHinta() {   // Aksessori
    return KERTALIPPU_AIKUINEN;
  }
  
  public static double annaRaitiovaunulipunHinta() {   // Aksessori
    return RAITIOVAUNULIPPU_AIKUINEN;
  }
}    

Tällöin Ihminen-luokan toteutuksessa täytyisikin kutsua metodiaannaKertalipunHinta() sen sijaan että kutsuttaisiin muuttujaa suoraan.

public class Ihminen {
  private double rahat; // rahat
  ...
  
  public boolean onkoRahaaKertalippuun() {  
    if(rahat >= HslHinnasto.annaKertalipunHinta()) {
      return true;
    }
    
    return false;
  }
  ...
}    

Ei-static

Ei-staattiset metodit ja muuttujat liittyvät olioihin. Oliomuuttujat, eli attribuutit, määritellään luokan alussa. Kun oliota luodaan new-kutsulla, kaikki oliomuuttujat saavat arvon olion liittyvän viitteen päässä, jolloin niihin pääsee käsiksi oliokohtaisesti. Esimerkiksi taas yksinkertainen luokka Ihminen, jolla on kaksi oliomuuttujaa: nimi ja rahat.

public class Ihminen {
  private String nimi; // oliomuuttujia, jokaiselle näistä on oliokohtainen arvo
  private double rahat;
  
  ...
}    

Kun luokasta Ihminen luodaan uusi ilmentymä, alustetaan myös siihen liittyvät muuttujat. Jos viittaustyyppistä muuttujaa nimi ei alusteta, saa se arvokseen null-viitteen. Lisätään luokan Ihminen toteutukseen vielä konstruktori ja muutama metodi.

public class Ihminen {
  private String nimi; // oliomuuttujia, jokaiselle näistä on oliokohtainen arvo
  private double rahat;
  
  // konstruktori
  public Ihminen(String nimi, double rahat) {
    this.nimi = nimi;
    this.rahat = rahat;
  }
  
  // annaNimi
  public String annaNimi() {
    return this.nimi;
  }
  
  // annaRahat
  public double annaRahat() {
    return this.rahat;
  }
  
  // lisaaRahaa, lisätään vain jos yritetään lisätä positiivinen määrä
  public void lisaaRahaa(double summa) {
    if(summa > 0) {
      this.rahat += summa;    
    }
  } 
  ...
}    

Konstruktori Ihminen(String nimi, double rahat) luo uuden ihmisolion ja palauttaa viitteen siihen. Aksessori annaNimi() palauttaa viitteen nimi-olioon, ja annaRahat()-metodi palauttaa alkeistyyppisen muuttujan rahat. Metodi lisaaRahaa(double summa) lisaa oliomuuttujaan rahat parametrina annetun summan jos parametrin arvo on suurempi kuin 0.

Oliometodeja kutsutaan olion viitteen kautta. Seuraava koodiesimerkki luo uuden Ihmis-olion, lisää sille rahaa, ja lopuksi tulostaa sen nimen. Huomaa että metodikutsut ovat muotoa olionNimi.metodinNimi()

Ihminen mattiV = new Ihminen("Matti V", 3.0);
mattiV.lisaaRahaa(5); // palkka, jes!
System.out.println(mattiV.annaNimi());

Esimerkki tulostaa "Matti V".

Metodit luokan sisällä

Luokan sisäisiä ei-staattisia metodeja voi tietysti kutsua myös ilman olio-etuliitettä. Esimerkiksi seuraava toString()-metodi Ihminen luokalle, joka kutsuu metodia annaNimi(). Metodi toString():han mahdollistaa olion tilan tulostamisen vain olion nimeä parametrina käyttäen.

public class Ihminen {
  private String nimi; // oliomuuttujia, jokaiselle näistä on oliokohtainen arvo
  ...
  
  public String annaNimi() {
    return this.nimi;
  }
  
  ...
  
  public String toString() {
    return annaNimi();
  }
}

Metodi toString() kutsuu siis luokan sisäistä, tähän olioon liittyvää annaNimi()-metodia. Metodikutsuun voi lisätä etuliitteen this jos haluaa korostaa kutsun liittyvän juuri tähän ilmentymään.

public class Ihminen {
  private String nimi; // oliomuuttujia, jokaiselle näistä on oliokohtainen arvo
  ...
  
  public String annaNimi() {
    return this.nimi;
  }
  
  ...
  
  public String toString() {
    return this.annaNimi();
  }
}

Ei-staattiset metodit voivat kutsua myös staattisia, eli luokkakohtaisia metodeja. Toisaalta, luokkakohtaiset metodit eivät voi kutsua oliokohtaisia metodeja ilman viitettä itse olioon, sillä ilman viitettä ei ole tietoa oliosta.

Muuttujat metodien sisällä

Poikkeuksena "Ei staattiset metodit ja muuttujat liittyvät olioihin" -sääntöön on metodien sisällä määriteltävät muuttujat. Metodien sisällä määriteltävät muuttujat eivät saa static-määrettä (eivätkä muitakaan määreitä tyyppinsä lisäksi). Metodien sisällä määriteltävät muuttujat ovat metodien suorituksessa käytettäviä apumuuttujia, eikä niitä tule sekoittaa oliomuuttujiin. Alla esimerkki metodista, jossa luodaan metodiin paikallinen muuttuja. Muuttuja indeksi on olemassa ja käytössä vain metodin suorituksen ajan.

public class ... {
  ...
  
  public static void tulostaTaulukko(String[] taulukko) {
    int indeksi = 0;
    
    while(indeksi < taulukko.length) {
      System.out.println(taulukko[indeksi]);
      indeksi++;
    }    
  }
  
  ...
}

Metodissa tulostaTaulukko() luodaan siis metodin sisäinen apumuuttuja indeksi, jota käytetään taulukon läpikäynnissä avuksi. Muuttuja indeksi on käytössä vain metodin suorituksen ajan.

Nyt on korkea aika alottaa ohjelmointi

tee viikon 1 tehtävät 1-2

Hajautustaulu (HashMap)

Hajautustaulu on yksi Javan yleishyödyllisistä tietorakenteista. Hajautustaulun ideana on laskea oliota kuvaavalle avaimelle, esimerkiksi ihmisen nimi, yksilöivä arvo. Tätä yksilöivää arvoa voidaan käyttää taulukon indeksinä, johon olion viite tallennetaan. Kun hajautustaulusta haetaan avaimen perusteella, löydetään suoraan taulun indeksi jossa olioviite on. Javan luokka HashMap kapseloi hajautustaulun toteutuksen, ja tarjoaa valmiit metodit sen käyttöön.

Hajautustaulu ottaa kaksi tyyppiparametria, avaimen tyypin ja tallennettavan olion tyypin. Seuraava esimerkki käyttää avaimena Integer-tyyppistä oliota, ja oliona String-tyyppistä oliota.

HashMap<Integer, String> numerot = new HashMap<Integer, String>();
numerot.put(1, "Yksi");
numerot.put(2, "Kaksi");

String merkkijono = numerot.get(1);
System.out.println(merkkijono);
merkkijono = numerot.get(42);
System.out.println(merkkijono);

Esimerkissä siis luodaan hajatustaulu, jonka avaimena on kokonaisluku (huomaa että avaimet ovat myös aina viitetyyppisiä muuttujia), ja tallennettavana oliona merkkijono. Hajautustauluun lisätään tietoa put()-metodilla, joka ottaa parametreikseen viitteet avaimeen ja tallennettavaan olioon. Metodi get()-palauttaa annettuun avaimeen liittyvän viitteen.

Koska hajautustauluun ei ole lisättyä oliota avaimelle 42, palauttaa get()-metodi avaimelle 42 null-viitteen. Esimerkin tulostus on siis seuraavanlainen.

Yksi
null

Hajautustaulussa tietty avain osoittaa aina tiettyyn paikkaan, jolloin avain ei voi osoittaa kahteen eri olioon samalla aikaa. Jos samalla avaimelle tallennetaan uusi olio, poistuu vanhan olion viite hajautustaulusta.

HashMap<Integer, String> numerot = new HashMap<Integer, String>();
numerot.put(1, "Yksi");
numerot.put(2, "Kaksi");
numerot.put(1, "Iiso yksi!");

String merkkijono = numerot.get(1);
System.out.println(merkkijono);
merkkijono = numerot.get(42);
System.out.println(merkkijono);

Koska avain 1 asetetaan uudestaan, on yllä olevan esimerkin tulostus seuraavanlainen.

Iiso yksi!
null

Kirjojen haku hajautustaulun avulla

Tutkitaan hajautustaulun toimintaa yksinkertaisen kirjastoesimerkin avulla. Kirjastosta voi hakea kirjoja kirjan nimellä, nimi toimii siis avaimena. Jos annetulle nimelle löytyy kirja, saadaan siihen liittyvä viite ja samalla kirjan tiedot. Toteutetaan ensiksi esimerkkiluokka Kirja, jolla on oliomuuttujina nimi ja sisältö.

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

  public Kirja() {  
  }

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

  public String getNimi() {
    return nimi;
  }

  public void setNimi(String nimi) {
    this.nimi = nimi;
  }

  public String getSisalto() {
    return sisalto;
  }

  public void setSisalto(String sisalto) {
    this.sisalto = sisalto;
  }
    
  public String toString() {
    String palautus = "Nimi: " + this.nimi + "\n"
      + "Sisältö: " + this.sisalto;
    return palautus;
  }
}

Luodaan seuraavaksi hajautustaulu, joka ottaa avaimekseen kirjan nimen, eli String-tyyppisen olion, ja tallettaa viitteitä Kirja-olioihin.

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

Yllä oleva hajautustaulu siis käyttää avaimena String-oliota. Hajautustaulu käyttää avaimen arvon laskemiseen Object-luokassa määriteltyä hashCode()-metodia, jonka perivän luokat voivat ylikirjoittaa. Emme kuitenkaan tutustu hajautustaulun toteutukseen tarkemmin tällä kurssilla.

Laajennetaan esimerkkiä siten, että kirjahakemistoon lisätään kaksi kirjaa, Matti L:n Matkat ja Matti P:n Tarinat.

Kirja mattiLMatkat = new Kirja();
mattiLMatkat.setNimi("Matti L:n Matkat");
mattiLMatkat.setSisalto("Eräänä rauhallisena iltana Saksassa, poliisi lähestyi tuttavallisesti minua..");

Kirja mattiPTarinat = new Kirja();
mattiPTarinat.setNimi("Matti P:n Tarinat");
mattiPTarinat.setSisalto("Palatessani eräältä lomalta ikkuna oli säpäleinä ja kodistani oli siivottu vanha elektroniikka..");

HashMap<String, Kirja> kirjahakemisto = new HashMap<String, Kirja>();
kirjahakemisto.put(mattiLMatkat.getNimi(), mattiLMatkat);
kirjahakemisto.put(mattiPTarinat.getNimi(), mattiPTarinat);

Nyt kirjahakemistosta voi hakea kirjan nimellä kirjoja. Seuraavan esimerkin ensimmäinen haku ei tuota osumaa, ja hajautustaulu palauttaa null-viitteen. Kirja "Matti L:n Matkat" kuitenkin löytyy.

Kirja k = kirjahakemisto.get("Matti V:n Jorinat");
System.out.println(k);
System.out.println();
k = kirjahakemisto.get("Matti L:n Matkat");
System.out.println(k);
null

Nimi: Matti L:n Matkat
Sisältö: Eräänä rauhallisena iltana Saksassa, poliisi lähestyi tuttavallisesti minua..

Hajautustaulu on hyödyllinen silloin kun tiedetään millaista tietoa halutaan hakea. Avaimet ovat aina yksilöllisiä, joten saman avaimen taakse ei voi tallettaa montaa eri oliota. Tallennettava olio voi toki olla lista tai toinen hajautustaulukko!

Kirjasto

Yllä olevan kirjahakemiston ongelmana on se, että kirjoja haettaessa täytyy muistaa kirjan nimi merkki merkiltä oikein. Javan valmis String-luokka tarjoaa meille välineet tähänkin. Metodi toLowerCase() muuttaa merkkijonon kirjaimet pieniksi, ja metodi trim() poistaa merkkijonon alusta ja lopusta tyhjät merkit (esimerkiksi välilyönnit).

String teksti = "  JEEEEEEEeeeEE";
teksti = teksti.toLowerCase(); // teksti nyt "  jeeeeeeeeeeee"
teksti = teksti.trim() // teksti nyt "jeeeeeeeeeeee"

Luodaan luokka Kirjasto, joka kapseloi hajautustaulun siten, että avaimien kirjainkoolla ei ole väliä. Lisätään Kirjasto-luokalle myös metodit lisaaKirja(Kirja kirja) ja poistaKirja(String kirjanNimi).

public class Kirjasto {
  private HashMap<String, Kirja> hakemisto;
  
  public Kirjasto() {
    hakemisto = new HashMap<String, Kirja>();
  }
  
  public void lisaaKirja(Kirja kirja) {
    String avainNimi = kirja.getNimi();
    avainNimi = avainNimi.toLowerCase(); // muunnetaan nimi pieniksi kirjaimiksi
    avainNimi = avainNimi.trim(); // poistetaan tyhjät merkit alusta ja lopusta
    
    if(hakemisto.containsKey(avainNimi)) {
      System.out.println("Kirja on jo kirjastossa!");
    } else {
      hakemisto.put(avainNimi, kirja);
    }
  }
  
  
  public void poistaKirja(String kirjanNimi) {
    kirjanNimi = kirjanNimi.toLowerCase(); // muunnetaan nimi pieniksi kirjaimiksi
    kirjanNimi = kirjanNimi.trim(); // poistetaan tyhjät merkit alusta ja lopusta
    
    if(hakemisto.containsKey(kirjanNimi)) {
      Kirja poistettava = hakemisto.get(kirjanNimi);
      hakemisto.remove(kirjanNimi);
    } else {
      System.out.println("Kirjaa ei löydy, ei voida poistaa!");
    }
  }
}

Yllä olevasta kirjastosta puuttuu vielä lista kaikista kirjoista, mikä on kirjastoille hyvin tärkeää. Käytetään tuttua luokkaa ArrayList kirjojen listaamiseen. Joudumme myös muuttamaan kirjojen lisäystä ja poistoa siten, että viitteet poistetaan kummastakin tietorakenteesta.

public class Kirjasto {
  private HashMap<String, Kirja> hakemisto;
  private ArrayList<Kirja> kirjat;
  
  public Kirjasto() {
    hakemisto = new HashMap<String, Kirja>();
    kirjat = new ArrayList<Kirja>();
  }
  
  public void lisaaKirja(Kirja kirja) {
    String avainNimi = kirja.getNimi();
    avainNimi = avainNimi.toLowerCase(); // muunnetaan nimi pieniksi kirjaimiksi
    avainNimi = avainNimi.trim(); // poistetaan tyhjät merkit alusta ja lopusta
    
    if(hakemisto.containsKey(avainNimi)) {
      System.out.println("Kirja on jo kirjastossa!");
    } else {
      hakemisto.put(avainNimi, kirja);
      kirjat.add(kirja);
    }
  }
  
  public void poistaKirja(String kirjanNimi) {
    kirjanNimi = kirjanNimi.toLowerCase(); // muunnetaan nimi pieniksi kirjaimiksi
    kirjanNimi = kirjanNimi.trim(); // poistetaan tyhjät merkit alusta ja lopusta
    
    if(hakemisto.containsKey(kirjanNimi)) {
      Kirja poistettava = hakemisto.get(kirjanNimi);
      kirjat.remove(poistettava);
      hakemisto.remove(kirjanNimi);
    } else {
      System.out.println("Kirjaa ei löydy, ei voida poistaa!");
    }
  }
}

Kirjojen listaaminen ArrayListin avulla jää lukijan itse toteutettavaksi. Toteutetaan vielä kirjan hakutoiminnallisuus siten, että kirjaa haetaan hajautusrakenteesta sen nimellä.

public Kirja haeKirja(String kirjanNimi) {
  kirjanNimi = kirjanNimi.toLowerCase(); // muunnetaan nimi pieniksi kirjaimiksi
  kirjanNimi = kirjanNimi.trim(); // poistetaan tyhjät merkit alusta ja lopusta
  return hakemisto.get(kirjanNimi);    
}

Yllä oleva metodi palauttaa haetun kirjan jos sellainen löytyy, muulloin null-arvon. Voimme myös käydä kaikki hakemiston avaimet läpi yksitellen, etsien esimerkiksi alkuosaa kirjan nimestä. Tällä tavalla etsiessä menetämme kuitenkin hajautustaulun nopeusedun, sillä huonoimmassa tapauksessa joudumme käymään kaikkien kirjojen nimet läpi.

public Kirja haeKirja(String kirjanNimi) {
  kirjanNimi = kirjanNimi.toLowerCase(); // muunnetaan nimi pieniksi kirjaimiksi
  kirjanNimi = kirjanNimi.trim(); // poistetaan tyhjät merkit alusta ja lopusta
  for(String nimi: hakemisto.keySet()) {
    if(nimi.startsWith(kirjanNimi)) {
      return hakemisto.get(nimi);
    }
  }
  return null;
}

Yksi ohjelmoinnin periaatteista on ns. DRY-periaate (Don't Repeat Yourself), jolla pyritään välttämään saman koodin olemista useassa paikassa. Merkkijonon pieneksi muuttaminen ja trimmaus, eli tyhjien merkkien poisto alusta ja lopusta, toistuu useasti kirjastoluokassamme. Siistitään Kirjasto-luokkaa siten, että kirjan nimen siistiminen tehdään erillisessä metodissa. Lisätään metodi siisti(), joka ottaa parametrina String-olion, ja palauttaa oliosta siistityn version.

private String siisti(String siistittava) {
  siistittava = siistittava.toLowerCase(); // muunnetaan nimi pieniksi kirjaimiksi
  return siistittava.trim(); // poistetaan tyhjät merkit alusta ja lopusta
}

Luokka Kirjasto vielä kokonaisuudessaan.

public class Kirjasto {
  private HashMap<String, Kirja> hakemisto;
  private ArrayList<Kirja> kirjat;
  
  public Kirjasto() {
    hakemisto = new HashMap<String, Kirja>();
    kirjat = new ArrayList<Kirja>();
  }
  
  public void lisaaKirja(Kirja kirja) {
    String avainNimi = kirja.getNimi();
    avainNimi = siisti(avainNimi);
    
    if(hakemisto.containsKey(avainNimi)) {
      System.out.println("Kirja on jo kirjastossa!");
    } else {
      hakemisto.put(avainNimi, kirja);
      kirjat.add(kirja);
    }
  }
  
  public void poistaKirja(String kirjanNimi) {
    kirjanNimi = siisti(kirjanNimi);
    
    if(hakemisto.containsKey(kirjanNimi)) {
      Kirja poistettava = hakemisto.get(kirjanNimi);
      kirjat.remove(poistettava);
      hakemisto.remove(kirjanNimi);
    } else {
      System.out.println("Kirjaa ei löydy, ei voida poistaa!");
    }
  }
  
  // kirjojen läpi käyminen yksi kerrallaan jätettiin pois Kirjasto-toteutuksesta
  public Kirja haeKirja(String kirjanNimi) {
    kirjanNimi = siisti(kirjanNimi);
    return hakemisto.get(kirjanNimi);    
  }
  
  private String siisti(String siistittava) {
    siistittava = siistittava.toLowerCase(); // muunnetaan nimi pieniksi kirjaimiksi
    return siistittava.trim(); // poistetaan tyhjät merkit alusta ja lopusta
  }
}
Tee nyt viikon 1 tehtävät 3 ja 4

Algoritmin suunnittelu

Monimutkaisen algoritmin suunnittelussa kynä ja paperi ovat usein hyödyllisiä. Aluksi kannattaa käydä läpi yksinkertaisia tapauksia ja yrittää löytää niistä säännöllisyyksiä, joiden avulla voidaan laatia yleisessä tapauksessa toimiva algoritmi. Sitten kun algoritmin peruslogiikka on selvillä, on mukavaa siirtyä tietokoneelle ja aloittaa koodaaminen.

Esimerkki: Kuusen tulostus

Ohjelmoinnin perusteet -kurssilla oli tehtävänä laatia ohjelma, joka tulostaa annetun kokoisen kuusen seuraavien esimerkkien mukaisesti.

Anna korkeus: 3
  *
 ***
*****
  *
Anna korkeus: 5
    *
   ***
  *****
 *******
*********
    *

Tällaista ongelmaa on järkevää lähestyä piirtämällä muutamia kuusia ja katsomalla, miten ne muodostuvat tähdistä. Seuraavassa on piirretty tapaukset, joissa korkeus on 3 ja 4. Lisäksi on piirretty tarkemmin tapaus, jossa korkeus on 4. Kuvista on jätetty pois viimeiselle riville tuleva kuusen juuri.

Havaitaan seuraavat asiat, kun kuusen korkeus on 4:

Hieman miettimällä yleinen säännöllisyys on seuraava:

Tästä saadaan seuraava koodin runko:

valilyonnit = korkeus - 1;
tahdet = 1;
for (int i = 0; i < korkeus; i++) {
    // tulosta "valilyonnit" välilyöntiä
    // tulosta "tahdet" tähteä
    valilyonnit = valilyonnit - 1;
    tahdet = tahdet + 2;
}

Lopullinen koodi voisi olla seuraavanlainen:

import java.util.Scanner;
public class KuusenTulostus {
    private static Scanner lukija = new Scanner(System.in);

    private static void tulostaMonta(int maara, char merkki) {
        for (int i = 0; i < maara; i++) {
            System.out.print(merkki);
        }
    }

    public static void main(String[] args) {
        System.out.print("Anna korkeus: ");
        int korkeus = Integer.parseInt(lukija.nextLine());
        int valilyonnit = korkeus - 1;
        int tahdet = 1;
        for (int i = 0; i < korkeus; i++) {
            tulostaMonta(valilyonnit, ' ');
            tulostaMonta(tahdet, '*');
            System.out.println();
            valilyonnit = valilyonnit - 1;
            tahdet = tahdet + 2;
        }
        tulostaMonta(korkeus - 1, ' ');
        tulostaMonta(1, '*');
        System.out.println();
    }
}
Tee nyt viikon tehtävät 5 ja 6

Abstrahointi

Abstrahoinnilla tarkoitetaan asioiden määrittelyä tarpeellisten toimintojen ja ominaisuuksien kautta, ottamatta kantaa siihen miten ne on toteutettu. Abstrahointi on perusteltua ohjelmistoja suunniteltaessa kun ei haluta ottaa kantaa siihen, miten yksittäiset toiminnot toteutetaan. Yksi abstrahoinnin välineistä on rajapinta, johon tutustutaan seuraavaksi.

Rajapinta

Rajapinta (engl. interface) kuvaa käyttäytymistä määrittelemällä listan metodeja, joita rajapinnan kuvaava käyttäytyminen toteuttaa. Rajapinta määritellään omassa tiedostossa, jonka nimi on Rajapinnannimi.java. Rajapinnan tyyppi on interface, ja se sisältää määrittelyn käyttäytymiseen liittyvistä metodeista, mutta ei niiden toteutusta. Rajapinta määrittelee siis vain metodien nimet ja paluuarvot, mutta ei sitä, miten ne on toteutettu. Tällöin eri luokat voivat toteuttaa rajapinnan määrittelemän käyttäytymisen niihin sopivalla tavalla. Katsotaan puhumiskäyttäytymistä määrittelevää rajapintaa Puhuva.

public interface Puhuva {
  public String puhu(); // vain metodin nimi, ei toteutusta
}

Rajapinta Puhuva määrittelee metodin puhu(), joka palauttaa String-tyyppisen olion. Metodin puhu() toteutuksen pitää siis palauttaa merkkijono, joka kuvaa puhetta. Puhuva-rajapintaa voidaan ajatella sopimuksena puhumiskäyttäytymisestä.

Kaikkien rajapinnan määrittelevien metodien näkyvyysmääre on automaattisesti public. Tämän takia näkyvyysmääre jätetään usein kokonaan merkkaamatta:

public interface Puhuva {
  String puhu(); // sama kuin public String puhu()
}

Rajapinta on vain sopimus käyttäytymisestä. Jotta käyttäytyminen toteutuu, täytyy luokan toteuttaa rajapinta. Luokka toteuttaa rajapinnan avainsanalla implements, joka kertoo luokan toteuttavan kaikki annetun rajapinnan metodit. Rajapinnan toteuttaminen tarkoittaa sopimuksen tekemistä siitä, että luokka tarjoaa kaikki rajapinnan määrittelemät toiminnot, eli metodit. Luokkaa, joka toteuttaa rajapinnan, mutta ei toteuta rajapinnan metodeja, ei voi olla olemassa. Seuraava luokka Luennoitsija toteuttaa rajapinnan Puhuva. Huomaa että Luennoitsija-luokan on pakko toteuttaa rajapinnan Puhuva määrittelevä metodi public String puhu().

public class Luennoitsija implements Puhuva {
  ...
  
  public String puhu() {
    String[] aiheet = {"kapselointi", "periytyminen", "polymorfismi", "abstrahointi"};
    Random arpoja = new Random();
    int indeksi = arpoja.nextInt(aiheet.length);

    return "Yksi olio-ohjelmoinnin peruskäsitteistä, " + aiheet[indeksi] + ".";
  }
}

Luennoitsija-luokka valitsee satunnaisesti yhden neljästä olio-ohjelmoinnin peruskäsitteestä, ja puhuu siitä. Satunnaisuutta varten luodaan uusi Random-olio, jolta voidaan kätevästi pyytää satunnaista indeksiä taulukkoon. Lopuksi palautetaan merkkijono "Yksi olio-ohjelmoinnin peruskäsitteistä, " katenoituna indeksin osoittamaan aiheeseen. Yllä olevan Luennoitsija-luokan metodi puhu() palauttaa siis satunnaisesti yhden seuraavasta neljästä erilaisesta lauseesta.

Yksi olio-ohjelmoinnin peruskäsitteistä, kapselointi.
Yksi olio-ohjelmoinnin peruskäsitteistä, periytyminen.
Yksi olio-ohjelmoinnin peruskäsitteistä, polymorfismi.
Yksi olio-ohjelmoinnin peruskäsitteistä, abstrahointi.

Toteutetaan vielä luokka Opiskelija, joka myös toteuttaa rajapinnan Puhuva.

public class Opiskelija implements Puhuva {
  ...
  public String puhu() {
    return "Mikä toi on?";
  }
}

Yllä oleva luokka Opiskelija toteuttaa rajapinnan Puhuva ja sen määrittelemän metodin puhu(). Metodi puhu() palauttaa aina tekstin "Mikä toi on?".

Luodaan vielä ohjelma, joka luo kolme rajapinnan Puhuva toteuttavaa luokkaa. Kaksi luennoitsijaa ja yhden opiskelijan.

Luennoitsija mattiL = new Luennoitsija();
Luennoitsija mattiP = new Luennoitsija();
Opiskelija mattiV = new Opiskelija();

System.out.println(mattiV.puhu());
System.out.println(mattiL.puhu());
System.out.println(mattiV.puhu());
System.out.println(mattiP.puhu());
System.out.println(mattiV.puhu());
System.out.println(mattiP.puhu());

Yllä olevan ohjelman tulostus voisi olla esimerkiksi seuraavaa.

Mikä toi on?
Yksi olio-ohjelmoinnin peruskäsitteistä, kapselointi.
Mikä toi on?
Yksi olio-ohjelmoinnin peruskäsitteistä, polymorfismi.
Mikä toi on?
Yksi olio-ohjelmoinnin peruskäsitteistä, kapselointi.

Rajapinta muuttujan tyyppinä

Koska rajapinta Puhuva määrittelee metodin puhu(), voidaan kaikilta Puhuva-rajapinnan toteuttavien luokkien olioilta kutsua metodia puhu(). Rajapinta toimii kuten viitetyyppiset muuttujat, eli rajapinta-tyyppinen muuttuja sisältää viitteen olioon. Jos olion tyyppinä käytetään rajapintaa, voidaan siltä kutsua vain rajapinnan määrittelemiä metodeja. Rajapintojen ja luokkien suurin ero on se, että rajapinnasta ei voi tehdä ilmentymää. Tietyn rajapinnan toteuttavan luokan voi kuitenkin asettaa rajapinta-tyyppiseen muuttujaan. Seuraavassa esimerkissä luodaan kaksi Puhuva-tyyppistä oliota, toinen Luennoija luokasta, toinen luokasta Opiskelija. Kummallekin kutsutaan myös metodia puhu()

Puhuva mattiP = new Luennoitsija();
Puhuva mattiV = new Opiskelija();

System.out.println(mattiV.puhu());
System.out.println(mattiP.puhu());

Yllä olevassa esimerkissä kutsutaan siis viitteiden mattiP ja mattiV takana oleville olioille metodia puhu(). Koska luokat Luennoitsija ja Opiskelija toteuttavat rajapinnan Puhuva, on niillä myös metodi puhu().

Edellä nähdyn dialogin olioiden mattiL, mattiP ja mattiV välillä voi kirjoittaa seuraavasti.

Puhuva mattiL = new Luennoitsija();
Puhuva mattiP = new Luennoitsija();
Puhuva mattiV = new Opiskelija();

System.out.println(mattiV.puhu());
System.out.println(mattiL.puhu());
System.out.println(mattiV.puhu());
System.out.println(mattiP.puhu());
System.out.println(mattiV.puhu());
System.out.println(mattiP.puhu());

Myös taulukkoon tai ArrayListille voidaan tallettaa olioita rajapinnan tyyppisenä.

Kuvataan pajaohjaustilannetta muutaman Puhuva-rajapinnan toteuttavan olion avulla. Ensin määritellään Pajaohjaaja:

public class PajaOhjaaja implements Puhuva {
  public void juoKahvia() {
     Sysytem.out.println("poistun nyt paikalta muutamaksi minuutiksi");
  }
  
  public String puhu() {
    String[] aiheet = {"kirjoittanut kaikkea koodia mainiin", "tehnyt liian pitkää metodia"};
    Random arpoja = new Random();
    int indeksi = arpoja.nextInt(aiheet.length);

    return "Et kai " + aiheet[indeksi] + "?";
  }
}

Pajassa on 25 opiskelijaa ja 3 pajaohjaajaa, kaikkia käsitellään Puhuva-tyyppisinä:

ArrayList>Puhuva< osallistujat = new ArrayList>Puhuva<(); 

osallistujat.add( PajaOhjaaja() );
osallistujat.add( PajaOhjaaja() );
osallistujat.add( PajaOhjaaja() );

for ( int i=0; i < 25; i++ ) {
  osallistujat.add( new Opiskelija() );
}

while ( true ) {
  for ( Puhuva p : osallistujat ) {
    System.out.println( p.puhu() );  
  }
  osallistujat.get(0).juoKahvia(); // ei toimi! osallistujat käsitellään Puhuva:na
} 

Tämä piinallinen pajaohjaustapahtuma kestää ikuisesti.

Huom: Kun rajapintaa käytetään muuttajan tyyppinä, ei tiedetä rajapinnan toteuttavan luokan muista metodeista. Voidaan käyttää ainoastaan rajapinnan määrittelemiä metodeja. Koska osallistujat tunnetaan ainoastaan Puhuva-rajapinnan kautta, ei viimeisessä rivillä oleva ohjaajan kahvinjuontiyritys onnistu.

Tee nyt viikon 2 tehtävät 1.1 ja 1.2

Rajapinta metodin parametrina

Rajapintojen todelliset hyödyt tulevat esille kun niitä käytetään metodille annettavan parametrin tyyppinä. Koska rajapintaa voidaan käyttää muuttujan tyyppinä, voidaan sitä käyttää myös metodikutsuissa parametrin tyyppinä.

Luodaan uusi käyttäytymistyyppi, eli rajapinta, Liikkuva. Rajapinta liikkuva määrittelee metodit liiku(), ja paljonkoLiikuttu(). Metodi liiku() määrittelee liikkumistoiminnon, ja metodi paljonkoLiikuttu() taas antaa tiedon kuljetusta matkasta.

public interface Liikkuva {
  public void liiku();
  public int paljonkoLiikuttu();
}

Toteutetaan myös kaksi luokkaa, Auto ja Formula, jotka kummatkin toteuttavat rajapinnan Liikkuva. Luokilla Auto ja Formula on attribuutti kilometrit, joka pitää kirjaa liikutuista kilometreistä.

public class Auto implements Liikkuva {
  private int kilometrit;
  ...
  
  public Auto() {
    this.kilometrit = 0;
  }
  ...
  
  public void liiku() {
    this.kilometrit++;
  }
  
  public int paljonkoLiikuttu() {
    return this.kilometrit;
  }
}
public class Formula implements Liikkuva {
  private int kilometrit;
  ...
  
  public Formula() {
    this.kilometrit = 0;
  }
  
  ...
  
  public void liiku() {
    this.kilometrit += 3;
  }
  
  public int paljonkoLiikuttu() {
    return this.kilometrit;
  }
}

Sekä luokka Auto, että luokka Formula toteuttavat rajapinnan liikkuva. Luodaan seuraavaksi pääohjelma, jossa on metodi Liikkuva-tyyppisten olioiden liikuttamiseen.

public static void main(String[] komentoriviParametrit) {
  Auto volga = new Auto();
  Formula f1 = new Formula();
  
  liikuta(volga);
  liikuta(f1);
  
  System.out.println("Autolla ajettu " + volga.paljonkoLiikuttu() + " km.");
  System.out.println("Formulalla ajettu " + f1.paljonkoLiikuttu() + " km.");
}

public static void liikuta(Liikkuva liikkuva) {
  liikkuva.liiku();
}

Esimerkki tulostaa seuraavan tulosteen

Autolla ajettu 1 km.
Formulalla ajettu 3 km.

Koska luokat Auto ja Formula toteuttavat rajapinnan Liikkuva, voidaan ne antaa parametrina metodille joka ottaa Liikkuva-tyyppisiä olioita. Metodin sisällä voi tietysti kutsua vain rajapinnan Liikkuva määrittelemiä metodeja.

Rajapinta metodin paluuarvona

Rajapinta voi olla myös metodin paluuarvo. Seuraava metodi luo uusia Liikkuva-rajapinnan toteuttavia olioita. Huomaa että rajapinnalla ei ole konstruktoria, vaan metodi palauttaa rajapinnan toteuttavista luokista tehtyjä ilmentymiä.

public static Liikkuva luoLiikkuva() {
  Random arpoja = new Random();
  if(arpoja.nextBoolean()) {
    return new Auto();
  }
  
  return new Formula();
}

Yllä oleva metodi luo 50% todennäköisyydellä auto-olion, ja 50% todennäköisyydellä formulaolion. Luotu Auto tai Formula palautetaan rajapinnan Liikkuva toteuttavana oliona.

Tee nyt viikon 2 tehtävät 1.3 - 1.5

Valmiit rajapinnat

Javan API tarjoaa ison määrän valmiita rajapintoja. Muunmuassa tuttu ArrayList-luokka toteuttaa rajapinnan List, joka määrittelee listan perustoiminnot. ArrayList-luokkaa voisikin käyttää myös seuraavasti:

List<String> merkkijonot = new ArrayList<String>();
merkkijonot.add("Moi taas!");

Koska luokka ArrayList toteuttaa rajapinnan List, ja rajapinta List määrittelee metodin add(), voidaan metodia add() kutsua yllä luodulle List-tyyppiselle oliolle.

Kuten Java API:sta huomaamme Rajapinnan List toteuttavia luokkia on muitakin, esim. LinkedList:

List<String> merkkijonot = new LinkedList<String>();
merkkijonot.add("Moi taas!");

Molemmat rajapinnan toteutukset ArrayList ja LinkedList toimivat käyttäjän näkökulmasta samoin. Rajapinta siis abstrahoi niiden sisäisen toiminnallisuuden täysin. Sisäinen rakenne ArrayList:illä ja LinkedListillä on kuitenkin huomattavan erilainen. Jos on kyse suurista syötteistä on tietyissä toiminnoissa suuri suorituskykyero. LinkedList:iin voi lisätä tai poistaa mistä kohtaa tahansa alkioita nopeasti, toisin kuin ArrayList:istä.

Seuraavassa lisätään listan alkuun eli add(0, "Moi taas"!) komennolla paikkaan 0 miljoona merkkijonoa:

List<String> merkkijonot = new LinkedList<String>();

for (int i=0; i < 1000000; i++ ) {
  merkkijonot.add(0,"Moi taas!");
}

LinkedList toimii nopeasti. Jos listana käytettäisiin ArrayList:iä, olisi suorituskyky katastrofaalinen! ArrayList:iin lisääminen kannattaa tehdä aina loppuun eli add(0, "Moi taas"!) sijaan add("Moi taas"), sillon suorituskykyhaittaa ei ole.

ArrayList:iä taas toimii nopeasti jos on tarve käydä läpi listaa "hyppimällä" eli jossain muussa järjestyksessä kuin alusta loppuun.

Seuraavassa lisätään ensin listalle miljoona merkkijonoa ja sen jälkeen tulostetaan satunnaisia merkkijonoja miljoona kappaletta:

List<String> merkkijonot = new ArrayList<String>();

// lisätään listalle miljoona merkkijonoa

Random arpa = new Random();
for (int i=0; i < 1000000; i++ ) {
  System.out.println(  merkkijonot.get( arpa(1000000) ) );  
}

Tähön tarkoitukseen ArrayList on tehokas, mutta LinkedList surkea.

Comparable

Yksi Javan valmiiksi tarjoamista rajapinnoista on Comparable. Rajapinta Comparable määrittelee metodin compareTo(), joka palauttaa this-olion paikan vertailujärjestyksessä verrattuna parametrina annettuun olioon (negatiivinen luku, 0 tai positiivinen luku). Jos this-olio on vertailujärjestyksessä ennen parametrina saatavaa olioa, tulee metodin palauttaa negatiivinen luku, jos taas parametrina saatava olio on järjestyksessä ennen, tulee metodin palauttaa positiivinen luku. Vertailujärjestyksellä tarkoitetaan tässä ohjelmoijan määrittelemää olioiden "suuruusjärjestystä", eli jos oliot järjestetään sort-metodilla, mikä on niiden järjestys.

Muutetaan aiemmin luotua luokkaa Opiskelija siten, että sillä on nimi ja se toteuttaa Comparable-rajapinnan. Lisätään opiskelijalle myös attribuutit pituus, jota käytetään compareTo()-metodissa. Comparable-rajapinta ottaa tyyppiparametrina myös luokan, johon sitä verrataan.

public class Opiskelija implements Puhuva, Comparable<Opiskelija> {
  private String nimi;
  private int pituus;
  
  public Opiskelija(String nimi, int pituus) {
    this.nimi = nimi;
    this.pituus = pituus;
  }
  
  public String annaNimi() {
    return this.nimi;
  }
  
  public int annaPituus() {
    return this.pituus;
  }
  
  // metodi saa parametrikseen Opiskelija-tyyppisen olion, sillä Comparable-rajapinnalle on annettu tyypiksi Opiskelija
  public int compareTo(Opiskelija toinen) {
    if(this.annaPituus() == toinen.annaPituus()) {
      return 0;
    } else if (this.annaPituus() > toinen.annaPituus()) {
      return 1;
    } else {
      return -1;
    }
  }
  
  public String puhu() {
    return "Mikä toi on?";
  }
}

Koska compareTo()-metodista riittää palauttaa negatiivinen luku, jos this-olio on pienempi kuin parametrina annettu olio, voitaisiin edellä esitelty metodi toteuttaa myös seuraavasti:

  public int compareTo(Opiskelija toinen) {
    return this.annaPituus() - toinen.annaPituus();
  }

Collections

Tässä on hiukan kertausta sekä joitakin uusia asioita Javan Collections-luokasta. Java tarjoaa suuren määrän valmiiksi tehtyjä toimintoja erilaisten tietorakenteiden käsittelyyn. Luokkakirjasto Collections on eräs kirjasto, jota tullaan käyttämään tällä kurssilla. Collections löytyy Javan pakkauksesta java.util.Collections, eli sen saa käyttöön kutsulla import java.util.Collections;, joka asetetaan lähdekooditiedoston alkuun.

Järjestäminen

Luokkakirjasto collections tarjoaa valmiiksi toteutetun järjestämisalgoritmin, jolla voidaan järjestää Comparable-rajapinnan toteuttavia olioita. Katsotaan seuraavaksi Opiskelija-olioiden järjestämistä pituusjärjestykseen.

ArrayList<Opiskelija> opiskelijat = new ArrayList<Opiskelija>();
opiskelijat.add(new Opiskelija("Matti L", 187));
opiskelijat.add(new Opiskelija("Robert W", 272));
opiskelijat.add(new Opiskelija("Aditya D", 56));

for(Opiskelija l: opiskelijat) {
  System.out.println(l.annaNimi());
}

System.out.println();
Collections.sort(opiskelijat);
for(Opiskelija l: opiskelijat) {
  System.out.println(l.annaNimi());
}

Esimerkin tulostus on seuraavanlainen

Matti L
Robert W
Aditya D

Robert W
Matti L
Aditya D
Tee nyt viikon 2 tehtävät 2.1 - 2.7

Hakeminen

Collections-luokkakirjasto tarjoaa myös valmiiksi toteutetun binäärihaun. Metodi binarySearch() palauttaa haetun alkion indeksin listasta, jos se löytyy. Jos alkioita ei löydy, hakualgoritmi palauttaa negatiivisen arvon. Metodi binarySearch() käyttää Comparable-rajapintaa haetun olion löytämiseen. Jos olion compareTo()-metodi palauttaa arvon 0, eli olio on sama, ajatellaan arvon löytyneen.

Opiskelija-luokkamme vertaa pituuksia compareTo()-metodissaan, eli listasta etsiessä etsisimme samanpituista opiskelijaa.

ArrayList<Opiskelija> opiskelijat = new ArrayList<Opiskelija>();
opiskelijat.add(new Opiskelija("Matti L", 187));
opiskelijat.add(new Opiskelija("Robert W", 272));
opiskelijat.add(new Opiskelija("Aditya D", 56));

Collections.sort(opiskelijat);

Opiskelija haettava = new Opiskelija("Nimi", 180);
int indeksi = Collections.binarySearch(opiskelijat, haettava);
if(indeksi >= 0) {
  System.out.println("180 senttiä pitkä löytyi indeksistä " + indeksi);
  System.out.println("Nimi: " + opiskelijat.get(indeksi).annaNimi());
}

haettava = new Opiskelija("Nimi", 187);
int indeksi = Collections.binarySearch(opiskelijat, haettava);
if(indeksi >= 0) {
  System.out.println("187 senttiä pitkä löytyi indeksistä " + indeksi);
  System.out.println("Nimi: " + opiskelijat.get(indeksi).annaNimi());
}

Esimerkkimme tulostaa seuraavaa

187 senttiä pitkä löytyi indeksistä 1
Nimi: Matti L

Huomaa että esimerkissä kutsuttiin myös metodia Collections.sort(). Tämä tehdään sen takia, että binäärihakua ei voida tehdä jos taulukko tai lista ei ole valmiiksi järjestyksessä.

Tee nyt viikon 2 tehtävät 2.8 - 2.11, tehtävät 3 ja tehtävät 4

Virheiden löytäminen ohjelmasta

Kurssilla ollaan usein törmätty tilanteeseen, missä ohjelman suoritus antaa virheen, eikä paikkaa josta virhe johtuu löydy helposti. Tässä kappaleessa esitellään muutama hyvä keino virheiden löytämiseksi.

Ohjelman tilan tulostaminen

Ensimmäinen tapa virheiden löytämiseen on muuttujien tilan tulostaminen System.out.println()-komentojen avulla. Jos metodi tai ohjelma toimii väärin, voi siihen lisätä välitulostuksia, jotka tulostavat arvot aina sopivissa kohdissa. Näitä arvoja tutkimalla voi katsoa, onko ohjelman toiminta halutunlaista.

Debuggerin käyttö

NetBeans tarjoaa myös niinsanotun debuggerin, eli työkalun jolla ohjelmaa voidaan suorittaa askeleittain. Lähdekoodi-ikkunan vasenta laitaa painamalla voidaan asettaa pysähdyspisteitä (engl. breakpoint), joissa ohjelman suoritus pysähtyy automaattisesti. Debugger näyttää pysähdyskohdissa muuttujat ja niiden arvot. Debuggerin saa valitsemalla Run-valikosta "Debug Main Project". Myös komento "Ctrl+F5" ajaa debuggerin. Seuraavaan pysähdyskohtaan pääsee etenemällä painamalla "F5". Huomaa että pysähdyskohdat täytyy määritellä sellaisille riveille, joissa on komento.

Youtube-video NetBeansin bebuggerin käytön perusteista.

Tee nyt viikon 2 tehtävät 5 ja ja tehtävä 6.

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.

Final

Final on samanlainen määre kuin static, eli sitä käytetään muuttujien määrittelyssä. Final-määre lukitsee muuttujan arvon siten, että sitä ei voi enää muuttaa. Alkeistyyppisille muuttujille tämä tarkoittaa sitä, että muuttujan arvo pysyy samana. Viitetyyppisille muuttujille lukitseminen tarkoittaa sitä, että viitettä ei voi muuttaa, mutta viitteen takana olevan olion sisäistä tilaa voi toki muuttaa.

Ohjelmoinnin perusteissa nähtiin viikon 6 Kassapääte-tehtävässä kuta kuinkin seuraanvanlaista koodia.

public class Kassapaate {
  public static double EDULLISESTI = 2.40;
  public static double MAUKKAASTI = 4.10; 
}    

Yllä olevan toteutuksen heikkous on se, että muuttujien arvoa voi muuttaa myös mistä tahansa. Final-määre lukitsee arvot paikalleen, eli määrittelemällä luokkakirjaston seuraavasti muuttujien arvoa ei enää voi muuttaa.

public class Kassapaate {
  public static final double EDULLISESTI = 2.40;
  public static final double MAUKKAASTI = 4.10; 
}    

Hyvä suunnittelutapa on lukita kaikkialle näkyvät staattiset muuttujat, eli määreen public static omaavat muuttujat, pysyviksi määreellä final.

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!.

Tee nyt viikon 3 tehtävät 1.1 ja 1.2

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.

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 oliomuuttuja 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.

Jos luokan Elainn oliomuuttujien näkyvyys olisi protected:

public class Elain {
  protected String nimi;
  protected double pituus;
  
  // ...
}

Voitaisiin eläimestä periytyvissä luokissa käyttää suoraan oliomuuttujia nimi ja pituus. Eli sensijaan että olisi käytettävä edellä olleen esimerkin tapaan getteriä:

public class Siipisimppu extends Vesielain {
  // ...
  
  public String toString() {
    return "Hei, olen Siipisimppu, ja nimeni on " + getNimi();
  }
}

Voitaisiin oliomuuttujaan nimi viitata suoraan:

public class Siipisimppu extends Vesielain {
  // ...
  
  public String toString() {
    return "Hei, olen Siipisimppu, ja nimeni on " + nimi;
  }
}
Tee nyt viikon 3 tehtävät 1.3 ja

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!

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.

Katso kuva Javan tutorialista

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 tiedoston 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 myöhemmin.

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,
Tee nyt tehtävä 1

Tiedostoon kirjoittaminen

Tiedostoon kirjoittamiseen on myös apuvälineet. Java tarjoaa luokan FileWriter, jolla voi kirjoittaa tekstiä tiedostoon. Luokan FileWriter konstruktori on kuormitettu, eli sillä on monta eri konstruktoria. Käytetään tiedoston nimen parametrina saavaa konstruktoria.

FileWriter kirjoittaja = new FileWriter("tiedosto.txt");
kirjoittaja.write("Hei tiedosto!\n");  // huomaa, että myös rivinvaihto \n n kirjoitettava tiedostoon 
kirjoittaja.write("Lisää tekstiä\n");  //                                       jos se sinne halutaan
kirjoittaja.write("Ja vielä lisää");
kirjoittaja.close(); // sulkemiskutsu sulkee tiedoston ja varmistaa kirjoitettu teksti menee tiedostoon

Esimerkissä kirjoitetaan tiedostoon "tiedosto.txt" merkkijono "Hei tiedosto!". Tiedosto löytyy Files-välilehdeltä projektin tiedostoista. Tiedostoon kirjoittaminen voi aiheuttaa poikkeustilanteen, eli heittää poikkeuksen. Emme edelleenkään varaudu poikkeuksiin, vaan annamme metodille määreen throws Exception. Metodi, jolle annetaan parametrina kirjoitettavan tiedoston nimi ja kirjoitettava sisältö voisi näyttää seuraavalta.

public static void kirjoitaTiedostoon(String tiedostonNimi, String teksti) throws Exception {
  FileWriter kirjoittaja = new FileWriter(tiedostonNimi);
  kirjoittaja.write(teksti);
  kirjoittaja.close();
}

Metodin (myös main()), joka kutsuu metodia kirjoitaTiedostoon() täytyy joko varautua poikkeukseen, tai heittää poikkeus. Luodaan vielä main()-metodi jossa kutsutaan kirjoitaTiedostoon()-metodia. Huomaa että myös main()-metodille on määritelty throws Exception-määre.

public static void main(String[] args) throws Exception {
  kirjoitaTiedostoon("paivakirja.txt", "Rakas päiväkirja, tänään oli kiva päivä.");
}

Yllä olevaa metodia kutsuttaessa luodaan tiedosto "paivakirja.txt", ja kirjoitetaan siihen teksti "Rakas päiväkirja, tänään oli kiva päivä.". Jos tiedosto on jo olemassa, pyyhkiytyy vanhan tiedoston sisältö uutta kirjoittaessa. Metodilla append() voidaan lisätä olemassaolevan tiedoston perään tekstiä, jolloin olemassaolevaa tekstiä ei poisteta. Seuraava metodi lisaaTiedostoon() lisää annetun tekstin tiedoston loppuun.

public static void lisaaTiedostoon(String tiedostonNimi, String teksti) throws Exception {
  FileWriter kirjoittaja = new FileWriter(tiedostonNimi);
  kirjoittaja.append(teksti);
  kirjoittaja.close();
}

Joskus tiedoston perään metodilla append kirjoittamisen sijasta on helpompi kirjoittaa koko tiedosto uudelleen.

Kirjoitus ja Lukeminen

Tehdään vielä pieni yhteenveto kirjoituksesta ja lukemisesta. Tiedoston voi lukea Scanner-luokan avulla jolle annetaan parametrina File-tyyppinen olio. Tiedoston kirjoitus taas tapahtuu FileWriter-luokan avulla. Tehdään vielä esimerkki jossa kirjoitetaan ensiksi tiedostoon, ja sitten luetaan tiedoston sisältö. Käytetään lukijan luonnissa FileWriter-luokan konstruktoria, joka saa parametrikseen File-olion.

File tiedosto = new File("tiedosto.txt");
FileWriter kirjoittaja = new FileWriter(tiedosto);
kirjoittaja.write("Heippa vaan!");
kirjoittaja.close();

Scanner lukija = new Scanner(tiedosto);
System.out.println(lukija.nextLine());

Esimerkin tulostus on Heippa vaan!

Poikkeukset

Poikkeustilanteet ovat tilanteita joissa ohjelman suoritus ei ole edennyt toivotusti. Edellä jätettiin poikkeustilanteet käsittelemättä määrittelemällä metodit ja pääohjelman sellaisiksi, että ne heittävät poikkeuksen (throws Exception). Katsotaan esimerkiksi metodia lueTiedosto(), joka lukee kokonaisen tiedoston sisällön merkkijonoon.

public static String lueTiedosto(String tiedostonNimi) throws Exception {
  File tiedosto = new File(tiedostonNimi);
  Scanner lukija = new Scanner(tiedosto);
  String teksti = "";
  while(lukija.hasNextLine()) {
    teksti += lukija.nextLine() + "\n";
  }
  return teksti;
}

Metodissa lueTiedosto() luokan Scanner tiedoston parametrina ottava konstruktori saattaa heittää poikkeuksen tilanteessa, jossa tiedoston lukeminen ei onnistu. Javan APIsta näkee poikkeuksen heittävät metodit ja konstruktorit (esim. tämä).

Voimme myös itse hoitaa poikkeustenkäsittelyn, jolloin poikkeusta ei siirretä eteenpäin. Tällöin poikkeuksen käsittelyvastuu pysyy metodilla, jossa poikkeus tapahtuu.

Poikkeukset käsitellään try { } catch () { } - lohkorakenteella, jossa try { } - lohkon sisällä on ohjelmakoodi joka halutaan suorittaa. Osio catch () { } kertoo poikkeuksen mihin varaudutaan, ja siihen liittyvässä lohkossa on ohjelmakoodi, mikä suoritetaan poikkeustilanteessa.

try {
  // mahdollisesti poikkeuksen heittävä ohjelmakoodi
} catch (Exception e) { // poikkeus johon varaudutaan
  // ohjelmakoodi, joka suoritetaan poikkeustilanteessa
}

Muutetaan metodia lueTiedosto() siten, että se varautuu itse mahdollisen poikkeustilanteen. Jos Scanner-konstruktori heittää poikkeuksen, palautetaan null-viite luettuna merkkijonona ja tulostetaan virheilmoitus. System-luokan staattinen muuttuja err tarjoaa samat tulostusmetodit kuin out, mutta niitä käytetään virhetapahtumien tulostamiseen.

public static String lueTiedosto(String tiedostonNimi) { // ei heitetä poikkeusta
  File tiedosto = new File(tiedostonNimi);
  Scanner lukija = null;
  
  try {
    lukija = new Scanner(tiedosto);
  } catch (Exception e) {
    System.err.println("Tiedoston avaaminen ei onnistunut!");
    return null;
  }
  
  String teksti = "";
  while(lukija.hasNextLine()) {
    teksti += lukija.nextLine() + "\n";
  }
  return teksti;
}

Metodi siis varautuu Scanner-luokan konstruktorin mahdolliseen poikkeukseen. Jos poikkeus tapahtuu, tulostetaan virheviesti ja palautetaan null-viite.

Lisätietoa: Katenointi, eli kahden merkkijonon yhdistäminen, luo aina uuden String-olion, jolloin tarvitaan myös roskienkeruuta vanhoille merkkijonoille. Luokkaa StringBuilder käyttämällä voimme yhdistää merkkijonoja ilman uusien String-olioiden luomista. Yllä oleva tiedoston lukeva metodi on paljon tehokkaampi seuraavanlaisena.

public static String lueTiedosto(String tiedostonNimi) { // ei heitetä poikkeusta
  File tiedosto = new File(tiedostonNimi);
  Scanner lukija = null;
  
  try {
    lukija = new Scanner(tiedosto);
  } catch (Exception e) {
    System.err.println("Tiedoston avaaminen ei onnistunut!");
    return null;
  }
  
  StringBuilder teksti = new StringBuilder();
  while(lukija.hasNextLine()) {
    teksti.append(lukija.nextLine());
    teksti.append("\n");
  }
  return teksti.toString();
}
Tee nyt tehtävät 2–4

Abstrakti Luokka

Abstrakti luokka on periytymiseen ja abstrahointiin liittyvä apuväline. Abstraktista luokasta ei voida tehdä ilmentymiä, eli olioita, mutta sen voi periä. Abstraktin luokan tunnistaa määreestä abstract sen määrittelyssä. Luodaan abstrakti luokka Korjaaja.

public abstract class Korjaaja {
  protected String korjattava;
  
  public Korjaaja(String korjattava) {
    this.korjattava = korjattava;
  }
  
  public void setKorjattava(String korjattava) {
    this.korjattava = korjattava;
  }
  
  public String getKorjattava() {
    return korjattava;
  }
}

Ilmentymän luonti luokasta Korjattava ei ole mahdollista, eli seuraava konstruktorikutsu ja viitteen asetus ei toimi.

Korjaaja mattiV = new Korjaaja("abstraktinen, sellainen, joka ei ole kouriintuntuva eikä konkreettinen");

Abstrakteilla luokilla määritellään yleiskäyttöinen runko runko sen periville luokille. Esimerkiksi abstraktista luokasta Korjaaja voidaan periä luokka KokeenKorjaaja. Luokka KokeenKorjaaja korjaa kokeita, ja sillä on abstraktin luokan Korjaaja määrittelemien metodien lisäksi metodi korjaa(). Abstraktin luokan periminen seuraa normaalin periytymisen sääntöjä, eli kutsumme yläluokan parametrillista konstruktoria super()-kutsulla. Myös yläluokan muuttujat ja metodit olisivat saatavilla.

public class KokeenKorjaaja extends Korjaaja {
  public KokeenKorjaaja(String korjattava) {
    super(korjattava);
  }

  public void korjaa() {
    this.korjattava += "\n - Pisteet " + Math.min(this.korjattava.length / 4, 60);
  }
}

Metodi korjaa() korjaa kokeen, eli tässä tapauksessa määrittelee siitä saatavat pisteet. Luokkakirjaston Math metodi min() palauttaa pienemmän kahdesta parametrista, eli kokeesta voi saada maksimissaan 60 pistettä. Luokasta KokeenKorjaaja voi tehdä ilmentymän normaalilla olion luontikutsulla.

String teksti = "An abstract class is designed only as a parent class " + 
  "from which child classes may be derived.";
KokeenKorjaaja mattiP = new KokeenKorjaaja(teksti);
mattiP.korjaa();
System.out.println(mattiP.getKorjattava());

Yllä oleva esimerkki luo ilmentymän luokasta KokeenKorjaaja ja kutsuu sen korjaa()-metodia. Esimerkin tulostus on seuraavanlainen:

An abstract class is designed only as a parent class from which child classes may be derived.
 - Pisteet 23

Abstrakteja luokkia käytetään tapauksissa joissa useat luokat tarvitsevat samaa rakennuspiirrustusta, mutta rakennuspiirrustukset itsessään eivät kuitenkaan ole sellaiset, joista haluttaisiin luoda konkreettinen ilmentymä.

Abstrakti Metodi

Abstraktin luokan metodit voidaan määritellä abstrakteiksi, jolloin niihin ei määritellä toteutusta. Abstrakti metodi määritellään avainsanalla abstract ja antamalla metodin paluuarvo, nimi ja parametrit. Esimerkiksi abstrakti metodi piirra().

public abstract void piirra();

Abstraktin metodin määrittely on lähes kuin rajapintametodin määrittely. Sillä ei ole aaltosuluilla rajattavaa metodirunkoa, ja metodimäärittely päättyy puolipisteeseen. Luodaan abstrakti luokka Hahmo, jolla on abstrakti metodi piirra(). Jokainen luokkaa Hahmo laajentava ei abstrakti luokka joutuu toteuttamaan oman piirra()-metodinsa.

public abstract class Hahmo {
  protected String nimi;
  
  public Hahmo(String nimi) {
    this.nimi = nimi;
  }
  
  public String getNimi() {
    return this.nimi;
  }
  
  public void setNimi(String nimi) {
    this.nimi = nimi;
  }
  
  public abstract void piirra();
}

Abstraktin metodin määrittelyssä ei siis määritellä metodin toteutusta, vaan vain tyyppi, nimi ja parametrit. Abstraktin luokan perivät ei abstraktit luokat joutuvat toteuttamaan abstraktit metodin. Jos abstrakti luokka perii abstraktin luokan, ei sen tarvitse toteuttaa yläluokan abstraktia metodia. Esimerkiksi seuraava luokka Elvis määrittelee myös Elviksen piirtämisen.

public class Elvis extends Hahmo {
  public Elvis(String nimi) {
    super(nimi);
  }

  public void piirra() {
    System.out.println("G   __");
    System.out.println("\\\\  ,,)_");
    System.out.println(" \\'-\\( /");
    System.out.println("  \\ | ,\\");
    System.out.println("   \\|_/\\\\");
    System.out.println("   / _ '.D");
    System.out.println("  / / \\ |");
    System.out.println(" /_\\  /_\\");
    System.out.println("'-    '-");
    System.out.println("\"" + this.nimi + "\"");
  }
}

Ylimääräiset kenoviivat \ ennen kenoviivoja ja lainausmerkkejä " johtuu siitä, että osa merkeistä toimii erikoismerkkeinä. Esimerkiksi lainausmerkki aloittaa ja lopettaa merkkijonon. Asettamalla kenoviivan ennen erikoismerkkiä, esimerkiksi lainausmerkkiä, tulostetaan kenoviivaa seuraava erikoismerkki normaalina merkkinä. Luodessamme luokan Elvis ilmentymän ja kutsuessamme sen piirra()-metodia seuraavasti saamme luontikappaleen alla olevan tulosteen.

Elvis e = new Elvis("Matti L");
e.piirra();
G   __
\\  ,,)_
 \'-\( /
  \ | ,\
   \|_/\\
   / _ '.D
  / / \ |
 /_\  /_\
'-    '-
"Matti L"

Voimme vastaavasti luoda luokan Janis (Jänis), jolla on oma piirra()-metodinsa.

public class Janis extends Hahmo {

  public Janis(String nimi) {
    super(nimi);
  }

  public void piirra() {
    System.out.println("(\\___/)");
    System.out.println("(='.'=)");
    System.out.println("(\")_(\")");
  }
}

Jäniksen piirra()-metodikutsu luo seuraavanlaisen tulosteen:

(\___/)
(='.'=)
(")_(")

Lisätään jänikselle vielä rajapinta Liikkuva.

public interface Liikkuva {
  public void liiku();
  public void liiku(int montaKertaa);
}

Ja muutetaan jäniksen toteutusta siten, että piirretty jänis oikeasti liikkuu.

public class Janis extends Hahmo implements Liikkuva {
  private int sijainti;

  public Janis(String nimi) {
    super(nimi);
    this.sijainti = 0;
  }

  public void piirra() {
    tulostaTyhjaa(); System.out.println("(\\___/)");
    tulostaTyhjaa(); System.out.println("(='.'=)");
    tulostaTyhjaa(); System.out.println("(\")_(\")");
  }
  
  private void tulostaTyhjaa() {
    for(int i = 0; i < sijainti; i++) {
      System.out.print(" ");
    }
  }

  public void liiku() {
    this.sijainti++;    
  }

  public void liiku(int montaKertaa) {
    for(int i = 0; i < montaKertaa; i++) {
      liiku();
    }
  }
}

Kokeile minkälaisella koodilla saat jäniksen liikkumaan seuraavasti!

(\___/)
(='.'=)
(")_(")
 (\___/)
 (='.'=)
 (")_(")
      (\___/)
      (='.'=)
      (")_(")

Rajapinnat Abstraktissa Luokassa

Abstraktille luokalle voidaan myös tehdä sopimus rajapintojen toteuttamisesta, aivan kuten kaikille muille luokille. Jos abstraktille luokalle määritellään rajapinta, ei rajapinnan metodeja ole kuitenkaan pakko toteuttaa abstraktin luokan sisällä. Tällöin abstraktin luokan perivä (ei abstrakti) luokka joutuu toteuttamaan rajapinnan määrittelemät metodit. Siirretään yllä määritelty Liikkuva-rajapinta abstraktiin luokkaan Hahmo.

public abstract class Hahmo implements Liikkuva {
  protected String nimi;
  
  public Hahmo(String nimi) {
    this.nimi = nimi;
  }
  
  public String getNimi() {
    return this.nimi;
  }
  
  public void setNimi(String nimi) {
    this.nimi = nimi;
  }
  
  public abstract void piirra();
}

Koska Liikkuva-rajapinnan metodit ovat vain sopimuksia metodien toteuttamisesta, aivan kuten abstraktit metoditkin, ei rajapinnan metodeja tarvitse määritellä erikseen Hahmo-luokassa. Tällöin, samoin kuin abstraktien metodien tapauksessa, perivät luokat joutuvat toteuttamaan määritellyt metodit.

Esimerkiksi Hahmo-luokan toteuttava luokka Elvis jouduttaisiin joko muuttamaan abstraktiksi tai määrittelemään rajapinnan vaatimat metodit. Abstraktin luokan ei tarvitse sisältää sovittujen metodien toteutuksia, kun taas normaalissa luokassa niiden on oltava joko kyseisessä luokassa tai periytymisen kautta. Lisätään Elvikselle myös liikkumismetodi.

public class Elvis extends Hahmo {
  private boolean vasen;

  public Elvis(String nimi) {
    super(nimi);
    this.vasen = true;
  }

  public void piirra() {
    if (vasen) {
      piirraVasen();
    } else {
      piirraOikea();
    }
    System.out.println("\"" + this.nimi + "\"");
  }

  private void piirraVasen() {
    System.out.println("G   __");
    System.out.println("\\\\  ,,)_");
    System.out.println(" \\'-\\( /");
    System.out.println("  \\ | ,\\");
    System.out.println("   \\|_/\\\\");
    System.out.println("   / _ '.D");
    System.out.println("  / / \\ |");
    System.out.println(" /_\\  /_\\");
    System.out.println("'-    '-");
  }

  private void piirraOikea() {
    System.out.println("   __    G");
    System.out.println("   _(,,  //");
    System.out.println("   \\ )/-'/");
    System.out.println("   /, | /");
    System.out.println("  //\\_|/");
    System.out.println(" D.' _ \\");
    System.out.println("  | / \\ \\");
    System.out.println("  / \\  / \\");
    System.out.println("   -'   -'");
  }

  public void liiku() {
    this.vasen = !this.vasen; // käänteinen totuusarvo huutomerkin avulla!
  }

  public void liiku(int montaKertaa) {
    System.out.println("Sori, boogie toimii askel kerrallaan.");
  }
}

Kuten seuraavasta esimerkistä ja tulosteesta huomaamme, Elviksemme osaa diskotanssin alkeet (jo ennen diskoa!).

Elvis e = new Elvis("Matti L");
e.piirra();
e.liiku();
e.piirra();
e.liiku();
e.piirra();
G   __
\\  ,,)_
 \'-\( /
  \ | ,\
   \|_/\\
   / _ '.D
  / / \ |
 /_\  /_\
'-    '-
"Matti L"
     __   D
   _(,,  //
   \ )/-'/
   /, | /
  //\_|/
 G.' _ \
  | / \ \
  / \  / \
   -'   -'
"Matti L"
G   __
\\  ,,)_
 \'-\( /
  \ | ,\
   \|_/\\
   / _ '.D
  / / \ |
 /_\  /_\
'-    '-
"Matti L"

Polymorfismi

Polymorfismi on neljäs olio-ohjelmoinnin peruskäsitteistä kapseloinnin, abstrahoinnin ja periytymisen lisäksi. Polymorfismilla tarkoitetaan olioiden monimuotoisuutta, eli oliolla voi olla monta eri tyyppiä. Eri tyypit, joita olio voi edustaa, koostuvat sen perintöhierarkiasta ja toteutetuista rajapinnoista.

Yllä olevalla Elvis-luokallamme on neljä eri tyyppiä:

Luokasta Elvis luodun olion voi siis antaa neljänä erityyppisenä parametrina. Elvis-olion voisi siis antaa kaikille neljästä seuraavasta metodista.

public void metodi(Elvis elvis) { ... }
public void metodi(Hahmo hahmo) { ... }
public void metodi(Liikkuva liikkuva) { ... }
public void metodi(Object object) { ... }

Kussakin tapauksessa annetulle oliolle voi kutsua vain siihen tyyppiin liittyviä metodeja, vaikka itse annettu viite oikeasti viittaisikin Elvis-tyyppiseen olioon. Esimerkiksi metodikutsu public void metodi(Liikkuva l), jolle annetaan parametrina Elvis-tyyppinen olio, tietää vain Liikkuva-rajapinnan määrittelevät metodit liiku() ja liiku(int montaKertaa). Seuraava metodikutsu yrittää liikuttaa Liikkuva-tyyppistä oliota yhteensä 5 kertaa.

public void liikuta(Liikkuva liikkuva) {
  // mahdollisia kutsuja siis vain
  liikkuva.liiku();
  // ja 
  liikkuva.liiku(4);
  // jossa parametrina annettu luku voi olla mikä tahansa kokonaisluku
}

Toisaalta, koska Liikkuva-rajapinta ei tiedä Elvis-luokan muista metodeista, ei esimerkiksi seuraava olisi mahdollista (vaikka parametrina annettaisiin Elvis-luokasta luotu olio!).

public void liikuta(Liikkuva liikkuva) {
  liikkuva.piirra(); // ei mahdollinen kutsu, sillä Liikkuva rajapinnassa ei määritelty piirra()-metodia
}

Luokalle Hahmo taas metodin piirra() kutsuminen olisi mahdollista. Huomaa että koska Hahmo on abstrakti luokka, kutsutaan oikeasti sen perivän luokan määrittelemää metodia piirra(). Esimerkiksi seuraavalle metodille voisi antaa minkä tahansa Hahmo-luokan perivän luokan ilmentymän.

public void piirraHahmo(Hahmo hahmo) {
  hahmo.piirra();
}

Voisimme antaa yllä olevalle metodille sekä Elvis-tyyppisiä olioita, että Jänis-tyyppisiä olioita, sillä ne molemmat perivät Hahmo-luokan, jolloin Hahmo on yksi niiden mahdollisista muodoista. Yllä olevaa metodia kutsuttaessa seuraavasti saisimme alla olevan esimerkin jälkeen olevan tulosteen.

Elvis elmeri = new Elvis("Matti L");
Janis jano = new Janis("Matti P");

piirraHahmo(elmeri);
System.out.println();
piirraHahmo(jano);
G   __
\\  ,,)_
 \'-\( /
  \ | ,\
   \|_/\\
   / _ '.D
  / / \ |
 /_\  /_\
'-    '-
"Matti L"

(\___/)
(='.'=)
(")_(")

Viitetyyppisen muuttujan tyyppi kertoo siis sen mitä metodeja ja attribuutteja muuttujalla on käytössä. Monimuotoisuutensa takia viite voi osoittaa oikeasti erityyppiseen olioon, kuin mikä tyyppi oliolle on annettu.

On kuitenkin oleellista huomata, että olipa olioon viittaavan muuttujan tyyppi mikä tahansa, kutsuttaessa olion metodia suoritetaan aina olion todellisen tyypin versio metodista.

Eli jos esim. peritään Elviksestä seuraava luokka:

public class ValeElvis extends Elvis {
  public Elvis(String nimi) {
    super(nimi);
  }

  public void piirra() {
     System.out.println("  o ");
     System.out.println(" /|\\"); 
     System.out.println(" / \\");
  }
}

Luodaan vale-elviksiä ja piirretään ne:

  ValeElvis e1 = new ValeElvis("Pekka");
  Elvis e2     = new ValeElvis("Martin");
  Hahmo e3     = new ValeElvis("Antti");

  e1.piirra();
  e2.piirra();
  e3.piirra();

Luodaan siis kolme vale-elvistä, mutta sijoitetaan vale-elvikset kaikki erityyppisiin oliomuuttujiin.

Kaikissa tapauksissa tulostuu samanlainen tikku-ukko, eli vaikka esim. on määritelty Elvis e2 = new ValeElvis("Martin") eli muuttujan tyyppinä on Elvis, on "langan päässä" kuitenkin vale-elvis ja metodikutsu e2.piirra() tuottaa vale-elvismäisen tikku-ukon.

Template-metodi ja Hollywood-periaate

Abstrakti luokka Kappale mallintaa "pohjaltaan ja kanneltaan" yhtä suuria kappaleita, joilla kaikilla on ominaisuus korkeus.

Kaikkien tälläisten kappaleiden tilavuus lasketaan samalla tavalla: korkeus * pohjan pinta-ala

Abstrakti luokka siis tietää miten miten tilavuus lasketaan:

public abstract class Kappale {
  // ...
  public double tilavuus() {
    return korkeus*pintaAla();
  }
  // ...
} 

Mutta pinta-alaa se ei osaa laskea. Pinta-ala riippuu konkreettisesta Kuviosta, esim. neliön pohjan pinta-ala on sivu*sivu, kun taas lieriöllä pinta-ala on pohjan säde toiseen kertaa pii.

Lisätäänkin luokalla Kappale abstrakti metodi pintaAla(), johon voidaan "ripustaa" erityisen kappaleen pohjan (ja samalla "kannen") pinta-alan laskentataito.

public abstract class Kappale {
  private double korkeus;

  public Kappale(double korkeus) {
    this.korkeus = korkeus;
  }

  public abstract double pintaAla();  // perivän luokan on ylikirjoitettava tämä

  public double tilavuus() {
    return korkeus*pintaAla();
  }
} 

Kuutio ylikirjoittaa metodiin pintaAla() sillä sillä on kyky laskea kuution pohjan (ja "kannen") pinta-ala:


public class Kuutio extends Kappale {
  private double sivu;

  public Kuutio(double sivu) {
    super(sivu);
    this.sivu = sivu;
  }

  public double pintaAla() {
    return sivu*sivu;
  }
} 

Lierio ylikirjoittaa metodiin pintaAla() sillä sillä on kyky laskea lieriön pohjan (ja "kannen") pinta-ala:

public class Lierio extends Kappale {
  private double sade;

  public Lierio(double korkeus, double sade) {
    super(korkeus);
    this.sade = sade;
  }

  public double pintaAla() {
    return sade*sade*Math.PI;
  }
} 

Näin jokaiseen Kappaleen perivään luokkaan saadaan valmiiksi taito laskea tilavuus, tarvitaan ainoastaan konkreettinen toteutus pinta-alan laskemiselle.

Yliluokassa Kappale määriteltyä metodia tilavuus() nimitetään "template-metodiksi", se määrittelee abstraktilla tavalla kaavan (engl. template) miten tilavuus lasketaan. Metodi kuitenkin käyttä abstraktia metodia pintaAla() , eli ei osaa itse konkreettisesti laskutoimitusta suorittaa.

Aliluokka ylikirjoittaa abstraktin metodin pintaAla() joka tulee kutsutuksi kun aliluokan tilavuutta selvitetään.

Tämäntyylisestä tekniikasta, jossa aliluokka joutuu määrittelemään metodin jota kutsutaan muualta nimitetään joskus "Hollywood-periaatteeksi: 'Don't call us, we call you'".

Tee nyt tehtävä 5

Iterable ja iteraattorit

Tarkastellaan ensin kuinka ArrayListin alkiot voidaan käydä läpi.

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

nimet.add("Antti");
nimet.add("Juhana");
nimet.add("Martin");
nimet.add("Matti");
nimet.add("Pekka");

// käsittelemällä suoraan ArrayListin paikkoja
for(int i = 0; i < nimet.size(); i++) {
    System.out.println(nimet.get(i));
}

// tai ns. for-each silmukalla
for(String nimi : nimet) {
    System.out.println(nimi);
}

Javan mekanismi for-each-silmukan toteuttamiseen on Iterable-rajapinta. Kaikki oliot, jotka toteuttavat Iterable-rajapinnan voidaan askeltaa (iteroida) läpi käyttäen for-each-silmukkaa. Iterable rajapinta on itsessään varsin yksinkertainen. Se edellyttää seuraavan metodin toteuttamista.

public Iterator<T> iterator()<T>;

Siis, jotta olion läpi pystyttäisiin askeltamaan for-each-silmukalla, pitää sillä olla metodi iterator(), joka palauttaa Iterator-tyyppisen olion. Iterator-oliot ovat indeksimuuttujien yleistyksiä. Ohessa on kolmas erilainen tapa iteroida ArrayListin alkiot läpi.

ArrayList<String> nimet = new ArrayList<String>();
...
Iterator<String> iteraattori = nimet.iterator();
while(iteraattori.hasNext()) {
    System.out.println(iteraattori.next());
}

Ylläoleva tapa käyttää hyväkseen Iterator-rajapintaa. Iteraattori-olio viittaa aina tiettyyn kohtaan sitä vastaavassa joukossa. Jos iteraattori on iteroinut joukon läpi, niin sen metodi hasNext() palauttaa arvon false. Mikäli joukossa on vielä jäljellä läpikäytäviä alkioita, niin hasNext() palauttaa arvon true. Iteratorin metodi next() edistää iteraattorin seuraavan elementtiin ja palauttaa nykyisen elementin paluuarvonaan. Paluuarvon tyyppi määräytyy Iterator-olion tyyppiparametrista (edellä String).

Iterator<E>-rajapinta edellyttää seuraavien metodien toteuttamista. Tässä E on ns. tyyppiparametri. Iteraattorin tapauksessa tyyppiparametri kertoo, minkä tyyppisiä objekteja next()-metodi palauttaa (eli minkä tyyppisten objektien yli iteroidaan).

public E next(); //edistää iteraattorin seuraavaan elementtiin ja palauttaa nykyisen arvon
public boolean hasNext(); //kertoo onko iteraattorilla vielä seuraavaa elementtiä
public void remove(); //poistaa nykyisen elementin

Seuraavassa esimerkissä luodaan oma iteraattori-luokka Hyppija, joka käy läpi annetun Integer-tyyppisen ArrayListin joka n:nnen alkion läpi, missä n konstruktorissa annettu parametri pompunPituus.

class Hyppija implements Iterator<Integer> {
  private int pompunPituus;
  private int kohta;
  private ArrayList<Integer> lista;
  
  public Hyppija(ArrayList<Integer> lista, int pompunPituus) {
    this.kohta = 0;
    this.lista = lista;
    this.pompunPituus = pompunPituus;
  }

  public void remove() {
    lista.remove(this.kohta);
  }

  public boolean hasNext() {
    return this.kohta  < lista.size();
  }

  public Integer next() {
    Integer nykyinen = this.lista.get(kohta);
    kohta += pompunPituus;
    return nykyinen;
  }
}

Seuraavassa ohjelmassa Hyppija-olio luodaan listalle, joka sisältää luvut 0..99.

  public static void main(String[] args) {
    ArrayList<Integer> lista = new ArrayList<Integer>();
    for(int i = 0; i < 100; ++i)
      lista.add(i);
    Hyppija hyppija = new Hyppija(lista, 7);
    while(hyppija.hasNext()) {
      System.out.print(hyppija.next() + "  ");
    }
    System.out.println();
  }
Hyppyjen pituudeksi määriteltiin 7, joten ohjelma tulostaa seitsemällä jaolliset luvut.
0  7  14  21  28  35  42  49  56  63  70  77  84  91  98

Luokan muuttaminen yleiseksi ns. geneeriseksi käy helposti. Seuraavassa on näytetty ainoastaan edellisestä muuttuneet kohdat.

class Hyppija<Tyyppi> implements Iterator<Tyyppi> {
  private int pompunPituus;
  private int kohta;
  private ArrayList<Tyyppi> lista;
  
  public Hyppija(ArrayList<Tyyppi> lista, int pompunPituus) {
    this.kohta = 0;
    this.lista = lista;
    this.pompunPituus = pompunPituus;
  }
...
  public Tyyppi next() {
    Tyyppi nykyinen = this.lista.get(kohta);
    kohta += pompunPituus;
    return nykyinen;
  }
}

Edellä <Tyyppi> on tyyppiparametri. Nyt luokka on paljon monikäyttöisempi. Sitä voidaan käyttää esimerkiksi merkkijonoja sisältävän ArrayListin yhteydessä.

  public static void main(String[] args) {
    ArrayList<String> lista = new ArrayList<String>();
    lista.add("eka");       lista.add("toka");      lista.add("kolmas");
    lista.add("neljäs");    lista.add("viides");    lista.add("kuudes");
    lista.add("seitsemäs"); lista.add("kahdeksas"); lista.add("yhdeksäs");
    Hyppija<String> hyppija = new Hyppija(lista, 2);
    while(hyppija.hasNext()) {
      System.out.print(hyppija.next() + "  ");
    }
    System.out.println();
  }

Huomaa, että edellä joudumme parametrisoimaan Hyppija-luokan ilmentymän samaan tapaan kuin jo tuttujen ArrayListien yhteydessä. Ohjelma tulostaa listan joka toisen alkion:

eka  kolmas  viides  seitsemäs  yhdeksäs 
Tee nyt tehtävä 6

Käyttöliittymät

Olemme tähän mennessä rakentaneet ohjelmamme kahdella eri tavalla. Ohjelmat ovat joko suorittaneet laskutoimituksia, jonka jälkeen ne ovat lopettaneet toimintansa, tai niitä on ohjattu tekstipohjaisen käyttöliittymän kautta. Tutustutaan tässä kappaleessa graafisen käyttöliittymän luomiseen Javalla.

Käyttöliittymät koostuvat pohjakerroksesta, niinkutsutusta containerista, ja siihen asetetuista komponenteista. Komponentteja ovat napit, tekstit, ym. Container voi sisältää myös toisen containerin. Seuraava esimerkki luo dialogityylisen ikkunan, ja asettaa siihen napin (luokka JButton) jossa on teksti "Ok!".

JFrame dlg = new JFrame();
JButton ok = new JButton("Ok!");
// lisätään dialogin containeriin nappi "ok"
Container container = dlg.getContentPane();
container.add(ok);

// asetetaan koko 240*120 pikseliä
dlg.setSize(240, 120);
// sulje ikkuna kun käyttäjä painaa X
dlg.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
// näytä ikkuna
dlg.setVisible(true);

Napin painaminen ei vielä tee yhtään mitään. Jos haluamme lisätä dialogiin uuden komponentin, esimerkiksi tekstielementin, joudumme päättämään sille sijainnin. Containerille on määritelty myös ulkoasu, eli jokaisella sen sisältämällä komponentilla on myös sijainti. Luokka BorderLayout on eräs ulkoasun määrittelyyn käytetty luokka. Sen avulla voidaan määritellä komponenttien sijainti ilmansuuntia käyttäen (NORTH, EAST, SOUTH, WEST). Muutetaan dialogia siten, että ikkunassa on pohjoisessa nappi "Ok!", ja etelässä teksti (luokka JLabel) "Eipäs!".

// luodaan ikkuna
JFrame dlg = new JFrame();

// luodaan komponentit
JButton ok = new JButton("Ok!");
JLabel eipas = new JLabel("Eipäs!");

// lisätään komponentit containeriin
Container container = dlg.getContentPane();
container.add(ok, BorderLayout.NORTH);
container.add(eipas, BorderLayout.SOUTH);

// ikkunan ulkoasun ja käyttäytymisen asettelua
dlg.setSize(240, 120);
dlg.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
dlg.setVisible(true);

Tässäkään esimerkissä napin painaminen ei vielä tee mitään.

Tapahtumapohjainen ohjelmointi

Käyttöliittymiä rakennettaessa jokainen komponentti on oma osansa käyttöliittymää. Tällöin jokaiselle komponentille täytyy myös tehdä oma tapahtumanhallinta. Java tarjoaa joukon erilaisia tapahtumankuuntelijoita, jotka voidaan liittää komponentteihin. Jos komponentilla (esimerkiksi nappi) on tapahtumankuuntelija, ja komponentissa tapahtuu toiminto (esimerkiksi nappia painetaan), kuulee tapahtumankuuntelija tapahtuman, ja suorittaa tapahtumankäsittelyyn ohjelmoidun ohjelman.

Tapahtumien kuuntelijat on määritelty rajapintoina. Esimerkiksi rajapinta ActionListener määrittelee metodin actionPerformed(), joka saa käyttöliittymästä parametrina ActionEvent-olion. Olio ActionEvent sisältää tapahtumaan liittyvät tiedot. Luodaan luokka, joka toteuttaa tapahtumankuuntelijan ActionListener.

public class NapinKuuntelija implements ActionListener {
  public void actionPerformed(ActionEvent e) {
    System.out.println("Nappia painettu!");
  }
}

Tapahtumankuuntelijan voi rekisteröidä, eli liittää komponenttiin, komponenttiin liittyvillä metodeilla. Esimerkiksi luokalla JButton on metodi addActionListener(), jolle voidaan antaa parametrina ActionListener-rajapinnan toteuttava luokka. Esimerkiksi ylläolevaan ohjelmaan voidaan liittää luokka Napinkuuntelija seuraavasti.

// luodaan ikkuna
JFrame dlg = new JFrame();

// luodaan komponentit
JButton ok = new JButton("Ok!");
JLabel eipas = new JLabel("Eipäs!");

// lisätään napille tapahtumankuuntelija
ok.addActionListener(new NapinKuuntelija());

// lisätään komponentit containeriin
Container container = dlg.getContentPane();
container.add(ok, BorderLayout.NORTH);
container.add(eipas, BorderLayout.SOUTH);

// ikkunan ulkoasun ja käyttäytymisen asettelua
dlg.setSize(240, 120);
dlg.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
dlg.setVisible(true);

Nyt jos nappia painetaan kolmesti saadaan konsoliin seuraavanlainen tuloste:

Nappia painettu!
Nappia painettu!
Nappia painettu!

Muutetaan luokkaa NapinKuuntelija siten, että se saa parametrina JLabel-tyyppisen tekstielementin. Kun nappia painetaan, tekstielementin tekstiksi asetetaan teksti "Nappia painettu!"

public class NapinKuuntelija implements ActionListener {
  private JLabel tekstielementti;
  
  public NapinKuuntelija(JLabel teksti) {
    this.tekstielementti = teksti;
  }

  public void actionPerformed(ActionEvent e) {
    teksti.setText("Nappia painettu!");
  }
}

Muutetaan dialoginluontia vielä siten, että NapinKuuntelija saa parametrikseen JLabel-olion

// lisätään napille tapahtumankuuntelija
ok.addActionListener(new NapinKuuntelija(eipas));

Napin painalluksen jälkeen ikkuna näyttää seuraavalta.

JFrame luokan periminen

Voimme myös periä JFrame luokan ja rakentaa oman ikkunamme sen avulla. Seuraava esimerkki NapinPainallus vastaa yllä olevaa esimerkkiä, mutta olemme käyttäneet perittyä luokkaa suoraan. Myös tapahtumankäsittely hoidetaan luokassa NapinPainallus.

public class NapinPainallus extends JFrame implements ActionListener {

  // käyttöliittymän komponentit
  private JButton okButton;
  private JLabel eipasLabel;

  public NapinPainallus() {
    super();

    okButton = new JButton("Ok!");
    eipasLabel = new JLabel("Eipäs!");
    okButton.addActionListener(this);

    Container container = getContentPane();
    container.add(okButton, BorderLayout.NORTH);
    container.add(eipasLabel, BorderLayout.SOUTH);

    setSize(240, 120);
    setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
    setVisible(true);
  }

  public void actionPerformed(ActionEvent e) {
    eipasLabel.setText("Nappia painettu!");
  }
}

Nyt voimme luoda uuden ikkunan main()-metodissa kutsumalla new NapinPainallus().

public class Main {
    public static void main(String[] args) {
        new NapinPainallus();
    }
}
Tee nyt viikon 5 tehtävä 1

JPanel

Luokkaa JPanel käytetään muunmuassa piirtoalustana. Luokalla JPanel on metodi paint(), jota kutsutaan aina kun paneeli halutaan piirtää. Metodi paint() saa käyttöliittymältä Graphics-tyyppisen olion, jolla voidaan piirtää paneelille. Laajennetaan JPanel luokkaa luokalla Piirturi, ja muutetaan sen piirtotoiminnallisuutta.

public class Piirturi extends JPanel {

    public void paint(Graphics g) {
        super.paint(g);
    }
}

Luokkamme Piirturi ei vielä tee mitään muuta, kuin kutsuu yläluokkansa (eli JPanel-luokan) paint()-metodia. Luodaan myös pääohjelma, jossa luodaan JDialog-tyyppinen ikkuna, ja asetetaan sen containeriin luokan Piirturi ilmentymä.

// luodaan ikkuna
JFrame dlg = new JFrame();

// luodaan piirturi
Piirturi piirturi = new Piirturi();

// otetaan viite ikkunan containeriin ja asetetaan piirturi siihen
Container container = dlg.getContentPane();
container.add(piirturi);

// ikkunan koko, sulkemistoiminnallisuus ja näkyväksi asetus
dlg.setSize(480, 360);
dlg.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
dlg.setVisible(true);

Ohjelmaa ajettaessa ikkuna näyttää seuraavalta.

Piirretään piirturiin ympyrä. Graphics-oliolla piirrettäessä iso osa metodeista määrittelee piirrettävän objektin siten, että ensiksi annetaan sen vasemman yläkulman koordinaatit, jonka jälkeen tulee piirrettävän kuvion leveys ja korkeus. Esimerkiksi seuraava kutsu luo kohdasta (30, 30) alkavan ympyrän, jonka halkaisija on 100. Huomaa että kohta 30, 30 lasketaan paneelin vasemmasta ylälaidasta!

g.fillOval(30, 30, 100, 100);

Ikkuna näyttää nyt seuraavanlaiselta.

Lisätään vielä toinen ja kolmas ympyrä. Kolmas ympyrä ei ole täytetty.

g.fillOval(250, 40, 80, 80);
g.drawOval(40, 200, 300, 100);

Ikkuna näyttää nyt kokonaisuudessaan seuraavanlaiselta.

Piirturi luokan sisältämä lähdekoodi on seuraavanlainen.

public class Piirturi extends JPanel {
  public void paint(Graphics g) {
    super.paint(g);

    g.fillOval(30, 30, 100, 100);
    g.fillOval(250, 40, 80, 80);
    g.drawOval(40, 200, 300, 100);
  }
}

Näppäimistön kuunteleminen

Tutustutaan näppäimistön kuunteluun tarkoitettuun rajapintaan, eli rajapintaan KeyListener. Rajapinta KeyListener tarjoaa metodit näppäimistön tapahtumien kuunteluun. Rajapinta määrittelee kolme erilaista näppäimistöltä tulevaa tapahtumaa, kirjaimen kirjoitus (keyTyped()), paino (keyPressed()) ja nosto (keyReleased(). Toteutetaan rajapinta KeyListener luokassa Piirturi.

public class Piirturi extends JPanel implements KeyListener {

  public void paint(Graphics g) {
    super.paint(g);

    g.fillOval(30, 30, 100, 100);
    g.fillOval(250, 40, 80, 80);
    g.drawOval(40, 200, 300, 100);
  }

  public void keyPressed(KeyEvent e) {
  }
  
  public void keyTyped(KeyEvent e) {
  }

  public void keyReleased(KeyEvent e) {
  }
}

KeyListener-rajapinnan tarjoavat metodit saavat parametrina KeyEvent-tyyppisen olion käyttöliittymältä. KeyEvent-oliolta voimme esimerkiksi kysyä, mitä nappia on painettu (metodi getKeyCode()). Muutetaan piirturi-luokkaa siten, että nuoli ylös pienentää hahmon suuta, nuoli alas suurentaa sitä (eli sen korkeus muuttuu). Käytetään tähän metodia keyPressed(), sillä jos näppäimistön nappia pidetään pohjassa metodia keyPressed() kutsutaan jatkuvasti.

public class Piirturi extends JPanel implements KeyListener {
    int y = 0;

  public void paint(Graphics g) {
    super.paint(g);

    g.fillOval(30, 30, 100, 100);
    g.fillOval(250, 40, 80, 80);
    g.drawOval(40, 200, 300, 100 + y);
  }

  public void keyPressed(KeyEvent e) {
    if (e.getKeyCode() == KeyEvent.VK_UP) {
      this.y--;
      repaint();
    }

    if (e.getKeyCode() == KeyEvent.VK_DOWN) {
      this.y++;
      repaint();
    }
  }

  public void keyTyped(KeyEvent e) {
  }

  public void keyReleased(KeyEvent e) {
  }
}

Tapahtumankäsittelijä pitää rekisteröidä ikkunaan, sillä se on uloin käyttöliittymän kerros. Voimme rekisteröidä näppäimistönkuuntelijan pääohjelmalle seuraavasti.

// luodaan ikkuna
JFrame dlg = new JFrame();

// luodaan piirturi
Piirturi piirturi = new Piirturi();

// otetaan viite ikkunan containeriin ja asetetaan piirturi siihen
Container container = dlg.getContentPane();
container.add(piirturi);

// lisätään näppäimistönkuuntelija
dlg.addKeyListener(piirturi);

// ikkunan koko, sulkemistoiminnallisuus ja näkyväksi asetus
dlg.setSize(480, 360);
dlg.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
dlg.setVisible(true);

Alla oleva kuva on kuva hahmosta kun ylöspäin nappia on pidetty muutama hetki pohjassa.

Tee nyt viikon 5 tehtävä 2

Ohjelmien automaattinen testaaminen

Paraskaan ohjelmoija tekee virheitä. Ohjelmia on siis testattava. Testauksen tekeminen antamalla ohjelmalle syötteitä käsin ja varmistamalla visuaalisesti että ohjelma toimii oikein on vaivalloista. Testaus onkin syytä automatisoida.

Olemme jo muutamassa tehtävässä "automatisoineet" syötteen, eli antaneet Scanner-oliolle parametriksi merkkijonon, jonka se tulkitesee käyttäjän näppäimistöltä antamaksi syötteeksi. Asia voidaan kuitenkin viedä vielä pitemmälle ja laittaa kone myös tarkistamaan että ohjelman tuottama vastaus on odotettu.

Tutustumme nyt automaattisen testaamisen alkeisiin tekemällä tehtävän 3

Lisää poikkeuksista

Poikkeuksen heittäminen

Poikkeuksia voi heittää throw-komennolla. throw-komennolle annetaan virheolio, joka on Exception luokan tai sen aliluokan ilmentymä. Tähän mennessä ollaan jo törmätty ainakin virheisiin NullPointerException ja IndexOutOfBoundsException.

Katsotaan seuraavaksi esimerkkiä virheen heittämisestä. Seuraava metodi etsii int-taulukosta sen pienimmän alkio ja heittää virheen, jos tätä ei voida tehdä (jos annettu taulukko olikin tyhjä).

public static int minimi(int[] taulu) {
	if (taulu == null)
	    throw new NullPointerException();
    if (taulu.length == 0)
        throw new IllegalArgumentException("ei voida laskea minimiä tyhjälle taulukolle");
    int min = taulu[0];
    for (int luku : taulu) {
        min = Math.min(min, luku);
    }
    return min;
}

Esimerkissä heitetään NullPointerException, jos parametrina annettiin null ja IllegalArgumentException jos taulukko olikin tyhjä. Virheluokkien konstruktoreille voi antaa String- parametrin, joka kuvaa virhetilannetta. Tämä parametri ei kuitenkaan ole välttämätön.

Yläluokka Exception

Luokka Exception on poikkeusten yläluokka, eli kaikki poikkeukset perivät luokan Exception. Polymorfismin ansiosta voimme varautua kaikkiin poikkeuksiin varautumalla poikkeustyyppiin Exception, eli kaikkien poikkeusten yläluokkaan. Tämä ei kuitenkaan aina ole tarpeellista tai toivottua, koska erilaiset poikkeustilanteet saattavat tarvita erilaista käsittelyä.

Yksi eniten nähdyistä poikkeuksista on NullPointerException, eli poikkeustilanne, joka johtuu null-viitteen käyttämisestä esimerkiksi metodikutsussa. Voimme nähdä NullPointerException poikkeuksen esimerkiksi seuraavan lähdekoodin avulla.

String merkkijono = null;
System.out.println("Merkkijonon pituus on " + merkkijono.length());

Koska viite merkkijono on null, ei String-olioon liittyvän length() metodin suoritus onnistu ja ohjelma heittää poikkeuksen NullPointerException.

Aiemmin luomamme metodi lueTiedosto() varautuu poikkeustyyppiin Exception, vaikka Scanner-luokan konstruktori oikeasti heittää FileNotFoundException-tyyppisen poikkeuksen. Poikkeukset ovat yleensä hyvin nimettyjä, esimerkiksi FileNotFoundException kertoo sen, että haettua tiedostoa ei löytynyt tai sen avaamisessa oli ongelmia. Voimme muuttaa lueTiedosto()-metodia varautumaan FileNotFound-tyyppiseen poikkeukseen seuraavasti.

public static String lueTiedosto(String tiedostonNimi) { // ei heitetä poikkeusta
  File tiedosto = new File(tiedostonNimi);
  Scanner lukija = null;
  
  try {
    lukija = new Scanner(tiedosto);
  } catch (FileNotFoundException e) {
    System.err.println("Tiedoston avaaminen ei onnistunut!");
    return null;
  }
  
  String teksti = "";
  while(lukija.hasNextLine()) {
    teksti += lukija.nextLine() + "\n";
  }
  return teksti;
}

Poikkeuksen tiedot

Lohko catch() {} määrittelee varauduttavan poikkeuksen lisäksi muuttujan, mihin poikkeuksen tiedot tallennetaan. Esimerkiksi seuraava rakenne määrittelee poikkeuksen FileNotFoundException tallentumisen muuttujaan e. Koska poikkeukset perivät yläluokan Exception, voimme käyttää sen metodeja poikkeuksen tietojen tulostamiseen.

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

Luokka Exception tarjoaa muutamia hyödyllisiä metodeja. Esimerkiksi metodi printStackTrace() tulostaa polun, joka johti poikkeukseen. Tutkitaan seuraavaa metodin printStactTrace() tulostamaa virhettä.

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

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

Tee nyt viikon 5 tehtävä 4

Finally

Poikkeuksiin varauduttaessa on mahdollista määritellä myös lohko, joka suoritetaan riippumatta siitä, tapahtuiko poikkeus vai ei. Lohko finally { } tulee poikkeuslohkon jälkeen, ja se sisältää joka tapauksessa suoritettavan koodin. Finally lohkon lähdekoodi suoritetaan sen jälkeen, kun try {} ja catch {} -lohkot on suoritettu. Katsotaan jo nähtyä esimerkkiä tiedostoon kirjoittamisesta.

public static void kirjoitaTiedostoon(String tiedostonNimi, String teksti) throws Exception {
  FileWriter kirjoittaja = new FileWriter(tiedostonNimi);
  kirjoittaja.write(teksti);
  kirjoittaja.close();
}

Luokan FileWriter konstruktori siis avaa parametrina annetun tiedoston kirjoittamista varten, ja yrittää kirjoittaa siihen. Yllä olevassa esimerkissä on kaksi erillistä poikkeuskohtaa. Luokka FileWriter voi heittää poikkeuksen IOException jos tiedostoa ei jostain syystä saada avattua kirjoittamista varten. Metodi write() voi myös heittää poikkeuksen jos tiedostoon kirjoittaminen ei onnistu, vaikka tiedoston avaaminen olisikin jo onnistunut. Muutetaan metodia siten, että poikkeustenkäsittely tapahtuu metodin sisällä.

public static void kirjoitaTiedostoon(String tiedostonNimi, String teksti) { // <- enää ei tarvitse heittää poikkeusta
  FileWriter kirjoittaja = null;
  try {
    kirjoittaja = new FileWriter(tiedostonNimi);
    kirjoittaja.write(teksti);
    kirjoittaja.close();
  } catch (Exception e) {
    System.err.println("Tiedostoon kirjoittaminen epäonnistui.");
    e.printStackTrace();
  }
}

Poikkeusten käsittelyyn siirrytään heti poikkeuksen tapahtuessa, esimerkiksi jos ylläolevassa esimerkissä konstruktorikutsu new FileWriter() heittää poikkeuksen, ei sitä seuraavaa kahta kutsua suoriteta ollenkaan. Jos poikkeus tapahtuu metodissa write(), jää metodikutsu close() suorittamatta, ja tiedosto jää auki.

Lohko finally {} on erinomainen tapauksiin, joissa täytyy sulkea resursseja lopuksi, riippumatta suorituksen kulusta. Siirretään metodikutsu close lohkoon finally {}, jotta sen suoritus tapahtuu aina.

public static void kirjoitaTiedostoon(String tiedostonNimi, String teksti) {
  FileWriter kirjoittaja = null;
  try {
    kirjoittaja = new FileWriter(tiedostonNimi);
    kirjoittaja.write(teksti);
  } catch (Exception e) {
    System.err.println("Tiedostoon kirjoittaminen epäonnistui.");
    e.printStackTrace();
  } finally {
    if(kirjoittaja != null) {
      kirjoittaja.close();
    } else {
      System.err.println("Kirjoittajaa ei koskaan luotu.");
    }
  }
}

Jos metodi palauttaa arvon try {} tai catch -lohkossa, lohkon finally sisältö suoritetaan silti. Tutkitaan metodia vitonen(), joka palauttaa arvon 5 try-lohkossa, ja tulostaa merkkijonon Moi finally lohkossa.

public static int vitonen() {
  try {
    return 5;
  } catch (Exception e) {
  } finally {
    System.out.println("Moi!");
  }
  
  return 0;
}
System.out.println(vitonen());
Moi!
5

Esimerkin tulostuksesta huomataan että lohko finally suoritetaan ennen arvon palauttamista, jolloin myös merkkijono Moi! tulostuu ennen lukua 5.

Pakkaukset

Huomaamme suurempia ohjelmia suunniteltaessa ja toteuttaessa luokkamäärän kasvavan suureksi. Pakkauksilla (package) voidaan jakaa luokat eri sijainteihin niiden toiminnallisuuden perusteella, ja parantaa ohjelmiston ylläpidettävyyttä ja hallittavuutta. Pakkausten käyttö tarkoittaa käytännössä sitä, että luokat sijaitsevat kansioissa projektin sisällä.

Luokka määrittelee oman pakkauksensa määreellä package. Esimerkiksi seuraava luokka Kirja sijaitsee pakkauksessa kirjasto.

package kirjasto;

public class Kirja {
  private String nimi;
  private String sisalto;
  
  public Kirja() {
  }
  
  ...
}

Huomaa että pakkausten nimet kirjoitetaan aina pienellä!

Jos luokan pakkauksena on kirjasto, sijaitsee se ohjelman lähdekoodikansion sisällä kansiossa kirjasto. Vastaavasti voimme luoda pakkauksia siten, että luokat on jaettu useammalle tasolle. Esimerkiksi seuraava luokka KirjanLainaaja sijaitsisi kansion kirjasto sisällä olevassa kansiossa lainaus.

package kirjasto.lainaaja;

public class KirjanLainaaja {
  private String sijainti;
  
  public KirjanLainaaja() {
  }
  
  ...
}

Kaikki hyvät ohjelmointiympäristöt, kuten NetBeans ja Eclipse, tarjoavat valmiit pakkausten hallintaan, jolloin ohjelmoijan ei tarvitse huolehtia kansioista. Uuden pakkauksen voi luoda NetBeansissa projektin Source Packages-osiossa oikeaa hiirennappia painamalla ja valitsemalla New -> Java Package.... Luodun pakkauksen sisälle voidaan luoda luokkia kuten oletuspakkaukseenkin (default package).

Pakkausten käyttö mahdollistaa myös lähdekoodin jakamisen siten, että eri projekteissa toteutetut lähdekoodit eivät sekoitu toisiinsa. Yksi nimeämiskäytäntö paketeille on maa.organisaation-webosoite.projekti...., esimerkiksi fi.visiojapojat.supertuote, jotka alla on supertuote-projektiin liittyvät lähdekoodit (ja pakkaukset!).

Javan API:n pakkaukset ovat yleensä pakkauksissa java.alue.. Esimerkiksi Javan luokka ArrayList sijaitsee pakkauksessa java.util, eli javan työkalut. Ottaessamme luokan ArrayList käyttöömme komennolla import java.util.ArrayList kerromme käytännössä halutun luokan nimen, sekä pakkauksen jonka sisällä luokka sijaitsee.

Jos pakkauksessa kirjasto.lainaaja sijaitseva luokka KirjanLainaaja haluaa käyttää pakkauksessa kirjasto sijaitsevaa luokkaa Kirja, täytyy sen myös tuoda kirja käyttöön komennolla import kirjasto.Kirja

package kirjasto.lainaaja;

import kirjasto.Kirja;

public class KirjanLainaaja {
  private String sijainti;
  ...
  
  public KirjanLainaaja() {
  }
  
  public void lainaa(Kirja kirja) {..}
  
  ...
}

Samassa pakkauksessa sijaitsevia luokkia ei tarvitse erikseen tuoda käyttöön, eli saman pakkauksen sisällä olevat luokat ovat oletuksena käytössä pakkauksessa oleville luokille.

Pakkausnäkyvyys

Pakkausnäkyvyydellä tarkoitetaan sitä, että metodeille ja luokille ei määritellä erikseen näkyvyyttä. Näkyvyysmääreitähän ovat public, protected ja private. Jos näkyvyysmääreet jätetään pois metodeilta, voi metodeja kutsua pakkauksen sisällä olevista luokista. Vastaavasti konstruktorilla ja muuttujilla.

Jos luokka Kirja olisi määritelty ilman näkyvyysmäärettä public, voi sitä käyttää siis vain saman pakkauksen sisällä.

package kirjasto;
 
class Kirja {
  private String nimi;
  private String sisalto;
  
  ...
}

Nyt luokka KirjanLainaaja ei voi käyttää luokkaa Kirja, koska ne sijaitsevat eri pakkauksissa. Näkyvyysmääre protected määrittelee näkyvyyden siten, että muuttujat ja metodit ovat näkyvissä vain luokan periville luokille sekä samassa pakkauksessa oleville luokille. Määrettä protected ei kuitenkaan voi käyttää määrittelemään luokan näkyvyyttä.

Tee nyt viikon 5 tehtävät 5 ja 6

Geneerisyys

Olemme jo jonkin aikaa käyttäneet luokkia kuten ArrayList ja HashMap, joille kerrotaan minkä tyyppistä dataa niihin säilötään. Esimerkiksi ArrayList<Integer> on ArrayList, johon voi tallettaa kokonaislukuja.

Tutustutaan nyt kuinka tällaisia luokkia voi tehdä itse. Seuraavassa esimerkissä tehdään luokka Lokero, johon tallettaa yhden olion ja hakea lokerosta sen nykyisen olion.

public class Lokero<Tyyppi> {
	private Tyyppi alkio;
	
	public void asetaArvo(Tyyppi alkio) {
		this.alkio = alkio;
	}
	
	public Tyyppi haeArvo() {
		return alkio;
	}
}

Tässä geneeriselle tyypille annetaan nimi Tyyppi, jota voidaan käyttää luokan määrittelyssä. Luokkaa voidaan nyt käyttää samaan tapaan kuin esimerkiksi geneeristä ArrayListiä.

Lokero<Integer> lukulokero = new Lokero<Integer>();
Lokero<String> merkkijonolokero = new Lokero<String>();

lukulokero.asetaArvo(5);
merkkijonolokero.asetaArvo(":)");

int luku = lukulokero.haeArvo();

Geneeriset staattiset metodit

Geneeristen staattisten metodien yhteydessä tyyppiparametri on laitettava metodinmäärittelyn yhteyteen, static-määreen jälkeen. Seuraavassa lokerotehdas, eli staattinen metodi, joka muodostaa ja palauttaa halutun tyyppisen lokeron ja asettaa sille sisällön:

public class Lokerotehdas {

    public static <Tyyppi> Lokero<Tyyppi> lokeroi(Tyyppi lokeroitava) {
        Lokero <Tyyppi> uusiLokero = new Lokero<Tyyppi>();
        uusiLokero.asetaArvo(lokeroitava);
        return uusiLokero;
    }
    
}

Jos metodi lokeroi olisi erillisen luokan sijasta luokalla Lokero, tulisi se määritellä samalla tavallasillä luokan tyyppiparametreihin ei voi viitata staattisessa metodissa.

Käyttöesimerkki:

    Lokero<String> merkkilokero = Lokerotehdas.lokeroi("Ohjelmoinnin jatkokurssi, viikko 6");
    System.out.println( merkkilokero.haeArvo() );
Tee nyt viikon 6 tehtävät