Arto Vihavainen, Matti Paksula, Matti Luukkainen
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.
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.
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.
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ö, 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öä.
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ä.
Projektin ollessa valittuna paina F6 (tai ylhäällä näkyvää play-nuolta).
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".
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
.
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ä.
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.
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; */ } }
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); }
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; }
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); } }
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.
Esittelemme tässä kappaleessa lyhyesti tällä viikolla tarvittavat kielioppisäännöt.
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ä";
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);
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
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 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ä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.
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 ""; } }
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; }
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-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.
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 // kutsuluku++
tarkoittaa samaa kuinluku += 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); } } }
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.
{
while, for, if, else
}
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
if (luku == 5) {
if (luku == 5){
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.
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 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.
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..
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
.
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.
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.
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
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; } }
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.
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.
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()
.
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.
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.
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"
Tarkennamme nyt hieman käyttämiämme muuttujien tyyppejä. Javassa muuttujat jaetaan alkeistyyppeihin ja viittaustyyppeihin.
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;
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.
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)
).
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.
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()); } }
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;
Tarkennetaan lisää nimeämiskäytäntöjä.
Esimerkkejä:
public class Laskuri {
Laskuri laskin = new Laskuri(0);
Laskuri[] laskuriTaulukko = new Laskuri[3];
Esimerkkjeä:
laskuri.kasvataArvoa();
public boolean onkoTaulukossa(int haettava) {
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.
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 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ä.
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).
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 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() { // attribuutillenimi
ei aseteta arvoa, jolloin se saa arvokseen tyhjän viitteen (null
) } public String annaNimi() { return nimi; // palauttaa tyhjän viitteen, elinull
-arvon } }
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
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
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;
Luokalla String
on valmiita julkisia metodeja. Metodit on listattu kokonaisuudessan Javan API-kuvauksen String-osiossa. Arto Wiklan materiaalissa metodeja esitellään muutama lisää.
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.
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ä");
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.
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.
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;
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 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ä.
Taulukon voi palauttaa metodin paluuarvona aivan kuten muunkintyyppisen olion. Katsotaan kahta erilaista esimerkkiä.
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
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
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ä.
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ä.
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.
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.