Ohjelmoinnin perusteet

Arto Vihavainen, Matti Paksula, Matti Luukkainen

Java

Vaikka ohjelmoinnin käsitteet ovat kieliriippumattomia, käytämme tällä kurssille jatkossa Java-kieltä. Kaikkea edellä oppimaamme voi soveltaa lähes suoraan myös Javassa.

Koska kielemme vaihtuu, opimme myös uusia kielioppisääntöjä, mutta muuten tämä viikko on vahvasti aiemman kertausta.

Hei Maailma!

public class HeiMaailma {
  public static void main(String[] komentoriviParametrit) {
    System.out.println("Hei Maailma!");
  }
}

Huomaa oleellinen osa, eli

println("Hei Maailma!");

Aiemmin käyttämämme println() on siis vieläkin olemassa, mutta Javan kieliopillisesti oikeassa muodossa. Ohjelman yläosa ja alaosa kannattaa ajatella tällä viikolla vielä apu.rb:n korvaajana.

Ohjelman kääntäminen ja ajaminen

Javassa lähdekoodi käännetään tavukoodiksi, jota voi ajaa kaikilla käyttöjärjestelmillä mille Java on toteutettu. Käytämme jatkossa ohjelmointiympäristöä, jolloin ohjelma käännetään ohjelmistoympäristön avulla. Nämä askeleet on kuitenkin hyvä kokeilla ainakin kerran.

Tallenna ylläoleva ohjelmakoodi tiedostoon HeiMaailma.java (huomaa että tiedoston ja ohjelman nimi on oltava sama). Kutsu javan kääntäjää komennolla javac, ja anna sille tiedosto HeiMaailma.java

javac HeiMaailma.java

Aja ohjelma komennolla "java HeiMaailma"

java HeiMaailma

Ohjelma tulostaa tekstin "Hei Maailma!".

Hei Maailma!

Vaikka isosta osasta tulevista esimerkeistä puuttuu ohjelman alku ja loppu, täytyy niiden kuitenkin olla mukana ohjelmakoodissa.

Apurunko

Emme tarvitse enää apu.rb:tä. Sen sijaan seuraavaa runkoa tulemme käyttämään ahkerasti. Muista nimetä tiedosto aina saman nimiseksi ohjelman kanssa. Tässä "ohjelmamme" nimi on OhjelmaRunko.

import java.util.Scanner;

public class OhjelmaRunko {
  public static Scanner lukija = new Scanner(System.in);
  
  public static void main(String[] komentoriviParametrit) {
    // tänne tulee lähdekoodi, josta ohjelman suoritus aloitetaan
  }
  
  // tänne tulevat itse määritellyt funktiot eli metodit kuten niitä Javassa kutsutaan
}

Lukijamme on nyt nimeltään lukija. Käytännössä ero aikaisempaan on se, että käytämme pientä alkukirjainta ison sijaan. Lukijamme tarjoaa nyt myös tavan liukulukujen lukemiseen (lukija.nextDouble()).

lukija:ssa on ikävänä piirteenä se, että luvun ja merkkijonon peräkkäin lukeminen ei toimi täysin odotetulla tavalla. Jos ensin luetaan käyttäjältä merkkijono ja sen jälkeen luku, toimii kaikki kuten halutaan, mutta tosin päin toimittaessa syntyy ongelma. Opimme myöhemmin kiertämään tämän ongelman.

Ohjelmointiympäristö

Ohjelmointiympäristö, eli IDE, tarjoaa lähdekoodin kääntäjän, tekstieditorin, yksinkertaisen virhetarkastuksen ja monia muita hyödyllisiä ominaisuuksia. Käytämme kurssilla NetBeans-nimistä ohjelmointiympäristöä.

Uuden projektin luominen

File -> New Project -> Java -> Java Application -> Next

Täytä kentille sopivat nimet ja paina Finish. NetBeans luo sinulle ohjelman rungon.

Huom: kohtaan Create Main Class tuleva "sopiva" nimi on ohjelmarungon haluttu nimi, esim. edellisessä esimerkissä OhjelmaRunko. Ensimmäisessä laskaritehtävässä nimeksi tulee Kuusi, kannattaa aina käyttää isoa alkukirjainta. Project name ja ohjelmarunko lienee selkeintä nimetä samalla nimellä.

Ohjelman ajaminen

Projektin ollessa valittuna paina F6 (tai ylhäällä näkyvää play-nuolta).

Aktiivisen projektin valinta

Oletusarvoisesti viimeksi luotu projekti on aktiivinen (Main Project). Aktiivinen projekti näkyy lihavoituna projektilistassa. Aktiivista projektia voi vaihtaa hiiren oikealla painikkeella avattavan valikon kohdasta "Set as Main Project".

NetBeans projektin kääntäminen ja suorittaminen komentoriviltä

Projektin voi kääntää ja suorittaa myös komentoriviltä. Avaa terminaali ja mene kansioon, jossa NetBeans säilyttää projektejaan. Oletusarvoisesti Linuxissa tämä on kotihakemistossa NetBeansProjects.

Komennolla cd NetBeansProjects/Projekti/src, esim. cd NetBeansProjects/Kuusi/src pääset projektin lähdekoodihakemistoon. Tämän jälkeen voit kääntää ohjelman komennolla javac Kuusi.java ja suorittaa sen komennolla java Kuusi.

Lähdekoodin läpikäynti askeleittain

Voit pysäyttää ohjelmakoodin suorituksen jossain kohtaa ohjelmakoodia lisäämällä sinne pysäytyspisteitä (breakpoint). Voit lisätä niitä klikkaamalla tekstieditorin vasemmassa laidassa näkyviä rivinumeroita. Voit tarkastella ohjelman tilaa pysäytyspisteissä.

Kun olet lisännyt sopivan määrän pysäytyspisteitä, paina ctrl - F5 (tai valitse play-nuolen oikealta puolelta "Debug Main Project"-nappula).

Jokaisessa pysäytyspisteessä voit tarkastella ohjelmointiympäristön alaosassa näkyvästä listasta muuttujien sisältämiä arvoja.

Painamalla F5 pysäytyspisteessä voit siirtyä seuraavaan pysäytyspisteeseen, jolloin näet sen ohjelman muuttujien tilan kyseisessä hetkessä.

Metodit

Javassa funktiot ovat metodeita. Metodit toimivat samalla tavalla, kuin aikaisemmin käytetyt funktiot. Myös jokaisesta ohjelmasta löytyvä main-metodi on metodi. Olemme jo aiemmin oppineet että metodien sisälle ei voi määritellä uusia metodeja, vaan ne määritellään aina metodien ulkopuolelle. Metodeista voi toki kutsua toisia metodeja.

Metodeille täytyy määritellä aina palautettavan arvon eli paluuarvon tyyppi. Mahdollisia paluuarvon tyyppejä ovat muunmuassa kaikki tyypit, joihin olemme aiemmin jo tutustuneet eli String, int ja boolean. Tyyppi voi olla myös esim. liukuluku double tai taulukko. Jos metodi ei palauta arvoa, merkitään paluuarvon tyypiksi void, joka tarkoittaa, että metodi ei palauta mitään. Metodin palauttama tyyppi määritellään ennen metodin nimeä.

// tulostaa tervehdyksen, mutta ei palauta mitään
public static void tervehdi() {
  System.out.println("Hei Kaikki!");
}

// palauttaa merkkijono-tyyppiä olevan tervehdyksen
public static String annaTervehdys() {
  return "Hei Kaikki!";
}

Huomaa että kaikki metodit saavat määreikseen myös public static. Tähän palataan ensi viikolla.

Paluuarvon käyttö

Paluuarvo on metodin palauttama arvo. Paluuarvon voi asettaa muuttujaan samalla tavalla kuin normaalin arvon, paluuarvo tulee vain metodilta.

// muuttujan arvon asettaminen
int summa = 5;

// muuttujan arvon asettaminen metodin palauttaman arvo avulla.
int summa = summaaja(5, 3);

Lähdekoodia kirjoitettaessa kannattaa ohjelma ajatella siten, että metodit tekevät toistettavan työn, esimerkiksi laskemisen. Main-metodissa taas tulostetaan laskuissa saadut tulokset.

Alla on apurunkoa käyttämällä tehty esimerkki ohjelmasta nimeltään Summa, joka tallennetaan tiedostoon nimeltä Summa.java. Summa laskee kahden annetun luvun summan. Ohjelmassa luemme ensiksi käyttäjältä luvut, jonka jälkeen kutsumme summa-nimistä metodia joka palauttaa meille parametriksi annettujen lukujen summan.

import java.util.Scanner;

public class Summa {
  public static Scanner lukija = new Scanner(System.in);
  
  public static void main(String[] komentoriviParametrit) {
    // tänne tulee lähdekoodi, josta ohjelman suoritus aloitetaan
    System.out.println("--- SUMMALASKURI ---");
    // luetaan ensimmäinen luku
    System.out.println("Anna ensimmäinen luku: ");
    int ekaLuettu = lukija.nextInt();
    
    // luetaan toinen luku
    System.out.println("Anna toinen luku: ");
    int tokaLuettu = lukija.nextInt();
    
    // kutsutaan metodia luetuilla arvoilla, ja asetetaan paluuarvo muuttujaan summa
    int summa = summaaja(ekaLuettu, tokaLuettu);
    
    // tulostetaan summa
    System.out.println("Annettujen lukujen summa on " + summa);
  }
  
  // tänne tulevat itse määritellyt funktiot eli metodit kuten niitä Javassa kutsutaan
  
  // summametodi, joka laskee kaksi parametrina annettua lukua yhteen ja 
  // palauttaa saadun arvon
  public static int summaaja(int eka, int toka) {
    return eka+toka;
  
    /* sama kuin:
    int tulos = eka+toka;
    return tulos;      
    */
  }
}

Parametrit

Metodeille annettavien parametrien tyyppi täytyy määritellä. Esimerkiksi metodi jolle annettaisiin merkkijono parametrinä olisi muotoa metodi(String merkkijono).

// palauttaa merkkijonon
public static String tervehdi(String nimi) {
  return "Hei " + nimi;
}

// palauttaa kokonaisluvun
public static int summa(int eka, int toka) {
  return eka+toka;
}

// palauttaa liukuluvun
public static double jako(int eka, int toka) {
  return 1.0 * eka / toka;
}

// palauttaa totuusarvon
public static boolean onkoViisi(int luku) {
  if(luku == 5) {
    return true;
  }
  
  return false;
}

// ei palauta arvoa, vaan tulostaa annetun merkkijonon
public static void tulostaTeksti(String teksti) {
  System.out.println(teksti);
}

Taulukko paluuarvona

