Viikko 3

13 Lisää metodeista

Edellisellä viikolla opettelimme kirjoittamaan omia metodeja. Metodien avulla voimme jäsennellä ohjelmaa pienemmiksi hyvin nimetyiksi. selkeän tehtävän omaaviksi loogisiksi kokonaisuuksiksi. Tämä sekä helpottaa ongelmanratkaisua että parantaa ohjelman luettavuutta niin ohjelmoijan kuin ohjelmaa myöhemmin mahdollisesti ylläpitävänkin osalta. Tästä lähtien oikeastaan jokainen tekemämme ohjelma sisältää metodeita.

Jatketaan edelleen metodien parissa.

13.1 Metodit ja muuttujien näkyvyys

Yritetään muuttaa metodin sisältä pääohjelman muuttujan arvoa.

// pääohjelma
public static void main(String[] args) {
    int luku = 1;
    kasvataKolmella();
}

// metodi
public static void kasvataKolmella() {
    luku = luku + 3;
}

Ohjelma ei kuitenkaan toimi, sillä metodi ei näe pääohjelman muuttujaa luku.

Yleisemminkin voi todeta, että pääohjelman muuttujat eivät näy metodien sisään, ja metodin muuttujat eivät näy muille metodeille tai pääohjelmalle. Ainoa keino viedä metodille tietoa ulkopuolelta on parametrin avulla.

// pääohjelma
public static void main(String[] args) {
    int luku = 1;
    System.out.println("Pääohjelman muuttujan luku arvo: " + luku);
    kasvataKolmella(luku);
    System.out.println("Pääohjelman muuttujan luku arvo: " + luku);
}

// metodi
public static void kasvataKolmella(int luku) {
    System.out.println("Metodin parametrin luku arvo: " + luku);
    luku = luku + 3;
    System.out.println("Metodin parametrin luku arvo: " + luku);
}

Yllä metodilla kasvataKolmella on parametri luku. Parametri luku kopioidaan metodin käyttöön. Kun yllä oleva ohjelma suoritetaan, nähdään seuraavanlainen tulostus.

Pääohjelman muuttujan luku arvo: 1
Metodin parametrin luku arvo: 1
Metodin parametrin luku arvo: 4
Pääohjelman muuttujan luku arvo: 1

Parametrina annettu luku siis kopioitiin metodin käyttöön. Jotta saisimme luvun uuden arvon myös pääohjelmaan, tulee metodin palauttaa arvo.

13.2 Metodin paluuarvot

Metodi voi palauttaa arvon. Tähän mennessä kurssilla olleissa esimerkeissä ja tehtävissä metodit eivät palauttaneet mitään. Tämä on merkitty kirjoittamalla metodin ylimmälle riville heti nimen vasemmalle puolelle void.

public static void kasvataKolmella() {
...

Arvon palauttavaa metodia määriteltäessä täytyy määritellä myös palautettavan arvon tyyppi. Paluuarvon tyyppi merkitään metodin nimen vasemmalle puolelle. Seuraavassa metodi joka palauttaa aina kokonaisluvun 10, metodin tyyppi on int. Palautus tapahtuu komennolla return:

public static int palautetaanAinaKymppi() {
    return 10;
}

Jotta metodin palauttamaa arvoa voisi käyttää, tulee paluuarvo ottaa talteen muuttujaan:

public static void main(String[] args) {
    int luku = palautetaanAinaKymppi();

    System.out.println( "metodi palautti luvun " + luku );
}

Metodin paluuarvo sijoitetaan int-tyyppiseen muuttujaan aivan kuin mikä tahansa muukin int-arvo. Paluuarvo voi toimia myös osana mitä tahansa lauseketta:

double luku = 4 * palautetaanAinaKymppi() + (palautetaanAinaKymppi() / 2) - 8;

System.out.println( "laskutoimituksen tulos " + luku );

Kaikki muuttujatyypit, mitä olemme tähän mennessä nähneet, voidaan palauttaa metodista:

public static void metodiJokaEiPalautaMitaan() {
    // metodin runko
}

public static int metodiJokaPalauttaaKokonaisLuvun() {
    // metodin runko, tarvitsee return-komennon
}

public static String metodiJokaPalauttaaTekstin() {
    // metodin runko, tarvitsee return-komennon
}

public static double metodiJokaPalauttaaLiukuluvun() {
    // metodin runko, tarvitsee return-komennon
}

Jos metodille määritellään paluuarvo, on sen myös pakko palauttaa arvo, esimerkiksi seuraava metodi on virheellinen.

public static String virheellinenMetodi() {
    System.out.println("Väitän palauttavani merkkijonon, mutten palauta sitä.");
}

Seuraavassa esimerkissä määritellään metodi summan laskemiseen. Tämän jälkeen metodia käytetään laskemaan luvut 2 ja 7 yhteen. Metodikutsusta saatava paluuarvo asetetaan muuttujaan lukujenSumma.

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

Metodin kutsu:

int lukujenSumma = summa(2, 7);
// lukujenSumma on nyt 9

Laajennetaan edellistä esimerkkiä siten, että käyttäjä syöttää luvut.

public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);

    System.out.print("Anna ensimmäinen luku: ");
    int eka = Integer.parseInt( lukija.nextLine() );

    System.out.print("Anna toinen luku: ");
    int toka = Integer.parseInt( lukija.nextLine() );

    System.out.print("Luvut ovat yhteensä: " + summa(eka,toka) );
}

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

Kuten huomataan, metodin paluuarvoa ei tarvitse välttämättä sijoittaa muuttujaan, se voi olla osana tulostuslausetta aivan kuten mikä tahansa muukin int-arvo.

Huomaa, että metodin parametrien nimillä ja metodin kutsujan puolella määritellyillä muuttujan nimillä ei ole mitään tekemistä keskenään. Edellisessä esimerkissä sekä pääohjelman muuttujat että metodin parametrit olivat "sattumalta" nimetty samoin (eli eka ja toka). Seuraava toimisi aivan yhtä hyvin:

public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);

    System.out.print("Anna ensimmäinen luku: ");
    int luku1 = Integer.parseInt( lukija.nextLine() );

    System.out.print("Anna toinen luku: ");
    int luku2 = Integer.parseInt( lukija.nextLine() );

    System.out.print("Luvut ovat yhteensä: " + summa(luku1, luku2) );
}

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

Nyt pääohjelman muuttujan luku1 arvo kopioituu metodin parametrin eka arvoksi ja pääohjelman muuttujan luku2 arvo kopioituu metodin parametrin toka arvoksi.

Seuraavassa esimerkissä metodia summa kutsutaan kokonaisluvuilla, jotka saadaan summa-metodin paluuarvoina.

int eka = 3;
int toka = 2;

int monenLuvunSumma = summa(summa(1, 2), summa(eka, toka));
// 1) suoritetaan sisemmät metodit:
//    summa(1, 2) = 3   ja summa(eka, toka) = 5
// 2) suoritetaan ulompi metodi:
//    summa(3, 5) = 8 
// 3) muuttujan monenLuvunSumma arvoksi siis tulee 8

13.3 Metodin omat muuttujat

Seuraava metodi laskee syötteinään saamiensa lukujen keskiarvon. Metodi käyttää apumuuttujia summa ja ka. Metodin sisäisen muuttujan määrittely tapahtuu tutulla tavalla.

public static double keskiarvo(int luku1, int luku2, int luku3) {

    int summa = luku1 + luku2 + luku3;
    double ka = summa / 3.0;

    return ka;
}

Metodin kutsu voi tapahtua esim seuraavasti

public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);

    System.out.print("Anna ensimmäinen luku: ");
    int eka = Integer.parseInt( lukija.nextLine() );

    System.out.print("Anna toinen luku: ");
    int toka = Integer.parseInt( lukija.nextLine() );

    System.out.print("ja kolmas luku: ");
    int kolmas = Integer.parseInt( lukija.nextLine() );

    double keskiarvonTulos = keskiarvo(eka, toka, kolmas);

    System.out.print("Lukujen keskiarvo: " + keskiarvonTulos );
}

Huomaa että metodin sisäiset muuttujat summa ja ka eivät näy pääohjelmaan. Yksi yleinen aloittelijan virhe olisikin yrittää käyttää metodia seuraavasi:

public static void main(String[] args) {
    // annetaan arvot muuttujille eka, toka ja kolmas

    keskiarvo(eka, toka, kolmas);

    // yritetään käyttää metodin sisäistä muuttujaa, EI TOIMI!
    System.out.print("Lukujen keskiarvo: " + ka );
}

Myös seuraavanlaista virhettä näkee usein:

