Tässä luvussa esitellään lyhyesti sekalainen joukko Java-ohjelmoinnissa tarjolla olevia tekniikoita ja välineitä.
Javassa on melkoinen joukko erilaisia kokoelmaluokkia, joiden ilmentymät tarjoavat kehittyneitä välineitä tietojoukkojen käsittelyyn.
Yksi hyvin käyttökelpoinen opetellaan jo nyt, toinen kurssin loppupuolella. Luokka ArrayList<E> toteuttaa näennäisesti "vaihtelevanmittaiset taulukot", joiden komponenttityyppi annetaan ArrayList-oliota luotaessa. Komponentit numeroidaan tavallisen taulukon tapaan indeksein alkaen nollasta. Komponenttien lisääminen ja poistaminen muuttaa lisäys- ja poistokohtaa seuraavien komponenttien indeksejä.
ArrayList<E> on yksi Javan ns. geneerisistä tyypeistä. Se tarkoittaa, että kääntäjälle kerrotaan ns. tyyppiparametrin avulla, millaisista alkioista kokoelma muodostuu. Tuon parametrin avulla kääntäjä osaa säästää ohjelmoijan monenlaiselta vaivannäöltä. [Tämä selitys on hieman yksinkertaistava ja vain osatotuus...]
Tyyppiparametri annetaan ArrayList-oliota luotaessa kulmasulkeissa. Javassa tyyppiparametri saa olla vain jokin luokka. Erityisesti siis alkeistyypit (int, double, boolean, ...) eivät kelpaa. Jos tällaisia arvoja halutaan tallettaa geneeriseen kokoelmaan, tyyppiparametriksi on annettava alkeistyyppiä vastaava ns. käärepaperiluokka ("wrapper"): Integer, Double, Boolean, ...
ArrayList-muuttujia määritellään ja ArrayList-oliota luodaan tähän tapaan:
ArrayList<Integer> kLukuja = new ArrayList<Integer>(); ArrayList<Double> dLukuja = new ArrayList<Double>(); ArrayList<Boolean> totuuksia = new ArrayList<Boolean>(); ArrayList<String> mJonoja = new ArrayList<String>(); ArrayList<Varasto> varastoja = new ArrayList<Varasto>(); ...
Kaikki Java-APIn valmiit luokat ja kaikki itse ohjelmoidut luokat kelpaavat ArrayList-olion komponenttityypiksi!
Joitakin ArrayList-luokan välineitä:
Esimerkki: Ohjelma lukee ensin joukon lukuja. Sitten lukuja voi poistaa. Lopuksi ohjelma tulostaa jäljelle jääneiden lukujen summan:
import java.util.*; // täältä löytyy ArrayList-luokka public class ArrayListEsim { private static Scanner lukija = new Scanner(System.in); public static void main(String[] args) { ArrayList<Integer> taulu = new ArrayList<Integer>(); int luku; System.out.println("Syötä lukuja, nolla lopettaa."); luku = lukija.nextInt(); while (luku != 0) { taulu.add(luku); luku = lukija.nextInt(); } System.out.println(taulu); System.out.println("Syötä poistettavia lukuja, nolla lopettaa."); luku = lukija.nextInt(); while (luku != 0) { boolean oli = taulu.remove(new Integer(luku)); if (oli) // remove(int index) poistaisi indeksin kohdalta! System.out.println(taulu); else System.out.println("Ei löydy"); luku = lukija.nextInt(); } System.out.println(taulu); System.out.println("Alkioiden summa = " + summa(taulu)); } private static int summa(ArrayList<Integer> tau) { int summa = 0; for (int luku: tau) // for each! summa += luku; // vaihtoehto: return summa; // for (int i=0; i < tau.size(); ++i) // summa += tau.get(i); } // }
Esimerkki: Omat luokat – kuten jo peruskurssilla opittiin – ovat tyyppejä. Samoin myös ArrayList on tyyppi!
import java.util.*; public class VarastoLista { public static void main(String[] args) { Varasto mehua = new Varasto(100.0); Varasto olutta = new Varasto(100.0, 20.2); ArrayList<Varasto> varastoja = new ArrayList<Varasto>(); varastoja.add(mehua); varastoja.add(olutta); varastoja.get(0).lisaaVarastoon(50.7); varastoja.get(0).otaVarastosta(3.14); System.out.println("Mehuvarasto: " + varastoja.get(0)); System.out.println("Koko lista: " + varastoja); } // myös parametri voi (opittuun tapaan) toki olla tyyppiä Varasto private static void sitaSunTata(Varasto v) { // ... } // eikä mikään estä tietenkään ArrayList<Varasto>-tyyppistä parametria private static void sitaSunTata2(ArrayList<Varasto> vv) { // ... } // toki se voi olla myös paluuarvona -- tyyppi kun tyyppi ... private static ArrayList<Varasto> sitaSunTata3() { // ... return null; // jotain ei-void-metodista pitää aina palauttaa... } // ... }
Javassa on näppärä väline valita kahdesta lausekkeesta jommankumman arvo:
(totuusarvo ? lauseke1 : lauseke2)
Totuusarvo lasketaan. Jos se on true, ehdollisen lausekkeen arvo on
lauseke1
, muuten lauseke2
. (Sulkeet eivät
ole välttämättömät, mutta suositeltavat.)
Joko lauseke1
:n täytyy olla tyyppiä, joka ilman
eksplisiittistä tyyppimuunnosta on sijoitettavissa lauseke2
:n
tyyppisen muuttujan arvoksi tai päinvastoin. Ehdollisen lausekkeen
tyyppi on valittavien lausekkeiden tyypeistä laajempi.
Tavallisesti toki molemmat valintavaihtoehdot ovat samaa tyyppiä.
Esimerkiksi ("a:lle ei-pienempi arvoista b ja c") ohjelmoidaan if-lauseella tuttuun tapaan:
if (b < c) a = c; else a = b;
Ehdollista lauseketta käyttäen saman voi sanoa toisinkin:
a = (b < c ? c : b);
On makuasia, kumpi tapa on selkeämpi, mutta ei ole makuasia, että valintaperusteena on oltava selkeys!
Eräitä Javan lausekkeita – arvon ilmauksia – voi käyttää lauseiden – komentojen – tapaan. Lausekkeen sivuvaikutukset tekevät jotakin (hyödyllistä?) ja lausekkeen varsinainen arvo jätetään kylmästi käyttämättä. Lauseke kirjoitetaan sellaisenaan ja päätetään puolipisteeseen.
Vain seuraavia lausekkeita voi käyttää lauseina:
i = 1; // Näillä saisi laskeakin, mutta emme laske: j = i + 4; // a = (i = 1) + (j = i + 4);
++i; // Kun 1:llä kasvatus- tai vähennyslauseketta j++; // käytetään lauseena, järjestyksellä ei ole väliä!
[Lausekkeina käytettäessä näiden arvot eroavat: esim.
++i
:n arvo on i
:n arvo kasvatuksen jälkeen,
i++
:n arvo on i
:n arvo ennen kasvatusta.]
mehua.otaVarastosta(15); //vaikka: // public double otaVarastosta(double) // ^^^^^^ lukija.nextLine(); // ohitetaan rivi välittämättä rivin sisällöstä
new Varasto(); // konstruktorin suoritus voi joskus olla ainoa, mitä halutaan // ehkä testataan vaikka roskienkerääjää?
Huom: Muita lausekkeita ei siis voi käyttää lauseina!
Keskeytyslauseella keskeytetään rakenteisen lauseen suoritus:
while (jotakin) { ... if (keskeytyttää) break; ... }
On kauniimpiakin tapoja tulostaa parittomat välillä 1-10 kuin seuraava:
for (int i=1; i<10; ++i) { if (i%2 == 0) continue; System.out.println(i); }
public static boolean intToBool(int luku) { return luku != 0; }
Return-lauseita voi olla metodissa useampiakin. Jokaisessa arvon palauttavassa metodissa on oltava ainakin yksi saavutettavissa oleva return-lause palautusarvoineen. Itse asiassa arvon palauttavan metodin jokaisen mahdollisen suorituspolun on päädyttävä return-lauseeseen!
Java-kääntäjän data-flow-analyysi, jolla se pyrkii varmistamaan tuon asian, ei ole kovin viisas! Edes seuraava ei kelpaa kääntäjän nykyversiolle (1.6.0_22):
public static boolean intToBool(int luku) { if (true) return luku != 0; }
Kääntäjä toteaa lakonisesti missing return statement ja jättää käännöksen tekemättä.
ulompi: while (jotakin) { int i = 1; ... sisempi: while (jotain muuta) { int a = 1; ... if (meni vähän pieleen) break sisempi; ... if (kaikki meni pieleen) break ulompi; ... } }
Keskeytyslauseet voivat joskus olla näppäriä esimerkiksi virhetilanteiden hoitamisessa. Ohjelman normaalin etenemisen ohjaamisessa niitä on syytä käyttää harkiten.
On kuitenkin yksi tyypillinen tilanne, jossa keskeytyslauseella ohjelman toimintalogiikka saadaan selvästi suoraviivaisemmaksi: "puolitoistakertainen toisto", johon tutustuttiin jo peruskurssilla.
Kertauksena esimerkki syötteiden tarkastamisesta while-, do-while- ja 1.5-toistolla.
While-ratkaisussa ensimmäinen syöttölukutarjokas joudutaan lukemaan ennen toiston aloittamista ja uusi tarjokas toistettavan alialgoritmin lopussa:
import java.util.Scanner;
public class Pariton {
private static Scanner lukija = new Scanner(System.in);
public static void main(String[] args) {
System.out.println("Anna pariton luku.");
int luku = lukija.nextInt();
while (luku % 2 == 0) {
System.out.println("Eihän luku " + luku + " ole pariton!");
System.out.println("Yritä uudelleen");
luku = lukija.nextInt();
}
System.out.println("Kyllä! " + luku + " todella on pariton.");
}
}
Do-while-tekniikalla parillisuutta joudutaan tutkimaan sekä virheilmoituksen ehtona että toistoehtona:
import java.util.Scanner;
public class Pariton2 {
private static Scanner lukija = new Scanner(System.in);
public static void main(String[] args) {
int luku;
boolean parillinen;
do {
System.out.println("Anna pariton luku!");
luku = lukija.nextInt();
parillinen = (luku%2 == 0);
if (parillinen) {
System.out.println("Eihän "+luku+" ole pariton ...");
System.out.println("Yritä uudelleen");
}
} while (parillinen);
System.out.println("Hienoa, "+luku+" on pariton!");
}
}
Keskeyttämällä "ikuinen toisto" toistettavan alialgoritmin keskellä ohjelman rakenne yksinkertaistuu:
import java.util.Scanner; public class Pariton3 { private static Scanner lukija = new Scanner(System.in); public static void main(String[] args) { int luku; while (true) { // keskeytetään toiston sisällä break-lauseella System.out.println("Anna pariton luku."); luku = lukija.nextInt(); if (luku % 2 != 0) break; // <<<<<<<<< toiston keskeytys <<<<<<<<< System.out.println("Eihän luku " + luku + " ole pariton!"); System.out.println("Yritä uudelleen"); } System.out.println("Kyllä! " + luku + " todella on pariton."); } }
Tätä tekniikkaa käytettäessä on syytä kommentein selventää ohjelman lukijalle, mistä on kyse, ja erityisesti osoittaa keskeytyskohdat selkeästi.
Tätä tapaa ohjelmoida toistorakenne kutsutaan joskus "1.5-kertaiseksi toistoksi". Keskeytyskohtia voi toki olla useampiakin:
while (true) { if (alkuehto) break; lauseita... if (ehto_1) break; lisää lauseita ... if (ehto_2) break; ... if (ehto_n) break; lisää lauseita ... if (loppuehto) break; }
[Ohjelmointikielessä ei itse asiassa tarvittaisi mitään muita toistolauseita – tällä voisi hoitaa kaikki tapaukset.]
Java-kieleen on (valitettavasti) valittu "monivalintalauseeksi" melko alkeellinen muoto switch-lauseesta. Sen käyttöä ei kurssilla vaadita, mutta koska se voi joskus tulla vastaan muiden ohjelmia korjatessa, tässä on aiheesta lyhyt selostus. Luennoilla ja harjoituksissa tämä kohta ohitetaan!
Switch-lause on muodoltaan seuraavanlainen:
switch (lauseke) { case vakio1: lause1; break; case vakio2: case vakio3: lause2; lause3; break; default: lause4; break; }
//lähtölaskenta (korkeintaan 3:sta) int lkm; // ... switch (lkm) { case 3: System.out.print("kolme, "); case 2: System.out.print("kaksi, "); case 1: System.out.print("yksi, "); case 0: System.out.print("nolla: "); } System.out.println("PUM!");Jos muuttujan lkm arvo on 3, ohjelmanpätkä tulostaa:
kolme, kaksi, yksi, nolla: PUM!
Klassinen tapa toteuttaa vuorovaikutteisen ohjelman käyttöliittymä on komentotulkki. Ideana on että kone tarjoaa kehotemerkin, "promptin", ja käyttäjä kirjoittaa komennon, jonka kone toteuttaa. Toteutuksen jälkeen kone tarjoaa taas kehotemerkin. Tyypillinen esimerkki komentotulkkilogiikasta on käyttöjärjestelmän ns. "shell".
Komentotulkkilogiikan voi ohjelmoida seuraavaan tapaan:
import java.util.Scanner; public class Komentotulkki { private static Scanner lukija = new Scanner(System.in); private static void teeA() { System.out.println("Toteutan operaation a"); // ... tehdään työt ... } private static void teeB() { System.out.println("Toteutan operaation b"); // ... tehdään työt ... } private static void teeC() { System.out.println("Toteutan operaation c"); // ... tehdään työt ... } private static void teeD() { System.out.println("Toteutan operaation d"); // ... tehdään työt ... } public static void main(String[] args) { char komento; while (true) { // "ikuinen" toisto keskeytetään break-lauseella // kirjoitetaan kehotemerkki eli "prompti" System.out.print("kone> "); // eristetään komento // Huom: Jos komentoon String rivi = lukija.nextLine(); // liittyisi parametreja, if ( rivi.length() > 0 ) // nekin löytyisivät komento = rivi.charAt(0); // muuttujasta rivi! else komento = 'ö'; // mikä vain virheellinen merkki! // valitaan operaatio if (komento=='a') teeA(); else if (komento=='b') teeB(); else if (komento=='c') teeC(); else if (komento=='d') teeD(); else if (komento=='l') break; // ************** katkaistaan "ikuinen" toisto **************** else System.out.println("Virheellinen komento!"); } // end of while (true) } }
Metodi voi olla myös ns. rekursiivinen. Se tarkoittaa, että metodi kutsuu itseään! Metodi ei tietenkään saa kutsua itseään ehdoitta vaan joskus kutsuketjun on päätyttävä. (Miten käy, jos vain kutsutaan ja kutsutaan?)
Laaditaan esimerkkinä rekursiivinen metodi Fibonaccin lukujen tulostamiseen:
public class Fibo { private static long fibo(int i) { if (i<3) return 1; else return fibo(i-1)+fibo(i-2); } public static void main(String[] args) { for (int i=1; i<10 ; ++i) System.out.println(i+ ". fibo on " + fibo(i)); } }
Odotetusti sovellus tulostaa:
1. fibo on 1 2. fibo on 1 3. fibo on 2 4. fibo on 3 5. fibo on 5 6. fibo on 8 7. fibo on 13 8. fibo on 21 9. fibo on 34
Tämä on erittäin tehoton tapa laskea Fibonaccin lukuja! Aikavaativuus on eksponentiaalinen. On vaikea laatia vieläkin tehottomampia algoritmeja! Kyse ei ole siitä, että Fibonaccin lukujen laskeminen rekursiivisesti sinänsä olisi tehotonta. Kyse on siitä, että nähdyssä tavassa samoja asioita lasketaan yhä uudelleen ja uudelleen. Paljon parempia rekursiivisiakin ratkaisuja löytyy.
Mutta on myös tilanteita, joissa rekursio on hyvä ratkaisu, esimerkkeinä vaikkapa binäärihaku ja ns. pikajärjestäminen.
Esimerkkinä binäärihaku rekursiivisena:
private static int binHaeRec(int[] taulu, int haettava, int vasen, int oikea) { int keskiKohta = (vasen+oikea)/2; if (vasen > oikea) return -1; if (haettava == taulu[keskiKohta]) return keskiKohta; if (haettava < taulu[keskiKohta]) return binHaeRec(taulu, haettava, vasen, keskiKohta-1); else return binHaeRec(taulu, haettava, keskiKohta+1, oikea); }
Kutsu on esimerkiksi:
int[] a = {10, 20, 30, 40, 50}; System.out.println(binHaeRec(a, 30, 0, a.length-1));
Huom: Yleensä hakualgoritmille tavataan antaa parametrina vain taulukko ja haettava arvo, ei hakualueen ala- ja yläindeksiä kuten yllä. Ja niin on myös hyvä tehdä! Tämän kurssin kannnalta asialla ei ole suurempaa merkitystä, mutta jatkossa rekursiivinen binäärihaku on syytä ohjelmoida seuraavaan tyyliin:
public static int binHaeRec(int[] taulu, int haettava) { binHaeRecAlueelta(taulu, haettava, 0, taulu.length-1) } private static int binHaeRecAlueelta(int[] taulu, int haettava, int vasen, int oikea) { int keskiKohta = (vasen+oikea)/2; // ... kuten yllä // ... if (haettava < taulu[keskiKohta]) return binHaeRecAlueelta(taulu, haettava, vasen, keskiKohta-1); else return binHaeRecAlueelta(taulu, haettava, keskiKohta+1, oikea); }
Ja silloin kutsu kirjoitetaan tyyliin:
int[] a = {10, 20, 30, 40, 50}; System.out.println(binHaeRec(a, 30));
Esimerkki: pikajärjestäminen (Quicksort):
private static void pikaJarjesta(int[] taulu, int alku, int loppu) { int jakoAlkio, vasen, oikea; vasen = alku; oikea = loppu; jakoAlkio = taulu[(alku+loppu)/2]; do { while (taulu[vasen]<jakoAlkio) ++vasen; while (jakoAlkio<taulu[oikea]) --oikea; if (vasen<=oikea) { int apu = taulu[vasen]; taulu[vasen] = taulu[oikea]; taulu[oikea] = apu; ++vasen; --oikea; } } while (vasen < oikea); if (alku < oikea) pikaJarjesta(taulu, alku, oikea); if (vasen < loppu) pikaJarjesta(taulu, vasen, loppu); }
Kutsu on esimerkiksi:
int[] a = {40, 20, 50, 10, 30}; pikaJarjesta(a, 0, a.length-1);
Huom: Yleensä järjestämisalgoritmille tavataan antaa parametrina vain järjestettävä taulukko, ei järjestettävän osan ala- ja yläindeksiä kuten yllä. Ja niin on myös hyvä tehdä! Tämän kurssin kannnalta asialla ei ole suurempaa merkitystä, mutta jatkossa pikajärjestäminen on syytä ohjelmoida seuraavaan tyyliin:
public static void pikaJarjesta(int[] taulu) { pikaJarjestaAlue(taulu, 0, taulu.length-1); } private static void pikaJarjestaAlue(int[] taulu, int alku, int loppu) { int jakoAlkio, vasen, oikea; vasen = alku; oikea = loppu; jakoAlkio = taulu[(alku+loppu)/2]; do { // ... kuten yllä } } while (vasen < oikea); if (alku < oikea) pikaJarjestaAlue(taulu, alku, oikea); if (vasen < loppu) pikaJarjestaAlue(taulu, vasen, loppu); }
Ja kutsu silloin kirjoitetaan tyyliin:
int[] a = {40, 20, 50, 10, 30}; pikaJarjesta(a);
Vaikka pikajärjestäminen onkin pahimmassa tapauksessa aikavaativuudeltaan O(n2) se on aivan ylivoimainen verrattuna vaikkapa lisäysjärjestämiseen!
Voit itse tutkia tilannetta ohjelmalla VertaileLisQuick.java (nanosekuntiversio: VertaileLisQuickB.java). Ohjelmoinnin perusteet -kurssilla vertailtiin vastaavalla tavalla alkeellisempia järjestämisalgoritmeja: VertaileJarjAlgoritmeja.java (nanosekuntiversio: VertaileJarjAlgoritmejaB.java).
Keskimäärin pikajärjestäminen onkin O(n*log n). Ja se "pahin tapaus" on erittäin harvinainen.
Myös ns. keskinäinen rekursio (mutual recursion) on mahdollista. Kyse on siitä, että metodi kutsuu toista metodia, joka puolestaan kutsuu ensin mainittua. Monimutkaisemmatkin rekursiosuhteet ovat mahdollisia &ndash ja sellaisia myös käytetään esimerkiksi ohjelmointikielten jäsentäjien ohjelmoinnissa.
Esimerkki kahden metodin keskinäisestä rekursiosta:
public class AbraKadAbra { private static void abra (String jono) { if (jono.length() < 44) { jono += "ABRA"; System.out.println("Abra ennen kadin kutsua : "+jono); kad (jono); System.out.println("Abra jälkeen kadin kutsun: "+jono); } } private static void kad (String jono) { if (jono.length() < 44) { jono += "KAD"; System.out.println("Kad ennen abran kutsua : "+jono); abra (jono); System.out.println("Kad jälkeen abran kutsun : "+jono); } } public static void main(String[] args) { abra(""); } }
Tämä taianomainen sovellus tulostaa:
Abra ennen kadin kutsua : ABRA Kad ennen abran kutsua : ABRAKAD Abra ennen kadin kutsua : ABRAKADABRA Kad ennen abran kutsua : ABRAKADABRAKAD Abra ennen kadin kutsua : ABRAKADABRAKADABRA Kad ennen abran kutsua : ABRAKADABRAKADABRAKAD Abra ennen kadin kutsua : ABRAKADABRAKADABRAKADABRA Kad ennen abran kutsua : ABRAKADABRAKADABRAKADABRAKAD Abra ennen kadin kutsua : ABRAKADABRAKADABRAKADABRAKADABRA Kad ennen abran kutsua : ABRAKADABRAKADABRAKADABRAKADABRAKAD Abra ennen kadin kutsua : ABRAKADABRAKADABRAKADABRAKADABRAKADABRA Kad ennen abran kutsua : ABRAKADABRAKADABRAKADABRAKADABRAKADABRAKAD Abra ennen kadin kutsua : ABRAKADABRAKADABRAKADABRAKADABRAKADABRAKADABRA Abra jälkeen kadin kutsun: ABRAKADABRAKADABRAKADABRAKADABRAKADABRAKADABRA Kad jälkeen abran kutsun : ABRAKADABRAKADABRAKADABRAKADABRAKADABRAKAD Abra jälkeen kadin kutsun: ABRAKADABRAKADABRAKADABRAKADABRAKADABRA Kad jälkeen abran kutsun : ABRAKADABRAKADABRAKADABRAKADABRAKAD Abra jälkeen kadin kutsun: ABRAKADABRAKADABRAKADABRAKADABRA Kad jälkeen abran kutsun : ABRAKADABRAKADABRAKADABRAKAD Abra jälkeen kadin kutsun: ABRAKADABRAKADABRAKADABRA Kad jälkeen abran kutsun : ABRAKADABRAKADABRAKAD Abra jälkeen kadin kutsun: ABRAKADABRAKADABRA Kad jälkeen abran kutsun : ABRAKADABRAKAD Abra jälkeen kadin kutsun: ABRAKADABRA Kad jälkeen abran kutsun : ABRAKAD Abra jälkeen kadin kutsun: ABRA