Metodit voivat yksittäisten arvojen lisäksi palauttaa myös muunmuassa taulukkoja. Esimerkiksi merkkijonotaulukon palauttava metodi voisi olla seuraavannäköinen.

public static String[] annaMerkkijonoTaulukko() {
  // palautetaan taas opejen nimet!
  String[] opet = {"Matti P.", "Matti V.", "Matti L."}; 
  return opet;
}

Tervehtijä

Esimerkki ohjelmasta, joka lukee käyttäjältä nimen ja tulostaa tervehdyksen tervehdi-metodin avulla.

import java.util.Scanner;

public class Tervehtija {
  public static Scanner lukija = new Scanner(System.in);
  
  public static void main(String[] komentoriviParametrit) {
    String nimi;
    
    System.out.println("Anna nimi niin tervehdin!");
    nimi = lukija.nextLine();
    tervehdi(nimi);
  }
  
  public static void tervehdi(String tervehdittava) {
    System.out.println("Tervehdys, " + tervehdittava);
  }
}

Yleinen suunnitteluneuvo

Metodin käyttämät muuttujat kannattaa määritellä jo metodin alussa. Tämä on hyvä käytäntö kahdesta syystä; Se pakottaa suunnittelemaan metodin toimintaa jo ennalta, ja lähdekoodin lukeminen helpottuu kun tietää mitä jatkossa on tiedossa. Muuttujien ymmärrettävästi nimeäminen on myös hyvin tärkeää.

Toinen neuvo on se, että metodeja kannattaa olla mielummin paljon kuin vähän. Eli älä laita kaikkea ohjelmakoodia samaan metodiin vaan pyri jakamaan koodi pieniin selkeisiin metodeihin. Metodit kannattaa nimetä niin kuvaavasti että metodin nimen perustellaa pystyy jo lähes näkemään mitä metodi tekee.

Lyhyt kielioppi

Esittelemme tässä kappaleessa lyhyesti tällä viikolla tarvittavat kielioppisäännöt.

Muuttujien Tyypit

Javassa muuttujien tyyppi määritellään aina. Muuttujien tyyppejä ovat esimerkiksi String (merkkijono), int (kokonaisluku), double (liukuluku) ja boolean (totuusarvo).

Muuttujan tyyppi ei voi muuttua määrittelyn jälkeen.

String teksti = "sisältää tekstiä";
int kokonaisluku = 123;
double liukuluku = 3.141592653;
boolean onkoTotta = true;

Huom! Muuttujien tyypit määritellään vain silloin kun muuttuja esitellään ensimmäistä kertaa. Oikein:

String teksti = "sisältää tekstiä";
teksti = teksti + " ja lisää tekstiä";

Väärin:

String teksti = "sisältää tekstiä";
String teksti = teksti + " ja lisää tekstiä";

Tulostaminen

Käytämme tulostamiseen jo tuttuja println() - ja print() - metodeja, mutta Javassa ne tunnetaan muodossa System.out.println() ja System.out.print().

String teksti = "sisältää tekstiä";
int kokonaisluku = 123;
double liukuluku = 3.141592653;
boolean onkoTotta = true;
System.out.println("Tekstimuuttujan arvo on " + teksti);
System.out.println("Kokonaislukumuuttujan arvo on " + kokonaisluku);
System.out.println("Liukulukumuuttujan arvo on " + liukuluku);
System.out.println("Totuusarvomuuttujan arvo on " + onkoTotta);

Kommentit

Yhden rivin kommentit asetetaan kahdella kenoviivalla (//). Useamman rivin kommentit voidaan tehdä kenoviivan (/) ja tähden (*) avulla. /* aloittaa kommenttilohkon ja */ lopettaa sen.

// yhden rivin kommentti

/*
  Useamman
  rivin
  kommentti, 
  eli
  kommenttilohko
*/

// Yhden rivin kommentteja
// voi tietysti olla myös
// monta peräkkäin

Koodilohkojen erottelu

Lohkot aloitetaan ja lopetetaan aaltosuluilla. Aiemmin jouduimme käyttämään do-end (toistaessa) tai then-end (valinnoissa) - yhdistelmää. Jatkossa tarvitsee muistaa vain se, että lohko alkaa avaavalla aaltosululla { ja loppuu sulkevalla aaltosululla }.

while(indeksi < 3) { 	// lohkon alku
  System.out.println(indeksi); // lohkon sisältö
  indeksi = indeksi + 1;
} // lohkon loppu

Taulukot

Taulukot sisältävät aina tietyn tyyppisiä muuttujia, ja uuden taulukon luonti tapahtuu lähes samalla tavalla kuin aiemminkin. Taulukko koostuu aina tietyn tyyppisistä muuttujista, joten muuttujan tyyppi pitää määritellä taulukkoa määriteltäessä. Taulukkoa luodessa käytämme aaltosulkuja { } hakasulkujen [ ] sijaan.

Aiemmin emme ole päätyneet pulmiin viitatessamme taulukon ulkopuolella oleviin alkioihin. Javassa on kuitenkin tärkeää että taulukkoa käyttäessämme emme viittaa sen ulkopuolelle. Viitatessamme taulukon ulkopuolelle saamme virheilmoituksen ArrayIndexOutOfBoundsException.

Muttuja nimet esitellään String[]-tyyppiseksi. Tyyppi kertoo, että taulukko sisältää vain String-arvoja.

String[] nimet = {"Matti P.", "Matti V.", "Matti L."};

Taulukon koon saa kysyttyä myös suoraan taulukolta komennon length avulla.

String[] nimet = {"Matti P.", "Matti V.", "Matti L."};
System.out.println(nimet.length);  // Tulostaa kokonaisluvun 3

Sisäkkäiset taulukot

Sisäkkäisiä taulukoita voi myös luoda, tällöin taulukkoa kuvaavan muuttujan täytyy ilmaista sisäkkäisten taulukkojen määrä hakasuluilla [].

String[] rivi1 = {"X", "O", "X"};
String[] rivi2 = {"O", "X", "X"};
String[] rivi3 = {"X", "O", "O"};

String[][] ristinolla = {rivi1, 
                    	 rivi2, 
                    	 rivi3};

Esimerkiksi ylläolevassa ristinolla-taulukossa on kaksi taulukkoa [][], ensimmäinen taulukko sisältää rivitaulukot, ja jokainen rivitaulukko sisältää riveillä olevat sarakkeet.

Valintaehto else if

Valintaehto elsif kirjoitetaan tästä eteenpäin else if.

int vuosi = 2010;
int kuukausi = 2;

System.out.println("Tällä hetkellä on käynnissä:");

if ( vuosi == 2010 ) {

  if (kuukausi >= 1 && kuukausi <= 2)) {
    System.out.println("Ohjelmoinnin perusteet");
  } else if (kuukausi >= 3 && kuukausi <= 5) {
    System.out.println("Ohjelmoinnin jatkokurssi");
  } else {
    System.out.println("LLLLLLLLLLLLLLLLOMA!");
  }
}

Seuraavaksi esitellään sama toiminnallisuus, mutta metodina. Tämä esimerkki on rakennettu apurungon sisään, jolloin sen voi tallentaa suoraan Kaynnissa.java tiedostoon ja ajaa kääntämisen jälkeen.

import java.util.Scanner;

public class Kaynnissa {
  public static Scanner lukija = new Scanner(System.in);
  
  public static void main(String[] komentoriviParametrit) {
    // tänne tulee lähdekoodi, josta ohjelman suoritus aloitetaan
    int vuosi = 2010;
    int kuukausi = 2;
    
    System.out.println("Tällä hetkellä on käynnissä:");
    String kaynnissaOn = mitaKaynnissa(vuosi, kuukausi);
    System.out.println(kaynnissaOn);    
  }
  
  // tänne tulevat itse määritellyt funktiot eli metodit kuten niitä Javassa kutsutaan
  
  // metodi kertoo tällä hetkellä käynnissä olevan kurssin
  public static String mitaKaynnissa(int vuosi, int kuukausi) {
    if ( vuosi == 2010 ) {
      if (kuukausi >= 1 && kuukausi <= 2)) {
        return "Ohjelmoinnin perusteet";
      } else if (kuukausi >= 3 && kuukausi <= 5) {
        return "Ohjelmoinnin jatkokurssi";
      } else {
        return "LLLLLLLLLLLLLLLLOMA!";
      }
    }
    
    // metodin, jolle on määritelty paluuarvotyyppi, täytyy aina palauttaa arvo
    return "";
  }
}

Toistoa

While

While toimii samoin kuin aiemminkin, mutta käytämme aaltosulkuja while-lohkon aloittamiseen ja lopettamiseen (kuten jo määritelty kielioppimme kertoo).

int luku = 0;
while(luku < 5) {
  System.out.println(luku);
  luku = luku + 1;
}

For each

Kaikki taulukon alkiot voidaan käydä läpi myös for-tyylisellä toistorakenteella. Aiemmin tuttu in korvataan kaksoispisteellä :. Alla oleva esimerkki tulostaa opet-taulukossa olevat merkkijonot, jokaisen omalle rivilleen.

String[] opet = {"Matti L.", "Matti P.", "Matti V."};

for(String opettaja: opet) {
  System.out.println(opettaja);
}

for

for-tyyppisestä toistorakenteesta on olemassa myös toinen versio. Versio koostuu toiston alustuksesta, jatkoehdosta ja käytetyn luvun muokkaamisesta. Kielioppi toistolle on muotoa for(alustus; jatkoehto; luvun muokkaus).

Alustuksessa määritellään muuttuja jota toistossa käytetään (esimerkiksi indeksimuuttuja), jatkoehto on ehto jonka voimassaolon aikana toistoa jatketaan, ja käytetyn muuttujan muokkaaminen voi olla esimerkiksi yhdellä kasvattaminen.

Seuraava esimerkki alustaa käytettävän muuttujan nollaksi, määrittelee toistoehdon olevan tosi kun muuttuja on pienempi kuin 51 ja kasvattaa käytettävää muuttujaa yhdellä jokaisen toiston jälkeen. Mitä ohjelma tulostaa?

for(int tulostettavaLuku = 0; tulostettavaLuku < 51; tulostettavaLuku += 1) {
  System.out.println(tulostettavaLuku);
} 

Tämä toistorakenne on myös hyvin kätevä taulukon alkioiden läpikäyntiin kun tarvitsemme tietoa taulukon indeksistä. Käyttämällä lukua indeksinä voimme käydä esimerkiksi vain taulukon joka kolmannen alkion läpi seuraavasti.

int[] taulukko = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int jokaKolmannenSumma = 0;

for(int indeksi = 0; indeksi < taulukko.length; indeksi += 3) {
  System.out.println("Indeksi: " + indeksi + ", arvo: " + taulukko[indeksi]);
  jokaKolmannenSumma = jokaKolmannenSumma + taulukko[indeksi];
} 
System.out.println("Joka kolmannen alkion summa: " + jokaKolmannenSumma);