public static void main(String[] args) {
    // annetaan arvot muuttujille eka, toka ja kolmas

    keskiarvo(eka, toka, kolmas);

    // yritetään käyttää pelkkää metodin nimeä, EI TOIMI!
    System.out.print("Lukujen keskiarvo: " + keskiarvo );
}

Eli tässä yritettiin käyttää pelkkää metodin nimeä muuttujamaisesti. Toimiva tapa metodin tuloksen sijoittamisen apumuuttujaan lisäksi on suorittaa metodikutsu suoraan tulostuslauseen sisällä:

public static void main(String[] args) {
    int eka = 3;
    int toka = 8;
    int kolmas = 4; 

    // kutsutaan metodia tulostuslauseessa, TOIMII!
    System.out.print("Lukujen keskiarvo: " + keskiarvo(eka, toka, kolmas) );
}

Tässä siis ensin tapahtuu metodikutsu joka palauttaa arvon 5.0 joka sitten tulostetaan tulostuskomennon avulla.

Screencast aiheesta:

Tee metodi summa, joka laskee parametrina olevien lukujen summan.

Tee metodi seuraavaan runkoon:

public static int summa(int luku1, int luku2, int luku3, int luku4) {
    // kirjoita koodia tähän
    // muista että metodissa on oltava (lopussa) return!
}

public static void main(String[] args) {
    int vastaus = summa(4, 3, 6, 1);
    System.out.println("Summa: " + vastaus);
}

Ohjelman tulostus:

Summa: 14

Huom: kun tehtävässä sanotaan että metodin pitää palauttaa jotain, tarkoittaa tämä sitä että metodissa tulee olla määritelty paluutyyppi ja return-komento jolla haluttu asia palautetaan. Metodi ei itse tulosta (eli käytä komentoa System.out.println(..)), tulostuksen hoitaa metodin kutsuja, eli tässä tapauksessa pääohjelma.

Tee metodi pienin, joka palauttaa parametrina saamistaan luvuista pienemmän arvon. Jos lukujen arvo on sama, voidaan palauttaa kumpi tahansa luvuista.

public static int pienin(int luku1, int luku2) {
    // kirjoita koodia tähän
    // älä tulosta metodin sisällä mitään

    // lopussa oltava komento return
}

public static void main(String[] args) {
    int vastaus =  pienin(2, 7);
    System.out.println("Pienin: " + vastaus);
}

Ohjelman tulostus:

Pienin: 2

Tee metodi suurin, joka saa kolme lukua ja palauttaa niistä suurimman. Jos suurimpia arvoja on useita, riittää niistä jonkun palauttaminen. Tulostus tapahtuu pääohjelmassa.

public static int suurin(int luku1, int luku2, int luku3) {
    // kirjoita koodia tähän
}

public static void main(String[] args) {
    int vastaus =  suurin(2, 7, 3);
    System.out.println("Suurin: " + vastaus);
}

Ohjelman tulostus:

Suurin: 7

Tee metodi keskiarvo, joka laskee parametrina olevien lukujen keskiarvon. Metodin sisällä tulee käyttää apuna tehtävän 43 metodia summa!

Tee metodi seuraavaan runkoon:

public static int summa(int luku1, int luku2, int luku3, int luku4) {
    // kopioi koodi tehtävästä 43
}

public static double keskiarvo(int luku1, int luku2, int luku3, int luku4) {
    // kirjoita koodia tähän
    // laske alkioiden summa kutsumalla metodia summa
}

public static void main(String[] args) {
    double vastaus = keskiarvo(4, 3, 6, 1);
    System.out.println("Keskiarvo: " + vastaus);
}

Ohjelman tulostus:

Keskiarvo: 3.5

Muistathan miten kokonaisluku (int) muutetaan desimaaliluvuksi (double)!

14 Merkkijonot

Tässä luvussa tutustaan tarkemmin Javan merkkijonoihin, eli String:eihin. Olemme jo käyttäneet String-tyyppisiä muuttujia tulostuksen yhteydessä sekä oppineet vertailemaan merkkijonoja toisiinsa. Merkkijonoja vertailtiin toisiinsa kutsumalla merkkijonon equals()-metodia.

String elain = "Koira";

if( elain.equals("Koira") ) {
    System.out.println(elain + " sanoo vuh vuh");
} else if ( elain.equals("Kissa") ) {
    System.out.println(elain + " sanoo miau miau");
}

Merkkijonoilta voi kysyä niiden pituutta kirjoittamalla merkkijonon perään .length() eli kutsumalla merkkijonolle sen pituuden kertovaa metodia.

String banaani = "banaani";
String kurkku = "kurkku";
String yhdessa = banaani + kurkku;

System.out.println("Banaanin pituus on " + banaani.length());
System.out.println("Kurkku pituus on " + kurkku.length());
System.out.println("Sanan " + yhdessa + " pituus on " + yhdessa.length());

Edellä kutsutaan metodia length() kolmelle eri merkkijonolle. Kutsu banaani.length() kutsuu nimenomaan merkkijonon banaani pituuden kertovaa metodia, kun taas kurkku.length() on merkkijonon kurkku pituuden kertovan metodin kutsu, jne.Pisteen vasemman puoleinen osa kertoo kenen metodia kutsutaan.

Javassa on erillinen char-tietotyyppi kirjaimia varten. Yksittäiseen char-muuttujaan voi tallentaa yhden kirjaimen. Merkkijonolta voidaan kysyä sen kirjaimia kirjaimen paikan perusteella käyttämällä charAt()-metodia. Huomaa että laskeminen alkaa nollasta!

String kirja = "Kalavale";

char merkki = kirja.charAt(3);
System.out.println("Kirjan neljäs kirjain on " + merkki); //tulostaa "a"
  

Koska merkkijonon kirjaimet numeroidaan (eli teknisemmin ilmaistuna merkkijonoja indeksoidaan) alkaen paikasta 0, on merkkijonon viimeisen kirjaimen numero eli indeksi "merkkijonon pituus miinus yksi", eli kirja.charAt(kirja.length()-1). Esimerkiksi seuraava kaataa ohjelman: yritämme hakea kirjainta kohdasta jota ei ole olemassa.

char merkki = kirja.charAt(kirja.length());
  
NetBeans-vihje
  • Kaikki NetBeans-vihjeet löytyvät täältä täältä
  • Uudelleennimentä

    Muuttujat, metodit ja ensi viikolla opittavat luokat kannattaa nimetä kuvaavasti. Usein käy, että valittu nimi on hieman epäkuvaava ja tulee tarve nimen muuttamiselle. NetBeans:issa tämä on todella helppoa. Maalaa huono nimi jostain kohtaa koodiasi hiirellä. Paina (yhtäaikaa) ctrl ja r ja kirjoita muuttujalle/metodille uusi nimi.

Tee ohjelma, joka kysyy käyttäjän nimen ja ilmoittaa, kuinka monta kirjainta siinä on.

Anna nimi: Pekka
Kirjainmäärä: 5
Anna nimi: Katariina
Kirjainmäärä: 9

Huom! Rakenna ohjelmasi niin että laitat pituuden laskemisen omaan metodiinsa: public static int laskeKirjaimet(String merkkijono). Testit testaavat sekä metodia laskeKirjaimet että koko ohjelman toimintaa.

Tee ohjelma, joka kysyy käyttäjän nimen ja ilmoittaa nimen ensimmäisen kirjaimen.

Anna nimi: Pekka
Ensimmäinen kirjain: P
Anna nimi: Katariina
Ensimmäinen kirjain: K

Huom! Rakenna ohjelmasi niin että laitat ensimmäisen kirjaimen hakemisen omaan metodiinsa: public static char ensimmainenKirjain(String merkkijono). Testit testaavat sekä metodia ensimmainenKirjain että koko ohjelman toimintaa.

Tee ohjelma, joka kysyy käyttäjän nimen ja ilmoittaa nimen viimeisen kirjaimen.

Anna nimi: Pekka
Viimeinen kirjain: a
Anna nimi: Katariina
Viimeinen kirjain: a

Huom! Rakenna ohjelmasi niin että laitat viimeisen kirjaimen hakemisen omaan metodiinsa: public static char viimeinenKirjain(String merkkijono). Testit testaavat sekä metodia viimeinenKirjain että koko ohjelman toimintaa.

Tee ohjelma, joka kysyy käyttäjän nimen ja ilmoittaa sen kolme ensimmäistä kirjainta erikseen. Jos nimen pituus on alle kolme, ei ohjelma tulosta mitään. Tehtävässä ei edellytetä erillisten metodien luomista.

Anna nimi: Pekka
1. kirjain: P
2. kirjain: e
3. kirjain: k
Anna nimi: me

