Ohjelmoinnin jatkokurssi

Matti Paksula, Arto Vihavainen, Matti Luukkainen

Alkeistyyppi ja viittaustyyppi

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, 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. Käytetään ohjelmoinnin perusteet -kurssilta tuttua Laskuri-luokkaa esimerkeissä.

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 5 |  <-------------|-- | 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

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 attribuutit saavat arvon olion liittyvän viitteen päässä, jolloin niihin pääsee käsiksi oliokohtaisesti. Esimerkiksi taas yksinkertainen luokka Ihminen, jolla on kaksi attribuuttia: nimi ja rahat.

public class Ihminen {
  private String nimi; // olioattribuutteja, 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; // olioattribuutteja, 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; // olioattribuutteja, 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; // olioattribuutteja, 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, eli attribuutteihin. Alla esimerkki metodista, jossa luodaan metodimuuttuja. 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.

Static ja ei-static yhteistyössä

Katsotaan esimerkkiä missä oliota luotaessa käytetään staattista muuttujaa järjestysnumeron antamiseen.

public class Jonotuspaikka {
  private static int VUORONUMERO = 0;
  
  private int vuoronumero;
  
  public Jonotuspaikka() {
    this.vuoronumero = VUORONUMERO;
    VUORONUMERO++;
  }
  
  public int annaVuoronumero() {
    return this.vuoronumero;
  }
}

Yllä olevassa luokassa käytetään staattista muuttujaa VUORONUMERO yleisen vuoronumeron ylläpitämiseen. Luokan Jonotuspaikka konstruktorissa uusi jonotuspaikka-olio saa vuoronumeron VUORONUMERO-muuttujalta, jonka jälkeen VUORONUMERO-muuttujan arvoa kasvatetaan yhdellä. Tällöin seuraavan jonotuspaikan vuoronumero olisi yhtä suurempi. Katsotaan vielä samaa toimintaa lähdekoodin avulla.

Jonotuspaikka mattiV = new Jonotuspaikka();
Jonotuspaikka mattiP = new Jonotuspaikka();
Jonotuspaikka mattiL = new Jonotuspaikka();

System.out.println("Matti V:n vuoronumero on: " + mattiV.annaVuoronumero());
System.out.println("Matti P:n vuoronumero on: " + mattiP.annaVuoronumero());
System.out.println("Matti L:n vuoronumero on: " + mattiL.annaVuoronumero());

Ohjelmapätkä tulostaisi seuraavaa:

Matti V:n vuoronumero on: 0
Matti P:n vuoronumero on: 1
Matti L:n vuoronumero on: 2

Yhteenveto

Alla vielä pieni yhteenveto staattisten ja ei-staattisten muuttujien ja metodien eroista.

 staticei-static
muuttujaLuokkaan liittyvä muuttuja, esimerkiksi numerolaskuri tai luokkakirjastotyyppinen vakioolioon liittyvä muuttuja, esimerkiksi nimi, tai metodin sisällä määritelty muuttuja
metodiLuokkaan liittyvä metodi, voi muokata vain staattisia muuttujia. Parametrina annetut muuttujat ovat tietysti aina muokattavissa. Käytössä esimerkiksi luokkakirjastoissa. Eivät voi kutsua oliometodeja ilman viitettä itse olioon.Olihin liittyvät, juuri sen (this) olion tilaa muokkaavat metodit. Voivat kutsua staattisia luokkametodeja.

Kapselointi

Kapselointi on yksi olio-ohjelmoinnin peruskäsitteistä. Muihin, eli periytymiseen, polymorfismiin ja abstrahointiin, palataan vielä myöhemmin tällä kurssilla. Kapseloinnilla tarkoitetaan toteutuksen, eli esimerkiksi oliomuuttujien, piilottamista asettamalla ne private-tyyppisiksi. Kun muuttuja on private-tyyppinen, siihen ei pääse käsiksi luokan ulkopuolelta. Kapselointia voi ajatella olion tilan suojaamisena, jolloin olion tilaa ei pääse muokkaamaan mistä tahansa. Ohjelmalla on siis pääsy olion tilaan vain tarkoin suunniteltujen metodien avulla.

Tutkitaan aiemmin luotua Jonotuspaikka-luokkaa.

public class Jonotuspaikka {
  private static int VUORONUMERO = 0;
  
  private int vuoronumero;
  
  public Jonotuspaikka() {
    this.vuoronumero = VUORONUMERO;
    VUORONUMERO++;
  }
  
  public int annaVuoronumero() {
    return this.vuoronumero;
  }
}

Luokan Jonotuspaikka muuttuja vuoronumero on toteutettu siten, että siihen pääsee käsiksi vain annaVuoronumero()-aksessorimetodin avulla. Jos luokkaa käytetään vain sen metodien kautta, voidaan sen sisäistä toteutusta muuttaa ilman että se näkyy ulkopuolelle, jolloin ohjelmia jotka käyttävät luokkaa Jonotuspaikka ei tarvitse muuttaa.

Ajatellaan tilannetta, missä arkistointisyistä halutaan jonotuspaikan numerot alkamaan kymmenestä miljoonasta. Tällöin voimme muuttaa metodia annaVuoronumero() siten, että se palauttaa vuoronumeron summattuna kymmeneen miljoonaan. Luokan Jonotuspaikka metodi annaVuoronumero() olisi silloin seuraavanlainen.

  public int annaVuoronumero() {
    return (this.vuoronumero + 10000000);
  }

Vastaavasti voisimme päätyä tilanteeseen missä joudumme muuttamaan Jonotuspaikka luokkaa siten, että vuoronumero tuleekin kolmannen osapuolen toteuttamalta komponentilta UlkopuolinenKomponentti. Ulkopuolinen komponentti pitää kirjaa annetuista vuoronumeroista, ja saattaa esimerkiksi mahdollistaa jonon ohittamisen korkeamman prioriteetin ihmisille. Tämä mahdollistaa useiden Jonotuspaikka-ohjelmien käyttämisen (esimerkiksi useat päätteet joista voi ottaa vuoronumeroita), kun vuoronumeroiden luominen on keskitettyä. Luokan Jonotuspaikka toteutus voisi olla silloin seuraavanlainen.

public class Jonotuspaikka {
  private JonotusNumero jonotusNumero;
  
  public Jonotuspaikka() {
    this.jonotusNumero = UlkopuolinenKomponentti.luoJonotusNumero();
  }
  