Lopetusehto tarkistetaan toiston alussa, sekä joka kerta toiston jälkeen tapahtuvan muuttujan muokkauksen jälkeen. Tästä johtuen emme päädy taulukon ulkopuolelle jos lopetusehto on kirjoitettu oikein.

Lisäesimerkkejä

Nimien lukeminen ja tulostaminen

Seuraava hieman isompi esimerkki pyytää ensiksi käyttäjältä seitsemän nimeä jotka talletetaan taulukkoon. Lopuksi ohjelma tulostaa taulukon sisällön.

import java.util.Scanner;

public class Tervehtija {
  public static Scanner lukija = new Scanner(System.in);
  
  public static void main(String[] komentoriviParametrit) {
    // luodaan tyhja taulukko johon nimet tallennetaan
    String[] nimet = {"", "", "", "", "", "", ""};
    // opimme myöhemmin hieman järkevämmän tavan taulukon luomiseen
    
    // käytetään for-toistoa nimien lukemiseen.
    // luemme yhden nimen jokaista taulukon indeksiä kohti
    
    // kutsu luku++ tarkoittaa samaa kuin luku += 1
    for(int luettavaIndeksi = 0; luettavaIndeksi < nimet.length; luettavaIndeksi++) {
      System.out.println("Anna nimi numero " + (luettavaIndeksi + 1));
      nimet[luettavaIndeksi] = lukija.nextLine();
    }
    
    System.out.println("Kiitos!");
    System.out.println("Tulostetaan seuraavaksi kaikki nimet!");
    System.out.println();
    
    for(String nimi: nimet) {
      System.out.println(nimi);
    }
  }
}

Sisentäminen

Lähdekoodin oikein sisentäminen helpottaa niiden lukemista ja ymmärtämistä huomattavasti. Jatkossa emme hyväksy laskaripalautuksia, joissa sisennykset eivät ole kunnossa. Seuraava sääntölista auttaa sisennysten oikein saamisessa.

  1. Lohkon aloitus: {
  2. Lohkon lopetus: }

    Esimerkki. Huomaa erityisesti lohkon sulkevan sulun sijainti:

    public class Ohjelma {                         // Ohjelma:n määrittely aloittaa lohkon	
       public static void main(String[] args) {    // main aloittaa lohkon
          int x = 0;
    			
          while ( x<10 ) {                         // while aloittaa lohkon
             i = i+1;
          }                                        // while:n lohko loppuu tähän
    	  
          System.out.println("i:n arvo "+i);       // huom: samalla sisennystasolla kuin while
       }                                           // main:in lohko loppuu tähän
    }                                              // Ohjelma:n lohko loppuu tähän
    	
  3. Tabulaattorin käyttö, ei välilyöntejä
  4. Ehdon jälkeen välilyönti ennen lohkon avausta
  5. Kokonaisuuksien erottaminen toisistaan tyhjällä rivillä

Metodien parametrit ja paluuarvot

Metodia kutsuttaessa annetun muuttujan nimellä ei ole väliä. Metodia joka on määritelty muodossa public static int metodinNimi(int arvo) voi hyvin kutsua minkä nimisillä muuttujilla tahansa, kunhan ne ovat tyyppiä int, eli kokonaislukuja.

Tarkastellaan seuraavaa ohjelmaa, jossa metodia kutsutaan eri arvoilla. Metodi palauttaa arvon, joka asetetaan jo olemassa oleviin muuttujiin.

public class Ketju {
  public static void main(String[] komentoRiviParametrit) {
    int a;
    int b;
    int c = 3;
    
    a = parametriPlusViisi(c);
    b = parametriPlusViisi(a);
    c = parametriPlusViisi(a);
  }
  
  public static int parametriPlusViisi(int parametrinaAnnettuLuku) {
    parametrinaAnnettuLuku = parametrinaAnnettuLuku + 5;
    return parametrinaAnnettuLuku;  
  }
}

Miten muuttujien a, b ja c arvot muuttuvat eri ohjelman vaiheissa, ja miksi?

Alussa muuttujat a ja b alustetaan tyhjiksi, ja muuttuja c sisältää arvon 3. Kun suoritetaan a = parametriPlusViisi(c) muuttuja a saa arvokseen muuttujan c sisältämän arvon + 5, eli 8. Seuraavaksi suoritetaan b = parametriPlusViisi(a), jolloin b saa arvokseen muuttujan a sisältämän arvon + 5, eli 13. Lopuksi suoritetaan rivi c = parametriPlusViisi(a), jolloin muuttuja c saa arvokseen muuttuja a sisältämän arvon + 5, eli 13.

Ohjelman suorituksen lopussa muuttujan a arvo on 8, muuttujan b arvo on 13 ja muuttujan c arvo on 13.

Luokka ja olio

Otamme nyt käyttöön kaksi keskeitä käsitettä: luokan ja olion.

Luokka määrittelee jonkin toiminnallisuuden, "koneen piirustuksen". Määrittelyn perusteella voidaan sitten luoda luokan ilmentymiä, olioita, "koneita", jotka toteuttavat tuon toiminnallisuuden.

Luokka

Luokka kuvaa minkälaisia piirteitä eli attribuutteja sen olioilla on. Luokan olioihin liittyvät attribuutit ovat samanlaisia muuttujia kuin mitä olemme jo aiemmin käyttäneet. Attribuutit kuvaavat olioiden sisäistä tilaa. Attribuuttien lisäksi luokka määrittelee myös olioiden toiminnallisuuden eli metodit. Luokka talletetaan omaan tiedostoonsa, jolla on sama nimi kuin toteutettavalla luokalla.

Tehdään luokka Laskuri, joka talletetaan tiedostoon Laskuri.java. Huomaa että luokkien nimet ovat aina samat lähdekooditiedostojen nimien kanssa. Luokka määrittelee attribuutin int arvo, eli kaikilla luokan olioilla on arvo-niminen attrivuutti. Lisäksi luokka määrittelee kaksi metodia, joilla olion attribuuttia arvo voidaan käsitellä sekä ns. konstruktori,n eli "metodin", joka suoritetaan automaattisesti kun luokan olioita syntyy.

Metodeilla voidaan kasvattaa luokan olioiden arvoa ja kysyä mikä arvo on tällä hetkellä. Konstruktorilla asetetaan luokan olion attribuuteille alkuarvo.

public class Laskuri {
  int arvo;

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

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

  public int annaArvo() {
    return arvo;
  }
}

Ylläolevalla Laskuri-luokan olioilla on attribuuttina kokonaisluku-tyyppiä oleva arvo-muuttuja. Konstruktori on aina saman niminen kuin itse luokka, eli tässä tapauksessa Laskuri.

Luokan luominen NetBeanssissä

Kun olet luonut pääohjelman ja sinulla on projekti auki, klikkaa vasemmalla olevasta projektivalikosta projektin nimen kohdalla hiiren oikeaa nappia, valitse new ja Java class ja anna luokalle nimi kohtaan Class name. Luokan nimen tulee alkaa isolla alkukirjaimella. Paina Finish niin luokka syntyy..

Olio

Ylläolevalla Laskuri-luokalla ei tee mitään ilman suoritettavaa ohjelmaa. Teemme varsinaisen ohjelman nimeltä Laskin, joka luo omaLaskuri-nimisen Laskuri-luokan olion.

Aluksi luomme olion omaLaskuri. Olion luominen tapahtuu komennolla new Laskuri(0). Olion omaLaskuri tyypiksi tulee sen luokan nimi, mistä se on luotu, eli tässä tapauksessa Laskuri. Luonnin yhteydessä annamme oliolle parametrina alkuarvon 0. Olion luomisen yhteydessä suoritetaan konstruktorin koodi. Konstruktori asettaa parametrina annetun luvun attribuuttiin arvo.

Olion luonnin jälkeen kysymme mikä on laskurin arvo, kasvatamme arvoa kahdesti ja kysymme arvoa uudestaan.

public class Laskin {

  public static void main(String[] komentoriviParametrit) {

    Laskuri omaLaskuri = new Laskuri(0);   // luodaan uusi Laskuri-olio 

    System.out.println("omaLaskurin arvo on " + omaLaskuri.annaArvo());

    omaLaskuri.kasvataArvoa();             // kutsutaan olion omaLaskuri metodia kasvataArvo
    omaLaskuri.kasvataArvoa();    

    System.out.println("omaLaskurin arvo on " + omaLaskuri.annaArvo());

  }
}

Ohjelman Laskin ajo tulostaa seuraavaa.

omaLaskurin arvo on 0
omaLaskurin arvo on 2

Ohjelma Laskin siis käyttää luokasta Laskuri tehtyä oliota omaLaskuri.

Kahvikassa

Käytämme samaa Laskuri-luokkaa toteuttamaan ohjelman nimeltä Kahvikassa.

Laskuri-luokka on toteutettu kuten aiemmin.

public class Laskuri {
  int arvo;

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

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

  public int annaArvo() {
    return arvo;
  }
}

Teemme uuden pääohjelman nimeltä Kahvikassa, joka käyttää luokkaa Laskuri samalla tavalla kuten aikaisempi ohjelmamme Laskin.

Alussa luomme olion nimeltä mattiL, joka on tyyppiä laskuri. Kuten aikaisemmin, kysymme laskurimme arvoa, kasvatamme sitä ja kysymme kasvatettuja arvoja.

public class Kahvikassa {
	
  public static void main(String[] komentoriviParametrit) {
    
    Laskuri mattiL = new Laskuri(0);
				
    System.out.println("Matti L. on juonnut " + mattiL.annaArvo() + " kahvia.");

    System.out.println("Juodaan kahvia.");
		
    mattiL.kasvataArvoa();
    mattiL.kasvataArvoa();
	
    System.out.println("Matti L. on juonnut " + mattiL.annaArvo() + " kahvia.");

  }
}

Kun ohjelma suoritetaan saadaan seuraava tuloste. Tulosteesta voidaan havaita, että Matti L. juo aina kaksi kuppia kahvia kerralla.

Matti L. on juonnut 0 kahvia.
Juodaan kahvia.
Matti L. on juonnut 2 kahvia.

Lisätään henkilö

Laajennetaan kahvikassaa siten, että myös Matti P. käyttää ohjelmaa ja tuo vanhat velkansa mukanaan. Luodessa oliota mattiP asetamme laskurin alkuarvoksi vanhojen velkojen lukumäärän (8 kuppia). Tämän jälkeen oliot mattiL ja mattiP juovat kahvia.

public class Kahvikassa {
	