Huom: ole tässä ja seuraavassa tehtävässä erityisen tarkka tulostusasun suhteen! Tulostuksessa tulee olla yksi välilyönti sekä pisteen että kaksoispisteen jälkeen!

Tee ohjelma, joka kysyy käyttäjän nimen ja ilmoittaa sen kirjaimet erikseen. Tehtävässä ei edellytetä erillisen metodin luomista.

Anna nimi: Pekka
1. kirjain: P
2. kirjain: e
3. kirjain: k
4. kirjain: k
5. kirjain: a

Vihje: while-toistolauseesta on tässä apua!

Anna nimi: Katariina
1. kirjain: K
2. kirjain: a
3. kirjain: t
4. kirjain: a
5. kirjain: r
6. kirjain: i
7. kirjain: i
8. kirjain: n
9. kirjain: a

Tee ohjelma, joka kysyy käyttäjän nimen ja tulostaa sen väärinpäin. Erillistä metodia nimen kääntämiselle ei tarvitse tehdä.

Anna nimi: Pekka
Väärinpäin: akkeP
Anna nimi: Katariina
Väärinpäin: aniirataK

Vihje: Yksittäisen merkin saa tulostettua komennolla System.out.print()

14.1 Muita merkkijonojen metodeja

Merkkijonosta halutaan usein lukea jokin tietty osa. Tämä onnistuu mekkkijonojen eli String-luokan metodilla substring. Sitä voidaan käyttää kahdella tavalla: yksiparametrisenä palauttamaan merkkijonon loppuosa tai kaksiparametrisena palauttamaan parametrien valitsema osajono merkkijonosta:

String kirja = "Kalavale";

System.out.println(kirja.substring(4)); //tulostaa "vale"
System.out.println(kirja.substring(2,6)); //tulostaa "lava"

Koska substring-metodin paluuarvo on String-tyyppinen, voidaan metodin paluuarvo ottaa talteen String-tyyppiseen muuttujaan kirja..

String kirja = "8 veljestä";

String loppuosa = kirja.substring(2);
System.out.println("7 " + loppuosa); // tulostaa: 7 veljestä

String-luokan metodit tarjoavat myös mahdollisuuden etsiä tekstistä tiettyä sanaa. Esimerkiksi sana "erkki" sisältyy tekstiin "merkki". Metodi indexOf() etsii parametrinaan annettua sanaa merkkijonosta. Jos sana löytyi, se palauttaa sanan ensimmäisen kirjaimen indeksin, eli paikan (muista että paikkanumerointi alkaa nollasta!). Jos taas sanaa ei löytynyt merkkijonosta palautetaan arvo -1.

String sana = "merkkijono";

int indeksi = sana.indexOf("erkki"); //indeksin arvoksi tulee 1
System.out.println(sana.substring(indeksi)); //tulostetaan "erkkijono"

indeksi = sana.indexOf("jono"); //indeksin arvoksi tulee 6
System.out.println(sana.substring(indeksi)); //tulostetaan "jono"

indeksi = sana.indexOf("kirja"); //sana "kirja" ei sisälly sanaan "merkkijono"
System.out.println(indeksi); //tulostetaan -1
System.out.println(sana.substring(indeksi)); //virhe!

Screencast aiheesta:

Tee ohjelma, joka tulostaa sanan alkuosan. Ohjelma kysyy käyttäjältä sanan ja alkuosan pituuden. Käytä ohjelmassa metodia substring.

Anna sana: esimerkki
Alkuosan pituus: 4
Tulos: esim
Anna sana: esimerkki
Alkuosan pituus: 7
Tulos: esimerk

Tee ohjelma, joka tulostaa sanan loppuosan. Ohjelma kysyy käyttäjältä sanan ja loppuosan pituuden. Käytä ohjelmassa merkkijonon metodia substring.

Anna sana: esimerkki
Loppuosan pituus: 4
Tulos: rkki
Anna sana: esimerkki
Loppuosan pituus: 7
Tulos: imerkki

Tee ohjelma, joka kysyy käyttäjältä kaksi sanaa. Sitten ohjelma kertoo, onko toinen sana ensimmäisen sanan osana. Käytä ohjelmassa merkkijonon metodia indexOf.

Anna 1. sana: suppilovahvero
Anna 2. sana: ilo
Sana 'ilo' on sanan 'suppilovahvero' osana.
Anna 1. sana: suppilovahvero
Anna 2. sana: suru
Sana 'suru' ei ole sanan 'suppilovahvero' osana.

Huom: tulosta ohjelmassasi täsmälleen samassa muodossa kuin esimerkissä!

Tee metodi kaanna, joka kääntää annetun merkkijonon. Käytä metodille seuraavaa runkoa:

public static String kaanna(String merkkijono) {
    // kirjoita koodia tähän
}

public static void main(String[] args) {
    System.out.print("Anna merkkijono: ");
    String merkkijono = lukija.nextLine();
    System.out.println("Väärinpäin: " + kaanna(merkkijono));
}
    

Vihje: joudut todennäköisesti kokoamaan metodin sisällä käänteisen merkkijonon merkki kerrallaan. Kokoamisessa kannattaa käyttää apuna String-tyyppistä apumuuttujaa. Aluksi apumuuttujan arvo on tyhjä merkkijono. Tämän jälkeen merkkijonon perään laitetaan uusia merkkejä merkki kerrallaan.

String apu = "";

// ...
// lisätään merkki apu-nimisen muuttujan perään
apu = apu + merkki;

Ohjelman tulostus:

Anna merkkijono: esimerkki
ikkremise
    

15 Oliot ja metodit

Merkkijonot poikkeavat luonteeltaan hieman esim. kokonaisluvuista. Kokonaisluvut ovat "pelkkiä arvoja", niiden avulla voi laskea ja niitä voi tulostella ruudulle:

int x = 1;
int y = 3;

y = 3*x + 2;

System.out.println( "y:n arvo nyt: " + y );
  

Merkkijonot ovat hieman "älykkäämpiä" ja tietävät esimerkiksi pituutensa:

String sana1 = "Ohjelmointi";
String sana2 = "Java";

System.out.println( "merkkijonon "+ sana1 +" pituus: " + sana1.length() );

System.out.println( "merkkijonon "+ sana2 +" pituus: " + sana2.length() );
  

Tulostuu:

merkkijonon Ohjelmointi pituus on 11
merkkijonon Java pituus on 4

Pituus saadaan selville kutsumalla merkkijonon metodia length(). Merkkijonoilla on joukko muitakin metodeja. Kokonaisluvuilla eli int:eillä ei ole metodeja ollenkaan, ne eivät itsessään "osaa" mitään.

Merkkijonot ovat olioita eli "asioita joihin liittyy metodeja sekä arvo". Jatkossa tulemme näkemään hyvin paljon muitakin olioita kuin merkkijonoja.

Kuten edellisestä esimerkistä huomaamme, kutsutaan olion metodia lisäämällä olion nimen perään piste ja metodikutsu:

sana1.length()    // kutsutaan merkkijono-olion sana1 metodia length()
sana2.length()    // kutsutaan merkkijono-olion sana2 metodia length()

Metodikutsu kohdistuu nimenomaan sihen olioon, mille metodia kutsutaan. Yllä kutsumme ensin sana1-nimisen merkkijonon length()-metodia, sitten merkkijonon sana2 metodia length().

Vanha tuttumme lukija on myös olio:

Scanner lukija = new Scanner(System.in);

Lukijat ja merkkijonot ovat molemmat oliota, ne ovat kuitenkin varsin erilaisia. Lukijoilla on mm. metodi nextLine() jota merkkijonoilla ei ole. Javassa oliot "synnytetään" eli luodaan melkein aina komennolla new, merkkijonot muodostavat tässä suhteessa poikkeuksen! -- Merkkijonoja voi luoda kahdella tavalla:

String banaani = new String("Banaani");
String porkkana = "porkkana";

Kumpikin ylläolevista riveistä luo uuden merkkijono-olion. Merkkijonojen luonnissa new-komentoa käytetään hyvin harvoin.

Olion "tyyppiä" sanotaan luokaksi. Merkkijonojen luokka on String, lukijoiden luokka taas on Scanner. Opimme jatkossa luokista ja olioista paljon lisää.

16 ArrayList eli "oliosäiliö"

Ohjelmoidessa tulee usein vastaan tilanteita joissa haluaisimme pitää muistissa esimerkiksi useita erilaisia merkkijonoja. Todella huono tapa olisi määritellä jokaiselle oma muuttujansa:

String sana1;
String sana2;
String sana3;
// ...
String sana10;

Tämä olisi aivan kelvoton ratkaisu ja sen huonoutta ei kannata oikeastaan edes perustella -- ajattele ylläoleva esimerkki vaikkapa sadalla tai tuhannella sanalla.

