Ohjelmoinnin jatkokurssi

Matti Paksula, Arto Vihavainen, Matti Luukkainen

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.