  public static void main(String[] komentoriviParametrit) {
    
    Laskuri mattiL = new Laskuri(0);
    Laskuri mattiP = new Laskuri(8);
		
		
    System.out.println("Matti L. on juonut " + mattiL.annaArvo() + " kahvia.");
    System.out.println("Matti P. on juonut " + mattiP.annaArvo() + " kahvia.");


    System.out.prinltn("Juodaan kahvia.");

    mattiL.kasvataArvoa();
    mattiL.kasvataArvoa();
		
    mattiP.kasvataArvoa();

	
    System.out.println("Matti L. on juonut " + mattiL.annaArvo() + " kahvia.");
    System.out.println("Matti P. on juonut " + mattiP.annaArvo() + " kahvia.");

  }
}

Ohjelman tuloste olisi seuraavanlainen.

Matti L. on juonut 0 kahvia.
Matti P. on juonut 8 kahvia.
Juodaan kahvia.
Matti L. on juonut 2 kahvia.
Matti P. on juonut 9 kahvia.

Kahvikassa pitää siis kirjaa henkilöiden mattiL ja mattiP juoduista kahveista Laskuri-luokasta tehtyjen olioiden avulla. Olioiden ja luokkien käyttäminen ohjelmoinnissa helpottaa jakamaan vastuuta eri ohjelman osien kesken, ja mahdollistaa ideoiden ja lähdekoodin uudelleenkäytön myös uusissa ohjelmissa.

Kuulaskuri

Otetaan esimerkiksi aikaisempi laskuharjoituksissa tehty tehtävä, jossa toteutimme kuulaskurin. Tehtävässä ohjelmalle piti antaa kuukausi syötteenä, jonka jälkeen ohjelma tulosti seuraavat 13 kuukautta.

Luodaan rakenne luokalle KuuLaskuri. Luokka KuuLaskuri määrittelee olioilleen metodin kuukauden kasvattamisen yhdellä ja kuukauden kysymisen. Lisäksi KuuLaskurilla on konstruktori jolla voidaan antaa aloituskuukausi. KuuLaskuri-olioiden attribuuttina on nykyistä kuukautta esittävä kokonaisluku.

public class KuuLaskuri {
  int nykyinenKuukausi;

  public KuuLaskuri(int moneskoKuu) {
    nykyinenKuukausi = moneskoKuu;
  }

  public void seuraavaKuukausi() {
    nykyinenKuukausi = nykyinenKuukausi + 1;
    
    if (nykyinenKuukausi == 13) {
      nykyinenKuukausi = 0;
    }	
  }

  public int mikaKuu() {
    return nykyinenKuukausi;
  }
}

Tehdään myös ohjelma, joka käyttää luokkaa KuuLaskuri. Kutsutaan ohjelmaa KuuTulostajaksi.

import java.util.Scanner;

public class KuuTulostaja {
  public static Scanner lukija = new Scanner(System.in);
  
  public static void main(String[] komentoriviParametrit) {
    System.out.print("Anna kuukausi: ");
    int kuukausi = lukija.nextInt();
    
    KuuLaskuri laskuri = new KuuLaskuri(kuukausi);
    
    System.out.println("Seuraavat 13 kuukautta:");
    
    for(int i = 0; i < 13; i++) {
      laskuri.seuraavaKuukausi();
      System.out.print(laskuri.mikaKuu() + " ");
    }
    
    System.out.println();
  }
}

Ohjelma KuuTulostaja kysyy ensiksi käyttäjältä kuukautta, jonka jälkeen se toistaa kuukauden kasvattamista ja tulostamista 13 kertaa.

Esimerkiksi syötteellä 9 ohjelman tulostus näyttäisi seuraavalta:

Anna kuukausi: 9

Seuraavat 13 kuukautta:
10 11 12 1 2 3 4 5 6 7 8 9 10

Varasto

Myös konstruktorilla voi olla useita parametreja. Pohditaan pienen varaston toteuttamista. Varasto-olioilla on tilavuus ja tämän hetkinen tilanne, eli saldo.

Varasto-olioiden attribuuteiksi, eli muuttujiksi, tulevat siis tilavuus ja saldo. Sovitaan myös että luokan nimeksi tulee Varasto.

public class Varasto {
  double tilavuus;
  double saldo;

Varasto-luokan konstruktori saa arvokseen luotavan varaston tilavuuden, sekä tämän hetkisen varastotilanteen. Konstruktoria siis kutsutaan kun luokasta luodaan uusi olio.

  public Varasto(double varastonTilavuus, double varastonTila) {
    tilavuus = varastonTilavuus;
    saldo = varastonTila;
    
    if(saldo > tilavuus) {
      saldo = tilavuus;
    }
  }

Varasto-oliot tarvitsevat myös muita toiminnallisuuksia. Määritellään metodit varastoon lisäämiseen ja sieltä ottamiseen. Metodi lisaaVarastoon saa parametrikseen lisättävän määrän. Jos tavaraa yritetään lisätä enemmän kuin varastoon mahtuu, heitetään loput pois.

Varastosta ottaminen tapahtuu metodin otaVarastosta-avulla. Jos varastossa ei ole haluttua määrää, annetaan vain sen verran kuin on. Metodille otaVarastosta annetaan siis parametrina haluttu määrä, ja se palauttaa saadun määrän.

  public void lisaaVarastoon(double maara) {
    saldo = saldo + maara;
    
    if(saldo > tilavuus) {
      saldo = tilavuus;
    }   
  }
  
  public double otaVarastosta(double maara) {
    double otettuMaara = maara;
    
    if(saldo > maara) {
      saldo = saldo - maara;
    } else {
      otettuMaara = saldo;
      saldo = 0;
    }
    
    return otettuMaara;
  }  

Tehdään vielä lopuksi varastollemme metodit annaSaldo, joka palauttaa varaston saldon, sekä annaTilavuus(), joka palauttaa varaston kokonaistilavuuden.

  public double annaSaldo() {
    return saldo;
  }  
  
  public double annaTilavuus() {
    return tilavuus;
  }  

Luokka Varasto vielä kokonaisuudessaan.

public class Varasto {
  double tilavuus;
  double saldo;

  public Varasto(double varastonTilavuus, double varastonTila) {
    tilavuus = varastonTilavuus;
    saldo = varastonTila;
    
    if(saldo > tilavuus) {
      saldo = tilavuus;
    }
  }
  
  public void lisaaVarastoon(double maara) {
    saldo = saldo + maara;
    
    if(saldo > tilavuus) {
      saldo = tilavuus;
    }   
  }
  
  public double otaVarastosta(double maara) {
    double otettuMaara = maara;
    
    if(saldo > maara) {
      saldo = saldo - maara;
    } else {
      otettuMaara = saldo;
      saldo = 0;
    }
    
    return otettuMaara;
  }
  
  public double annaSaldo() {
    return saldo;
  }
  
  public double annaTilavuus() {
    return tilavuus;
  }  
}

Kirjanpito

Luodaan ohjelma bensa-aseman kirjanpitoa varten. Kirjanpito-ohjelma hallinnoi bensa-asemaa ja päävarastoa.

Hallinnoitavat varastot ovat pikku ABC, sekä päävarasto. Pikku ABC pienehkö bensa-asema, joten sinne mahtuu myös vähemmän bensaa kuin päävarastolle. Toteutetaan kummatkin varastot Varasto-olioina.

public class BensaKirjanpito {

  public static void main(String[] args) {
    Varasto pikkuAbc = new Varasto(2000.0, 100.0);
    Varasto paaVarasto = new Varasto(40000.0, 32000.0);
    double saatuMaara;
    
    saatuMaara = pikkuAbc.otaVarastosta(50);
    System.out.println("Pikku ABC:ltä tankattiin " + saatuMaara + " litraa bensaa.");
    System.out.println("Pikku ABC:llä jäljellä " + pikkuAbc.annaSaldo() + " litraa.");
    
    saatuMaara = pikkuAbc.otaVarastosta(50);
    System.out.println("Pikku ABC:ltä tankattiin " + saatuMaara + " litraa bensaa.");
    System.out.println("Pikku ABC:llä jäljellä " + pikkuAbc.annaSaldo() + " litraa.");

    System.out.println("Siirretään päävarastosta lisää bensaa Pikku ABC:lle");
    
    double siirrettavaMaara = paaVarasto.otaVarastosta(2000);
    pikkuAbc.lisaaVarastoon(siirrettavaMaara);
    
    System.out.println("Pikku ABC:lle sirrettiin " + siirrettavaMaara + " litraa");
    System.out.println("Pikku ABC:llä jäljellä " + pikkuAbc.annaSaldo() + " litraa.");
  }
}

Ohjelma tulostaa seuraavanlaisen kirjanpidon:

Pikku ABC:ltä tankattiin 50 litraa bensaa.
Pikku ABC:llä jäljellä 50.0 litraa.
Pikku ABC:ltä tankattiin 50 litraa bensaa.
Pikku ABC:llä jäljellä 0.0 litraa.
Siirretään päävarastosta lisää bensaa Pikku ABC:lle
Pikku ABC:lle sirrettiin 2000.0 litraa
Pikku ABC:llä jäljellä 2000.0 litraa.

Olion tilan tulostaminen

Jos ohjelmassa tarvitaan koko olion tilanteen tulostamista, kannattaa sitä varten toteuttaa myös metodi. Metodi toString() palauttaa olion tilan merkkijonona, jolloin saatua merkkijonoa voidaan käyttää suoraan ohjelmassa.

Toteutetaan luokka Ihminen, joka saa alkuarvokseen nimen ja iän. Ihmisen ikää voi kasvattaa ja siltä voidaan metodin toString() - avulla saada sen tämän hetkinen tilanne merkkijonona.

public class Ihminen {
  String nimi;
  int ika;
  
  public Ihminen(String annettuNimi, int annettuIka) {
    nimi = annettuNimi;
    ika = annettuIka;
  }
  
  public void vanhene() {
    ika = ika+1;
  }
  