  public int annaVuoronumero() {
    return this.jonotusNumero.annaArvo(); // palauttaa kokonaisluvun
  }
}

Vaikka luokan sisäinen toteutus onkin muuttunut täysin alkuperäisestä, on ulkopuolelle näkyvät toiminnot täysin samanlaiset. Tällöin Jonotuspaikka-luokkaa käyttävät ohjelmat eivät tarvitse minkäänlaisia muutoksia.

Ajatellaan vielä Jonotuspaikka-luokan toteutusta siten, että vuoronumero-oliomuuttuja olisikin ollut kaikille näkyvissa. Tällöin Jonotuspaikka-luokkaa käyttävä ohjelmoija olisi toteuttanut ohjelmansa esimerkiksi seuraavasti.

Jonotuspaikka paikka = new Jonotuspaikka();
System.out.println("Vuoronumero: " + paikka.vuoronumero); // ei näin!

Ohjelma toimisi alkuperäisen Jonotuspaikka-luokan kanssa jotenkuten (vaikka ei noudatakaan hyvää ohjelmointityyliä!). Kun toteutusta muutetaan siten, että vuoronumero alkaa miljoonasta, täytyisi yllä olevaa ohjelmakoodia muokata. Samoin ulkopuolisen komponentin lisäämisessä. Toisaalta, jos toteutus on käyttänyt aksessoreita ja Jonotuspaikka-luokka on toteutettu kapseloinnin periaatteita seuraten, ei mikään ylläolevista muutoksista olisi vaatinut muutoksia. Seuraava esimerkki toimisi jokaisessa kolmesta tapauksesta.

Jonotuspaikka paikka = new Jonotuspaikka();
System.out.println("Vuoronumero: " + paikka.annaVuoronumero()); // jes!

Taulukot

Taulukot ovat viittaustyyppisiä muuttujia, eli taulukko-muuttuja viittaa muualle missä itse taulukon alkiot ovat. Taulukot, kuten kaikki muutkin viittaustyyppiset muuttujat, luodaan komennon new-avulla. Taulukolle määritellään aina sen koko ja tyyppi, eli kuinka monta alkiota siihen mahtuu ja minkätyyppisiä alkiot ovat. Taulukon alkiot voivat olla alkeis- tai viittaustyyppisiä. Seuraavassa esimerkissä luodaan 100 alkion kokoinen kokonaislukutaulukko, ja asetetaan sen alkoihin luvut yhdestä sataan.

int[] kokonaislukutaulukko = new int[100];
for(int i = 0; i < kokonaislukutaulukko.length; i++) {
  kokonaislukutaulukko[i] = i + 1;
}

Taulukot voivat olla sisäkkäisiä. Sisäkkäisiä taulukoita kutsutaan joskus myös moniulotteisiksi taulukoiksi. Sisäkkäisissä taulukoissa aina ulommassa taulukossa on viite sisempään taulukkoon. Seuraavassa esimerkissä luodaan kaksiulotteinen String-tyyppinen taulukko, ja täytetään kaikki alkiot merkkijonolla "Moi". Huomaa että sisemmät taulukot luodaan esimerkissä vasta toiston aikana, tällöin voimme määritellä sisemmille taulukoille erilaisia kokoja. Lopuksi vielä taulukon sisältö tulostetaan.

String[][] moikka = new String[8][];  // luodaan 8 paikkainen taulukko, jonka alkiot merkkijonotaulukkoja

// luodaan nyt taulukon sisälle tulevat 8 merkkijonotaulukkoa
for(int i = 0; i < moikka.length; i++) { 
  moikka[i] = new String[i+1];   // kaikki ovat eripituisia
  
  for(int j = 0; j < moikka[i].length; j++) {
    moikka[i][j] = "Moi";
  }
}

for(String[] taul: moikka) {
  for(String moi: taul) {
    System.out.print(moi + " ");
  }
  
  System.out.println();
}

Yllä oleva taulukon koodinpätkä tuottaa seuraavanlaisen tulostuksen:

Moi 
Moi Moi 
Moi Moi Moi 
Moi Moi Moi Moi 
Moi Moi Moi Moi Moi 
Moi Moi Moi Moi Moi Moi 
Moi Moi Moi Moi Moi Moi Moi 
Moi Moi Moi Moi Moi Moi Moi Moi

Taulukoista löytyy lisää ohjelmoinnin perusteiden kurssimateriaalista!

API

Eräs tärkeistä taidoista ohjelmoinnissa on tiedon hakeminen ja soveltaminen. Java tarjoaa kattavan Luokkakirjaston, joka sisältää hyvin paljon käyttökelposia luokkia. Javan API (eli ohjelmointirajapinta) löytyy osoitteesta http://java.sun.com/javase/6/docs/api/. Tällä kurssilla ehditään tutustutaan vain muutamaan luokkaan, tärkeämpää on API:n lukutaito.

Etsitään String-luokan kuvaus. Vasemman alakulman laatikossa on listattu kaikki Javan API:n tarjoamat luokat. String löytyy luokkalistauksesta, suora osoite String-luokan kuvaukseen on http://java.sun.com/javase/6/docs/api/java/lang/String.html.

Integer.parseInt()

Integer-luokka tarjoaa metodin merkkijonon kokonaisluvuksi muuttamiseksi. Seuraavassa esimerkissä luodaan aluksi merkkijonon "12345" sisältävä String-olio, jonka jälkeen kutsutaan Integer-luokan staattista parseInt()-metodia. Metodi parseInt() saa parametrikseen merkkijonon, ja palauttaa kokonaisluvun.

String merkkijono = "12345";
int luku = Integer.parseInt(merkkijono);
System.out.println(luku);

Esimerkki tulostaa luvun 12345. Huomaa että Integer-luokan parseInt()-metodi olettaa merkkijonon olevan luku. Jos merkkijono ei esitä lukua, saamme poikkeuksen. Poikkeuksista lisää kurssin loppupuolella.

Math.random()

Javan Math-luokka tarjoaa paljon hyödyllisiä apuvälineitä. Yksi paljon käytetyistä apuvälineistä on staattinen metodi random(), joka palauttaa liukuluvun nollan ja yhden väliltä. Metodia random() voi käyttää myös isompien arvovälien arpomiseen. Seuraava esimerkki arpoo luvun 1 ja 39 väliltä ja tulostaa sen.

int luku = (int) (Math.random() * 39) + 1;
System.out.println(luku);

ArrayList

Yksi eniten käytetyistä Javan valmiista Luokista on ArrayList. ArrayList on kapseloitu taulukko, jota kasvatetaan aina tarpeen tullen. ArrayList-luokan API-kuvaus löytyy osoitteesta http://java.sun.com/javase/6/docs/api/java/util/ArrayList.html. ArrayList saa tyyppiparametrikseen viittaustyyppisen muuttujan, mikä kertoo minkälaisia arvoja se sisältää. Esimerkiksi merkkijonoja sisältävän ArrayList-olion voi luoda seuraavasti.

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

Huomaa tyyppiparametrin välitys <String>. Tämä kertoo ArrayList-oliolle että kaikki sen sisältämät arvot ovat tyyppiä String. Koska ArrayList-luokka voi ottaa vain viittaustyyppisiä parametreja arvoikseen, on alkeistyypeille olemassa luokat jotka kapseloivat ne. Integer-luokka kapseloi int-tyyppisen muuttujan, Double-luokka kapseloi double-tyyppisen muuttujan ym. Katsotaan vielä esimerkkiä missä luodaan ArrayList-olio kokonaisluvuille, lisätään sinne 5 lukua ja tulostetaan ne lopuksi.

ArrayList<Integer> kluvut = new ArrayList<Integer>();
kluvut.add(3);
kluvut.add(5);
kluvut.add(7);

for(int luku: kluvut) {
  System.out.println(luku);
}

Esimerkin tuloste on seuraavanlainen:

3
5
7

Huomaa, että Integer-tyyppisestä muuttujasta int-tyyppiseen muuttujaan tapahtuu automaattinen muunnos toistossa.

ArrayList

Viime luennolla nähty ArrayList on eräs Javan API:n tarjoamista valmiista listatoteutuksista, joka kasvaa sitä mukaa kun sinne lisätään alkioita. ArrayList löytyy Javan pakkauksesta java.util.ArrayList, eli sen saa otettua ohjelmassa käyttöön komennolla import java.util.ArrayList;, joka asetetaan lähdekoodin alkuun.

Alla oleva esimerkki lisää merkkijonot-nimiseen ArrayList-olioon kaksi String-oliota. Ensimmäinen String-olio viittaa arvoon "Moi Kaikki!", toinen arvoon "Moi Uudestaan!". Huomaa että ArrayList-olio pitää kirjaa viitteistä. Kun kutsumme sen metodia add(), sille annetaan parametrina viitetyyppinen muuttuja. Viitetyyppiset muuttujathan toimivat metodikutsuissa siten, että viitteiden arvot kopioituvat metodin parametreihin. Alla oleva esimerkki siis lisää kaksi eri String-oliota ArrayList-olioon, vaikka viitetyyppisten muuttujien nimet ovat samat. Lopuksi ArrayList-olion sisältämät merkkijono-oliot tulostetaan.

ArrayList<String> merkkijonot = new ArrayList();
String viesti = "Moi Kaikki!");
merkkijonot.add(viesti);
viesti = "Moi Uudestaan!";
merkkijonot.add(viesti);

for(String merkkijono: merkkijonot) {
  System.out.println(merkkijono);
}

Esimerkin tulostus on seuraavanlainen:

Moi Kaikki!
Moi Uudestaan!

Vaikka ArrayListin sisäinen toteutus perustuu taulukkoon, ei sitä voi indeksoida kuten taulukkoa. ArrayList kapseloi taulukon. Kapseloinnilla tarkoitetaan sisäisen toteutuksen piilottamista, jolloin luokasta luotuja olioita käytetään niiden tarjoamien metodien kautta. Indeksointi, eli taulukon alkioiden käsittely niiden taulukkoindeksien avulla, on siis mahdollista vain taulukko-olioille. Taulukko-oliot tunnistaa niiden tyypistä, esimerkiksi tyyppi int[] määrittelisi taulukko-olion, joka sisältää alkeistyyppisiä int-arvoja, eli kokonaislukuja.

Seuraava esimerkki ei siis toimi, sillä ArrayList-tyyppistä oliota ei voi indeksoida.

ArrayList<String> merkkijonot = new ArrayList();
String viesti = "Moi Kaikki!");
merkkijonot.add(viesti);
viesti = "Moi Uudestaan!";
merkkijonot.add(viesti);

for(int i = 0; i < merkkijonot.size(); i++)) {
  System.out.println(merkkijonot[i]); // ei toimi, sillä ArrayList ei ole taulukko-tyyppinen muuttuja
}

Kaikista olioista, kuten ArrayList-tyyppisistä olioista voi kuitenkin myös luoda taulukkoja. Seuraava esimerkki luo taulukon johon mahtuu kolme ArrayList-oliota, ja luo ArrayList-oliot taulukkoa indeksoimalla.

ArrayList<String>[] merkkijonotaulukot = new ArrayList[3];
for(int i = 0; i < merkkijonotaulukot.length; i++) {
  merkkijonotaulukot[i] = new ArrayList<String>();
}

Koska ArrayList-tyyppinen olio sisältää viitetyyppisiä muuttujia, on seuraavanlainen toteutus mahdollinen.

ArrayList<ArrayList<String>> arrayListSisakkain = new ArrayList<ArrayList<String>>();
for(int i = 0; i < 3; i++) {
  ArrayList<String> uusiArrayList = new ArrayList<String>();
  arrayListSisakkain.add(uusiArrayList);      
}

String viesti = "Heippa!";
arrayListSisakkain.get(0).add(viesti);