Java, kuten kaikki modernit ohjelmointikielet, tarjoaa erilaisia apuvälineitä joiden avulla on helppo säilyttää ohjelmassa monia olioita. Tutustumme nyt Javan ehkä eniten käytettyyn oliosäiliöön ArrayList:iin.

Seuraava ohjelmanpätkä ottaa käyttöönsä merkkijono-olioita tallentavan ArrayList:in sekä tallettaa listalle pari merkkijonoa.

import java.util.ArrayList;

public class ListaOhjelma {

    public static void main(String[] args) {
        ArrayList<String> sanaLista = new ArrayList<String>();

        sanaLista.add("Ensimmäinen");
        sanaLista.add("Toinen");
    }
}

Yllä olevan pääohjelman ensimmäinen rivi luo sanaLista-nimisen merkkijonoja tallettavan arraylistin. Listamuuttujan tyypin nimi on ArrayList<String>, eli merkkijonoja tallettava ArrayList. Itse lista luodaan sanomalla new ArrayList<String>();.

Huom: Jotta ArrayList toimisi, on ohjelman ylälaitaan kirjoitettava import java.util.ArrayList; tai import java.util.*;

Kun lista on luotu, siihen lisätään kaksi merkkijonoa kutsumalla listan metodia add. Tila ei lopu listalla missään vaiheessa kesken, eli periaatteessa listalle saa lisätä niin monta merkkijonoa kun "koneen" muistiin mahtuu.

Sisäisesti ArrayList on nimensä mukaisesti lista. Lisätyt merkkijonot menevät automaattisesti ArrayList:in loppuun.

16.1 ArrayList:in metodeja

ArrayList tarjoaa monia hyödyllisiä metodeita:

public static void main(String[] args) {
    ArrayList<String> opettajat = new ArrayList<String>();

    opettajat.add("Antti");
    opettajat.add("Arto");
    opettajat.add("Pekka");
    opettajat.add("Juhana");
    opettajat.add("Martin");
    opettajat.add("Matti");

    System.out.println("opettajien lukumäärä " + opettajat.size() );

    System.out.println("listalla ensimmäisenä " + opettajat.get(0));
    System.out.println("listalla kolmantena " + opettajat.get(2));

    opettajat.remove("Arto");

    if ( opettajat.contains("Arto") ) {
        System.out.println("Arto on opettajien listalla");
    } else {
        System.out.println("Arto ei ole opettajien listalla");
    }
}

Ensin luodaan merkkijonolista jolle lisätään 6 nimeä. size kertoo listalla olevien merkkijonojen lukumäärän. Huom: kun metodia kutsutaan, on kutsu muotoa opettajat.size(), eli metodin nimeä edeltää piste ja sen listan nimi kenen metodia kutsutaan.

Merkkijonot ovat listalla siinä järjestyksessä missä ne listalle laitettiin. Metodilla get(i) saadaan tietoon listan paikan i sisältö. Listan alkioiden paikkanumerointi alkaa nollasta, eli ensimmäisenä lisätty on paikassa numero 0, toisen a lisätty paikassa numero 1 jne.

Metodin remove avulla voidaan listalta poistaa merkkijonoja. Jos käytetään metodia muodossa remove("merkkejä"), poistetaan parametrina annettu merkkijono. Metodia voi käyttää myös siten, että annetaan parametriksi tietty luku. Esim. jos sanotaan remove(3) poistuu listalla neljäntenä oleva merkkijono.

Esimerkin lopussa kutsutaan metodia contains jonka avulla kysytään listalta sisältääkö se parametrina annettavan merkkijonon. Jos sisältää, palauttaa metodi arvon true.

Ohjelman tulostus:

opettajien lukumäärä 6
listalla ensimmäisena Antti
listalla kolmantena Pekka
Arto ei ole opettajien listalla

Huom! Metodit remove ja contains olettavat että listaan tallennetuilla olioilla on metodi equals. Palaamme tähän myöhemmin kurssilla.

16.2 ArrayList:in läpikäynti

Seuraavassa esimerkissä lisätään listalle 4 nimeä ja tulostetaan listan sisältö:

public static void main(String[] args) {
    ArrayList<String> opettajat = new ArrayList<String>();

    opettajat.add("Antti");
    opettajat.add("Pekka");
    opettajat.add("Juhana");
    opettajat.add("Martin");

    System.out.println( opettajat.get(0) );
    System.out.println( opettajat.get(1) );
    System.out.println( opettajat.get(2) );
    System.out.println( opettajat.get(3) );
}

Ratkaisu on kuitenkin erittäin kömpelö. Entäs jos listalla olisi enemmän alkiota? Tai vähemmän? Entäs jos ei olisi edes tiedossa listalla olevien alkioiden määrää?

Tehdään ensin välivaiheen versio jossa pidetään:

public static void main(String[] args) {
    ArrayList<String> opettajat = new ArrayList<String>();

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

    int paikka = 0;
    System.out.println( opettajat.get(paikka) );
    paikka++;
    System.out.println( opettajat.get(paikka) );  // paikka = 1
    paikka++;
    System.out.println( opettajat.get(paikka) );  // paikka = 2
    paikka++;
    System.out.println( opettajat.get(paikka) );  // paikka = 3
}

Vanhan tutun while-komennon avulla voimme kasvataa muuttujaa paikka niin kauan kunnes se kasvaa liian suureksi:

public static void main(String[] args) {
    ArrayList<String> opettajat = new ArrayList<String>();

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

    int paikka = 0;
    while ( paikka < opettajat.size() )  // muistatko miksi paikka <= opettajat.size() ei toimi?
        System.out.println( opettajat.get(paikka) );
        paikka++;
    }
}

Nyt tulostus toimii riippumatta listalla olevien alkioiden määrästä.

While-toistolauseen käyttö ja listan paikkojen "indeksointi" ei kuitenkaan ole yleensä järkevin tapa listan läpikäyntiin. Suositeltavampi on seuraavassa esiteltävä for-each -toistolauseke.

16.3 for-each

Vaikka komennosta käytetään nimitystä for-each, komennon nimi on pelkästään for. for:ista on olemassa kaksi versiota, perinteinen (jonka nopea esittely oli jo viimeviikolla mutta mitä alame varsinaisesti käyttämään vasta viikolla 6) ja "for-each" jota käytämme nyt.

ArrayList:in alkioiden läpikäynti for-each:illa on lasten leikkiä:

public static void main(String[] args) {
    ArrayList<String> opettajat = new ArrayList<String>();

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

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

Kuten huomaame, ei listalla olevien merkkijonojen paikkanumeroista tarvitse välittää, for käy listan sisällön läpi "automaattisesti".

Komennon for aaltosulkujen sisällä olevassa koodissa käytetään muuttujaa opettaja, joka on määritelty for-rivillä kaksoispisteen vasemmalla puolella. Käy niin, että kukin listalla opettajat oleva merkkijono tulee vuorollaan muuttujan opettaja arvoksi. Eli kun for:iin mennään, on opettaja ensin Antti, forin toisella toistolla opettaja on Pekka, jne

Vaikka for-komento voi tuntua aluksi hieman oudolta sitä kannattaa ehdottomasti totutella käyttämään!

Screencast aiheesta:

Tee ohjelma, joka kysyy käyttäjältä sanoja, kunnes käyttäjä antaa tyhjän merkkijonon. Sitten ohjelma tulostaa käyttäjän antamat sanat uudestaan. Kokeile tässä for-toistolauseketta. Käytä ohjelmassa ArrayList-rakennetta, joka määritellään seuraavasti:

ArrayList<String> sanat = new ArrayList<String>();
Anna sana: Mozart
Anna sana: Schubert
Anna sana: Bach
Anna sana: Sibelius
Anna sana: Liszt
Anna sana:
Annoit seuraavat sanat:
Mozart
Schubert
Bach
Sibelius
Liszt

Vihje: tyhjä merkkijono voidaan havaita seuraavasti

String sana = lukija.nextLine();

if ( sana.isEmpty() ) {  // myös tämä tomisi: sana.equals("")
    // sana oli tyhjä eli pelkkä enterin painallus
}

Tee ohjelma, joka kysyy käyttäjältä sanoja, kunnes käyttäjä antaa saman sanan uudestaan. Käytä ohjelmassa ArrayList-rakennetta, joka määritellään seuraavasti:

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

Kun sama sana toistuu, ilmoittaa ohjelma asiasta seuraavasti:

Anna sana: porkkana
Anna sana: selleri
Anna sana: nauris
Anna sana: lanttu
Anna sana: selleri
Annoit uudestaan sanan selleri

Vihje: Muista arraylistin metodi .contains()

16.4 Listan järjestäminen, kääntäminen ja sekoittaminen

ArrayList:n sisältö on helppo järjestää suuruusjärjestykseen. Suuruusjärjestys merkkijonojen yhteydessä tarkoittaa aakkosjärjestystä. Järjestäminen tapahtuu seuraavasti:

public static void main(String[] args) {
    ArrayList<String> opettajat = new ArrayList<String>();

    // ...

    Collections.sort(opettajat);

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

Tulostuu:

Antti
Arto
Juhana
Martin
Matti
Pekka

Annetaan siis lista parametriksi metodille Collections.sort. Jotta Collections:in apuvälineet toimisivat, on ohjelman yläosassa oltava import java.util.Collections; tai import java.util.*;

Collections:ista löytyy muutakin hyödyllistä:

  • shuffle sekoittaa listan sisällön, metodista voi olla hyötyä esimerkiksi peleissä
  • reverse kääntää listan sisällön

Tee ohjelma, joka kysyy käyttäjältä sanoja, kunnes käyttäjä antaa tyhjän merkkijonon. Sitten ohjelma tulostaa käyttäjän antamat sanat päinvastaisessa järjestyksessä, eli viimeinen syötetty sana ensin jne.

Anna sana: Mozart
Anna sana: Schubert
Anna sana: Bach
Anna sana: Sibelius
Anna sana: Liszt
Anna sana:
Annoit seuraavat sanat:
Liszt
Sibelius
Bach
Schubert
Mozart

Tee edellistä tehtävää vastaava ohjelma, jossa sanat tulostetaan aakkosjärjestyksessä.

Anna sana: Mozart
Anna sana: Schubert
Anna sana: Bach
Anna sana: Sibelius
Anna sana: Liszt
Anna sana:
Annoit seuraavat sanat:
Bach
Liszt
Mozart
Schubert
Sibelius

16.5 ArrayList metodin parametrina

ArrayList:in voi antaa metodille parametrina:

public static void tulosta(ArrayList<String> lista) {
    for (String sana : lista) {
        System.out.println( sana );
    }
}

public static void main(String[] args) {
    ArrayList<String> lista = new ArrayList<String>();
    lista.add("Java");
    lista.add("Python");
    lista.add("Ruby");
    lista.add("C++");

    tulosta(lista);
}

Parametrin tyyppi siis määritellään listaksi täsmälleen samalla tavalla eli ArrayList<String> kuin listamuuttujan määrittely tapahtuu.

Huomaa, että parametrin nimellä ei ole merkitystä:

public static void tulosta(ArrayList<String> tulostettava) {
    for (String sana : tulostettava) {
        System.out.println( sana );
    }
}

public static void main(String[] args) {
    ArrayList<String> ohjelmointikielet = new ArrayList<String>();
    ohjelmointikielet.add("Java");
    ohjelmointikielet.add("Python");
    ohjelmointikielet.add("Ruby");
    ohjelmointikielet.add("C++");

    ArrayList<String> maat = new ArrayList<String>();
    maat.add("Suomi");
    maat.add("Ruotsi");
    maat.add("Norja");

    tulosta(ohjelmointikielet);    // annetaan metodille parametriksi lista ohjelmointikielet

    tulosta(maat);                 // annetaan metodille parametriksi lista maat
}

Ohjelmassa on nyt kaksi listaa ohjelmointikielet ja maat. Metodille annetaan ensin tulostettavaksi lista ohjelmointikielet. Metodi tulosta käyttää parametriksi saamastaan listasta sisäisesti nimellä tulostettava! Seuraavaksi metodille annetaan tulostettavaksi lista maat. Jälleen metodi käyttää parametrinaan saamasta listasta sisäisesti nimeä tulostettava.

Tee metodi public static int laskeAlkiot(ArrayList<String> lista) joka palauttaa listan alkioiden määrän. Metodisi ei siis tulosta mitään vaan palauttaa return:illa alkioiden lukumäärän seuraavan esimerkin mukaisesti

ArrayList<String> lista = new ArrayList<String>();
lista.add("Moi");
lista.add("Ciao");
lista.add("Hello");
System.out.println("Listalla on alkioita:");
System.out.println(laskeAlkiot(lista));
Listalla on alkioita:
3

Metodin sisällä on mahdollisuus vaikuttaa parametrina saadun listan sisältöön. Seuraavassa esimerkissä metodi poistaEnsimmainen nimensä mukaisesti poistaa listalla ensimmäisenä olevan merkkijonon (mitähän tapahtuu jos listalla ei ole mitään?).

public static void tulosta(ArrayList<String> tulostettava) {
    for (String sana : tulostettava) {
        System.out.println( sana );
    }
}

public static void poistaEnsimmainen(ArrayList<String> lista) {
    lista.remove(0);  // poistetaan listalta ensimmäinen eli "nollas"
}

public static void main(String[] args) {
    ArrayList<String> ohjelmointikielet = new ArrayList<String>();
    ohjelmointikielet.add("Pascal");
    ohjelmointikielet.add("Java");
    ohjelmointikielet.add("Python");
    ohjelmointikielet.add("Ruby");
    ohjelmointikielet.add("C++");


    tulosta(ohjelmointikielet);

    poistaEnsimmainen(ohjelmointikielet);

    System.out.println();  // tulostetaan tyhjä rivi

    tulosta(ohjelmointikielet);
}

Tulostuu:

Pascal
Java
Python
Ruby
C++

Java
Python
Ruby
C++

Vastaavalla tavalla metodi voisi esim. lisätä parametrina saamaansa listaan lisää merkkijonoja.

Tee metodi public static void poistaViimeinen(ArrayList<String> lista) joka poistaa listalla viimeisenä olevan alkion. Tällöin esimerkiksi seuraava koodi:

ArrayList<String> tyypit = new ArrayList<String>();
tyypit.add("Pekka");
tyypit.add("Mauri");
tyypit.add("Jore");
tyypit.add("Simppa");

System.out.println("Tyypit:");
System.out.println(tyypit);

// tyypit järjestykseen!
tyypit.sort();

// heitetään viimeinen mäkeen!
poistaViimeinen(tyypit);

System.out.println(tyypit);

Tulostaa:

Tyypit:
[Pekka, Mauri, Jore, Simppa]
[Jore, Mauri, Pekka]

Kuten edellisen tehtävän esimerkkitulostuksesta näemme, voi ArrayList:in tulostaa sellaisenaan. Tulostusmuoto ei kuitenkaan yleensä ole halutun kaltainen ja tulostus joudutaan hoitamaan itse esim. for-komennon avulla.

16.6 Lukuja ArrayList:issä

ArrayList:eihin voi tallettaa minkä tahansa tyyppisiä arvoja. Jos talletetaan kokonaislukuja eli int:ejä, tulee muistaa pari detaljia. int:ejä tallettava lista tulee määritellä ArrayList<Integer>, eli int:n sijaan tulee kirjoittaa Integer.

Kun listalle talletetaan int-lukuja, ei metodi remove toimi aivan odotetulla tavalla:

public static void main(String[] args) {
    ArrayList<Integer> luvut = new ArrayList<Integer>();

    luvut.add(4);
    luvut.add(8);

    // yrittää poistaa luvun listan kohdasta 4, eli ei toimi odotetulla tavalla!
    luvut.remove(4);

    // tämä poistaa listalta luvun 4
    luvut.remove( Integer.valueOf(4) );
}

Eli luvut.remove(4) yrittää poistaa listalla kohdassa 4 olevan alkion. Listalla on vain 2 alkiota, joten komento aiheuttaa virheen. Jos halutaan poistaa luku 4, täytyy käyttää hieman monimutkaisempaa muotoa: luvut.remove( Integer.valueOf(4) );

Listalle voi tallettaa myös liukulukuja eli double:ja ja merkkejä eli char:eja. Tällöin listat luodaan seuraavasti:

ArrayList<Double> doublet = new ArrayList<Double>();
ArrayList<Character> merkit = new ArrayList<Character>();
    

Tee metodi summa, joka laskee parametrinaan saamansa kokonaislukuja sisältävän, eli tyyppiä ArrayList<Integer> olevan listan summan.

Tee metodi seuraavaan runkoon:

public static int summa(ArrayList<Integer> lista) {
    // kirjoita koodia tähän
}

public static void main(String[] args) {
    ArrayList<Integer> lista = new ArrayList<Integer>();
    lista.add(3);
    lista.add(2);
    lista.add(7);
    lista.add(2);

    System.out.println("Summa: " + summa(lista));

    lista.add(10);

    System.out.println("Summa: " + summa(lista));
}
  

Ohjelman tulostus:

Summa: 14
Summa: 24

Tee metodi keskiarvo, joka laskee parametrinaan saamansa kokonaislukuja sisältävän listan lukujen keskiarvon. Metodin on laskettava parametriensa summa käyttäen apuna edellisen tehtävän metodia summa.

Tee metodi seuraavaan runkoon:

public static double keskiarvo(ArrayList<Integer> lista) {
    // kirjoita koodia tähän
}

public static void main(String[] args) {
    ArrayList<Integer> lista = new ArrayList<Integer>();
    lista.add(3);
    lista.add(2);
    lista.add(7);
    lista.add(2);

    System.out.println("Keskiarvo: " + keskiarvo(lista));
}

Ohjelman tulostus:

Keskiarvo: 3.5

Tee metodi suurin, joka palauttaa parametrina saamansa kokonaislukuja sisältävän listan suurimman luvun.

public static int suurin(ArrayList<Integer> lista) {
    // kirjoita koodia tähän
}

public static void main(String[] args) {
    ArrayList<Integer> lista = new ArrayList<Integer>();
    lista.add(3);
    lista.add(2);
    lista.add(7);
    lista.add(2);

    System.out.println("Suurin: " + suurin(lista));
}

Ohjelman tulostus:

Suurin: 7

Tee metodi varianssi, joka laskee palauttaa saamansa kokonaislukuja sisältävän listan otosvarianssin. Ohjeen varianssin laskemiseksi voit katsoa esimerkiksi Wikipediasta kohdasta populaatio- ja otosvarianssi.

Tee metodi käytäen apuna tehtävän 64 metodia keskiarvo, kutsu metodia kuitenkin vain kertaalleen yhden varianssin laskemisen aikana.

public static double varianssi(ArrayList<Integer> lista) {
    // kirjoita koodia tähän
}

public static void main(String[] args) {
    ArrayList<Integer> lista = new ArrayList<Integer>();
    lista.add(3);
    lista.add(2);
    lista.add(7);
    lista.add(2);

    System.out.println("Varianssi: " + varianssi(lista));
}

Ohjelman tulostus:

Varianssi: 5.666667

(Lukujen keskiarvo on 3.5, joten otosvarianssi on ((3 - 3.5)² + (2 - 3.5)² + (7 - 3.5)² + (2 - 3.5)²)/(4 - 1) ˜ 5,666667.)

Huom! Muistathan kokeillessasi ohjelmaa, että yhden alkion kokoisen listan (otos)varianssia ei ole määritelty! Kaavassa tapahtuu tällöin nollalla jakaminen. Java esittää nollalla jakamisen tuloksen epänumerona NaN

17 Totuusarvojen käyttö

Totuusarvoinen eli boolean-muuttuja voi saada vain kaksi arvoa true tai false. Seuraavassa esimerkki booleanin käytöstä:

int luku1 = 1;
int luku2 = 5;

boolean ekaSuurempi = true;

if (luku1 <= luku2) {
    ekaSuurempi = false;
}

if (ekaSuurempi==true) {
    System.out.println("luku1 suurempi");
} else {
    System.out.println("luku1 ei ollut suurempi");
}

Eli ensin asetetaan totuusarvon ekaSuurempi arvoksi tosi eli true. Ensimmäinen if tarkastaa onko luku1 pienempi tai yhtä pieni kuin luku2. Jos näin on, vaihdetaan totuusarvon arvoksi epätosi eli false. Myöhempi if valitsee tulostuksen totuusarvoon perustuen.

Totuusarvon käyttö ehtolauseessa on itseasiassa edellistä esimerkkiä yksinkertaisempaa, jälkimmäinen if voidaan kirjoittaa seuraavasti:

if (ekaSuurempi) {  // tarkoittaa samaa kuin ekaSuurempi==true
    System.out.println("luku1 suurempi");
} else {
    System.out.println("luku1 ei ollut suurempi");
}

Eli jos halutaan tarkistaa että booleanmuuttujan arvo on tosi, eli ole tarvetta kirjoittaa ==true, pelkkä muuttujan nimi riittää!

Epätoden tarkastaminen onnistuu negaatio-operaation eli huutomerkin avulla:

if (!ekaSuurempi) {  // tarkoittaa samaa kuin ekaSuurempi==false
    System.out.println("luku1 ei ollut suurempi");
} else {
    System.out.println("luku1 suurempi");
}

17.1 totuusarvon palauttava metodi

Totuusarvot ovat erityisen käteviä jonkun asian voimassaolon tarkistavien metodien paluuarvoina. Tehdään metodi joka tarkastaa sisältääkö sen parametrina saama lista ainoastaan positiivisia lukuja (tulkitaan 0 positiiviseksi). Tieto positiivisuudesta palautetaan totuusarvona.

public static boolean kaikkiPositiivisia(ArrayList<Integer> luvut) {
    boolean eiNegatiivisia = true;

    for (int luku : luvut) {
        if (luku < 0) {
            eiNegatiivisia = false;
        }
    }
    // jos jonkun listan luvuista arvo oli pienempi kuin 0, on eiNegatiivisia nyt false
    return eiNegatiivisia;
}

Metodilla on totuusarvoinen apumuuttuja eiNegatiivisia. Apumuuttujan arvoksi asetetaan ensin true. Metodi käy läpi kaikki listan luvut. Jos jonkun (siis vähintään yhden) luvun arvo on pienempi kuin nolla, asetetaan apumuuttujan arvoksi false. Lopuksi palautetaan apumuuttujan arvo. Apumuuttuja on edelleen true jos yhtään negatiivista lukua ei löytynyt, muuten false.

Metodia käytetään seuraavasti:

public static void main(String[] args) {

    ArrayList<Integer> luvut = new ArrayList<Integer>();
    luvut.add(3);
    luvut.add(1);
    luvut.add(-1);

    boolean vastaus = kaikkiPositiivisia(luvut);

    if (vastaus) {  // tarkoittaa siis samaa kuin vastaus == true
        System.out.println("luvut positiivisia");
    } else {
        System.out.println("joukossa oli ainakin yksi negatiivinen");
    }
}

Vastauksen tallettaminen ensin muuttujaan ei yleensä ole tarpeen, ja metodikutsu voidaan kirjottaa suoraan ehdoksi:

ArrayList<Integer> luvut = new ArrayList<Integer>();
luvut.add(4);
luvut.add(7);
luvut.add(12);
luvut.add(9);

if (kaikkiPositiivisia(luvut)) {
    System.out.println("luvut positiivisia");
} else {
    System.out.println("joukossa oli ainakin yksi negatiivinen");
}

17.2 Komento return ja metodin lopetus

Metodin suoritus loppuu välittömästi kun metodissa suoritetaan return-käsky. Käyttämällä tätä tietoa hyväksi voimme kirjoittaa kaikkiPositiivisia-metodin hiukan suoraviivaisemmin ja selkeämmin.

public static boolean kaikkiPositiivisia(ArrayList<Integer> luvut) {
    for (int luku : luvut) {
        if (luku < 0) {
            return false;
        }
    }

    // jos tultiin tänne asti, ei yhtään negatiivista löytynyt
    // siispä palautetaan true
    return true;
    }
}

Eli jos lukujen listaa läpikäydessä törmätään negatiiviseen lukuun, voidaan metodista poistua heti palauttamalla false. Jos listalla ei ole yhtään negatiivista, päädytään loppuun ja voidaan palauttaa true. Olemme päässeet metodissa kokonaan eroon apumuuttujan käytöstä!

Tee metodi onkoListallaUseasti, joka saa parametrinaan kokonaislukuja sisältävän listan ja int-luvun. Jos luku esiintyy listalla yli yhden kerran, metodi palauttaa true ja muulloin false.

Ohjelman rakenne on seuraava:

public static boolean onkoListallaUseasti(ArrayList<Integer> lista, int luku) {
    // kirjoita koodia tähän
}

public static void main(String[] args) {
    ArrayList<Integer> lista = new ArrayList<Integer>();
    lista.add(3);
    lista.add(2);
    lista.add(7);
    lista.add(2);

    System.out.println("Anna luku: ");
    int luku = Integer.parseInt(lukija.nextLine());
    if (onkoListallaUseasti(luvut, luku)) {
        System.out.println(luku + " on listalla useasti.");
    } else {
        System.out.println(luku + " ei ole listalla useasti.");
    }
}
Anna luku: 2
Luku on on listalla useasti.
Anna luku: 3
Luku ei ole listalla useasti.

Tee metodi palindromi, joka kertoo, onko merkkijono palindromi (merkkijonon sisältö on sama alusta loppuun ja lopusta alkuun luettuna).

Metodi voi käyttää apuna metodia kaanna tehtävästä 56. Metodin tyyppi on boolean, joten se pa lauttaa jokoarvon true (merkkijono on palindromi) tai false (merkkijono ei ole palindromi).

public static boolean palindromi(String merkkijono) {
    // kirjoita koodia tähän
}

public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);

    System.out.println("Anna merkkijono: ");
    String merkkijono = lukija.nextLine();
    if (palindromi(merkkijono)) {
        System.out.println("Merkkijono on palindromi!");
    } else {
        System.out.println("Merkkijono ei ole palindromi!");
    }
}