  public String toString() {
    return "Hei, olen " + nimi + ". Ikäni on " + ika;
  }
}

Toteutetaan myös ohjelma jossa ihminen elää. Luodaan sinne ihmis-olio nimeltä mattiV ja eletään viisi vuotta.

public class Elama {
  public static void main(String[] args) {
    Ihminen mattiV = new Ihminen("Matti V", 31);
    
    System.out.println("Tulostetaan olion mattiV tiedot:");
    System.out.println(mattiV);
    
    System.out.println("Eletään viisi vuotta..");
    for(int i = 0; i < 5; i++) {
      mattiV.vanhene();
      System.out.println("Vuosi vierähti!");
    }
        
    System.out.println("Tulostetaan olion mattiV tiedot:");
    System.out.println(mattiV);
  }
}

Ohjelma Elama tulostaa ajettaessa seuraavaa:

Tulostetaan olion mattiV tiedot:
Hei, olen Matti V. Ikäni on 31
Eletään viisi vuotta..
Vuosi vierähti!
Vuosi vierähti!
Vuosi vierähti!
Vuosi vierähti!
Vuosi vierähti!
Tulostetaan olion mattiV tiedot:
Hei, olen Matti V. Ikäni on 36

Metodia toString() kutsutaan siis kun oliota yritetään tulostaa.

Juuri tämä olio - this

Javassa ilmaisulla this tarkoitetaan juuri tämän olion attribuutteja ja metodeja. Ilmaisu kertoo Javalle, että viittaamme nimenomaan tämän olion tietoihin.

Esimerkiksi this.luku = 3; kertoo Javalle, että haluamme asettaa nimenomaan tämän olion luku-attribuutin. Vastaavasti this.annaLuku() kertoo, että kutsumme nimenomaan tämän olion metodia annaLuku().

Attribuutteihin viittaaminen

Tarkastellaan Laskuri-luokkaa, jossa on nyt myös toString()-metodi.

public class Laskuri {
  int arvo;

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

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

  public int annaArvo() {
    return arvo;
  }

  public String toString() {
    return "Arvoni on: " + annaArvo();	
  }
}

Laskurissa konstruktori asettaa attribuuttiin arvo konstruktorin parametrina annetun kokonaisluvun alkuarvo mukaisesti. Konstruktorin parametri ja olion attribuuti on nimetty eri nimisiksi, jotta asetus onnistuisi.

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

Käyttämällä ilmaisua this.arvo voimme erottaa toisistaan nimenomaan tämän olion arvon ja konstruktorin parametrin arvon. Tällöin voimme käyttää samannimisiä muuttujia seuraavasti.

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

Näin attribuutin arvo asetus onnistuu, sillä Java tietää mikä (arvo) asetetaan mihin (this.arvo) samasta nimestä huolimatta.

Metodiin viittaaminen

Luokkaa Laskuri voidaan käyttää esimerkiksi seuraavasti.

Laskuri moottori = new Moottori(0);

while ( moottori.annaArvo() < 1000 ) {
	moottori.kasvataArvoa();
}	

Esimerkissä oliolta moottori kutsutaan metodeja annaArvo() ja kasvataArvoa(). Olion metodin toString() toteutuksessa kutsutaan saman olion metodia annaArvo().

public String toString() {
  return "Arvoni on: " + annaArvo();	
}

Muuttamalla kutsun muotoon this.annaArvo() tarkennamme, että haluamme nimenomaan kutsua tämän olion ilmentymän metodia.

public String toString() {
  return "Arvoni on: " + this.annaArvo();	
}

Nyt olion sisäinen toteutus on kuten oliota käyttävässä esimerkissä ylhäällä: metodia annaArvo() kutsutaan oliota moottori. Olion metodin toString() toteutuksessa on painotettu, että metodia annaArvo() kutsutaan nimenomaan tältä samalta ilmentymältä.

Lisäämme vielä jäljellä oleviin muuttujien asetukseen ilmaisun this. Laskurin toteutus tarkennettuna ilmaisujen this kanssa näyttää nyt seuraavalta.

public class Laskuri {
  int arvo;

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

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

  public int annaArvo() {
    return this.arvo;
  }

  public String toString() {
    return "Arvoni on: " + this.annaArvo();	
  }
}

Tästälähtien käytämme aina ilmaisua this painottamaan mitä haluamme sanoa olion toteutuksessa. Näin koodista tulee luettavampaa.

Olion yksityiset attribuutit

Tähän asti kaikkiin olioiden attribuutteihin on päässyt olion ulkopuolelta, kuten esimerkiksi pääohjelmasta.

Nykyisellä laskurin toteutuksella seuraava on mahdollista:

Laskuri kahvit = new Laskuri(0);

kahvit.kasvataArvoa();
kahvit.arvo = 100;
kahvit.kasvataArvoa();

Olion kahvit arvo on nyt 101.

Luokan Laskuri tehtävänä on tarjota julkinen metodi (kasvataArvoa()), joka pitää huolen laskurin arvon kasvatuksesta. Laskuria ei tule muuttaa suoraan, sillä luokan sisäinen toteutus saattaa vaihtua. Voimme esimerkiksi päättää, että laskurin pitää aina olla parillinen luku tai että se ei koskaan saa ylittää arvoa 100.

Jos määrittelemme olion attribuuten arvo yksityiseksi (private), estämme sen suoran käsittelyn olion ulkopuolelta.

public class Laskuri {
  private int arvo;

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

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

  public int annaArvo() {
    return this.arvo;
  }

  public String toString() {
    return "Arvoni on: " + this.annaArvo();	
  }
}

Nyt pääohjelmassa suora olion attribuutin arvo käsittely antaa virheen.

Laskuri kahvit = new Laskuri(0);

kahvit.arvo = 100;   // "arvo has private access in Laskuri"

Alkeistyypit ja viittaukset

Tarkennamme nyt hieman käyttämiämme muuttujien tyyppejä. Javassa muuttujat jaetaan alkeistyyppeihin ja viittaustyyppeihin.

Alkeistyypit

Alkeistyyppejä ovat mm. jo entuudestaan tutut int, double ja boolean.

Jos muuttujan tyypiksi määritellään alkeistyyppi, muuttujan todellinen arvo on alkeistyypin arvo. Esimerkiksi muuttujan kokonaisluku tyyppi on alkeistyyppi int, jonka arvo on 8.

int kokonaisluku =  8;

Viittaustyypit

Kun muuttujan tyyppinä on luokka, tulee muuttujan arvoksi viite luokan tyyppiseen olioon. Olio elää siis omassa muistipaikassaan, johon voimme vain viitata käyttämällä muuttujaa.

Seuraavassa esimerkissä alustetaan kaksi viittaustyyppistä muuttujaa, jotka voivat saada arvokseen viittauksen olioon laskuri. Ensimmäiseen muuttujaan kahvit asetetaan viite uuteen Laskuri-olioon. Seuraavalla rivillä muuttujaan samalaskuri asetetaan viite samaan olioon. Tämän jälkeen tulostetaan kummankin muuttujan olio (joka siis on sama olio) ja kasvatetaan olion arvoa kutsumalla sitä kummankin muuttujan kautta.

public class Viitelaskuri {

  public static void main(String[] komentoriviParametrit) {

    Laskuri kahvit;
    Laskuri samalaskuri;

    kahvit = new Laskuri(0); // kahvit viittaa olioon
    samalaskuri = kahvit;    // samalaskuri viittaa samaan olioon, kuin kahvit

    System.out.println(kahvit);
    System.out.println(samalaskuri);

    kahvit.kasvataArvoa();

    System.out.println(kahvit);
    System.out.println(samalaskuri);

    samalaskuri.kasvataArvoa();

    System.out.println(kahvit);
    System.out.println(samalaskuri);

  }
}

Ohjelma tulostaa seuraavaa.

Arvoni on: 0
Arvoni on: 0
Arvoni on: 1
Arvoni on: 1
Arvoni on: 2
Arvoni on: 2

Koska molemmissa muuttujissa kahvit ja samalaskuri oli viite samaan olioon, tapahtui muutos aina vain yhdessä oliossa.

Olio parametrinä

Kun olio annetaan metodille parametrinä, välitetään olion (tai "arvon") sijasta viite olioon.

Luodaan uusi luokka Kasvattaja. Luokalla on metodi kasvataKahdesti(Laskuri), joka kutsuu parametrinään saadun Laskuri-olion ilmentymän kasvataArvoa()-metodia kahdesti.

public class Kasvattaja {
    // jos konstruktoria ei tarvita, jätetään se määrittelemättä

    public void kasvataKahdesti(Laskuri laskuri) {
        laskuri.kasvataArvoa();
        laskuri.kasvataArvoa();
    }

}

Luokkaa Kasvattaja voi käyttää siis seuraavasti.

Laskuri kerrat = new Laskuri(0);
Kasvattaja kasvattaja = new Kasvattaja();

kasvattaja.kasvataKahdesti(kerrat);

System.out.println(kerrat);  // Laskuri kerrat on nyt arvossa 2

Kun metodin parametrina on olio, "näkee" metodi olion itsensä, eli kaikki metodin oliolle tekemät muutokset näkyvät myös kutsujassa. Tämä johtuu siitä, että oliot ovat viittaustyyppisiä muuttuja toisin kuin esim int.

Niin kuin olemme aiemmin nähneet, jos parametrin tyyppi on int (tai joku muu alkeistyyppi), välittyy metodiin ainoastaan kopio parametrin arvosta, eli metodin alkeistyyppisille parametreille tekemät muutokset eivät näy kutsujalle.

Tämä ero alkeistyyppisten ja viittaustyyppisten parametrien erilaisesta käyttäytymisestä on erittäin tärkeä ymmärtää.

Toteutetaan kasvattajan avulla kahvikassa.

public class Kahvikassa {

  public static void main(String[] komentoriviParametrit) {

    Laskuri mattiL = new Laskuri(0);
    Kasvattaja kahvikassa = new Kasvattaja();

    System.out.println("Juodaan kahvia..");
    kahvikassa.kasvataKahdesti(mattiL);
    System.out.println("..juotu.\n");
		
    System.out.println("Matti L. on juonut: " + mattiL.annaArvo());

  }
}

Laskuri-olio mattiL annetaan parametrinä Kasvattaja-olio kahvikassa:n metodille kasvataKahdesti(Laskuri).

Juodaan kahvia..
..juotu.

Matti L. on juonut: 2

Ohjelman tulosteesta haivaitsemme (taas), että Matti L. juo aina kaksi kuppia kahvia.

Laajennetaan esimerkkiä siten, että myös Matti V. juo kupin kahvia. Toteutetaan luokkaan Kasvattaja ensin metodi kasvata(Laskuri), joka kutsuu yhden kerran parametrinään saamaansa Laskuri-olion metodia kasvataArvoa().

public class Kasvattaja {
    public void kasvata(Laskuri laskuri) {
        laskuri.kasvataArvoa();
    }

    public void kasvataKahdesti(Laskuri laskuri) {
        laskuri.kasvataArvoa();
        laskuri.kasvataArvoa();
    } 
}

Lisätään Matti V. pääohjelmaan.

public class Kahvikassa {