Yllä olevassa esimerkissä luodaan ensiksi ArrayList-tyyppisiä olioita sisältävä ArrayList-olio. Tämän jälkeen lisätään kolme ArrayList-oliota alussa luotuun ArrayList-olioon. Lopuksi ensimmäiseen ArrayList-olioita sisältävän ArrayList-olion arrayListSisakkain ensimmäiseen ArrayList-olioon lisätään vielä merkkijono "Heippa!"

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

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 Luennoiva {
  ...
  
  public String puhu() {
    String[] aiheet = {"kapselointi", "periytyminen", "polymorfismi", "abstrahointi"};
    double satunnainen = Math.random() * 4;
    // Double-luokasta löytyy metodi intValue(), joka palauttaa liukuluvun kokonaislukuna
    int indeksi = new Double(satunnainen).intValue();

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

Luennoitsija-luokka valitsee satunnaisesti yhden neljästä olio-ohjelmoinnin peruskäsitteestä, ja puhuu siitä. Satunnaisuus on toteutettu Math-luokan staattisen metodin random()-avulla. Kutsu Math.random() palauttaa liukuluvun väliltä 0 ja 1, joka kerrotaan neljällä. Tämän jälkeen käytetään Double-luokan oliometodia intValue() aiheen indeksin valintaan. 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 Lapsi, joka myös toteuttaa rajapinnan Puhuva.

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

Yllä oleva luokka Lapsi 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 lapsen.

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

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 Lapsi. Kummallekin kutsutaan myös metodia puhu()

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

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 Lapsi toteuttavat rajapinnan Puhuva, on niillä myös metodi puhu().

Rajapintaa voidaan käyttää muuttujan tyyppinä. Tällöin ei tiedetä rajapinnan toteuttavan luokan muista metodeista, vaan käytetään vain metodeja, joita rajapinta määrittelee. Edellisessä kappaleessa nähdyn dialogin olioiden mattiL, mattiP ja mattiV välillä voi kirjoittaa seuraavasti.

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

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

Rajapinta metodin parametrina

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() {
  if(Math.random() > 0.5) {
    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.

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.

Comparable

Yksi Javan valmiiksi tarjoamista rajapinnoista on Comparable. Rajapinta Comparable määrittelee metodin compareTo(), joka palauttaa this-olion paikan verrattuna parametrina annettuun olioon. Muutetaan aiemmin luotua luokkaa Lapsi siten, että sillä on nimi ja se toteuttaa Comparable-rajapinnan. Lisätään lapselle myös attribuutit pituus, jota käytetään compareTo()-metodissa. Comparable-rajapinta ottaa tyyppiparametrina myös luokan, johon sitä verrataan.

public class Lapsi implements Puhuva, Comparable<Lapsi> {
  private String nimi;
  private int pituus;
  
  public Lapsi(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 Lapsi-tyyppisen olion, sillä Comparable-rajapinnalle on annettu tyypiksi Lapsi
  public int compareTo(Lapsi 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?";
  }
}

Collections

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 Lapsi-olioiden järjestämistä pituusjärjestykseen.

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

for(Lapsi l: lapset) {
  System.out.println(l.annaNimi());
}

System.out.println();
Collections.sort(lapset);
for(Lapsi l: lapset) {
  System.out.println(l.annaNimi());
}

Esimerkin tulostus on seuraavanlainen

Matti L
Robert W
Aditya D

Robert W
Matti L
Aditya D

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.

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

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

Collections.sort(lapset);

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

haettava = new Lapsi("Nimi", 187);
int indeksi = Collections.binarySearch(lapset, haettava);
if(indeksi >= 0) {
  System.out.println("187 senttiä pitkä löytyi indeksistä " + indeksi);
  System.out.println("Nimi: " + lapset.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ä.

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.

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.

Viime viikolla nähtiin luokkakirjasto HslHinnasto, jossa muuttujat oli määritelty kaikille näkyviksi.

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

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 HslHinnasto {
  public static final double KERTALIPPU_AIKUINEN = 2.50;
  public static final double RAITIOVAUNULIPPU_AIKUINEN = 2.50; 
}    

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

Metodien nimeäminen

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

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

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

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

Periytyminen

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

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

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

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

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

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

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

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

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

Luodaan vielä toinen tehdas, luokka KarkkiTehdas.

public class KarkkiTehdas extends Tehdas {
  private String karkkimerkki;

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

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

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

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

Yläluokan konstruktorit - super

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

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

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

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

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

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

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

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

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

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

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

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

Yläluokan metodit

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

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

Luodaan vielä Siipisimppu-olio ja tulostetaan sen tila.

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

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

Metodien ylikirjoitus

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

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

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

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

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

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

Esimerkin tulostus on seuraavanlainen

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

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

Yliluokan ylikirjoitettujen metodien kutsuminen - super

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

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

Vielä käyttöesimerkki:

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

Esimerkin tulostus on seuraavanlainen

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

Näkyvyys - Protected

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

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

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

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

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

Yläluokka Object

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

Katso kuva Javan tutorialista

Rajapinnat ja niiden periytyminen

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

Rajapintojen periytyminen

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

public class KahviLaskuri implements Laskuri {
  protected int arvo;

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

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

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

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

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

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

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

EspressoLaskuri espressoLaskuri = new EspressoLaskuri();

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

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

Esimerkin tulostus tulostaa merkkijonon Kahvilaskurissa arvona 4.

Rajapinnan periminen

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

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

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

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

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

Tiedosto

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

Tiedostot NetBeanssissa

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

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

Tiedoston lukeminen

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

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

public class TiedostonLuku {

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

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

Useampia rivejä voi lukea esimerkiksi seuraavanlaisen toistorakenteen avulla.

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

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

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

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

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

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

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

Ohjelman tulostus on seuraavanlainen

tilanteita"
loppuu,
odotetun
taulukon
sopimattomaksi,

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. Olemme jo sivunneet polymorfismia, vaikkakin salaa. Viime viikon materiaalissa olevan Kahvilaskuri-luokan metodi lisaaArvoon()-ottaa parametrikseen Laskuri-tyyppisen olion, joka siis voi olla mikä tahansa olio, mikä toteuttaa Laskuri-rajapinnan.

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. Tutkitaan vielä seuraavaksi Jänis-oliota, joka annetaan parametriksi metodille joka ottaa Hahmo-tyyppisiä muuttujia parametrikseen. Metodilla kaannaNimi() käännetään hahmon nimi ympäri.

public void kaannaNimi(Hahmo hahmo) {
  String hahmonNimi = hahmo.getNimi();
  char[] merkit = new char[hahmonNimi.length()];
  for (int i = 0; i < hahmonNimi.length(); i++) {
    merkit[i] = hahmonNimi.charAt(hahmonNimi.length() - 1 - i);
  }
  hahmo.setNimi(new String(merkit));
}

Kun annamme Jänis-olion kaannaNimi()-metodille, viittaa hahmo-viite metodin sisällä vieläkin Jänis-olioon, vaikka sen tyypiksi tuleekin metodin sisällä Hahmo. Jänis-olio on siis monimuotoinen, ja siihen voi viitata myös Hahmo-tyyppisellä viitteellä.

Janis jano = new Janis("Matti P");
System.out.println(jano.getNimi());
kaannaNimi(jano);
System.out.println(jano.getNimi());
Matti P
P ittaM

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 attribuutteina 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
  }
}

Tiedostoon kirjoittaminen

Tutustuimme viime viikolla tiedoston lukemiseen, joka tapahtui luokkien Scanner ja File avulla. 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!");
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 varaudu poikkeuksiin vielä, 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();
}

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!

Toiston kontrollointi

Toistorakenteet (for, while) ovat hyvin hyödyllisiä listojen ja taulukoiden läpikäyntiin. Tutustutaan tässä kappaleessa kahteen toiston kontrollimahdollisuuteen, break ja continue, eli lopeta ja jatka. Tutustutaan myös niinkutsuttuun while-true-toistotapaan, jossa toistoa jatketaan niin pitkään kunnes tietty ehto toteutuu. Avainsanat break ja continue toimivat kaikissa toistorakenteissa.

Break

Avainsanalla break poistutaan suoritettavasta toistorakenteesta. Toistosta poistuminen on hyvin hyödyllistä esimerkiksi tilanteissa, joissa toivottu tulos on jo saavutettu, eikä haluta jatkaa enää toistoa. Katsotaan muutamaa esimerkkiä merkkijonon hakemiseen merkkijonolistasta. Oletetaan että merkkijonot-olio on luotu seuraavasti, eli se sisältää 50000 erilaista merkkijonoa.

ArrayList<String> merkkijonot = new ArrayList();
for(int i = 0; i < 50000; i++) {
  merkkijonot.add("Hei " + i);
}

Ensimmäinen vaihtoehto sanan hakemiseen listasta on kaikkien sanojen läpikäynti. Fiksua olisi tietenkin käyttää binäärihakua tai sopivampaa tietorakennetta, kuten HashMap, mutta pidetään asiat yksinkertaisina esimerkin vuoksi. Merkkijonon "Hei 2" etsiminen merkkijonot listasta ilman lopetusehtoa käy kaikki 50000 alkiota läpi. Alla oleva esimerkki ei ole hyvää ohjelmointityyliä!

boolean loytyi = false;
String haettava = "Hei 2";

for(String merkkijono: merkkijonot) {
  System.out.println("Tutkitaan merkkijono: " + merkkijono);

  if(haettava.equals(merkkijono)) {
    loytyi = true;  
  }
}

if(loytyi) {
  System.out.println();
  System.out.println("Haettu löytyi!");
}
Tutkitaan merkkijono: Hei 0
Tutkitaan merkkijono: Hei 1
Tutkitaan merkkijono: Hei 2
...
Tutkitaan merkkijono: Hei 49998
Tutkitaan merkkijono: Hei 49999

Haettu löytyi!

Esimerkki tulostaa jokaiselle alkiolle tulostuksen "Tutkitaan merkkijono: Hei numero", koska jokainen alkio käydään läpi riippumatta merkkijonon löytymisestä.

Avainsanaa break käytetään toiston lopettamiseen halutussa tilanteessa. Muokataan ylläolevaa esimerkkiä siten, että toistosta poistutaan heti kun haettava merkkijono löytyy.

boolean loytyi = false;
String haettava = "Hei 2";

for(String merkkijono: merkkijonot) {
  System.out.println("Tutkitaan merkkijono: " + merkkijono);

  if(haettava.equals(merkkijono)) {
    loytyi = true;
    break;
  }
}

if(loytyi) {
  System.out.println();
  System.out.println("Haettu löytyi!");
}
Tutkitaan merkkijono: Hei 0
Tutkitaan merkkijono: Hei 1
Tutkitaan merkkijono: Hei 2

Haettu löytyi!

Esimerkissä toisto lopetetaan siis avainsanaan break, jolloin kaikkia merkkijonoja ei tarvitse tutkia.

Avainsanalla break poistutaan vain tällä hetkellä suoritettavasta toistosta. Jos toistoja on useampia sisäkkäin, jatkuu ulompi toisto kuten ennenkin. Katsotaan seuraavaa esimerkkiä, jossa rivin alkioita tulostetaan kunnes saavutaan lukuun jonka arvo on yli 100.

int[][] numerot = {
    {17, 4, 2009},
    {12, 53},
};

for(int[] rivi: numerot) {
  for(int luku: rivi) {
    if(luku > 100) {
      break;
    }
    
    System.out.print(luku + " ");
  }
  System.out.println();
}

Yllä olevan hieman teennäisen esimerkin sisempi toisto lopetetaan jos luku on suurempi kuin 100. Koska break-ehto poistuu vain tällä hetkellä suoritettavasta toistosta, jatkuu ulomman toiston suoritus normaalisti. Esimerkin tulostus on seuraavanlainen.

17 4 
12 53

Toinen esimerkki break-avainsanan käytöstä ja hyödyllisyydestä sisäkkäisissä toistorakenteissa on seuraava (ei kovin tehokas!) alkulukuja etsivä algoritmi. Alkuluvut ovat lukuja, jotka ovat jaollisia vain yhdellä ja itsellään. Ulompi toistorakenne määrittelee luvut, joita tutkitaan, ja sisemmässä toistorakenteessa jaetaan lukua mahdollisilla luvuilla. Jos jakojäännös (%) on 0, on tutkittava luku jaollinen jakajalla, eikä se voi olla alkuluku.

int tutkiLukuun = 500;

for (int alkulukuEhdokas = 2; alkulukuEhdokas < tutkiLukuun; alkulukuEhdokas++) {
  boolean onAlkuluku = true;
  for (int jakaja = 2; jakaja < alkulukuEhdokas; jakaja++) {
    if (alkulukuEhdokas % jakaja == 0) {
      onAlkuluku = false;
      break;
    }
  }

  if (onAlkuluku) {
    System.out.println(alkulukuEhdokas);
  }
}
2
3
5
7
11
13
17
19
...

While-true

Eräs hyödyllisistä toistorakenteista on while-true rakenne. While-true - rakenteen idea on jatkaa toistoa loputtomiin, tai niin pitkään kunnes poistutaan toistosta. Yksi esimerkki while-true-toistorakenteen käytöstä on salasanan kysyminen. Salasanaa kysytään niin pitkään, kunnes käyttäjä syöttää oikean salasanan.

while (true) {
  System.out.print("Kirjoita salasana: ");
  
  if("salasana".equals(lukija.nextLine())) {
    break;
  } else {
    System.out.println("Väärin!");
  }
}

System.out.println("Kiitos!");

Yllä olevassa esimerkissä toistoa jatketaan niin pitkään kunnes käyttäjä antaa syötteeksi merkkijonon salasana.

Kirjoita salasana: kala
Väärin!
Kirjoita salasana: salakana
Väärin!
Kirjoita salasana: salasana
Kiitos!

Continue

Avainsana continue määrittelee tilanteen, jossa kyseinen toistokierros lopetetaan ja jatketaan seuraavasta. Käytetään aiemmin luotua merkkijonot-oliota continue avainsanan esittämiseen.

for(String merkkijono: merkkijonot) {
  if("Hei 2".equals(merkkijono)) {
    continue;
  }
  
  System.out.println("Tutkitaan merkkijono: " + merkkijono);
}

Kun toisto kohtaa merkkijonon "Hei 2" päätyy se ehtoon, jossa on avainsana continue. Tällöin se lopettaa kyseisen toistokierroksen ja jatkaa seuraavasta. Ylläolevan esimerkin tulostus olisi seuraavanlainen.

Tutkitaan merkkijono: Hei 0
Tutkitaan merkkijono: Hei 1
Tutkitaan merkkijono: Hei 3
Tutkitaan merkkijono: Hei 4
...

Avainsana continue on hyödyllinen esimerkiksi tilanteissa, missä kaikki listan tai taulukon alkiot täytyy käydä läpi, mutta tiedetään että tietynlaisia alkioita ei haluta ottaa huomioon. Seuraava esimerkki laskee lukujen 1..100 summan siten, että kolmella ja viidellä jaollisia lukuja ei oteta huomioon.

int summa = 0;

for(int luku = 1; luku <= 100; luku++) {
  if(luku % 3 == 0 || luku % 5 == 0) {
    continue;
  }
  
  summa += luku;
}

System.out.println(summa);

Yllä oleva esimerkki tulostaa luvun 2632.

Toistorakenteiden nimeäminen

Toistorakenteille on myös mahdollista antaa nimet. Toistorakenteiden nimeäminen tapahtuu määrittelemällä nimi ennen toistorakenteen tyyppiä, esimerkiksi Nimi: for.... Tätä ohjelmointityyliä näkee harvemmin, sillä se saattaa johtaa tilanteisiin joissa ohjelman seuraaminen vaikeutuu. Seuraavassa esimerkissä toistorakenteen nimeksi on määritelty LueSalasana.

LueSalasana: while (true) {
  System.out.print("Kirjoita salasana: ");
  
  if("salasana".equals(lukija.nextLine())) {
    break;
  } else {
    System.out.println("Väärin!");
  }
}

System.out.println("Kiitos!");

Toistorakenteiden nimet mahdollistavat break ja continue avainsanojen käytön siten, että voidaan jatkaa tai poistua tietyn nimisestä toistorakenteesta vaikka se olisi ulompi toistorakenne. Seuraavassa esimerkissä luetaan käyttäjätunnus ja salasana. Jos salasana menee oikein, poistutaan LueKayttaja-nimisestä toistorakenteesta, eli ulommasta toistosta.

String kayttaja;
String salasana;

LueKayttaja: while(true) {
  System.out.print("Anna käyttäjätunnus: ");
  kayttaja = lukija.nextLine();
  
  for(int yritykset = 0; yritykset < 3; yritykset++) {
    System.out.print("Anna salasana: ");
    salasana = lukija.nextLine();
    
    if("salasana".equals(salasana)) {
      break LueKayttaja; // poistutaan toistosta jonka nimi on LueKayttaja
    } else {
      System.out.println("Väärä salasana!");
    }
  }
  
  System.out.println("Liian monta yritystä!");
  System.out.println();
}

System.out.println();
System.out.println("Tervetuloa " + kayttaja);
System.out.println("Annoit salasanaksi " + salasana);
Anna käyttäjätunnus: Matti
Anna salasana: kala
Väärä salasana!
Anna salasana: sala
Väärä salasana!
Anna salasana: mikäsenytoli
Väärä salasana!
Liian monta yritystä!

Anna käyttäjätunnus: Matti
Anna salasana: salasana

Tervetuloa Matti
Annoit salasanaksi salasana

Voimme käyttää toistorakenteiden nimeämistä myös alkulukulaskuriimme. Sen sijaan, että pitäisimme yllä muuttujaa onAlkuluku, voimme jatkaa ulommasta toistosta jos huomaamme että tarkastelemamme luku ei ole alkuluku. Alkuluvun tarkistava toisto on nimetty AL:ksi, eli Alkuluvuksi. Jos huomaamme että jakaja pystyy jakamaan alkulukuehdokkaan, siirrymme tarkastelemaan seuraavaa lukua.

int tutkiLukuun = 500;

AL: for (int alkulukuEhdokas = 2; alkulukuEhdokas < tutkiLukuun; alkulukuEhdokas++) {
  for (int jakaja = 2; jakaja < alkulukuEhdokas; jakaja++) {
    if (alkulukuEhdokas % jakaja == 0) {
      continue AL;
    }
  }

  System.out.println(alkulukuEhdokas);
}
2
3
5
7
11
13
17
19
23
29
31
37
...

Toistorakenteet ja niiden kontrollointi avainsanojen break ja continue on hyvin hyödyllistä. Ne kuitenkin mahdollistavat tilanteita, joissa ohjelmakoodin kulku ei ole kovin loogista. Ohjelmakoodin dokumentointi onkin tärkeää niiltä kohdin, joissa ohjelman kulku ei ole selkeää!

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

Poikkeukset

Poikkeustilanteet ovat tilanteita joissa ohjelman suoritus ei ole edennyt toivotusti. Olemme tähän mennessä jättäneet 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.

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

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

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 viime viikolla 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) throws Exception {
  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) throws Exception {
  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.

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

JDialog dlg = new JDialog();
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 dialogi-tyyppinen ikkuna
JDialog dlg = new JDialog();

// 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 dialogi-tyyppinen ikkuna
JDialog dlg = new JDialog();

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

JDialog luokan periminen

Voimme myös periä JDialog luokan ja rakentaa oman dialogimme 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 JDialog 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();
    }
}

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
JDialog dlg = new JDialog();

// 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, eli käytettävään dialogiin, sillä se on uloin käyttöliittymän kerros. Voimme rekisteröidä näppäimistönkuuntelijan pääohjelmalle seuraavasti.

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

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

Säikeistä

Ei kuulu koealueeseen!

Ohjelmissa halutaan usein suorittaa asioita samaan aikaan esimerkiksi siten, että osa ohjelmasta tekee laskentaa, ja toinen osa tulostaa tietoa. Useimmat hyvin suunnitellut käyttöliittymät toimivat siten, että ne vain näyttävät tiedon, jolloin itse tiedon laskenta tapahtuu taustalla. Tutustutaan Javassa oleviin säikeisiin, eli suorituksen jakamiseen, rajapinnan Runnable avulla.

Rajapinta Runnable määrittelee metodin run(), joka mahdollistaa olion suorituksen erillisessä säikeessä. Toteutetaan luokka Hiekkalaatikko, jossa on yksinäinen liikkuva muurahainen. Esitetään muurahainen pisteenä. Toteutetaan metodi run() seuraavasti.

public void run() {
  while(true) {
    liikutaMuurahaista();
    System.out.println(getMuurahainen());

    try {
      Thread.sleep(30);
    } catch (InterruptedException ex) {}            
  }
}

Kutsu Thread.sleep(30) komentaa kyseisen säikeen nukkumaan 30 millisekunniksi. Jos säiettä ei pyydettäisi nukkumaan, pyrkisi se käyttämään niin paljon tietokoneen tehoa kuin mahdollista. Käytännössä muurahainen kulkisi niin lujaa ettei sen liikkeestä saisi kunnolla selvää. Vanhojen pelien ystäville: Tämä on syynä siihen, että osa hyvin vanhoista peleistä toimivat järjettömillä nopeuksilla jos niitä yrittää pelata nykyajan tietokoneilla. Ennen muinoin pelejä suunniteltaessa ei otettu huomioon sitä, että eri tietokoneet ovat eri tehoisia, ja kaikki pelit pyrkivät ottamaan kaiken tehon irti koneesta.

Luokka Hiekkalaatikko on kokonaisuudessaan seuraavanlainen.

public class Hiekkalaatikko implements Runnable {
  private Point muurahainen;

  private int leveys;
  private boolean oikealle = true;

  public Hiekkalaatikko(int leveys) {
    this.leveys = leveys;

    // käytetään luokkaa Random satunnaisen pisteen saamiseen
    Random random = new Random();
    this.muurahainen = new Point(random.nextInt(leveys), 5);
  }

  private void liikutaMuurahaista() {
    if(oikealle) {
      muurahainen.x++;
    } else {
      muurahainen.x--;
    }

    if(muurahainen.x < 0) {
      oikealle = true;
    }

    if(muurahainen.x > leveys) {
      oikealle = false;
    }
  }

  public Point getMuurahainen() {
    return muurahainen;
  }

  public void run() {
    while(true) {
      liikutaMuurahaista();
      System.out.println(getMuurahainen());

      try {
        Thread.sleep(30);
      } catch (InterruptedException ex) {}            
    }
  }
}

Jos ylläoleva luokka ajetaan seuraavalla main()-metodilla, ei ohjelman suoritus pääty kun main()-metodi päättyy. Kutsu new Thread().start(), jolle annetaan parametrina Runnable-rajapinnan toteuttava olio, aloittaa uuden säikeen parametrina annetulle oliolle.

Hiekkalaatikko hiekkis = new Hiekkalaatikko(250);
new Thread(hiekkis).start();

Koska muurahainen on esitetty Javan Point-luokan avulla, on ohjelman tulostus seuraavanlainen (ja päättyy vasta kun ohjelma tapetaan).

java.awt.Point[x=126,y=5]
java.awt.Point[x=127,y=5]
java.awt.Point[x=128,y=5]
java.awt.Point[x=129,y=5]
java.awt.Point[x=130,y=5]
java.awt.Point[x=131,y=5]
java.awt.Point[x=132,y=5]
java.awt.Point[x=133,y=5]
...

Luodaan hiekkalaatikolle vielä oma pieni graafinen käyttöliittymä. Hiekkalaatikko ottaa parametrina graafisen käyttöliittymän, ja kutsuu omassa run()-metodissaan käyttöliittymän päivittämistä. Toteutetaan piirtäminen JPanel-luokkaa laajentaen.

public class Paneeli extends JPanel {
  private Point piste;
  
  public void paint(Graphics g) {
    super.paint(g);
    
    if(piste != null) {
      g.drawRect(piste.x, piste.y, 2, 2);
    }
  }

  public void piirra(Point p) {
    this.piste = p;
    repaint();
  }
}

Luodaan dialogi, jonka sisään paneeli asetetaan pääohjelmassa. Koska ohjelmassa on useampia säikeitä, joudumme muokkaamaan ikkunan sulkemismetodia siten, että se sulkee koko ohjelman.

Paneeli paneeli = new Paneeli();

JDialog dialog = new JDialog();
dialog.getContentPane().add(paneeli);
// ikkunan koko, sulkemistoiminnallisuus ja näkyväksi asetus
dialog.setSize(300, 100);

// suljetaan koko ohjelma kun ikkuna suljetaan, muuten säie jäisi
// pyörimään
dialog.addWindowListener(new WindowAdapter() {
  public void windowClosing(WindowEvent e) {
    System.exit(0);
  }
});

dialog.setVisible(true);

Hiekkalaatikko hiekkis = new Hiekkalaatikko(paneeli, 250);
new Thread(hiekkis).start();

Hiekkalaatikko vielä siten, että se saa parametrina Paneeli-olion.

public class Hiekkalaatikko implements Runnable {
  private Point muurahainen;
  private Paneeli paneeli;
  
  private int leveys;
  private boolean oikealle = true;

  public Hiekkalaatikko(Paneeli paneeli, int leveys) {
    this.leveys = leveys;
    this.paneeli = paneeli;

    // käytetään luokkaa Random satunnaisen pisteen saamiseen
    Random random = new Random();
    this.muurahainen = new Point(random.nextInt(leveys), 5);
  }

  private void liikutaMuurahaista() {
    if(oikealle) {
      muurahainen.x++;
    } else {
      muurahainen.x--;
    }

    if(muurahainen.x < 0) {
      oikealle = true;
    }

    if(muurahainen.x > leveys) {
      oikealle = false;
    }
  }

  public Point getMuurahainen() {
    return muurahainen;
  }

  public void run() {
    while(true) {
      liikutaMuurahaista();

      if(paneeli != null) {
        paneeli.piirra(muurahainen);
      }

      try {
        Thread.sleep(30);
      } catch (InterruptedException ex) {}            
    }
  }
}

Nyt ohjelma on seuraavanlainen (muurahainen toki liikkuu!).

Samanaikaisuudesta

Jos useampi säie käyttää samaa resurssia, esimerkiksi ArrayList-luokan ilmentymää, voivat ne päätyä tilanteeseen, missä toinen yrittää poistaa alkiota mitä toinen lukee. Tällöin puhumme rinnakkaisuusongelmista. Kurssilla rinnakkaisohjelmistot pääsemme tutustumaan rinnakkaisuuteen liittyviin ongelmiin (ja rinnakkaisuuden mahdollistamaan tehokkuuteen! :)).

Olioiden suunnittelusta ja toteutuksesta

Seuraavassa esitellään joukko neuvoja olioiden suunnitteluun ja toteutukseen.

Olioiden attribuuteista

Olion attribuutit, eli luokkaan liittyvät ei-staattiset muuttujat, kuvaavat olion rakennetta ja ominaisuuksia. Esimerkiksi luokalla Opiskelija on opiskelijan nimi ja opiskelijanumero.

public class Opiskelija {
  // yksittäisen opiskelijan attribuutit
  private String nimi;
  private int opiskelijanumero;  
  ...

Opiskelijan attribuutit ovat vain Opiskelija-tyyppiin, eli konkreettiseen opiskelijaan liittyviä tietoja. Rakentaessamme tallennus- ja latausmahdollisuutta opiskelijalle käyttäen Javan valmista luokkakirjastoa, eivät opiskelijan lataamiseen tai tallentamiseen käytetyt luokat kuulu Opiskelija-olion ominaisuuksiin. Esimerkiksi seuraavanlainen toteutus, missä opiskelijan tallennukseen käytettävä FileWriter-luokka on opiskelijan attribuuttina on väärin.

public class Opiskelija {
  // yksittäisen opiskelijan attribuutit
  private String nimi;
  private int opiskelijanumero;
  private FileWriter tallentaja;
  
  ...
  
  public void tallenna() throws Exception {
    tallentaja = new FileWriter(nimi);
    tallentaja.write(nimi + "\n");
    tallentaja.write("" + opiskelijanumero + "\n");
    tallentaja.close();
  }  
  ...

Vaikka esimerkki toimii, ei se rakenteellisesti ole hyvin suunniteltu. Olioiden attribuuttien on tarkoitus kuvata aina tiettyä käsitettä (esimerkiksi opiskelija) selkeärajaisesti. Opiskelija nimeltä Matti, jolla on opiskelijanumero on selkeä opiskelijan määritelmä. Toisaalta, jos opiskelija määritellään siten, että sillä on nimen ja opiskelijanumeron lisäksi myös attribuutti tallentaja, kadotamme olion selkeärajaisuuden. Selkeärajaisuudesta puhutaan myös termillä koheesio, jolla tarkoitetaan sitä, että luokan rakenne liittyy aina tiettyyn selkeästi rajattavaan toiminnallisuuteen. Esimerkiksi Opiskelija-luokan attribuuttien tulee liittyä opiskelijaan.

Yllä oleva esimerkki pitääkin muuttaa sellaiseksi, että FileWriter-olio liittyy vain metodiin, ei koko olioon. Tällöin opiskelijaan liittyy vain siihen kuuluvat attribuutit, jotka oikeasti kuvaavat opiskelijan rakennetta.

public class Opiskelija {
  // yksittäisen opiskelijan attribuutit
  private String nimi;
  private int opiskelijanumero;
  
  ...
  
  public void tallenna() throws Exception {
    FileWriter tallentaja = new FileWriter(nimi);
    tallentaja.write(nimi + "\n");
    tallentaja.write("" + opiskelijanumero + "\n");
    tallentaja.close();
  }  
  ...

Nyt myös metodi tallenna() on selkeärajainen. Tallenna-metodi käyttää vain siinä tarvittavaa FileWriter-oliota, ja rajaa tallentamistoiminnallisuuden omaan toteutukseensa. Huom! Selkeärajaisuudella ei kuitenkaan tarkoiteta sitä, että kaikki toiminnallisuus pitäisi toteuttaa aina saman metodin sisällä. Luettavuutta voidaan helpottaa rajaamalla metodien toteutusta siten, että metodit ovat lyhyitä. Metodista tallenna() voikin erottaa vielä tallennettavan tiedon luomisen omaan metodiinsa tallennettavaOpiskelija() seuraavasti.

public class Opiskelija {
  // yksittäisen opiskelijan attribuutit
  private String nimi;
  private String opiskelijaNumero;
  
  ...
  
  public void tallenna() throws Exception {
    FileWriter tallentaja = new FileWriter(nimi);
    tallentaja.write(tallennettavaOpiskelija());
    tallentaja.close();
  }  
  
  private String tallennettavaOpiskelija() {
    StringBuilder sb = new StringBuilder();
    sb.append(nimi + "\n");
    sb.append("" + opiskelijanumero + "\n");
    ...
    return sb.toString();    
  }
  ...

Olioiden suunnittelusta

Jatketaan vielä yllä-esitetyn Opiskelija-luokan avulla. Opiskelijalla on attribuutteina opiskelijan nimi ja opiskelijanumero.

public class Opiskelija {
  // yksittäisen opiskelijan attribuutit
  private String nimi;
  private int opiskelijanumero;  
  ...

Olioiden suunnittelussa oliot pyritään pitämään yksittäisinä kokonaisuuksina. Esimerkiksi jos opiskelija on ilmoittautunut kursseille, on opiskelijalla attribuuttina vain viitteet Kurssi-olioihin. Kurssi-oliot taas sisältävät kurssien tiedot, kuten kurssikoodin ja nimen.

public class Kurssi {
  // yksittäisen kurssin attribuutit
  private String kurssikoodi;
  private String nimi;
  ...

Kun Opiskelija-luokalle lisätään attribuutiksi kurssit, joihin opiskelija on ilmoittautunut, halutaan lisätä viitteet oikeisiin kursseihin. Seuraava esimerkki, jossa kursseihin viitataan kurssikoodin avulla (String-tyyppinen muuttuja) ei ole oikein toteutettu, sillä kurssin sisällön ja tietojen muuttuessa ilmoittautumisviite pysyy samana ja opiskelija-olio ei tiedä kurssin muutoksista.

public class Opiskelija {
  // yksittäisen opiskelijan attribuutit
  private String nimi;
  private int opiskelijanumero;
  
  private ArrayList<String> kurssiIlmoittautumiset;  
  ...

Jos ilmoittautumiset eivät viittaa Kurssi-olioihin, eivät mahdolliset muutokset Kurssi-olioissa näy opiskelijan kurssilistalla, sillä viitteet eivät viittaa oikeisiin olioihin. Parempi tapa onkin toteuttaa kurssi-ilmoittautuminen siten, että kurssi-ilmoittautumiset ovat viitteitä oikeisiin Kurssi-olioihin.

public class Opiskelija {
  // yksittäisen opiskelijan attribuutit
  private String nimi;
  private int opiskelijanumero;
  
  private ArrayList<Kurssi> kurssiIlmoittautumiset;  
  ...

On myös mahdollista rakentaa ilmoittautuminen siten, että vastuu opiskelijoiden kurssi-ilmoittautumisten ylläpidosta ei ole opiskelijoilla, vaan jollain muulla oliolla. Olio KurssiIlmoittautumiset on esimerkki vastuun siirtämisestä kolmannelle osapuolelle. Tällöin Opiskelija-olion ei myöskään tarvitse pitää kirjaa ilmoittautumisista.

public class KurssiIlmoittatumiset {
  private HashMap<Opiskelija, ArrayList<Kurssi>> opiskelijoidenKurssit;
  ...
public class Opiskelija {
  // yksittäisen opiskelijan attribuutit
  private String nimi;
  private int opiskelijanumero;
  ...

Jos haluamme tietää jokaiseen kurssiin liittyneet opiskelijat voimme joko käydä KurssiIlmoittautumiset-tyyppisen olion opiskelijoidenKurssit-hajautustaulun läpi yksitellen, tai muuttaa toteutusta siten, että se pitää kirjaa myös jokaiseen Kurssi-olioon ilmoittatuneista opiskelijoista. Tällöin tiettyyn Kurssi-ilmentymään liittyneet opiskelijat saadaan nopeasti selville.

public class KurssiIlmoittatumiset {
  private HashMap<Opiskelija, ArrayList<Kurssi>> opiskelijoidenKurssit;
  private HashMap<Kurssi, ArrayList<Opiskelija>> kurssienOpiskelijat;
  ...

Toteutettavaa järjestelmää määriteltäessä tulee suunnitella myös järjestelmän hienojakoisuus, eli kuinka pieniin osiin järjestelmä jaetaan. Esimerkiksi kursseihin voi liittyä myös harjoitusajat ja luennoitsijat. Luennoitsijat voidaan myös toteuttaa omana luokkana. Miten kannattaa toimia jos luennoitsija on (jatko-)opiskelija?

Gettereistä ja settereistä

Get- ja set-metodien tarkoituksena on luoda näkyvyyttä luokan sisäisiin (kapseloituihin) attribuutteihin, joihin ei muuten ole mahdollista päästä käsiksi luokan ulkopuolelta. Luokan sisäisessä toteutuksessa näihin muuttujiin kuitenkin pääsee käsiksi. Getterien käyttö luokan sisäisessä toteutuksessa kannattaa jättää väliin, jos sille ei ole erityistä tarvetta. Seuraavassa esimerkissä metodi lisaaElementti() tekee yhden ylimääräisen askeleen kutsuessaan metodia getElementit().

public class Dokumentti {
  private ArrayList<Elementti> elementit;
  ...
  
  public ArrayList<Elementti> getElementit() {
    return elementit;
  }
  
  public void lisaaElementti(Elementti elementti) {
    getElementit().add(elementti);
  }
  ...

Yllä oleva metodi lisaaElemetti() kannattaakin kirjoittaa siten, että se käyttää luokan sisäistä attribuuttia elementit suoraan. Tällöin toteutus näyttää seuraavanlaiselta.

public class Dokumentti {
  private ArrayList<Elementti> elementit;
  ...
  
  public ArrayList<Elementti> getElementit() {
    return elementit;
  }
  
  public void lisaaElementti(Elementti elementti) {
    elementit.add(elementti);
  }
  ...

Nyrkkisääntönä voidaan ajatella seuraavaa; Jos muuttuja on näkyvissä, käytä sitä suoraan. Jos muuttuja ei ole näkyvissä, käytä getteriä. Tämä nyrkkisääntö vaatii toteuttajalta huolellisuutta olioiden suunnittelussa ja kapseloinnissa, sillä ilman kapselointia kaikki muuttujat ovat näkyvissä.

Static ja ei-static

Staattiset ja ei-staattiset muuttujat rajaavat eron olioiden ja luokkien välille. Staattiset muuttujat liittyvät luokkiin, ei olioihin, mutta niihin voi viitata olion sisältä. Ei-staattiset muuttujat taas liittyvät aina olioon, eli jos luokasta luodaan uusi olio, luodaan olioon liittyvät (ei staattiset) muuttujat myös oliota varten. Opiskelija-luokalla on staattinen muuttuja OPISKELIJANUMERO, jonka avulla pidetään kirjaa opiskelijanumeroista siten, että yksikään opiskelija ei saa samaa opiskelijanumeroa. Luokalla Opiskelija on myös ei-staattinen muuttuja opiskelijanumero, joka liittyy aina kyseiseen olioon.

public class Opiskelija {
  private static int OPISKELIJANUMERO = 0;
  
  private String nimi;
  private int opiskelijanumero;
  
  public Opiskelija(String nimi) {
    OPISKELIJANUMERO++;

    this.nimi = nimi;
    this.opiskelijanumero = OPISKELIJANUMERO;
  }
  ...

Kun luomme luokasta Opiskelija ilmentymän, kasvatetaan luokkamuuttujaa OPISKELIJANUMERO yhdellä, ja kopioidaan sen arvo uuden olion opiskelijanumero-muuttujan arvoksi. Havainnollistetaan tilannetta vielä hifigrafiikalla. Alussa on luokka Opiskelija, josta ei ole luotu yhtään ilmentymää.

Luokka Opiskelija:
  private:
    OPISKELIJANUMERO: 0

Uutta Opiskelija-ilmentymää luotaessa (kutsulla new) ajetaan luokan konstruktori läpi. Luodaan uusi opiskelija viitteen matti taakse (Opiskelija matti = new Opiskelija("Matti");). Huomaa että matti on vain muuttujan nimi, johon viite on tallennettu.

Luokka Opiskelija:
  private:
    OPISKELIJANUMERO: 1
  
Luokasta opiskelija tehdyt ilmentymät (oliot):

          _______________________
matti->  | private:              |
         |   nimi: Matti         |
         |   opiskelijanumero: 1 |
          ----------------------- 

Luodaan toinen Opiskelija-ilmentymä. Uudelle oliolle luodaan kentät nimi ja opiskelijanumero. Luodaan uusi opiskelija viitteen arto taakse (Opiskelija arto = new Opiskelija("Arto");).

Luokka Opiskelija:
  private:
    OPISKELIJANUMERO: 2
      
Luokasta opiskelija tehdyt ilmentymät (oliot):

          _______________________
matti->  | private:              |
         |   nimi: Matti         |
         |   opiskelijanumero: 1 |
          ----------------------- 
          
          _______________________
arto->   | private:              |
         |   nimi: Arto          |
         |   opiskelijanumero: 2 |
          -----------------------          

Staattiset muuttujat siis liittyvät aina luokkaan. Staattisiin muuttujiin voi viitata ei-staattisesta ympäristöstä, eli olioiden maailmasta.

Tutkitaan samaa tilannetta vielä siten, että luokalla on staattinen metodi opiskelijoitaLuotu(), ja ei-staattinen metodi getOpiskelijanumero(). Metodit näkyvät kaikkialle, eli niillä on näkyvyysmääre public.

public class Opiskelija {
  private static int OPISKELIJANUMERO = 0;
  
  private String nimi;
  private int opiskelijanumero;
  
  public Opiskelija(String nimi) {
    OPISKELIJANUMERO++;

    this.nimi = nimi;
    this.opiskelijanumero = OPISKELIJANUMERO;
  }
  
  public int getOpiskelijanumero() {
    return opiskelijanumero;
  }
  
  ...
  
  public static int opiskelijoitaLuotu() {
    return OPISKELIJANUMERO;
  }

Staattiset metodit liittyvät luokkaan Opiskelija, kun taas ei-staattiset metodit liittyvät luokasta tehtyihin ilmentymiin. Luokassa Opiskelija olevaan staattiseen metodiin opiskelijoitaLuotu() voidaan viitata suoraan kutsulla Opiskelija.opiskelijoitaLuotu(), mutta metodiin getOpiskelijanumero() ei ole pääsyä luokasta, sillä metodi ei liity luokkaan vaan olioon. Jatketaan hifigrafiikkaamme vielä hieman.

Luokka Opiskelija:
  private:
    OPISKELIJANUMERO: 2
  public:
    opiskelijoitaLuotu()
  
Luokasta opiskelija tehdyt ilmentymät (oliot):

          _________________________
matti->  | private:                |
         |   nimi: Matti           |
         |   opiskelijanumero: 1   |
         | public:                 |
         |   getOpiskelijanumero() |
          ------------------------- 
          
          _________________________
arto->   | private:                |
         |   nimi: Arto            |
         |   opiskelijanumero: 2   |
         | public:                 |
         |   getOpiskelijanumero() |
          -------------------------        

Metodi getOpiskelijanumero() liittyy siis olion ilmentymään, eikä sitä voi kutsua ilman viitettä itse olioon. Esimerkiksi metodi opiskelijoitaLuotu() ei voi kutsua matti-viitteen takana olevan olion getOpiskelijanumero()-metodia ilman viitettä matti-olioon. Toisaalta, oliot voivat käyttää luokan muuttujia ja metodeja.

Olioihin liittyvien metodien toteutus ei kopioidu, eli metodeista ei tehdä kopioita uusia olioita luotaessa. Vain olioihin liittyvät muuttujat kopioituvat uudelle oliolle, jolloin jokaisella oliolla on omat arvot muuttujissaan. Ei-staattisten metodien toiminta liittyy kuitenkin aina viitattavaan olioon.

Staattisia luokkamuuttujia käytetään siis silloin, kun tarvitaan jotain luokkaan yleisesti liittyvää. Hyvä esimerkki yleisesti käytettävästä muuttujasta on tekstikäyttöliittymissä tarvittu Scanner-luokka, jolla voidaan lukea syötettä näppäimistöltä. Scanner-luokasta kannattaa tehdä vain yksi ilmentymä näppäimistön lukemiseen, jolloin kaikki metodit voivat käyttää samaa näppäimistönlukijaa.

Esimerkiksi seuraava luokka syötteiden lukemiseen on toteutettu vaikeasti (luokassa on muitakin pulmia!), sillä jokaisessa metodissa luodaan aina uusi Scanner-ilmentymä.

public class MunLukija {

  public static String lueEkaSana() {
    Scanner lukija = new Scanner(System.in);
    
    while(true) {
      System.out.println("Kirjoita rivi: ");
      String rivi = lukija.nextLine();
      rivi = rivi.trim();
      
      if(rivi.isEmpty()) {
        continue;
      }
      
      String[] sanat = rivi.split(" ");
      return sanat[0];    
    }
  }
  
  public static String lueVikaSana() {
    Scanner lukija = new Scanner(System.in);
    
    while(true) {
      System.out.println("Kirjoita rivi: ");
      String rivi = lukija.nextLine();
      rivi = rivi.trim();
      
      if(rivi.isEmpty()) {
        continue;
      }
      
      String[] sanat = rivi.split(" ");
      return sanat[sanat.length-1];    
    }
  }
  ...

Scanner-olio kannattaa siirtää luokkamuuttujaksi, jolloin siihen voidaan viitata aina tarvittaessa.

public class MunLukija {
  private static final Scanner lukija = new Scanner(System.in);

  public static String lueEkaSana() {
    while(true) {
      System.out.println("Kirjoita rivi: ");
      String rivi = lukija.nextLine();
      rivi = rivi.trim();
      
      if(rivi.isEmpty()) {
        continue;
      }
      
      String[] sanat = rivi.split(" ");
      return sanat[0];    
    }
  }
  
  public static String lueVikaSana() {
    while(true) {
      System.out.println("Kirjoita rivi: ");
      String rivi = lukija.nextLine();
      rivi = rivi.trim();
      
      if(rivi.isEmpty()) {
        continue;
      }
      
      String[] sanat = rivi.split(" ");
      return sanat[sanat.length-1];    
    }
  }
  ...

Luokan metodeissa on myös paljon toistoa, itse lukeminen kannattaakin tässä tapauksessa siirtää kokonaan omaan metodiinsa.

public class MunLukija {
  private static final Scanner lukija = new Scanner(System.in);

  public static String lueEkaSana() {
    String[] sanat = lueRivinSanat();
    return sanat[0];    
  }
  
  public static String lueVikaSana() {
    String[] sanat = lueRivinSanat();
    return sanat[sanat.length-1];    
  }
  
  private static String[] lueRivinSanat() {
    while(true) {
      System.out.println("Kirjoita rivi: ");
      String rivi = lukija.nextLine();
      rivi = rivi.trim();
      
      if(rivi.isEmpty()) {
        continue;
      }
      return rivi.split(" ");
    }
  }
  ...

Viite this

Viitteen this-käytöstä on ollut paljon puhetta, ja tärkeintä sen käytössä on jatkuvuus. Jos viitettä this käytetään tilanteissa, joissa sitä ei vaadita, kannattaa sitä käyttää aina. Viitteen this vaativat tilanteet ovat tilanteita, joissa metodin (tai konstruktorin) parametrina saadaan muuttuja, jolla on sama nimi, kuin olioon liittyvällä muuttujalla. Tällöin parametrina saatu muuttuja peittää olion muuttujan.

Seuraava konstruktorikutsu Opiskelija-luokalle asettaa parametrina saadulle muuttujalle nimi parametrina saadun muuttujan nimi arvon. Tällöin Opiskelija-luokan attribuutti nimi ei saa mitään arvoa.

public class Opiskelija {
  private String nimi;
  ...
  
  public Opiskelija(String nimi) {
    nimi = nimi;
    ...

Toinen tapa tehdä samankaltainen virhe on asettaa oliomuuttuja nimi parametrina saatuun muuttujaan. Tällöin parametrina saatu muuttuja saa oliomuuttujassa nimi olevan arvon, mutta olion muuttujan arvo ei muutu.

public class Opiskelija {
  private String nimi;
  ...
  
  public Opiskelija(String nimi) {
    nimi = this.nimi;
    ...

Oikea tapa tehdä ylläoleva asetus on seuraava. Tällöin luotuun opiskelijaan liittyvä muuttuja nimi saa arvokseen parametrina annetun muuttujan nimi arvon.

public class Opiskelija {
  private String nimi;
  ...
  
  public Opiskelija(String nimi) {
    this.nimi = nimi;
    ...

Esimerkki this-viitteen epäkonsistentista (ei jatkuvasta) käytöstä:

public class Opiskelija {
  private String nimi;
  private int opiskelijanumero;
  ...
  
  public Opiskelija(String nimi) {
    this.nimi = nimi;
    ...
    
  public String getNimi() {
    return nimi;
  }
  
  public int getOpiskelijanumero() {
    return this.opiskelijanumero;
  }
  ..

Tarkoitus onkin rakentaa lähdekoodi siten, että se seuraa samoja sääntöjä kaikkialla. Seuraavat esimerkit ovat molemmat paremmin tehty kuin ylläoleva esimerkki.

public class Opiskelija {
  private String nimi;
  private int opiskelijanumero;
  ...
  
  public Opiskelija(String nimi) {
    this.nimi = nimi;
    ...
    
  public String getNimi() {
    return this.nimi;
  }
  
  public int getOpiskelijanumero() {
    return this.opiskelijanumero;
  }
  ..
public class Opiskelija {
  private String nimi;
  private int opiskelijanumero;
  ...
  
  public Opiskelija(String nimi) {
    this.nimi = nimi;
    ...
    
  public String getNimi() {
    return nimi;
  }
  
  public int getOpiskelijanumero() {
    return opiskelijanumero;
  }
  ..

Ongelmia ja ratkaisuja

Seuraavassa esitellään joitakin viikkojen 3-4 laskarikertoimesta poimittuja esimerkkejä.

Asetus ylä- ja alaluokassa

Ongelma: rekisterinumeron etuliite asetetaan alaluokassa yläluokassa määriteltyyn kenttään.

public Saab(String rekkari){
	super(rekkari);
	rekkari = "SA-" + rekkari;
}

Ongelma: Rekisterinumeron alkuosa asetetaan yläluokan metodilla setRekisteriKilpi(String).

public Saab(String rekisteriNumero) {
	super(rekisteriNumero);
	super.setRekisteriKilpi("sa");
}

Ongelma: Yläluokassa ja alaluokassa on molemmissa rekisterinumero.

public abstract class Auto {
    private String rekisterinumero;

    public Auto(String rekisterinumero) {
        this.rekisterinumero=rekisterinumero;
    }

    public void aja(){
        System.out.println(this.rekisterinumero+" Ajetaan...");
    }

}

// Saab
public class Saab extends Auto{
    private String rekisterinumero;

    public Saab(String numero) {
        super("SA-"+numero);
        numero="SA-"+numero;
        this.rekisterinumero=numero;
    }

Ongelma: Alaluokka asettaa yläluokan attribuutin rekisteroi(String) "konstruktorilla".

public abstract class Auto {

   String rekisterinro;

   public String rekisteroi(String numero) {
       return this.rekisterinro = numero;
   }

   public void aja() {
       System.out.println(this.rekisterinro + " sanoo Vrrrrrrooooooooooom!");
   }

   public String toString() {
       return "" + this.rekisterinro;
   }

}

// Saab
public class Saab extends Auto {

   public Saab(String numero) {
       this.rekisterinro = "SA-" + this.rekisteroi(numero);
   }

}

Ratkaisu: Etuliite asetetaan siten, että se välittyy yläluokan konstruktorille ja asettuu siellä olion attribuutiksi.

public Saab(String rekkari){
	super("SA-" + rekkari);	
}

Metodia aja() ei ole toteutettu yläluokassa

Ongelma: Metodin aja() toteutus on toistettu kaikissa alaluokissa.

public abstract class Auto {
    String rekisterinumero;

    public Auto(String rekisterinumero) {
        this.rekisterinumero = rekisterinumero;
    }

    public abstract void aja();

}

// Saab

public class Saab extends Auto {

    public Saab(String rekisterinumero) {
        super(rekisterinumero);
    }

    public void aja() {
        System.out.println("SA-" + rekisterinumero + " sanoo Vrrrrrrooooooooooom!");
    }
}

Ratkaisu: Korjataan siten, että tehdään rekisterinumeron etuliitteen asetus kuten edellisessä esimerkissä ja siirretään aja()-metodin toteutus yläluokkaan.

public abstract class Auto {
    String rekisterinumero;

    public Auto(String rekisterinumero) {
        this.rekisterinumero = rekisterinumero;
    }

    public abstract void aja() {
        System.out.println(this.rekisterinumero + " sanoo Vrrrrrrooooooooooom!");
    }
        
}

// Saab

public class Saab extends Auto {

    public Saab(String rekisterinumero) {
        super("SA"+rekisterinumero);
    }

}

Abstrakti luokka kuten rajapinta

Ongelma: Abstrakti luokka on toteutettu kuten rajapinta, tällöin jokaisessa alaluokassa täytyy toteuttaa kaikki samansisältöiset metodit.

public abstract class Auto {
	public abstract void talletaRekkari(String rekkari);
	public abstract void aja();
	public abstract String getRekkari();
	public abstract void lisaaBongaus();
	public abstract int getBongaukset();
}

Bongausmuistiolla laskuri

Kaksi seuraavaa esimerkkiä eivät toimi ollenkaan oikein. Ohjelman tarkoituksena on bongata rekisterinumeroita ja niiden esiintymiskertoja.

Ongelma: Bongausmuistio-oliolla on attribuutti arvo. Tällöin arvo kasvaa jokaisen bongauksen kanssa, eikä jokaiselle rekisterinumerolle erikseen.

public class Bongausmuistio {
   int arvo=0;
   HashMap<Auto,Integer> muistio;

   public Bongausmuistio() {
       muistio=new HashMap();
   }

   public void bongaa(Auto a) {
       this.arvo=arvo+1;
       muistio.put(a, arvo);
   }

   public String toString() {
       return muistio.toString();
   }

}

Ongelma: Kuten edellisessä esimerkissä. Tämän lisäksi ensimmäinen bongausluku asettaa arvon nolla.

   private int bongausluku = 0;

   public Bongausmuistio() {
       muistio = new HashMap<Auto, Integer>();
   }

   public void bongaa(Auto a) {
       muistio.put(a, bongausluku);
       bongausluku++;
   }

Seuraavassa kolmessa esimerkissä ohjelma toimii oikein, mutta ratkaisu voisi olla vielä parempi.

Ongelmia: ylimääräinen muuttuja paljon ja komento put kahdesti.

public void bongaa(Auto a){
	if (muistio.containsKey(a)){
   		int paljon = muistio.get(a);
		paljon++;
		muistio.put(a, paljon);
	} else {
		muistio.put(a, 1);
	}
}
public void bongaa(Auto a) {
	if (this.bongaukset.containsKey(a)) {
		int nahty = this.bongaukset.get(a);
		nahty++;
		this.bongaukset.put(a, nahty);
	} else
		this.bongaukset.put(a, 1);
}
public void bongaa(Auto auto) {
	if (bm.containsKey(auto)) {
		int montako = bm.get(auto);
		montako++;
		bm.put(auto, montako);
	} else {
		bm.put(auto, 1);
	}
}

Ratkaisu: Alustetaan muuttuja aikaisemmat ja haetaan siihen arvo HashMap:stä. Tämän jälkeen asetaan yhdellä komennolla aikaisemmat + 1.

public void bongaa(Auto auto) {

	int aikaisemmat = 0;

	if (bm.containsKey(auto))
		aikaisemmat = bm.get(auto);
	
        bm.put(auto, aikaisemmat + 1);
}

Periytymisen suunnittelu

Rekisterinumero on toteutettu jakamalla "SA-313" tyyppiin ("SA") ja numerotunnisteeseen ("313"). Toteutuksessa alaluokka kysyy yläluokalta metodia getRekkari(), joka taas kysyy alaluokalta tyyppiä ja yhdistää sen rekisterinumeroon, jonka vihdoin palauttaa. Jos tyyppi ja numerotunniste halutaan pitää edelleen, voisi toteutuksessa käyttää suoraan attribuutteja, eikä gettereitä.

Lisäksi aja()-metodia ei ole toteuttetu yläluokassa.

public abstract class Autot {
    protected String rekkari;

    public Autot(String rekkari) {
        this.rekkari = rekkari;
    }

    public abstract void aja();

    public abstract String getTyyppi();

    public String getRekkari() {
        return getTyyppi() + this.rekkari;
    }
}

public class Saab extends Autot {

    public Saab(String rekkari) {
        super(rekkari);
    }

    public void aja() {
        System.out.println(getRekkari() + " sanoo Vrrrrroooooooooom");
    }

    public String getTyyppi() {
        return "SA-";
    }
}

public class Volvo extends Autot{

    public Volvo(String rekkari) {
        super(rekkari);
    }

    public void aja() {
        System.out.println(getRekkari() + " sanoo Vrrrrroooooooooom");
    }

    public String getTyyppi() {
        return "VO-";
    }
}

Olioden vastuut

Bongauksessa bongausten lukumäärä talletetaan bongausmuistioon. Seuraavassa esimerkissä auto tietää bongauksiensa lukumäärän. Tämä tarkoittaisi reaalimaailmassa sitä, että autobongaaja raaputtaisi auton kohdatessaan bongatun auton kylkeen merkinnän.

public void bongaa(Auto a) {
	a.talletus();
	int lkm = a.getLkm();
	String rek = a.getRekisterinumero();
	lista.put(rek, lkm);
}
public class Saab extends Auto {

    private String apu;

    public Saab (String rekisterinumero) {
      this.rekisterinumero = rekisterinumero;
      apu = this.rekisterinumero;
      this.rekisterinumero = "SA-"+apu;
    }

}

Attribuuttien näkyvyys

Metodissa aja() nähdään attribuutti rekisterinumero, sitä voi käyttää suoraan.

Lisäksi muuttuja aja on turha, sen sisältämä String voidaan antaa suoraan tulostuskomennolle. Metodien nimissä käytetään aina camelCasea: getrekisterinro()getRekisterinumero.

public String getrekisterinro(){
    return this.rekisterinro;
}

protected void aja(){
    String aja = " -"+ getrekisterinro() + " sanoo Vrrrrrrooooooooooom!";
    System.out.println(aja);
}

Hyviä käytäntöjä

while(true)

Toistot tehdään useasti kunnes jokin ehto täyttyy. Usein voi olla helpompaa muotoilla ehto siten, että toistoa tehdään ikuisesti ja poistuminen tarkistetaan toistorakenteen sisällä.

int valinta;

while (true) {
	int valinta = lukija.nextInt();

	if (valinta > 0)
		break;
}

Portsari

Ehtolauseita käytettäessä metodien luettavuus helposti kärsii. Portsarin tavoitteena on, että sisäkkäisiä tai hankalasti luettavia ehtolauseita ei enää tarvita.

Seuraavasta esimerkistä ei suoraan näe millä ehdoilla käyttäjä lisätään ilmoittautuneisiin:

void ilmoittaudu(String kayttaja) {
	if (!ilmoittautuneet.contains(kayttaja)) {
		
		if (ilmoittautuneet.size() < maksimiKoko) {
			ilmoittautuneet.add(kayttaja);
		}
	}

}

Tai sama toisella tavalla:

void ilmoittaudu(String kayttaja) {
	if (ilmoittautuneet.contains(kayttaja)) {
		// Ei tehdä mitään
	} else if (ilmoittautuneet.size() < maksimiKoko) {
		ilmoittautuneet.add(kayttaja);
	}
}

Muotoillaan ehtolauseet uudestaan. Metodin alussa on kaksi ehtolausetta, eli portsaria, jotka tarkistavat pääseekö käyttäjä sisään. Jos ensimmäinen ehto "Käyttäjä on jo ilmoittautunut" toteutuu, ohjaa portsari käyttäjän ystävällisesti ulos. Tämän jälkeen voidaan tarkistaa toinen ehto välittämättä ensimmäisestä, koska suoritus on jo päättynyt jos ehto on ollut tosi.

Samoin tomitaan jos toinen ehto "Ilmoittautuneita on maksimimäärä" toteutuu. Tämän jälkeen voidaan haluttu tapaus, ilmoittautuminen suorittaa ilman ympäröivää sotkuista ehtolausetta.

void ilmoittaudu(String kayttaja) {
	if (ilmoittautuneet.contains(kayttaja))
		return;

 	if (ilmoittautuneet.size() == maksimiKoko)
		return;

	ilmoittautuneet.add(kayttaja);
}

Saman voi tietysti sanoa vielä tiiviimmin:

void ilmoittaudu(String kayttaja) {
	if (ilmoittautuneet.contains(kayttaja) || ilmoittautuneet.size() == maksimiKoko)
		return;

	ilmoittautuneet.add(kayttaja);
}