Ohjelman tulostuksia:

Anna merkkijono: saippuakauppias
Merkkijono on palindromi!
Anna merkkijono: esimerkki
Merkkijono ei ole palindromi!

17.3 ArrayListin kopioiminen

Joskus on tarpeen muodostaa ArrayLististä kopio, johon voi tehdä muutoksia vaikuttamatta alkuperäisen ArrayListin sisältöön. Kopion voi muodostaa luomalla uuden ArrayListin käyttäen vanhaa ArrayListiä parametrina:

ArrayList<String> nimet = new ArrayList<String>();
nimet.add("Kyösti");
nimet.add("Risto");
nimet.add("Carl");
nimet.add("Urho");

//luodaan kopio nimet-listasta
ArrayList<String> kopio = new ArrayList<String>(nimet);
//järjestetään kopio
Collections.sort(kopio);

System.out.println(kopio);  //tulostuu [Carl, Kyösti, Risto, Urho]
System.out.println(nimet);  //tulostuu [Kyösti, Risto, Carl, Urho]

Tee metodi kaikkiEri, joka palauttaa true jos sen parametrina saamassa kokonaislukuja sisältävässä listassa olevat luvut ovat kaikki erisuuruisia. Metodi ei saa muuttaa listan sisältöä.

Seuraavassa kaksi esimerkkiä metodin toiminnasta:

public static void main(String[] args) {
    ArrayList<Integer> lista1 = new ArrayList<Integer>();
    lista1.add(3);
    lista1.add(7);
    lista1.add(1);

    boolean eri = kaikkiEri(lista1);
    // muuttujan eri arvo true

    ArrayList<Integer> lista2 = new ArrayList<Integer>();
    lista2.add(2);
    lista2.add(3);
    lista2.add(7);
    lista2.add(1);
    lista2.add(3);
    lista2.add(99);

    eri = kaikkiEri(lista2);
    // muuttujan eri arvo false sillä luku 3 on listalla kahteen kertaan
}

18 Metodit ja parametrien kopioituminen

Tarkastellaan muutamaa metodeihin liittyvää tärkeää yksityiskohtaa.

Luvussa 15 oli esimerkki, jossa yritettiin muuttaa pääohjelmassa olevan muuttujan arvoa metodin sisällä.

public static void main(String[] args) {
    int luku = 1;
    kasvataKolmella();
    System.out.println("luku on " + luku);
}

public static void kasvataKolmella() {
    luku = luku + 3;
}

Ohjelma ei toimi, sillä metodi ei pääse käsiksi pääohjelman muuttujaan luku.

Tämä johtuu siitä, että pääohjelman muuttujat eivät näy metodien sisään. Ja yleisemmin: minkään metodin muuttujat eivät näy muille metodeille. Koska pääohjelma main on myös metodi, pätee sääntö myös pääohjelmalle. Ainoa keino viedä metodille tietoa ulkopuolelta on parametrin avulla.

Yritetään korjata edellinen esimerkki välittämällä pääohjelman muuttuja luku parametrina metodille.

public static void main(String[] args) {
    int luku = 1;
    kasvataKolmella(luku);
    System.out.println(luku);  // tulostaa 1, eli arvo luku ei muuttunut
}

public static void kasvataKolmella(int luku) {
    luku = luku + 3;
}

Ohjelma ei toimi toivotulla tavalla. Metodissa olevat parametrit ovat eri muuttujia kuin pääohjelmassa esitellyt muuttujat. Edellä metodi siis kasvattaa samannimistä, mutta ei samaa parametria luku.

Kun metodille annetaan parametri, parametrin arvo kopioidaan uuteen muuttujaan metodissa käytettäväksi. Yllä olevassa esimerkissä metodille kasvataKolmella annetusta muuttujasta luku luodaan kopio, jota metodin sisällä lopulta käsitellään. Metodi käsittelee siis pääohjelmassa olevan muuttujan kopiota, ei alkuperäistä muuttujaa -- pääohjelmametodissa olevalle muuttujalle luku ei tehdä mitään.

Voidaan ajatella, että pääohjelmametodi main ja metodi kasvataKolmella toimivat kumpikin omassa kohtaa tietokoneen muistia. Allaolevassa kuvassa on main:in muuttujaa luku varten oma "lokero". Kun metodia kutsutaan, tehdään tälle oma muuttuja luku jonka arvoksi kopioituu main:in luku-muuttujan arvo eli 1. Molemmat luku-nimiset muuttujat ovat kuitenkin täysin erillisiä, eli kun metodissa kasvataKolmella muutetaan sen luku-muuttujan arvoa, ei muutos vaikuta millään tavalla pääohjelman muuttujaan luku.

Allaoleva kuva antaa lisävalaisua tilanteeseen.

Metodista saa toki välitettyä tietoa kutsujalle käyttäen paluuarvoa, eli palauttamalla arvon return-komennolla. Edellinen saadaan toimimaan muuttamalla koodia hiukan:

public static void main(String[] args) {
    int luku = 1;
    luku = kasvataKolmellaJaPalauta(luku);

    System.out.println(luku);  // tulostaa 4, sillä luku on saanut arvokseen metodin palauttaman arvon
}

public static int kasvataKolmellaJaPalauta(int luku) {
    luku = luku + 3;

    return luku;
}

Edelleen on niin, että metodi käsittelee pääohjelman luku-muuttujan arvon kopiota. Pääohjelmassa metodin palauttama arvo sijoitetaan muuttujaan luku, joten muutos tulee tämän takia voimaan myös pääohjelmassa. Huomaa, että edellisessä ei ole mitään merkitystä sillä, mikä nimi metodin parametrilla on. Koodi toimii täysin samoin oli nimi mikä tahansa, esim.

public static void main(String[] args) {
    int luku = 1;
    luku = kasvataKolmellaJaPalauta(luku);

    System.out.println(luku);
}

public static int kasvataKolmellaJaPalauta(int kasvatettavaLuku) {
    kasvatettavaLuku = kasvatettavaLuku + 3;

    return kasvatettavaLuku;
}

Huomasimme että metodissa olevat parametrit ovat eri muuttujia kuin metodin kutsujassa esitellyt muuttujat. Ainoastaan parametrin arvo kopioituu kutsujasta metodiin.

Asia ei kuitenkaan ole ihan näin yksinkertainen. Jos metodille annetaan parametrina ArrayList, käy niin että sama lista näkyy metodille ja kaikki metodin listalle tekemät muutokset tulevat kaikkialla voimaan.

public static void poistaAlussaOleva(ArrayList<Integer> lista) {
    lista.remove(0); // poistaa paikassa 0 olevan luvun
}
ArrayList<Integer> luvut = new ArrayList<Integer>();
luvut.add(4);
luvut.add(3);
luvut.add(7);
luvut.add(3);

System.out.println(luvut); // tulostuu [4,3,7,3]

poistaAlussaOleva(luvut);

System.out.println(luvut); // tulostuu [3,7,3]

Toisin kuin int-tyyppinen parametri, lista ei kopioidu vaan metodi käsittelee suoraan parametrina annettua listaa.

Tilannetta valaisee allaoleva kuva. Toisin kuin int-tyyppinen muuttuja, ArrayList ei sijaitsekaan samalla tapaa "lokerossa", vaan muuttujan nimi, eli mainin tapauksessa luvut onkin ainoastaan viite paikkaan missä ArrayList sijaitsee. Yksi tapa ajatella asiaa, on että ArrayList on "langan päässä", eli listan nimi luvut on lanka jonka toisesta päästä lista löytyy. Kun metodikutsun parametrina on ArrayList, käykin niin että metodille annetaan "lanka" jonka päässä on sama lista jonka metodin kutsuja näkee. Eli main:illa ja metodilla on kyllä molemmilla oma lanka, mutta langan päässä on sama lista ja kaikki muutokset mitä metodi tekee listaan tapahtuvat täsmälleen samaan listaan jota pääohjelma käyttää. Tästä viikosta alkaen tulemme huomaamaan että Java:ssa hyvin moni asia on "langan päässä".