  public static void main(String[] komentoriviParametrit) {

    Laskuri mattiL = new Laskuri(0);
    Laskuri mattiV = new Laskuri(0);
	
    Kasvattaja kahvikassa = new Kasvattaja();

    System.out.println("Matti L. juo kahvia..");
    kahvikassa.kasvataKahdesti(mattiL);
    System.out.println("..juotu.\n");

    System.out.println("Matti V. juo kahvia..");
    kahvikassa.kasvata(mattiV);
    System.out.println("..juotu.\n");

    System.out.println("Matti L. on juonut: " + mattiL.annaArvo());
    System.out.println("Matti V. on juonut: " + mattiV.annaArvo());
  }
}

Suoritetaan ohjelma ja todetaan sen toimivan oikein: Matti L. juo yhden kahvittelun yhteydessä kaksi kuppia kahvia ja Matti V. yhden.

Matti L. juo kahvia..
..juotu.

Matti V. juo kahvia..
..juotu.

Matti L. on juonut: 2
Matti V. on juonut: 1	

Muokataan esimerkkiä siten, että Matti V. maksattaa kahvinsa Matti L:n laskurilla. Ennen kasvattamista Matti V. vaihtaa Laskuri-olionsa viittaamaan Matti L:n laskuriin.

    mattiV = mattiL;
    kahvikassa.kasvata(mattiV);

Nyt ohjelma kasvattaa oikean nimistä laskuria (mattiV), mutta tämä muuttuja viittaakin Matti L:n Laskuri-olioon.

public class Kahvikassa {

  public static void main(String[] komentoriviParametrit) {

    Laskuri mattiL = new Laskuri(0);
    Laskuri mattiV = new Laskuri(0);

    Kasvattaja kahvikassa = new Kasvattaja();

    System.out.println("Matti L. juo kahvia..");
    kahvikassa.kasvataKahdesti(mattiL);
    System.out.println("..juotu.\n");

    System.out.println("Matti V. juo kahvia..");
    mattiV = mattiL;
    kahvikassa.kasvata(mattiV);
    System.out.println("..juotu.\n");

    System.out.println("Matti L. on juonut: " + mattiL.annaArvo());
    System.out.println("Matti V. on juonut: " + mattiV.annaArvo());
  }
}

Valitettavasti ohjelman tulosteessa Matti V. jää kiinni.

Matti L. juo kahvia..
..juotu.

Matti V. juo kahvia..
..juotu.

Matti L. on juonut: 3
Matti V. on juonut: 3

Arvot omat samat koska mattiV ja mattiL viittaavat nyt samaan olioon (mattiV = mattiL).

Muokataan esimerkkiä siten, että Matti V. ei jää kiinni. Otetaan käyttöön uusi Laskuri muuttuja piilo, joka saa arvokseen viitteen olioon mattiV. Tämän jälkeen vaihdetaan mattiV:n viite ja kasvatetaan arvoa kuten yllä. Kasvatuksen jälkeen palautetaan viite oikeaan mattiV olioon muuttujasta piilo.

    Laskuri piilo = mattiV;
    mattiV = mattiL;
    kahvikassa.kasvata(mattiV);
    mattiV = piilo;

Ohjelma kokonaisuudessaan näyttää nyt seuraavalta.

public class Kahvikassa {

  public static void main(String[] komentoriviParametrit) {

    Laskuri mattiL = new Laskuri(0);
    Laskuri mattiV = new Laskuri(0);

    Kasvattaja kahvikassa = new Kasvattaja();

    System.out.println("Matti L. juo kahvia..");
    kahvikassa.kasvataKahdesti(mattiL);
    System.out.println("..juotu.\n");

    System.out.println("Matti V. juo kahvia..");

    Laskuri piilo = mattiV;
    mattiV = mattiL;
    kahvikassa.kasvata(mattiV);
    mattiV = piilo;

    System.out.println("..juotu.\n");

    System.out.println("Matti L. on juonut: " + mattiL.annaArvo());
    System.out.println("Matti V. on juonut: " + mattiV.annaArvo());
  }
}

Suoritetaan ohjelma.

Matti L. juo kahvia..
..juotu.

Matti V. juo kahvia..
..juotu.

Matti L. on juonut: 3
Matti V. on juonut: 0

Nyt Matti L. on onnistuneesti maksattanut kahvinsa Matti L:llä (vaikka kasvatus tapahtuu komennolla kahvikassa.kasvata(mattiV)).

Olioiden yksityiset metodit

Luokassa Kasvattaja toistetaan laskurin kasvatusta metodeissa kasvata() ja kasvataKahdesti(). Muutetaan vielä kasvatuslogiikka olion omaksi sisäiseksi toiminnoksi. Olion sisäiseen käyttöön tarkoitettu metodi esitellään ilmaisulla private.

private void kasvataKertaa(laskuri, int kertoja) {
  for (int i = 0; i < kertoja; i++) {
    laskuri.kasvataArvoa();
  }	
}

Muutetaan metodit käyttämään sisäistä yksityistä kasvataKertaa(Laskuri, int)-metodia. Koko luokan toteutus näyttää seuraavalta.

public class Kasvattaja {
    public void kasvata(Laskuri laskuri) {
        this.kasvataKertaa(laskuri, 1);
    }

    public void kasvataKahdesti(Laskuri laskuri) {
        this.kasvataKertaa(laskuri, 2);
    }

    private void kasvataKertaa(Laskuri laskuri, int kertoja) {
        for (int i = 0; i < kertoja; i++) {
            laskuri.kasvataArvoa();
        }
    }
}

Nyt luokalla Kasvattaja on kaksi julkista metodia: kasvataKahdesti() ja kasvataViidesti(), jotka jakavat sisäisen toteutuksensa. Sisäisen toteutuksen kutsuminen olion ilmentymän ulkopuolelta ei toimi.

Viite olioon attribuuttina

Tehdään toinen toteutus luokasta Kasvattaja, jonka konstruktori ottaa parametriksi olion, joka on tyyppiä Laskuri. Parametrissä kasvatettava on siis viite olioon, joka on luotu jo aiemmin. Tämä viite asetetaan myös olion yksityiseen attribuuttiin this.kasvatettava.

public class Kasvattaja {
  private Laskuri kasvatettava;

  public Kasvattaja(Laskuri kasvatettava) {
    this.kasvatettava = kasvatettava;
  }

  public kasvataKahdesti() {
    this.kasvatettava.kasvataArvoa();
    this.kasvatettava.kasvataArvoa();
  }
}

Teemme taas toisenlaisen toteutuksen kahvikassasta.

public class Kahvikassa {

  public static void main(String[] komentoriviParametrit) {
	
    Laskuri mattiL = new Laskuri(0);
	
    Kasvattaja kahvikassa = new Kasvattaja(mattiL);
	
    kahvikassa.kasvataKahdesti();

    System.out.println("Matti L. on juonut: " + mattiL.annaArvo());
  }
}

Olion kahvikassa attribuuttina on nyt siis laskuri-olio. Kun kahvikassan metodia kasvataKahdesti kutsutaan, kasvattaa kahvikassa attribuuttinaan olevan laskurin arvoa. Yhden kahvikassan avulla voidaan nyt kasvattaa vain sen laskuri-olion arvoa, jonka viite on kahvikassan attribuuttina.

Jos on tarvetta kasvattaa useiden laskureiden arvoa, on jokaisella laskurilla oltava oma Kasvattajansa:

public class Kahvikassa {

  public static void main(String[] komentoriviParametrit) {
    Laskuri mattiL = new Laskuri(0);
	Laskuri mattiV = new Laskuri(0);
	
    Kasvattaja kahvikassaMattiL = new Kasvattaja(mattiL);
    Kasvattaja kahvikassaMattiV = new Kasvattaja(mattiV);
	
    kahvikassaMattiL.kasvataKahdesti();

    kahvikassaMattiV.kasvataKahdesti();
    kahvikassaMattiV.kasvataKahdesti();
	
    System.out.println("Matti L. on juonut: " + mattiL.annaArvo());
    System.out.println("Matti V. on juonut: " + mattiV.annaArvo());
  }
}

Kuormittaminen

Samanniminen metodi voidaan määritellä useammin siten, että parametrit poikkeavat aikaisemmista määrittelyistä. Tätä kutsutaan metodin kuormittamiseksi.

Kuorimitetaan luokan Laskuri konstruktoria siten, että se hyväksyy olion luontikutsun myös ilman alkuarvoa. Tällöin luokasta voidaan luoda olioita myös ilman arvoparametria.

public class Laskuri {
  private int arvo;

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

  public Laskuri() { // Konstruktori
    this.arvo = 0;
  }
  
  public void kasvataArvoa() {  
    this.arvo = this.arvo + 1;
  }

  public int annaArvo() {
    return this.arvo;
  }

  public String toString() {
    return "Arvoni on: " + this.annaArvo();	
  }

Nyt voimme luoda uuden olion kahdella eri tavalla.

Laskuri laskuri = new Laskuri(5);         // Laskurin sisäinen yksityinen kokonaisluku on 5
Laskuri oletuslaskuri = new Laskuri();    // Laskurin sisäinen yksityinen kokonaisluku on 0 

Kuormitetaan vielä kasvataArvoa()-metodia.

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

public void kasvataArvoa(int maara) {  
  this.arvo = this.arvo + maara;
}

Nyt luokan Laskuri oliolla on kaksi samannimistä metodia: kasvataArvoa() ja kasvataArvoa(int).

Laskuri kahvit = new Laskuri(0);
kahvit.kasvataArvoa();
kahvit.kasvataArvoa(2);

System.out.println(kahvit);   // 3;

Nimeäminen

Tarkennetaan lisää nimeämiskäytäntöjä.

  1. Luokan nimi kirjoitetaan aina isolla alkukirjaimella.
  2. Muuttuja (ja täten myös luokan ilmentymä) kirjoitetaan pienellä alkukirjaimella ja camelCasella.

    Esimerkkejä:

    public class Laskuri {
    
    Laskuri laskin = new Laskuri(0);
    
    Laskuri[] laskuriTaulukko = new Laskuri[3];
    
  3. Kaikki metodien nimet kirjoitetaan pienellä alkukirjaimella.

    Esimerkkjeä:

    laskuri.kasvataArvoa();
    
    public boolean onkoTaulukossa(int haettava) {
    

Määre static

Olemme nähneet metodien määrittelyssä sanan static. Määre static kertoo liittyykö metodi (tai muuttuja) luokkaan vai olioon. Luokkakohtaiset, eli static-määreen saavat metodit eivät muokkaa olioiden tilaa, vaan liittyvät esimerkiksi pääohjelman suoritukseen. Oliokohtaiset, eli metodit jotka eivät saa static-määrettä, liittyvät olioihin ja muokkaavat niiden sisäistä tilaa.

Luokkakohtaiset metodit

Katsotaan tuttua Tervehtija-ohjelmaa. Ohjelma pyytää käyttäjältä nimeä, lukee näppäimistöltä syötteen, ja kutsuu lopuksi staattista metodia tervehdi.

import java.util.Scanner;

public class Tervehtija {
  public static Scanner lukija = new Scanner(System.in);
  
  public static void main(String[] komentoriviParametrit) {
    String nimi;
    
    System.out.println("Anna nimi niin tervehdin!");
    nimi = lukija.nextLine();
    tervehdi(nimi);
  }
  
  public static void tervehdi(String tervehdittava) {
    System.out.println("Tervehdys, " + tervehdittava);
  }
}

Sekä metodi main, että metodi tervehdi ovat määritelty staattisiksi, eli luokkakohtaisiksi. Luokkakohtaiset metodit eivät siis liity mihinkään olioon. Luokkakohtaiset metodit toimivat vaikka luokasta ei olisi luotu yhtään ilmentymää, eli oliota.

Oliokohtaiset metodit

Oliokohtaiset metodit liittyvät olioihin ja voivat muokata olion sisäistä tilaa. Oliokohtaiset metodit eivät saa määrettä static. Katsotaan tuttua Laskuri-olioiden rakennuspiirrustukset määrittelevää luokkaa Laskuri.

public class Laskuri {
  private int arvo;

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

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

  public int annaArvo() {
    return this.arvo;
  }
}

Luokka Laskuri määrittelee Laskuri-tyyppisten olioiden rakenteen ja metodit, joilla Laskuri-olioihin liittyvien attribuuttien arvoja voi muuttaa. Luokan Laskuri metodit ovat oliokohtaisia, ja niissä ei ole static-määrettä.

Tyhjä viite - null

Ilmaisu null tarkoittaa, että viitettä ei ole asetettu. Voidaan myös ajatella, että viite viittaa erikoisarvoon null.

Seuraavassa esimerkissä kerromme Javalle, että meillä on kahvikassa niminen muuttuja, joka on tyyppiä Laskuri. Alussa muuttuja viittaa arvoon null. Sitten asetamme muuttujaan kahvikassa viitteen olioon mattiL ja kasvatamme laskurin arvoa. Asetamme hetkeksi viitteen arvoon null ja palautamme viiteen takaisin olioon mattiL.

Laskuri kahvikassa = null;
Laskuri mattiL = new Laskuri(0);

kahvikassa = mattiL;
kahvikassa.kasvataArvoa();

kahvikassa = null;
// kahvikassa.kasvataArvoa(); ei toimi, koska kahvikassa ei viittaa laskuriin vaan arvoon null

kahvikassa = mattiL;
System.out.println(kahvikassa);		// Arvo on 1

Jos ylläolevassa esimerkissä kommenteissa olevaa kahvikassa.kasvataArvoa()-lausetta yritettäisiin suorittaa saataisiin virhe NullPointerException, joka kertoo viitteen tyhjyydestä. Lauseke yrittää suorittaa kasvataArvoa() metodia muuttujan kahvikassa viittaamalle oliolle. Viite on null eli ei viitata mihinkään olioon, jolloin saadaan poikkeus (eli ajonaikainen virhe).

Viitteen olemassaolon tarkistaminen

Voimme käyttää null-ilmaisua tarkistamaan viitteen olemassaoloa, eli viittaako viite johonkin olioon.

Viitteeseen null-verrattaessa tarkistamme viittaako muuttuja mihinkään.

Laskuri kahvikassa = null;
if(kahvikassa == null) {
  System.out.println("Kahvikassaa ei ole vielä asetettu!");
} 
else {
  System.out.println("Kahvikassa on asetettu!");
}

kahvikassa = new Laskuri(0);
if(kahvikassa == null) {
  System.out.println("Kahvikassaa ei ole vielä asetettu!");
} 
else {
  System.out.println("Kahvikassa on asetettu!");
}

Ylläoleva esimerkki tuottaa seuraavanlaisen tulosteen.

Kahvikassaa ei ole vielä asetettu!
Kahvikassa on asetettu!

Luokan viiteattribuutit

Luokan viiteattribuutit saavat konstruktorissa tyhjän viitteen (null) jos niihin ei aseteta arvoa. Tällöin ne eivät siis viittaa mihinkään olioon. Esimerkiksi seuraava luokka Ihminen saa attribuuttinsa nimi arvoksi null seuraavan esimerkin konstruktorissa.

public class Ihminen {
  private String nimi;
  
  public Ihminen() {
    // attribuutille nimi ei aseteta arvoa, jolloin se saa arvokseen tyhjän viitteen (null)
  }
  
  public String annaNimi() {
    return nimi; // palauttaa tyhjän viitteen, eli null-arvon
  }
}

String

Luokka String on Javan mukana tuleva luokka. Java luo String-tyyppisen olion automaattisesti asetusoperaatiolla String hei = "Hei Maailma!". Varsinainen sisäinen toteutus on tekstin kirjainten pituinen taulukko, joka sisältää char-, eli aakkosalkeistyyppejä.

Alkeistyyppi char

Alkeistyyppi char kuvaa yhtä merkkiä. Jokainen merkki on talletettu kokonaislukuna. Komento System.out.println() osaa tulostaa yksittäisen char merkin. Mutta jos suoritamme alkeistyypeille laskuoperaatioita, käyttäytyvät ne kuin kokonaisluvut.

char a = 'A';
char b = 'B';

// Tulostetaan: ABBA
System.out.print(a);
System.out.print(b);
System.out.print(b);
System.out.print(a);


System.out.println(a+b+b+a);	// Tulostetaan: 262

int aPlusNolla = a+0;

System.out.println(aPlusNolla);		// Tulostetaan: 65

System.out.println(b+0);		// Tulostetaan: 66

System.out.println(a-b);		// Tulostetaan: -1

Stringin luonti

String-luokan ilmentymä voidaan luoda kahdella tavalla. Joko entuudestaan tutusti asettamalla tai luomalla uusi olio, joka saa sisältönsä konstruktorin parametrinä. Seuraavassa esimerkissä luodaan kaksi String-oliota kahdella eri tavalla. Kummatkin toimivat samalla tavalla: String muuttujaan talletetaan vain viite luotuun olioon.

String tekstimuuttuja = null;

// Sama asia, jälkimmäinen tutumpi tapa on oikotie ensimmäiseen.

tekstimuuttuja = new String("Hei StringMaailma");
tekstimuuttuja = "Uusi olio, olion tekstimuuttujan viite vaihtoon!";

Seuraavassa esimerkissä ensimmäisellä rivillä luodaan kolme String-luokan ilmentymää (oliota). Yksittäisistä String-oliosta luodaan yksi uusi olio lause. Toisella rivillä String-luokan ilmentymiä on enää yksi (olio4), sillä muut häviävät, koska niihin ei ole viitettä. Viimeisellä rivillä viite hävitetään, jolloin ilmentymiä ei ole enää olemassa.

//              olio1         olio2                  olio3
String lause = "Nimeni on" + "Arto ja olen " + 19 + " vuotta vanha";

//                 olio4
System.out.println(lause);

lause = null;

Metodeja

Luokalla String on valmiita julkisia metodeja. Metodit on listattu kokonaisuudessan Javan API-kuvauksen String-osiossa. Arto Wiklan materiaalissa metodeja esitellään muutama lisää.

Kahden Stringin vertailu: equals

Kahden String-tyyppisen muuttujan yhtäsuuruutta ei voi verrata kahdella yhtäsuuruusmerkillä (==) koska ne ovat viitetyyppisiä. Vertailu ei vertaa olioiden sisältöjen samanlaisuutta, vaan viitteitä. Alla olevassa esimerkissä vertaillaan kahden samanlaisen tekstin sisältävien String-olioiden viitteitä

String eka = new String("Matti V.");
String toka = new String("Matti V.");
  
if(eka == toka) {
  System.out.println("Eka ei ole sama kuin toka");
}
else {
  System.out.println("Eka on sama kuin toka");
}

Vertailun tuloksena saadaan tuloste Eka ei ole sama kuin toka. sillä emme vertaa merkkijonojen sisältöä vaan niiden viitteitä.

Merkkijonoja vertailtaessa pitää käydä kahden String-olion sisällöt läpi. Vertailua ei onneksi tarvitse tehdä itse, vaan siihen voi käyttää String-luokan valmista metodia equals(String).

String tervehdys = "Hei maailma!";

if (tervehdys.equals("Hei maailma!"))
  System.out.println("Sisältö on sama.");

Yllä oleva esimerkki antaa tulosteen Sisältö on sama.

Vertailu riippumatta kirjoitusasusta

Useassa ohjelmassa tarvitaan myös vertailua riippumatta isoista ja pienistä kirjaimista. Tähänkin löytyy valmiiksi tehty metodi, equalsIgnoreCase(String anotherString).


String vastaus = lukija.nextLine();  // Käyttäjä syöttää "KYllä";

if (vastaus.equalsIgnoreCase("kyllä"))
  System.out.println("Käyttäjä vastasi kyllä");
	

Järjestyksen vertailu: compareTo

Emme voi myöskään verrata onko ensimmäinen String-muuttuja toisen String-muuttujan jälkeen aakkostossa suurempi tai yhtäsuuri kuin (>=) operaatiolla.

String-luokka tarjoaa valmiin metodin compareTo(String), jota voi käyttää järjestyksen vertailuun. Jos compareTo-metodi palauttaa arvon, joka on suurempi kuin 0, tämä merkkijono-olio tulee aakkostossa verrattavan jälkeen. Jos arvo on pienempi kuin 0, tulee on tämä merkkijono-olio ennen verrattavaa.

String mattiV = "Matti V.";
String mattiP = "Matti P.";

if (mattiV.compareTo(mattiP) > 0) {
  System.out.println(mattiV + " tulee " + mattiP + ":n jälkeen.");
} 
else if (mattiV.compareTo(mattiP) == 0) {
  System.out.println(mattiV + " ja " + mattiP + " sisältävät saman merkkijonon.");
}
else { // mattiV.compareTo(mattiP) < 0
  System.out.println(mattiV + " tulee ennen " + mattiP + ":tä.");
}  

Yllä oleva ohjelma palauttaisi seuraavan tulosteen

Matti V. tulee Matti P.:n jälkeen.

"Matti V." on siis aakkostossa "Matti P.":n jälkeen.

Taulukko

Javassa myös taulukko on olio. Taulukko on aina jotain tyyppiä, eikä sen tyyppiä voi vaihtaa. Taulukkotyyppi esitellään muodossa tyyppi[] muuttuja, joka kertoo, että muuttujaan voidaan asettaa viite taulukko-olioon.

Taulukko-oliot, kuten kaikki oliot, luodaan komennon new-avulla. Aiemmin opittu int[] luvut = {3, 5, 2, 11, 5}; on Javan kehittäjien suunnittelema oikotie taulukon luontiin ja arvojen asettamiseen.

Alkeistyypit taulukossa

Kokonaislukuja sisältävä taulukko-olio luodaan seuraavasti:

// luodaan taulukko jolla valmis sisältö
int[] luvut1 = {1, 2, 3, 4, 5};

// luodaan 5:n kokoinen taulukko:
int[] luvut2 = new int[5]; 

// kysytään käyttäjältä luotavan taulukon koko
int koko = lukija.nextInt();

int[] luvut3 = new int[koko];

Seuraavassa esimerkissä esittelemme muuttujan lokerikko. Luomme uuden kokonaislukutyyppisen taulukko-olion nimeltä lokerikko, jonka pituus on kolme. Taulukko voi sisältää vain ja ainoastaan int-tyyppisiä muuttujia, eli kokonaislukuja. Alkeistyyppiä int oleva taulukko alustaa alkioden arvoksi nolla.


int[] lokerikko;

lokerikko = new int[3];		// Taulukon pituus on kolme ja kaikki arvot ovat 0;

lokerikko[0] = 100;
lokerikko[1] = 101;
lokerikko[2] = 102;

Viitetyyppit taulukossa

Teemme uuden Laskuri-taulukon, jonka jokainen alkio on viite Laskuri-olioon. Luonnin jälkeen kaikki paikat viittaat arvoon null. Luomme kolme laskuria ja asetamme ne taulukkoon.

Laskuri[] kahviLaskurit;

kahviLaskurit = new Laskuri[3];		// Kaikki kolme paikkaa viittaavat arvoon null

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

kahviLaskurit[0] = mattiL;
kahviLaskurit[1] = mattiV;
kahviLaskurit[2] = mattiP;

Voimme kasvattaa laskuria mattiL kahdesti kutsumalla kasvataArvoa()-metodia oliolta ja taulukon indeksistä 0, sillä molemmat viittaavat samaan olioon.

kahviLaskurit[0] = mattiL;

mattiL.kasvataArvoa();
kahviLaskurit[0].kasvataArvoa();

System.out.println(mattiL);			// Arvo on: 2
System.out.println(kahviLaskurit[0]);		// Arvo on: 2

Taulukko metodin parametrina

Taulukko on olio. Kun taulukko annetaan parametrina metodille saa metodi parametrikseen taulukon viitteen. Kaikki muutokset jotka taulukon sisältöön tehdään metodin sisällä säilyvät taulukossa myös metodin suorituksen jälkeen.

Esimerkiksi seuraava PienennysEsimerkki, jossa int-tyyppisen taulukon alkioiden arvoa vähennetään yhdellä.

public class PienennysEsimerkki {  
  public static void main(String[] args) {
    int[] luvut = {3, 5, 2, 11, 5};
    tulosta(luvut);
    pienennaYhdella(luvut);
    tulosta(luvut);
  }
  
  private static void pienennaYhdella(int[] taulukko) {
    for(int indeksi = 0; indeksi < taulukko.length; indeksi++) {
      taulukko[indeksi]--;
    }    
  }
  
  private static void tulosta(int[] taulukko) {
    for(int numero: taulukko) {
      System.out.print(numero + " ");
    }
    System.out.println();
  }  
}

Ohjelman tulostus on seuraavanlainen

3 5 2 11 5 
2 4 1 10 4

Staattinen, eli luokkakohtainen metodi pienennaYhdella saa siis parametrikseen viitteen kokonaislukutaulukkoon, ja vähentää jokaisen taulukon alkion arvoa yhdellä.

Taulukko metodin paluuarvona

Taulukon voi palauttaa metodin paluuarvona aivan kuten muunkintyyppisen olion. Katsotaan kahta erilaista esimerkkiä.

Uuden taulukon luominen metodissa

Metodi luoUusiTaulukko luo uuden kokonaislukutaulukko-olion, ja palauttaa viitteen siihen. Jos metodia kutsuttaessa viite asetetaan int[] - tyyppiseen muuttujaan, voidaan taulukkoon viitata muuttujan avulla.

public static int[] luoUusiTaulukko() {
  int[] luvut = {3, 5, 2, 11, 5};
  return luvut;
}

Metodin luoUusiTaulukko kutsuminen ja paluuarvon asettaminen.

int[] arvot = luoUusiTaulukko(); 
// arvot-olio viittaa nyt luoUusiTaulukko - metodissa luotuun taulukko-olioon 

Taulukon kopiointi metodissa

Metodi luoKopioAnnetustaTaulukosta saa parametrikseen viitteen taulukko-olioon, luo uuden (samankokoisen) taulukko-olion ja kopioi vanhan taulukon sisällön uuteen. Koska int[]-tyyppinen taulukko sisältää alkeistyyppisiä (int) arvoja, uusi taulukon tulee olemaan kopio vanhasta. Jos taulukon sisältö olisi viitetyyppisiä muuttujia, vain viitteet kopioituisivat.

public static int[] luoKopioAnnetustaTaulukosta(int[] taulukko) {
  int[] luvut = new int[taulukko.length];
  for(int i = 0; i < taulukko.length; i++) {
    luvut[i] = taulukko[i];
  }
  return luvut;
}

Metodin luoKopioAnnetustaTaulukosta kutsuminen ja paluuarvon asettaminen.

int[] alkup = {1,2,3,4,5};

int[] kopio = luoKopioAnnetustaTaulukosta(alkup); 
// kopio-olio sisältää nyt samat luvut kuin taulukko alkup, kyseessä on kuitenkin kaksi eri taulukkoa
kopio[1] = 99;
// taulukko alkup ei muutu

Järjestäminen

Taulukkojen läpikäyntiä helpottaa usein jos taulukko on järjestyksessä. Jos puhelinluettelon nimet eivät olisi aakkosjärjestyksessä, olisi tietyn nimen hakeminen hyvin vaikeaa. Ainut tapa tietyn henkilön puhelinnumeron löytämiseen olisi tällöin puhelinluettelon läpikäynti nimi nimeltä.

Tutustutaan järjestämisalgoritmiin nimeltä vaihtojärjestäminen.

Vaihtojärjestämisen ideana on käydä taulukko alusta loppuun läpi taulukon pienintä alkiota etsien. Kun pienin alkio löytyy, vaihdetaan sen paikkaa ensimmäisen alkion kanssa. Seuraavaksi käydään taulukko läpi toisesta alkiosta lähtien pienintä alkiota etsien. Kun pienin alkio löydetään, vaihdetaan sen paikkaa toisen alkion kanssa. Sama jatkuu kunnes lähtöalkiona on viimeisessä indeksissä oleva alkio, ja taulukko on järjestyksessä.

Vaihtojärjestämisesimerkki

Järjestetään seuraava neljän alkion kokoinen taulukko. Lähdetään etenemään indeksistä nolla taulukon loppuun etsien pienintä alkiota.

   
i: 0  1    2   3
   7, 12, -4, -2

Pienin alkio löytyy indeksistä 2, vaihdetaan indeksien 0 ja 2 sisältö. Lähdetään etenemään indeksistä yksi eteenpäin taulukon loppuun etsien pienintä alkiota.

       
i:  0  1   2   3
   -4, 12, 7, -2

Pienin alkio löytyy indeksistä 3, vaihdetaan indeksien 1 ja 3 sisältö. Lähdetään etenemään indeksistä kaksi eteenpäin etsien pienintä alkiota.

            
i:  0   1   2   3
   -4, -2,  7, 12

Pienin alkio löytyi indeksistä kaksi, ei vaihdeta alkioiden sisältöä. Kolmannessa indeksissä on taulukon viimeinen alkio, joten taulukko on järjestyksessä.

           
                
i:  0   1   2   3
   -4, -2,  7, 12

Tutustumme laskuharjoituksissa vaihtojärjestämisen toteuttamiseen käytännössä.

Etsiminen

Olemme jo tehneet harjoituksissa peräkkäishaku-periaatteella toimivan etsimisalgoritmin. Peräkkäishaku käy taulukon alkiot yksi kerrallaan läpi, verraten haettavaa alkiota aina vuorossa olevaan alkioon. Tutustutaan vielä toisenlaiseen hakualgoritmiin nimeltä binäärihaku.

Puolitus- eli binäärihaun ideana on tutkia järjestetyn taulukon keskimmäistä alkiota. Jos keskimmäinen alkio on haettu alkio, palautetaan se ja lopetetaan haku. Jos haettavan alkion arvo on pienempi kuin keskimmäisen alkion, tutkitaan taulukon vasempaa puoliskoa. Jos taas haettava alkio on suurempi kuin keskimmäinen alkio, tutkitaan oikeaa puolta. Taulukon puolittamista jatketaan siihen asti kunnes löydetään haettava alkio, tai huomataan ettei haettavaa alkiota ole olemassa.

Binäärihakuesimerkki

Etsitään arvoa 113 seuraavasta 16 alkiota sisältävästä kokonaislukutaulukosta. Etsiessämme lähdemme taulun keskimmäisestä alkiosta liikkeelle.

                                     
i: 0  1  2   3   4    5    6    7    8    9   10    11    12    13    14    15
   1, 5, 7, 11, 29, 113, 114, 115, 231, 511, 612, 1312, 1322, 1334, 1434, 2000

Vertaamme haettua lukua 113 arvoon 231 ja huomaamme että haettu arvo on pienempi. Lähdemme siis vasemmalle. Tässä vaiheessa voimme unohtaa kaikkien keskikohdasta oikealle olevien alkioiden olemassaolon. Tutkitaan taas jäljellä olevan taulukon keskimmäistä alkiota.

             
i: 0  1  2   3   4    5    6    7    8    9   10    11    12    13    14    15                 
   1, 5, 7, 11, 29, 113, 114, 115, 231, 511, 612, 1312, 1322, 1334, 1434, 2000

Vertaamme haettua lukua 113 arvoon 11 ja huomaamme että haettu arvo on suurempi. Lähdemme siis oikealle. Voimme myös unohtaa viime keskikohdan ja kaikki sen vasemmalla puolella olevat alkiot. Tutkitaan taas jäljellä olevan taulukon keskimmäistä alkiota.

                      
i: 0  1  2   3   4    5    6    7    8    9   10    11    12    13    14    15
   1, 5, 7, 11, 29, 113, 114, 115, 231, 511, 612, 1312, 1322, 1334, 1434, 2000

Verratessamme haettua lukua 113 jäljelläolevan taulukon keskimmäiseen alkioon huomaamme sen olevan haettava arvomme. Löysimme siis haetun arvomme!

Binäärihaku on erittäin tehokas tapa löytää alkioita. Kaksi miljoonaa alkiota sisältävästä kokonaislukutaulukosta peräkkäishaulla etsien joutuu tekemään pahimmassa tapauksessa kaksi miljoonaa vertailua. Binäärihaulla taas löytäisi kahdesta miljoonasta alkiosta halutun alkion noin 21 vertailulla, sillä kaksi miljoonaa alkiota sisältävän taulukon voi jakaa kahteen osaan, eli puolittaa, vain 21 kertaa.