Huomaa jälleen että parametrin nimi metodin sisällä voi olla aivan vapaasti valittu, nimen ei tarvitse missään tapauksessa olla sama kuin kutsuvassa metodissa oleva nimi. Edellä listaa kutsutaan metodin sisällä nimellä lista, metodin kutsuja taas näkee saman listan luvut-nimisenä.

Miksi int-parametrista ainoastaan arvo kopioituu metodille mutta parametrin ollessa ArrayList metodi käsittelee suoraan listan sisältöä? Javassa ainoastaan alkeistietotyyppisten eli tyyppien int, double, char, boolean (ja muutamien muiden joita emme ole käsitelleet) arvot kopioidaan metodille. Muun tyyppisten parametrien tapauksessa metodille kopioidaan viite, ja metodista käsitellään viitteen takana olevaa parametria suoraan. Ei-alkeistyyppiset muuttujat -- eli viittaustyyppiset muuttujat ovat siis edellisen kuvan tapaan "langan päässä" -- metodille välitetään lanka parametriin, ja näin metodi käsittelee parametria suoraan.

Toteuta metodi public static void yhdista(ArrayList<Integer> eka, ArrayList<Integer> toka), joka lisää toisena parametrina toka olevassa ArrayListissa olevat luvut ensimmäisenä parametrina olevaan ArrayList:iin eka. Alkioiden talletusjärjestyksellä ei ole väliä, ja sama alkio voi päätyä listalle useamman kerran. Esimerkki metodin toiminnasta:

ArrayList<Integer> lista1= new ArrayList<Integer>();
ArrayList<Integer> lista2= new ArrayList<Integer>();

lista1.add(4);
lista1.add(3);

lista2.add(5);
lista2.add(10);
lista2.add(7);

yhdista(lista1, lista2);

System.out.println(lista1); // tulostuu [4, 3, 5, 10, 7]

System.out.println(lista2); // tulostuu [5, 10, 7]
  

Listalle voi lisätä toisen listan sisällön ArrayList-luokan tarjoaman addAll-metodin avulla. Lista saa parametrinä toisen listan, jonka alkiot listalle lisätään.

Toteuta metodi joukkoYhdista joka toimii muuten samoin kuin edellisen tehtävän yhdista-metodi, mutta parantele sitä niin, että yhdistäminen lisää listaan eka lukuja vain, jos ne eivät jo ennestään löydy listalta. Tehtävässä kannattaa käyttää hyväkseen ArrayListin contains-metodia, jolla voit tarkistaa sisältääkö lista jo jonkin luvun.

19 Ohjeita koodin kirjoittamiseen ja ongelmanratkaisuun

Kaksi maailman johtavaa ohjelmistonkehittäjää, Martin Fowler ja Kent Beck ovat lausunut kirjassa Refactoring: Improving the Design of Existing Codeseuraavasti seuraavasti:

  • Fowler: "Any fool can write code that a computer can understand. Good programmers write code that humans can understand."
  • Beck: "I'm not a great programmer, I'm just a good programmer with great habits."

[Päivitys: aiemmin molemmat lainaukset olivat merkitty Kent Beckille. Kiitos Esko Luontolalle tämän virheen ilmoittamisesta.]

Otamme viimeistään nyt ensimmäisiä askelia Fowlerin ja Beckin viitoittamalla tiellä.

19.1 Oikein sisennetty ja "hengittävä" koodi

Tarkastellaan koodia joka ensin lisää listalle lukuja ja tulostaa listan sisällön. Tämän jälkeen listalta poistetaan kaikki tietyn luvun esiintymät ja tulostetaan lista uudelleen.

Kirjoitetaan koodi ensin huonosti ja jätetään se sisentämättä:

public static void main(String[] args) {
ArrayList<Integer> luvut = new ArrayList<Integer>();
luvut.add(4);
luvut.add(3);
luvut.add(7);
luvut.add(3);
System.out.println("luvut alussa:");

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

while (luvut.contains(Integer.valueOf(3))) {
luvut.remove(Integer.valueOf(3));
}

System.out.println("luvut poiston jälkeen:");

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

Vaikka sisentämätön koodi toimii, on sitä hyvin ikävä lukea. Sisennetään koodi oikein (NetBeansissa sisennyksen saa korjattua automaattisesti painamalla alt+shift+f), ja erotellaan loogiset kokonaisuudet rivinvaihdoin:

public static void main(String[] args) {
    ArrayList<Integer> luvut = new ArrayList<Integer>();
    luvut.add(4);
    luvut.add(3);
    luvut.add(7);
    luvut.add(3);

    System.out.println("luvut alussa:");

    // tässä tulostetaan luvut
    for (int luku : luvut) {
        System.out.println(luku);
    }

    // tarkastetaan onko listalla luku 3
    while (luvut.contains(Integer.valueOf(3))) {
        luvut.remove(Integer.valueOf(3));  // jos löytyi, niin poistetaan se
    }
    // tehdään tämä whilessä jotta saadaan kaikki kolmoset poistetua!

    System.out.println("luvut poiston jälkeen:");

    // tässä tulostetaan luvut
    for (int luku : luvut) {
        System.out.println(luku);
    }
}

Nyt koodissa alkaa olla jo järkeä. Esimerkiksi tulostus ja kolmosten poisto ovat omia loogisia kokonaisuuksia, joten ne on erotettu rivinvaihdolla. Koodissa on ilmavuutta ja koodin lukeminen alkaa olla miellyttävämpää.

Koodiin on vieläpä kirjoitettu kommentteja selventämään muutaman kohdan toimintaa.

19.2 Copy-pasten eliminointi metodeilla

Ohjelmoijan lähes pahin mahdollinen perisynti on copy-paste -koodi, eli samanlaisen koodinpätkän toistaminen koodissa useaan kertaan. Esimerkissämme listan tulostus tapahtuu kahteen kertaan. Tulostuksen hoitava koodi on syytä erottaa omaksi metodikseen ja kutsua uutta metodia pääohjelmasta:

public static void main(String[] args) {
    ArrayList<Integer> luvut = new ArrayList<Integer>();
    luvut.add(4);
    luvut.add(3);
    luvut.add(7);
    luvut.add(3);

    System.out.println("luvut alussa:");

    // tässä tulostetaan luvut
    tulosta(luvut);

    while (luvut.contains(Integer.valueOf(3))) {
      luvut.remove(Integer.valueOf(3));
    }

    System.out.println("luvut poiston jälkeen:");

    // tässä tulostetaan luvut
    tulosta(luvut);
}

public static void tulosta(ArrayList<Integer> luvut) {
    for (int luku : luvut) {
        System.out.println( luku );
    }
}

19.3 Erillisten tehtävien erottaminen omiksi, selkeästi nimetyiksi metodeiksi

Koodi alkaa olla jo selkeämpää. Selvästi erillinen kokonaisuus, eli listan tulostus on oma helposti ymmärrettävä metodinsa. Uuden metodin esittelyn myötä myös pääohjelman luettavuus on kasvanut. Huomaa että uusi metodi on nimetty mahdollisimman kuvaavasti, eli siten että metodin nimi kertoo mitä metodi tekee. Ohjelmaan kirjoitetut kommentit tässä tulostetaan luvut ovatkin tarpeettomia, joten poistetaan ne.

Ohjelmassa on vielä hiukan siistimisen varaa. Pääohjelma on vielä sikäli ikävä, että siistien metodikutsujen seassa on vielä suoraan listaa käsittelevä "epäesteettinen" koodinpätkä. Erotetaan tämäkin omaksi metodikseen:

public static void main(String[] args) {
    ArrayList<Integer> luvut = new ArrayList<Integer>();
    luvut.add(4);
    luvut.add(3);
    luvut.add(7);
    luvut.add(3);

    System.out.println("luvut alussa:");
    tulosta(luvut);

    poista(luvut, 3);

    System.out.println("luvut poiston jälkeen:");
    tulosta(luvut);
}

public static void tulosta(ArrayList<Integer> luvut) {
    for (int luku : luvut) {
        System.out.println( luku );
    }
}

public static void poista(ArrayList<Integer> luvut, int poistettava) {
    while (luvut.contains(Integer.valueOf(poistettava))) {
        luvut.remove(Integer.valueOf(poistettava));
    }
}

Loimme yllä loogiselle kokonaisuudelle -- tietyn luvun kaikkien esiintymien poistolle -- oman kuvaavasti nimetyn metodin. Lopputuloksena oleva pääohjelma on nyt erittäin ymmärrettävä, lähes suomen kieltä. Molemmat metodit ovat myös erittäin yksinkertaisia ja selkeitä ymmärtää.

Kent Beck olisi varmaan tyytyväinen aikaansaannokseemme, koodi on helposti ymmärrettävää, helposti muokattavaa eikä sisällä copy-pastea.