1. Olioiden alkeet

Luokka ja olio

Luokka muodostuu muuttujista ja metodeista, ja se kuvaa siitä luotavien olioiden rakenteen. Luokan muuttujat määrittelevät olion tietosisällön, kun taas luokan metodit tarjoavat keinot tietosisällön käsittelyyn.

Koodissa luokka määritellään seuraavasti:

public class [nimi] {
    ...
}

Tässä kohtaan [nimi] tulee luokan nimi, joka on Java-kielessä tapana aloittaa isolla kirjaimella.

Seuraava koodi luo luokkaa vastaavan olion:

[nimi] olio = new [nimi]();

Tutustumme seuraavaksi luokan määrittelyyn käytännössä esimerkin avulla. Esimerkissä tehdään luokka Tuote, josta luodut oliot vastaavat kaupan tuotteita. Kunkin olion tietosisältönä ovat tuotteen nimi ja hinta.

Esimerkki

Ohjelmamme tulee sisältämään kaksi luokkaa:

Ohjelmassa on monta luokkaa, joten siitä kannattaa tehdä oma projekti NetBeansissa. Tässä projektin nimeksi (Project Name) sopii vaikkapa TuoteTesti ja pääluokka (Main Class) on Main. Kokeile, että saat luotua ja suoritettua ohjelman NetBeansissa.

Luokan Tuote koodi on seuraava:

public class Tuote {
    private String nimi;
    private int hinta;

    public Tuote(String nimi) {
	this.nimi = nimi;
	this.hinta = 0;
    }

    public String haeNimi() {
	return this.nimi;
    }

    public int haeHinta() {
	return this.hinta;
    }

    public void asetaHinta(int hinta) {
	if (kelvollinenHinta(hinta)) {
	    this.hinta = hinta;
	}
    }

    private boolean kelvollinenHinta(int hinta) {
	return hinta >= 0;
    }

    public String toString() {
	return this.nimi + " (" + this.hinta + " euroa)";
    }
}

Luokan Main koodi on seuraava:

public class Main {
    public static void main(String[] args) {
        Tuote kirja = new Tuote("aapinen");
        kirja.asetaHinta(15);
        System.out.println(kirja.haeNimi());
        System.out.println(kirja.haeHinta());
        System.out.println(kirja);
        kirja.asetaHinta(20);
        System.out.println(kirja);
        kirja.asetaHinta(-5);
        System.out.println(kirja);
    }
}

Tässä Main on pääluokka, joten ohjelman suoritus alkaa luokan metodista main. Ohjelman tulostus on seuraava:

aapinen
15
aapinen (15 euroa)
aapinen (20 euroa)
aapinen (20 euroa)

Seuraavaksi katsomme tarkemmin, mitä osia luokka Tuote sisältää.

Muuttujat

    private String nimi;
    private int hinta;

Luokan alussa olevat muuttujat ilmaisevat, mitä tietoa olio tulee sisältämään. Tässä tapauksessa luokkaan liittyy kaksi muuttujaa: nimi vastaa tuotteen nimeä ja hinta vastaa tuotteen hintaa.

Tässä muuttujien edessä on sana private. Tämä tarkoittaa, että muuttujia ei voi käsitellä suoraan luokan ulkopuolelta.

Konstruktori

    public Tuote(String nimi) {
	this.nimi = nimi;
	this.hinta = 0;
    }

Konstruktori suoritetaan silloin, kun luokasta luodaan olio. Konstruktorin tavallinen tehtävä on asettaa sopivat arvot olion muuttujille.

Konstruktorin nimi on sama kuin luokan nimi. Konstruktorin muoto vastaa muuten tavallista metodia, mutta siinä ei ilmoiteta lainkaan palautusarvon tyyppiä.

Konstruktorin parametrit määrittävät, mitä tietoa oliolle annetaan sen luontivaiheessa. Tässä konstruktorilla on parametri nimi, mikä tarkoittaa, että olion luonnin yhteydessä ilmoitetaan tuotteen nimi. Tuotteen hinnaksi konstruktori asettaa aina arvon 0.

Luokasta Tuote voidaan luoda olio seuraavasti:

Tuote kirja = new Tuote("aapinen");

Tällöin tuloksena on seuraava olio:

nimihinta
aapinen0

Huomaa, että konstruktorissa on kahdenlaisia muuttujia: this.nimi ja this.hinta viittaavat olion muuttujiin, kun taas nimi on konstruktorin parametri. Nimi täytyy kopioida konstruktorin parametrista olion muuttujaan, jotta se säilyy tallessa oliossa.

Metodit

    public String haeNimi() {
	return this.nimi;
    }

    public int haeHinta() {
	return this.hinta;
    }

    private boolean kelvollinenHinta(int hinta) {
	return hinta >= 0;
    }

    public void asetaHinta(int hinta) {
	if (kelvollinenHinta(hinta)) {
	    this.hinta = hinta;
	}
    }

Luokkaan liittyy kolme julkista (public) metodia: haeNimi palauttaa tuotteen nimen, haeHinta palauttaa tuotteen hinnan ja asetaHinta muuttaa tuotteen hintaa. Hinta muuttuu vain, jos uusi hinta ei ole negatiivinen.

Lisäksi luokassa on yksityinen (private) metodi kelvollinenHinta, jonka avulla metodi asetaHinta tarkistaa, että uusi hinta on kelvollinen eli se ei ole negatiivinen.

Julkiset metodit muodostavat luokan rajapinnan: niiden avulla olion tietosisältöä voi hakea ja muuttaa. Yksityinen metodi on luokan sisäinen apumetodi, jota ei voi kutsua luokan ulkopuolelta.

toString-metodi

    public String toString() {
	return this.nimi + " (" + this.hinta + " euroa)";
    }

Metodi toString on erityisasemassa luokassa: se määrittää, millaisen merkkijonoesityksen olio antaa itsestään tulostuskomennossa ja silloin, kun se liitetään osaksi merkkijonoa.

Tässä esimerkissä olion merkkijonoesitys on "X (Y euroa)", jossa X on tuotteen nimi ja Y sen hinta.

Olion käyttö

Pääluokassa oleva koodi alkaa seuraavasti:

Tuote kirja = new Tuote("aapinen");
kirja.asetaHinta(15);

Tämä saa aikaan seuraavan olion:

nimihinta
aapinen15

Seuraavaksi tulostetaan olion tiedot ensin hakumetodien avulla ja sitten toString-metodin merkkijonoesityksinä:

System.out.println(kirja.haeNimi());
System.out.println(kirja.haeHinta());
System.out.println(kirja);
aapinen
15
aapinen (15 euroa)

Sitten tuotteelle annetaan uusi hinta:

kirja.asetaHinta(20);

Nyt olion sisältö on seuraava:

nimihinta
aapinen20

Lopuksi tuotteelle yritetään antaa negatiivinen hinta:

kirja.asetaHinta(-5);

Metodi asetaHinta on toteutettu niin, että se ei salli tuotteen hinnan muuttamista negatiiviseksi. Niinpä olion sisältö säilyy ennallaan:

nimihinta
aapinen20
Lisää olioista

2. Lisää olioista

Mitä hyötyä olioista on?

Oliot ovat hyvin erilainen ohjelmoinnin käsite kuin vaikkapa silmukka. Muutaman rivin koodiesimerkin avulla voi näyttää, mitä konkreettista hyötyä silmukasta on ohjelmoinnissa. Sen sijaan oliot saattavat tuntua aluksi lähinnä tekevän yksinkertaisista asioista monimutkaisia.

Ongelmana on, että oliot pääsevät oikeuksiinsa vasta laajoissa ohjelmissa ja niiden etuja on vaikeaa havainnollistaa pienen kauniin esimerkin kautta. Vasta kokemus opettaa, miksi oliot voivat olla aidosti hyödyllisiä.

Huomaa, että olioiden käyttäminen ei ole itsetarkoitus. Olioita ei kannata lisätä koodiin "varmuuden vuoksi", vaan vain silloin, kun niistä saatavan hyödyn ymmärtää itse.

Milloin olioita käytetään?

Olio-ohjelmointi ei ole koskaan pakollista, vaan kaikkein hienoimmankin ohjelman voi toteuttaa ilman olioita. Tämä ei ole yllättävää, koska monissa ohjelmointikielissä ei edes ole olioita.

Herää kysymys, miksi olioita pitäisi käyttää, jos niitä ilmankin selviää. Selitys on siinä, että oliot auttavat mutkikkaan ohjelman toteuttamisessa, vaikka ne eivät sinänsä lisääkään kielen ilmaisuvoimaa. Jos Javasta poistettaisiin kaikki "turhat" ominaisuudet, vain vähän jäisi jäljelle.

Laajan ohjelman tekemisessä ongelmaksi tulee usein, että ohjelman toimintaa ja riippuvuuksia alkaa olla vaikeaa hahmottaa. Oliot tulevat tässä apuun, koska niiden avulla ohjelman voi jakaa erillisiin osiin, joiden sisäisestä toteutuksesta ei tarvitse välittää niiden ulkopuolella.

Nyt voimme jatkaa tutustumista olio-ohjelmointiin liittyviin asioihin.

Kuormittaminen

Tarkastellaan seuraavaa luokkaa Saastopossu:

public class Saastopossu {
    public String nimi;
    public int rahat;
    
    public Saastopossu(String nimi, int rahat) {
        this.nimi = nimi;
        this.rahat = rahat;
    }
    
    public Saastopossu(String nimi) {
        this(nimi, 0);
    }
    
    public Saastopossu() {
        this("Putte", 0);
    }
    
    public void lisaaRahaa(int maara) {
        this.rahat += maara;
    }
    
    public void lisaaRahaa() {
        this.lisaaRahaa(1);
    }
    
    public String toString() {
        return this.nimi + " (" + this.rahat + " euroa)";
    }
}

Seuraavassa on esimerkkejä luokan käyttämisestä:

Tapa 1:

Saastopossu possu = new Saastopossu("Nökö");
possu.lisaaRahaa(5);
System.out.println(possu);
Nökö (5 euroa)

Tapa 2:

Saastopossu possu = new Saastopossu();
possu.lisaaRahaa(2);
possu.lisaaRahaa();
possu.lisaaRahaa(4);
System.out.println(possu);
Putte (7 euroa)

Tapa 3:

Saastopossu possu = new Saastopossu("Nökö", 10);
System.out.println(possu);
Nökö (10 euroa)

Erityistä luokassa on, että siinä on monta eri konstruktoria ja metodia lisaaRahaa voi käyttää monella tavalla. Tätä kutsutaan metodien kuormittamiseksi: metodeista on käytössä eri versioita, joissa parametrien määrät ja tyypit vaihtelevat.

Luokka Saastopossu tarjoaa kolme erilaista konstruktoria:

Konstruktorit on toteutettu niin, että ne kutsuvat toisiaan: koodissa this metodin nimenä viittaa olion konstruktoriin. Tämä toteutustapa on mahdollinen, koska kaksi jälkimmäistä konstruktoria ovat oikeastaan vain ensimmäisen konstruktorin erikoistapauksia.

Metodista lisaaRahaa on myös käytössä kaksi eri versiota: jos rahamäärää ei ole parametrina, se on oletuksena 1 euro.

Milloin this on tarpeen?

Merkintä this viittaa olioon itseensä. Sitä voi käyttää aina viitattaessa olion muuttujiin ja konstruktoriin. Kuitenkaan merkintää ei ole pakko käyttää olion muuttujien yhteydessä, jos sekaantumisvaaraa muihin muuttujiin ei ole.

Esimerkiksi luokassa Saastopossu metodin toString voisi kirjoittaa lyhyemmin näin:

public String toString() {
    return nimi + " (" + rahat + " euroa)";
}

Konstruktorissa this on kuitenkin pakollinen:

public Saastopossu(String nimi, int rahat) {
    this.nimi = nimi;
    this.rahat = rahat;
}

Tämä johtuu siitä, että koodissa on pakko tehdä selväksi, milloin viitataan olion muuttujiin ja milloin konstruktorin parametreihin. Jos this puuttuu, tulkinta on, että viitataan parametreihin.

Luokan käyttömahdollisuudet

Omia luokkia voi käyttää kaikissa samoissa yhteyksissä kuin luokkaa String ja muita Javan valmiita luokkia: metodien parametreina, metodien palautusarvoina, listan arvojen tyyppinä jne.

Seuraavat luokat havainnollistavat mainittuja asioita:

public class Henkilo {
    private String nimi;
    private String sahkoposti;

    public Henkilo(String nimi, String sahkoposti) {
	this.nimi = nimi;
	this.sahkoposti = sahkoposti;
    }

    public String haeNimi() {
	return nimi;
    }

    public String haeSahkoposti() {
	return sahkoposti;
    }

    public String toString() {
	return nimi + " <" + sahkoposti + ">";
    }
}
import java.util.*;

public class Henkilorekisteri {
    private ArrayList<Henkilo> lista = new ArrayList<Henkilo>();

    public void lisaaHenkilo(Henkilo henkilo) {
	lista.add(henkilo);
    }

    public Henkilo haeHenkilo(String nimi) {
	for (Henkilo henkilo : lista) {
	    if (nimi.equals(henkilo.haeNimi())) {
		return henkilo;
	    }
	}
	return null;
    }
}

Luokkia voisi käyttää koodissa seuraavasti:

Henkilorekisteri henkilot = new Henkilorekisteri();

henkilot.lisaaHenkilo(new Henkilo("Aapeli", "aapeli@helsinki.fi"));
henkilot.lisaaHenkilo(new Henkilo("Maija", "maija@helsinki.fi"));
henkilot.lisaaHenkilo(new Henkilo("Uolevi", "uolevi@helsinki.fi"));

System.out.print("Anna nimi: ");
String nimi = input.nextLine();

Henkilo henkilo = henkilot.haeHenkilo(nimi);
if (henkilo == null) {
    System.out.println("Henkilöä ei löytynyt!");
} else {
    System.out.println("Tulos: " + henkilo);
}

Seuraavassa on esimerkkejä koodin suorituksesta:

Anna nimi: Maija
Tulos: Maija <maija@helsinki.fi>
Anna nimi: Hillevi
Henkilöä ei löytynyt!

Huomaa arvon null käyttötapa metodissa haeHenkilo: jos henkilöä ei ole listalla, metodin pitää silti palauttaa jotain, jolloin luonteva palautusarvo on olematon olio.

Näkyvyysalue

Näkyvyysalue tarkoittaa, missä ohjelman osassa muuttuja tai metodi on käytettävissä.

Luokan sisällä muuttujan näkyvyysalue riippuu siitä, missä kohdassa muuttuja on määritelty. Jos muuttuja on määritelty luokan alussa, se on käytettävissä kaikissa luokan metodeissa. Jos taas muuttuja on määritelty metodin sisällä, se on käytettävissä vain kyseisessä metodissa.

Seuraavassa esimerkissä muuttuja a on käytettävissä metodeissa eka ja toka, koska se on määritelty luokan alussa. Sen sijaan muuttuja b on käytettävissä vain metodissa eka, jossa se on määritelty.

public class Testi {
    private int a;

    public void eka() {
	int b;
	a = 1;
	b = 1;
    }

    public void toka() {
	a = 2;
	// ei toimi:
	// b = 2;
    }
}

private vs. public

Luokan alussa olevat muuttujat ja luokan metodit voivat olla yksityisiä (private) tai julkisia (public). Yksityiset muuttujat ja metodit ovat käytettävissä vain luokan sisällä, kun taas julkisiin muuttujiin ja metodeihin voi viitata luokan ulkopuolelta.

Tarkastellaan esimerkiksi seuraavaa luokkaa:

public class Testi {
    public int a;
    private int b;

    public void eka() {
    }

    private void toka() {
    }
}

Nyt seuraava koodi on mahdollinen, koska muuttuja a ja metodi eka ovat julkisia:

Testi olio = new Testi();
olio.a = 5;
olio.eka();

Sen sijaan seuraava koodi ei toimi, koska muuttuja b ja metodi toka ovat yksityisiä:

Testi olio = new Testi();
olio.b = 5;
olio.toka();

Mitä järkeä on määritellä muuttuja tai metodi yksityiseksi? Miksei voisi vain sallia kaikkien muuttujien ja metodien käyttämistä vapaasti?

Ideana on määritellä julkisiksi vain metodit, joita on tarkoitus kutsua luokan ulkopuolelta. Kun luokan muuttujat ovat yksityisiä, niitä voi muuttaa vain metodien kautta, jotka taas voivat huolehtia, että muuttujien arvot säilyvät järkevinä. Lisäksi saadaan piiloon luokan toteutuksen sisäiset yksityiskohdat, jotka eivät ole kiinnostavia luokan ulkopuoliselle käyttäjälle.

Huomaa, että yksityisyyden tarkoitus on vain auttaa ohjelmoijaa. Ohjelman toiminta ei muutu mitenkään, jos jonkin yksityisen muuttujan tai metodin vaihtaa julkiseksi.

Staattisuus

3. Staattisuus

Staattisuus

Staattinen muuttuja tai metodi on käytettävissä ilman, että luokasta luodaan olio. Staattisuuden ilmaisee sana static ennen muuttujan tai metodin nimeä.

Pääluokka

Ohjelman pääluokan muuttujat ja metodit ovat staattisia:

public class Main {
    private static String viesti = "Hei hei!";

    private static void tervehdi() {
	System.out.println(viesti);
    }

    public static void main(String[] args) {
	tervehdi();
    }
}

Kirjasto

Kirjasto sisältää staattisia metodeja, joita kutsutaan muualta ohjelmasta. Esimerkki tällaisesta luokasta on Javan valmis Math, joka sisältää matemaattisia funktioita.

Seuraava luokka Listat sisältää metodeita listan käsittelyyn:

import java.util.*;

public class Listat {
    public static int laskeMaara(ArrayList<Integer> luvut) {
	return luvut.size();
    }

    public static int laskeSumma(ArrayList<Integer> luvut) {
	int summa = 0;
	for (int luku : luvut) {
	    summa += luku;
	}
	return luku;
    }

    public static double laskeKeskiarvo(ArrayList<Integer> luvut) {
	return (double)laskeSumma(luvut) / laskeMaara(luvut);
    }
}

Luokkaa voisi käyttää muualla koodissa seuraavasti:

ArayList<Integer> lista = new ArrayList<Integer>();
lista.add(8);
lista.add(2);
lista.add(5);
lista.add(9);
lista.add(2);
System.out.println(Listat.laskeMaara(lista));
System.out.println(Listat.laskeSumma(lista));
System.out.println(Listat.laskeKeskiarvo(lista));

Koodin tulostus on seuraava:

5
26
5.2

Huomaa luokan nimi Listat ennen metodien nimiä.

Esimerkki: Tunnusnumerot

Luokassa oleva staattinen muuttuja on yhteinen kaikille luokasta luoduille olioille.

Seuraavassa esimerkissä jokainen uusi olio saa yksilöllisen tunnusnumeron. Tämän mahdollistaa luokassa oleva staattinen muuttuja yhteinenLaskuri, jonka arvo kasvaa jokaisen olion luonnin jälkeen.

public class LaskuriOlio {
    private static int yhteinenLaskuri = 0;
    private int omaTunnus;

    public LaskuriOlio() {
	omaTunnus = yhteinenLaskuri;
	yhteinenLaskuri++;
    }

    public void tervehdi() {
	System.out.println("Hei, minun tunnukseni on " + omaTunnus);
    }
}

Luokan toimintaa havainnollistaa seuraava koodi:

LaskuriOlio eka = new LaskuriOlio();
LaskuriOlio toka = new LaskuriOlio();
LaskuriOlio kolmas = new LaskuriOlio();
eka.tervehdi();
toka.tervehdi();
kolmas.tervehdi();

Koodin tulostus on seuraava:

Hei, minun tunnukseni on 0
Hei, minun tunnukseni on 1
Hei, minun tunnukseni on 2
Tiedostot

4. Tiedostot

Tiedostot

Tiedostojen käyttäminen on tavallinen tapa tallentaa tietoa, jonka tulee säilyä muistissa ohjelman suorituskertojen välillä.

Tekstitiedosto muodostuu joukosta tekstirivejä. Seuraavassa on yksinkertainen tekstitiedosto, jossa on kolme riviä:

Tämä rivi aloittaa tiedoston.
Sitten tulee toinen rivi.
Kolmas rivi on jo viimeinen.

Tekstitiedostojen etuna on, että niitä on helppo käsitellä tavallisilla tekstieditoreilla ja eri ohjelmointikielissä. Kaikki tällä kurssilla käsiteltävät tiedostot ovat tekstitiedostoja.

Tiedostosta lukeminen

Seuraava ohjelma lukee ja tulostaa tekstitiedoston sisällön:

import java.util.*;
import java.io.*;

public class TiedostostaLuku {
    public static void main(String[] args) throws Exception {
        Scanner tiedosto = new Scanner(new File("esimerkki.txt"));
        while (tiedosto.hasNextLine()) {
            String rivi = tiedosto.nextLine();
            System.out.println(rivi);
        }
    }
}

Ohjelman tulostus voi olla seuraava:

Tämä rivi aloittaa tiedoston.
Sitten tulee toinen rivi.
Kolmas rivi on jo viimeinen.

Kokeile, että saat ohjelman toimimaan omalla tiedostollasi. Tässä tiedoston nimenä on esimerkki.txt, ja tiedoston tulee olla NetBeans-projektin päähakemistossa, jotta ohjelma löytää tiedoston.

Tiedoston lukeminen onnistuu Scanner-olion kautta samaan tapaan kuin käyttäjän kirjoittaman tekstin lukeminen, kunhan oliolle annetaan tiedostoa vastaava File-olio. Luokan File käyttäminen vaatii uuden import-rivin koodin alkuun.

Lisäksi metodin main loppuun on lisätty throws Exception, joka liittyy tiedostojen käsittelyssä mahdollisesti tapahtuvien virheiden käsittelyyn. Palaamme tähän asiaan seuraavassa luvussa.

Tiedostoon tallentaminen

Seuraava ohjelma tallentaa nimiä tiedostoon:

import java.io.*;
import java.util.*;

public class TiedostoonTallennus {
    public static void main(String[] args) throws Exception {
        PrintWriter tiedosto = new PrintWriter(new File("nimet.txt"));
        tiedosto.println("Aapeli");
        tiedosto.println("Maija");
        tiedosto.println("Uolevi");
        tiedosto.close();
    }
}

Tämä ohjelma saa aikaan seuraavan tiedoston:

Aapeli
Maija
Uolevi

Tässä on käytössä PrintWriter-olio, jonka avulla tiedostoon voi tulostaa samaan tapaan kuin käyttäjän näytölle. Viimeinen komento close sulkee tiedoston. Älä unohda komentoa, koska muuten osa riveistä saattaa jäädä puuttumaan tiedoston lopusta.

Huomaa, että jos tiedosto on olemassa valmiiksi, ohjelma korvaa tiedoston sisällön. Jos tiedostoa ei ole olemassa valmiiksi, ohjelma luo uuden tiedoston.

Esimerkki: Sanalista

Tiedostossa sanalista.txt on Kotimaisten kielten tutkimuskeskuksen julkaisema nykysuomen sanalista.

Seuraava ohjelma etsii listalta kaikki palindromisanat eli sanat, joiden kirjaimet ovat samat alusta loppuun ja lopusta alkuun luettuna:

import java.util.*;
import java.io.*;

public class PalindromiSanat {
    public static boolean palindromi(String sana) {
        int pituus = sana.length();
        for (int i = 0; i < pituus / 2; i++) {
            if (sana.charAt(i) != sana.charAt(pituus - i - 1)) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) throws Exception {
        Scanner tiedosto = new Scanner(new File("sanalista.txt"));
        while (tiedosto.hasNextLine()) {
            String sana = tiedosto.nextLine();
            if (palindromi(sana)) {
                System.out.println(sana);
            }
        }
    }
}

Ohjelman tulostus on seuraava:

ajaja
akka
ala
alla
autioitua
ele
enne
hah
heh
huh
hyh
häh
imaami
isi
niin
oho
olo
opo
otto
piip
pop
sadas
sammas
sees
siis
sus
suuruus
sylys
sytytys
syys
syöppöys
tuut
tyyt
tööt
utu
yty
älä
ämmä
ässä

Tiedostoformaatti

Tiedostoformaatti tarkoittaa tapaa, jolla ohjelma tallentaa tietonsa tiedostoon. Tiedostoformaatti on ohjelmoijan päätettävissä, ja vaihtoehtoja on yleensä monia. Ainoa vaatimus on, että ohjelma tallentaa ja lukee tiedot samaa formaattia käyttäen.

Esimerkki: Ostoslista

Seuraavassa ohjelmassa käyttäjä voi lisätä tavaroita ostoslistalle sekä tallentaa ja ladata ostoslistan.

import java.util.*;
import java.io.*;

public class Ostoslista {
    private static Scanner input = new Scanner(System.in);

    private static String tiedostonNimi = "ostoslista.txt";
    private static ArrayList<String> lista = new ArrayList<String>();

    private static void lisaaListalle() {
        System.out.print("Tavaran nimi: ");
        String tavara = input.nextLine();
        lista.add(tavara);
    }

    private static void tulostaLista() {
        System.out.println("Listan sisältö:");
        for (String tavara : lista) {
           System.out.println(tavara);
        }
    }

    private static void tallennaLista() throws Exception {
        PrintWriter tiedosto = new PrintWriter(new File(tiedostonNimi));
        int tavaramaara = lista.size();
        tiedosto.println(tavaramaara);
        for (String tavara : lista) {
            tiedosto.println(tavara);
        }
        tiedosto.close();
    }

    private static void lataaLista() throws Exception {
        Scanner tiedosto = new Scanner(new File(tiedostonNimi));
        lista.clear();
        int tavaramaara = Integer.parseInt(tiedosto.nextLine());
        for (int i = 0; i < tavaramaara; i++) {
            String tavara = tiedosto.nextLine();
            lista.add(tavara);
        }
    }


    public static void main(String[] args) throws Exception {
        System.out.println("1: Lisää tavara");
        System.out.println("2: Tulosta lista");
        System.out.println("3: Tallenna lista");
        System.out.println("4: Lataa lista");
        System.out.println("5: Sulje ohjelma");
        while (true) {
            System.out.print("Anna komento: ");
            String komento = input.nextLine();
            if (komento.equals("1")) {
                lisaaListalle();
            }
            if (komento.equals("2")) {
                tulostaLista();
            }
            if (komento.equals("3")) {
                tallennaLista();
            }
            if (komento.equals("4")) {
                lataaLista();
            }
            if (komento.equals("5")) {
                return;
            }
        }
    }
}

Tässä tiedostoformaattina on, että tiedoston ensimmäisellä rivillä lukee tavaroiden määrä ja kullakin seuraavalla rivillä lukee yhden tavaran nimi.

Ohjelman ensimmäinen suorituskerta:

1: Lisää tavara
2: Tulosta lista
3: Tallenna lista
4: Lataa lista
5: Sulje ohjelma
Anna komento: 1
Tavaran nimi: selleri
Anna komento: 1
Tavaran nimi: retiisi
Anna komento: 1
Tavaran nimi: nauris
Anna komento: 2
Listan sisältö:
selleri
retiisi
nauris
Anna komento: 3
Anna komento: 5

Nyt tiedoston ostoslista.txt sisältö on seuraava:

3
selleri
retiisi
nauris

Ohjelman toinen suorituskerta:

1: Lisää tavara
2: Tulosta lista
3: Tallenna lista
4: Lataa lista
5: Sulje ohjelma
Anna komento: 4
Anna komento: 2
Listan sisältö:
selleri
retiisi
nauris
Anna komento: 5
Poikkeukset

5. Poikkeukset

Poikkeus

Poikkeus on ohjelmassa esiintyvä häiriötilanne. Poikkeus keskeyttää ohjelman suorituksen, ellei ohjelma ole varautunut siihen.

Poikkeus on usein merkkinä siitä, että ohjelman koodissa on virhe. Toisaalta esimerkiksi tiedostojen käsittelyssä voi tapahtua poikkeuksia, vaikka koodi olisi virheetön.

Esimerkkejä

Seuraavassa on esimerkkejä koodinpätkistä, jotka voivat aiheuttaa poikkeuksen.

Muunnos kokonaisluvuksi

Seuraava koodi kysyy käyttäjältä kokonaislukua:

System.out.print("Anna kokonaisluku: ");
int luku = Integer.parseInt(input.nextLine());

Poikkeus tapahtuu, jos käyttäjä antaa jotain muuta kuin kokonaisluvun:

Anna kokonaisluku: abc
Exception in thread "main" java.lang.NumberFormatException: ...

Tässä poikkeuksena on NumberFormatException.

Listan indeksointi

Seuraava koodi yrittää lukea arvon listan ulkopuolelta:

ArrayList<Integer> lista = new ArrayList<Integer>();
lista.add(1);
lista.add(2);
lista.add(3);
System.out.println(lista.get(10));

Ohjelman suoritus keskeytyy seuraavasti:

Exception in thread "main" java.lang.IndexOutOfBoundsException: ...

Tässä poikkeuksena on IndexOutOfBoundsException.

Tiedostosta lukeminen

Seuraava koodi yrittää lukea kaksi riviä tiedostosta:

Scanner tiedosto = new Scanner(new File("testi.txt"));
String eka = tiedosto.nextLine();
String toka = tiedosto.nextLine();

Jos tiedostoa ei ole olemassa, käy seuraavasti:

Exception in thread "main" java.io.FileNotFoundException: ...

Jos tiedostossa on vain yksi rivi, käy seuraavasti:

Exception in thread "main" java.util.NoSuchElementException: ...

Tiedoston lukemiseen liittyvät siis ainakin poikkeukset FileNotFoundException ja NoSuchElementException.

Poikkeukseen varautuminen

Ohjelma voi varautua poikkeukseen try...catch-rakenteen avulla. Siinä try-osaan kirjoitetaan koodi, joka saattaa aiheuttaa poikkeuksen, ja catch-osaan kirjoitetaan koodi, joka käsittelee poikkeuksen.

Seuraava koodi varautuu siihen, että käyttäjä ei anna kokonaislukua:

System.out.print("Anna kokonaisluku: ");
int luku = 0;
try {
    luku = Integer.parseInt(input.nextLine());
    System.out.println("Kiitos!");
} catch (NumberFormatException e) {
    System.out.println("Luku on virheellinen!");
}
System.out.println("Luku oli " + luku);

Nyt ohjelma toimii seuraavasti:

Anna kokonaisluku: 15
Kiitos!
Luku oli 15
Anna kokonaisluku: abc
Luku on virheellinen!
Luku oli 0

Poikkeuksen sattuessa ohjelman suoritus siirtyy catch-osaan, jonka alussa lukee käsiteltävä poikkeus.

Huomaa, että muuttuja luku on pakko määritellä poikkeuksen käsittelyn ulkopuolella, jotta se on käytettävissä sen jälkeen.

Tiedostopoikkeukset

Tiedostojen käsittelyssä uhkana ovat aina poikkeukset, koska tiedostosta lukeminen tai tiedostoon kirjoittaminen voi epäonnistua monesta eri syystä.

Javassa tiedostoihin liittyvät poikkeukset on määritelty niin, että ohjelmoijan on pakko ottaa kantaa niihin. Yksi tapa on varautua poikkeuksiin samalla tavalla kuin edellisessä esimerkissä:

try {
    Scanner tiedosto = new Scanner(new File("testi.txt"));
    String rivi = tiedosto.nextLine();
    System.out.println(rivi);
} catch (Exception e) {
    System.out.println("Virhe tiedoston käsittelyssä!");
}

Tässä Exception on yleispoikkeus, jolla voi viitata kaikkiin mahdollisiin poikkeuksiin. Nyt jos tiedoston käsittelyn aikana tapahtuu mikä tahansa poikkeus, ohjelma ilmoittaa siitä selkeästi:

Virhe tiedoston käsittelyssä!

Edellisen luvun tiedostokoodeissa tällaista rakennetta ei kuitenkaan käytetty. Toinen mahdollisuus poikkeusten käsittelyyn onkin ilmoittaa metodin alussa, että metodi välittää mahdolliset poikkeukset ylemmälle tasolle metodia kutsuneeseen metodiin:

private static void lueRivi() throws Exception {
private static void main(String[] args) throws Exception {

Tämä ratkaisu käytännössä kiertää Javan vaatimuksen ottaa kantaa tiedostopoikkeuksiin, koska nyt tiedostopoikkeukset keskeyttävät ohjelman suorituksen samaan tapaan kuin muutkin poikkeukset.

Miten käsitellä poikkeukset?

Järkevä poikkeusten käsittelytapa riippuu ohjelman käyttötarkoituksesta. Esimerkiksi jos ohjelma on tarkoitettu suuren yleisön käytettäväksi, ei ole toivottavaa, että ohjelma keskeytyy poikkeukseen, jos tiedosto puuttuu. Toisaalta omissa testiohjelmissa mahdolliset poikkeukset eivät usein haittaa.

Monia poikkeuksia ei käytännössä ilmene toimivassa koodissa. Esimerkiksi koodiin ei ole järkevää lisätä poikkeuksen käsittelyä siltä varalta, että ohjelma lukee listaa väärästä kohdasta, koska tähän on syynä ohjelmointivirhe ja ratkaisu on korjata koodi toimivaksi.

Tietorakenne

6. Tietorakenteet

Tietorakenne

Tietorakenne tarkoittaa ohjelman käsittelemän tiedon säilytystapaa. Tähän mennessä olemme käyttäneet kaikkialla tietorakennetta ArrayList. Periaatteessa kaikki ohjelmat voi tehdä pelkän ArrayList-rakenteen avulla. Kuitenkin jos ohjelman käsittelemän tiedon määrä on suuri, ArrayList voi osoittautua liian hitaaksi tietorakenteeksi.

Jokaisella tietorakenteella on omat vahvuutensa ja heikkoutensa. Tärkeä ohjelmoijan taito onkin valita oikea tietorakenne käyttötilanteen mukaan. Ohjelmoinnin peruskursseilla tietorakenteet ovat sivuosassa, mutta niistä on silti hyvä tietää jotain. Aiheeseen tutustutaan syvällisemmin erillisellä Tietorakenteet-kurssilla.

Nopeusmittaus

Tietorakenteen soveltuvuutta tiettyyn tarkoitukseen voi tutkia laatimalla suuren testiaineiston ja mittaamalla, kuinka paljon sen käsittelyyn menee aikaa.

Tarkastellaan tässä esimerkkiä, jossa ohjelman käytössä ovat seuraavat aineistot:

Tehtävänä on etsiä kaikki sanat, jotka esiintyvät molemmissa sanalistoissa.

Seuraava ohjelma etsii yhteiset sanat ArrayList-rakenteen avulla. Se tallentaa ensin kaikki suomen kielen sanat ArrayList-rakenteeseen ja tulostaa sitten kaikki siinä esiintyvät englannin kielen sanat.

Ohjelman koodi on seuraava:

// aloitusaika millisekunteina
long alkuaika = System.currentTimeMillis();

// suomen kielen sanojen lukeminen
ArrayList<String> suomenSanat = new ArrayList<String>();
Scanner tiedosto1 = new Scanner(new File("suomi.txt"));
while (tiedosto1.hasNextLine()) {
    String suomenSana = tiedosto1.nextLine();
    suomenSanat.add(suomenSana);
}

// englannin kielen sanojen tarkistaminen
Scanner tiedosto2 = new Scanner(new File("englanti.txt"));
while (tiedosto2.hasNextLine()) {
    String englanninSana = tiedosto2.nextLine();
    if (suomenSanat.contains(englanninSana)) {
        System.out.println(englanninSana);
    }
}

//lopetusaika millisekunteina
long loppuaika = System.currentTimeMillis();

// suorituksen kesto
double kesto = (double)(loppuaika - alkuaika) / 1000;
System.out.println("Aika: " + kesto + " s");

Huomaa ajanmittaukseen liittyvät komennot koodin alussa ja lopussa. Metodi System.currentTimeMillis palauttaa tietokoneen sisäisen ajan millisekunteina, joten ohjelman suoritusajan voi laskea vähentämällä lopetusajan aloitusajasta.

Ohjelman tulostus voi olla seuraava:

abo
adagio
adonis
(paljon sanoja välissä)
zombie
zucchini
zulu
Aika: 139.669 s

Ohjelma löytää onnistuneesti kaikki yhteiset sanat, ja aikaa kuluu 140 sekuntia eli 2 minuuttia ja 20 sekuntia.

Tämä ei ehkä tunnu huomattavan hitaalta ohjelmalta: onhan sanojen määrä sentään suuri. Mutta valitsemalla tietorakenteen paremmin ohjelman saa toimimaan silmänräpäyksessä!

Milloin ArrayList on hidas?

Yllä olevan ohjelman pullonkaula on seuraava koodirivi:

    if (suomenSanat.contains(englanninSana)) {

Tässä metodi contains toimii käytännössä niin, että se käy ArrayList-rakenteen läpi alusta loppuun ja tarkistaa, onko jossakin kohdassa haettu arvo. Nyt jos sana on listan loppuosassa tai sitä ei esiinny listalla, ohjelma joutuu käymään koko listan läpi. Kun ohjelma joutuu tarkastamaan suuren määrän sanoja, tässä kestää kauan.

TreeSet

Tehdään yllä olevaan ohjelmaan pieni muutos. Korvataan rivi

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

rivillä

TreeSet<String> suomenSanat = new TreeSet<String>();

Tässä otettiin käyttöön tietorakenne TreeSet. Se tuntee samat metodit add ja contains kuin ArrayList, eli muu koodi voi säilyä ennallaan. Mutta onko muutoksesta hyötyä?

Muutoksesta on hyötyä:

abo
adagio
adonis
(paljon sanoja välissä)
zombie
zucchini
zulu
Aika: 0.731 s

Nyt ohjelman suoritus vie aikaa alle sekunnin, joten se nopeutui lähes 200-kertaisesti.

Miten TreeSet toimii?

TreeSet on nimensä mukaisesti puumainen tietorakenne, jonka vahvuutena on, että molemmat metodit add ja contains toimivat nopeasti.

Käytännössä TreeSet pitää sanoja muistissa seuraavaan tapaan:

Jokaisessa puun haarassa on yksi sanalistan sana. Kaikki sanat, jotka ovat ennen tätä sanaa aakkosissa, ovat vasemmassa haarassa, kun taas kaikki sanat, jotka ovat tämän sanan jälkeen aakkosissa, ovat oikeassa haarassa. Tämän ansiosta puusta voi tarkistaa hyvin nopeasti, onko siinä tiettyä sanaa.

TreeMap

TreeMap on hyödyllinen tietorakenne, johon tallennetaan tietopareja. Pari muodostuu kahdesta jäsenestä: ensimmäinen jäsen on avain ja toinen jäsen on arvo. Myös TreeMap on toteutettu puurakenteen avulla, minkä ansiosta siitä pystyy etsimään nopeasti parin arvoa sen avaimen perusteella.

Seuraavassa esimerkissä TreeMap-rakenteen avulla tehdään sanakirja:

TreeMap<String, String> sanakirja = new TreeMap<String, String>();
sanakirja.put("apina", "monkey");
sanakirja.put("banaani", "banana");
sanakirja.put("cembalo", "harpsichord");

System.out.println("Anna sana: ");
String sana = input.nextLine();
if (sanakirja.containsKey(sana)) {
    System.out.println("Käännös: " + sanakirja.get(sana));
} else {
    System.out.println("Tuntematon sana!");
}

Ohjelman toiminta on seuraava:

Anna sana: banaani
Käännös: banana
Anna sana: selleri
Tuntematon sana!

Tässä käytettiin seuraavia TreeMap-rakenteen metodeita:

Esimerkki: Sanojen lukumäärät

TreeMap-rakenteen avulla voi myös pitää kirjaa lukumääristä. Seuraava ohjelma laskee, montako kertaa käyttäjä antaa eri merkkijonoja.

TreeMap<String, Integer> lukumaarat = new TreeMap<String, Integer>();

System.out.println("Anna sanoja (tyhjä lopettaa):");
while (true) {
    String sana = input.nextLine();
    if (sana.equals("")) {
	break;
    }
    if (lukumaarat.containsKey(sana)) {
	int vanhaMaara = lukumaarat.get(sana);
	int uusiMaara = vanhaMaara + 1;
	lukumaarat.put(sana, uusiMaara);
    } else {
	lukumaarat.put(sana, 1);
    }
}

for (String sana : lukumaarat.keySet()) {
    System.out.println(sana + ": " + lukumaarat.get(sana) + " kpl");
}

Ohjelman suoritus voi olla seuraava:

Anna sanoja (tyhjä lopettaa):
apina
apina
banaani
apina
cembalo
banaani
apina

apina: 4 kpl
banaani: 2 kpl
cembalo: 1 kpl
Rajapinnat

7. Rajapinnat

Rajapinta

Rajapinta vaatii, että luokka sisältää tietyt metodit. Rajapintojen avulla samalla koodilla voidaan käsitellä erityyppisiä olioita, joissa on kuitenkin yhteisiä piirteitä.

Rajapinta määritellään muuten samalla tavalla kuin luokka, mutta sanan class tilalla on sana interface ja rajapinnan ainoa sisältö on joukko metodien runkoja.

Esimerkki: Puhuvat eläimet

Seuraava rajapinta Puhuva vaatii, että luokassa on metodi puhu:

public interface Puhuva {
    public void puhu();
}

Luokat Koira, Kissa ja Lehma toteuttavat rajapinnan Puhuva:

public class Koira implements Puhuva {
    public void puhu() {
	System.out.println("Hau hau!");
    }
}
public class Kissa implements Puhuva {
    public void puhu() {
	System.out.println("Miau!");
    }
}
public class Lehma implements Puhuva {
    public void puhu() {
	System.out.println("Ammuu!");
    }
}

Seuraava koodi esittelee luokkien käyttämistä:

ArrayList<Puhuva> elaimet = new ArrayList<Puhuva>();

elaimet.add(new Koira());
elaimet.add(new Kissa());
elaimet.add(new Lehma());

for (Puhuva elain : elaimet) {
    elain.puhu();
}

Ohjelman tulostus on seuraava:

Hau hau!
Miau!
Ammuu!

Tässä rajapinta Puhuva takaa, että kaikki rajapinnan toteuttavat luokat sisältävät metodin puhu. Tämän ansiosta metodia voidaan kutsua kaikille listan olioille, vaikka niiden tarkemmasta tyypistä ei ole tietoa.

Comparable-rajapinta

Yksi Javan valmiista rajapinnoista on Comparable. Tämä rajapinta vaatii, että luokassa on metodi compareTo, jolla voi verrata kahden luokkaa vastaavan olion järjestystä keskenään.

Metodin kutsutapa on a.compareTo(b), ja metodin palautusarvon tulisi olla seuraava:

Luokka String sisältää valmiiksi metodin compareTo. Tässä tapauksessa järjestys tarkoittaa merkkijonojen aakkosjärjestystä. Seuraava koodi esittelee metodin toimintaa:

String eka = "Aapeli";
String toka = "Maija";
String kolmas = "Aapeli";

System.out.println(eka.compareTo(toka));
System.out.println(toka.compareTo(eka));
System.out.println(eka.compareTo(kolmas));

Koodin tulostus on seuraava:

-12
12
0

Merkkijono "Aapeli" on aakkosissa ennen kuin "Maija", joten ensimmäinen arvo on negatiivinen ja toinen on positiivinen. Tässä ei ole oleellista se, että arvot sattuvat olemaan nimenomaan -12 ja 12, vaan se, että niiden negatiivisuus ja positiivisuus kertovat merkkijonojen järjestyksen.

Metodia compareTo tarvitaan esimerkiksi silloin, kun listan sisältö halutaan järjestää. Esimerkiksi seuraava koodi toimii, koska luokkaan String on toteutettu metodi compareTo:

ArrayList<String> nimet = new ArrayList<String>();
nimet.add("Uolevi");
nimet.add("Aapeli");
nimet.add("Maija");

Collections.sort(nimet);

System.out.println(nimet);

Koodin tulostus on seuraava:

[Aapeli, Maija, Uolevi]

Jos oman luokan olioita on tarpeen vertailla, metodi compareTo täytyy toteuttaa itse. Näin luokan saa toteuttamaan rajapinnan Comparable, joka on vaatimuksena esimerkiksi metodin Collections.sort käyttämiselle.

Seuraavassa esimerkissä luokkaan Henkilo on lisätty metodi compareTo. Tässä järjestys määräytyy ensisijaisesti henkilön sukunimen ja toissijaisesti henkilön etunimen perusteella.

public class Henkilo implements Comparable<Henkilo> {
    private String etunimi;
    private String sukunimi;

    public Henkilo(String etunimi, String sukunimi) {
	this.etunimi = etunimi;
	this.sukunimi = sukunimi;
    }

    public String toString() {
	return etunimi + " " + sukunimi;
    }

    public int compareTo(Henkilo toinen) {
	if (sukunimi.equals(toinen.sukunimi)) {
	    return etunimi.compareTo(toinen.etunimi);
	} else {
	    return sukunimi.compareTo(toinen.sukunimi);
	}
    }
}

Luokka hyödyntää merkkijonometodeja equals ja compareTo: jos sukunimet ovat samat, palautetaan etunimien vertailun tulos, ja muuten palautetaan sukunimien vertailun tulos.

Huomaa, että metodissa compareTo viitataan toisen olion yksityisiin muuttujiin. Tämä on mahdollista silloin, kun toisen olion luokka on sama kuin oman olion luokka.

Nyt henkilöiden lista voidan järjestää seuraavasti:

ArrayList<Henkilo> lista = new ArrayList<Henkilo>();
lista.add(new Henkilo("Roope", "Ankka");
lista.add(new Henkilo("Mikki", "Hiiri");
lista.add(new Henkilo("Aku", "Ankka");

Collections.sort(lista);

System.out.println(lista);

Koodin tulostus on seuraava:

[Aku Ankka, Roope Ankka, Mikki Hiiri]

Esimerkki: Alueet

Seuraava rajapinta Alue vastaa maantieteellistä aluetta:

public interface Alue {
    public int vakiluku();
    public void tulosta(int sisennys);
}

Metodi vakiluku palauttaa alueen asukasmäärän ja metodi tulosta tulostaa alueen tiedot halutusti sisennettynä. Esimerkkiohjelma selventää, mitä sisennys tarkoittaa käytännössä.

Määritellään rajapinnan toteuttavia luokkia:

public class Valtio implements Alue {
    private String nimi;
    private int vakiluku;

    public Valtio(String nimi, int vakiluku) {
	this.nimi = nimi;
	this.vakiluku = vakiluku;
    }

    public int vakiluku() {
	return vakiluku;
    }

    private void tulostaSisennys(int sisennys) {
	for (int i = 0; i < sisennys; i++) {
	    System.out.print(" ");
	}
    }

    public void tulosta(int sisennys) {
	tulostaSisennys(sisennys);
	System.out.println(nimi  + " (" + vakiluku + " asukasta)");
    }
}
import java.util.*;

public class Aluejoukko implements Alue {
    private String nimi;
    private ArrayList<Alue> alueet = new ArrayList<Alue>();

    public Aluejoukko(String nimi) {
	this.nimi = nimi;
    }

    public void lisaaAlue(Alue alue) {
	alueet.add(alue);
    }

    public int vakiluku() {
	int tulos = 0;
	for (Alue alue : alueet) {
	    tulos += alue.vakiluku();
	}
	return tulos;
    }

    private void tulostaSisennys(int sisennys) {
	for (int i = 0; i < sisennys; i++) {
	    System.out.print(" ");
	}
    }

    public void tulosta(int sisennys) {
	tulostaSisennys(sisennys);
	System.out.println(nimi);
	for (Alue alue : alueet) {
	    alue.tulosta(sisennys + 4);
	}
    }
}

Seuraava ohjelma esittelee luokkien käyttämistä:

Valtio suomi = new Valtio("Suomi", 5300000);
Valtio ruotsi = new Valtio("Ruotsi", 9400000);
Valtio norja = new Valtio("Norja", 4600000);
Valtio tanska = new Valtio("Tanska", 5400000);
Valtio viro = new Valtio("Viro", 1200000);
Valtio latvia = new Valtio("Latvia", 2200000);
Valtio liettua = new Valtio("Liettua", 3500000);

Aluejoukko pohjoismaat = new Aluejoukko("Pohjoismaat");
pohjoismaat.lisaaAlue(suomi);
pohjoismaat.lisaaAlue(ruotsi);
pohjoismaat.lisaaAlue(norja);
pohjoismaat.lisaaAlue(tanska);

Aluejoukko baltianMaat = new Aluejoukko("Baltian maat");
baltianMaat.lisaaAlue(viro);
baltianMaat.lisaaAlue(latvia);
baltianMaat.lisaaAlue(liettua);

Aluejoukko itamerenMaat = new Aluejoukko("Itämeren maat");
itamerenMaat.lisaaAlue(pohjoismaat);
itamerenMaat.lisaaAlue(baltianMaat);

System.out.println("Itämeren maiden väkiluku: " + itamerenMaat.vakiluku());
itamerenMaat.tulosta(0);

Ohjelman tulostus on seuraava:

Itämeren maiden väkiluku: 31600000
Itämeren maat
    Pohjoismaat
        Suomi (5300000 asukasta)
        Ruotsi (9400000 asukasta)
        Norja (4600000 asukasta)
        Tanska (5400000 asukasta)
    Baltian maat
        Viro (1200000 asukasta)
        Latvia (2200000 asukasta)
        Liettua (3500000 asukasta)

Tässä aluejoukko voi muodostua toisista aluejoukoista, mikä mahdollistaa puumaisen rakenteen toteuttamisen.

Perintä

8. Perintä

Perintä

Perintä tarkoittaa, että luokan toteutuksen lähtökohtana on toisen luokan toteutus.

Luokan määrittelyssä perintä ilmoitetaan sanan extends avulla. Lähtökohtana olevaa luokkaa sanotaan yliluokaksi, ja uutta luokkaa sanotaan aliluokaksi.

Esimerkki

Luokka Henkilo on määritelty seuraavasti:

public class Henkilo {
    private String nimi;

    public Henkilo(String nimi) {
	this.nimi = nimi;
    }
    
    public void esittaydy() {
	System.out.println("Minun nimeni on " + nimi + ".");
    }

    public String toString() {
	return nimi;
    }
}

Seuraava luokka Opiskelija perii luokan Henkilo:

public class Opiskelija extends Henkilo {
    private int opintopisteet;

    public Opiskelija(String nimi) {
	super(nimi);
	this.opintopisteet = 0;
    }

    public void esittaydy() {
	super.esittaydy();
	System.out.println("Olen saanut kokoon " + opintopisteet + " op.");
    }

    public void opiskele() {
	opintopisteet++;
    }
}

Luokkia voidaan käyttää seuraavasti pääohjelmassa:

public class Main {
    public static void main(String[] args) {
	Henkilo aapeli = new Henkilo("Aapeli");	
	System.out.println(aapeli);

	aapeli.esittaydy();

	Opiskelija maija = new Opiskelija("Maija");
	maija.opiskele();
	maija.opiskele();

	System.out.println(maija);
	maija.esittaydy();
    }
}

Ohjelman tulostus on seuraava:

Aapeli
Minun nimeni on Aapeli.
Maija
Minun nimeni on Maija.
Olen saanut kokoon 2 op.

Tässä siis luokka Henkilo on yliluokka ja luokka Opiskelija on aliluokka. Seuraavaksi katsomme tarkemmin, mitä perinnässä tapahtuu.

Mitä peritään?

Perintä tuo aliluokan osaksi yliluokan julkiset (public) muuttujat ja metodit. Konstruktorit eivät kuitenkaan periydy, vaan ne täytyy määritellä aina erikseen. Lisäksi perittyjä metodeja on mahdollista korvata uusilla toteutuksilla.

Yllä olevassa esimerkissä luokassa Opiskelija on luokasta Henkilo peritty metodi toString. Sen sijaan metodi esittaydy on korvattu omalla toteutuksella. Lisäksi luokassa on uusina asioina muuttuja opintopisteet ja metodi opiskele.

super viittaa yliluokkaan

Sanalla super voi viitata yliluokan muuttujiin ja metodeihin.

Yllä olevassa esimerkissä luokan Opiskelija konstruktori suorittaa ensin yliluokan konstruktorin. Lisäksi metodi esittaydy suorittaa ensin yliluokan metodin esittaydy.

protected-merkintä

Toisinaan yliluokasta olisi hyödyllistä periä asioita, joita ei kuitenkaan haluaisi päästää julkisiksi. Tämän mahdollistaa sana protected, jota käytetään sanan private tai public tilalla. Tällaista muuttujaa tai metodia sanotaan suojatuksi.

Esimerkiksi luokan Henkilo muuttuja nimi voisi olla suojattu:

public class Henkilo {
    protected String nimi;

    // luokan muu sisältö
}

Nyt luokassa Opiskelija voisi tehdä näin:

public class Opiskelija {
    // luokan muu sisältö

    public void esittaydy() {
	System.out.print("Minun nimeni on " + nimi + " ja olen ");
	System.out.println("saanut kokoon " + opintopisteet + " op.");
    }
}

Perintähierarkia

Luokka voi periä toisen luokan, joka taas on perinyt toisen luokan jne. Perintää voi siis tapahtua monella tasolla. Rajoituksena on kuitenkin, että luokka voi periä suoraan vain yhden luokan eli se ei voi yhdistellä monen luokan aineksia.

Javassa jokaisen luokan perimmäisenä yliluokkana on luokka Object. Kaikki uudet luokat perivät sen automaattisesti, jos ne eivät peri mitään muuta luokkaa.

Esimerkissä tilanne on seuraava:

Object -> Henkilo -> Opiskelija

Huomaa, että luokkaan voi viitata sen minkä tahansa yliluokan nimellä. Kaikki seuraavat määrittelyt ovat siis mahdollisia:

Opiskelija maija = new Opiskelija("Maija");
Henkilo maija = new Opiskelija("Maija");
Object maija = new Opiskelija("Maija");

Muuttujan tyyppi määrittää, mitä luokan ominaisuuksia on käytettävissä. Esimerkiksi jos muuttujan tyyppinä on Henkilo, metodia opiskele ei voi käyttää, vaikka muuttuja viittaisi Opiskelija-olioon.

Abstrakti luokka

Abstrakti luokka on luokka, jonka kaikkia metodeja ei ole toteutettu, vaan niistä on annettu vain runko. Abstraktia luokkaa ei voi käyttää sellaisenaan, vaan toisen luokan täytyy ensin periä se ja toteuttaa puuttuvat metodit.

Seuraavassa on abtrakti luokka Saveltaja:

public abstract class Saveltaja {
    private String nimi;

    public Saveltaja(String nimi) {
	this.nimi = nimi;
    }

    public void esittaydy() {
	System.out.println("Hei, nimeni on " + nimi);
    }

    public abstract void savella();
}

Tässä metodi esittaydy on toteutettu täydellisesti, mutta metodista savella on annettu vain runko.

Seuraavat luokat täydentävät abstraktin luokan:

public class Beethoven extends Saveltaja {
    public Beethoven() {
	super("Beethoven");
    }

    public void savella() {
	// Kohtalonsinfonia
	System.out.println("aaa f ggg e");
    }
}
public class Sibelius extends Saveltaja {
    public Sibelius() {
	super("Sibelius");
    }

    public void savella() {
	// Finlandia
	System.out.println("e d e f e d e c d d e");
    }
}

Luokkien toimintaa esittelee seuraava pääohjelma:

public class Main {
    public static void main(String[] args) {
	Beethoven beethoven = new Beethoven();
	beethoven.esittaydy();
	beethoven.savella();

	Sibelius sibelius = new Sibelius();
	sibelius.esittaydy();
	sibelius.savella();
    }
}

Ohjelman tulostus on seuraava:

Hei, nimeni on Beethoven
aaa f ggg e
Hei, nimeni on Sibelius
e d e f e d e c d d e

Rajapinnat vs. abstraktit luokat

Rajapinnat ja abstraktit luokat ovat melko samanlaisia. Erot ovat:

Käyttöliittymät

9. Käyttöliittymät

Käyttöliittymät

Kaikki ohjelmamme ovat olleet tähän mennessä tekstipohjaisia. Syynä tähän on se, että tekstipohjaiset ohjelmat soveltuvat hyvin ohjelmoinnin perusasioiden opetteluun niiden yksinkertaisuuden ansiosta. Nyt on kuitenkin tullut aika tutustua graafisen käyttöliittymän toteuttamiseen.

Käyttöliittymän ohjelmoinnissa ei ole sinänsä mitään erityisen vaikeaa, mutta siihen liittyy paljon asioita, jotka pitää vain tietää. Luokkia ja metodeita on valtava määrä, minkä vuoksi käyttöliittymän ohjelmointi voi tuntua aluksi sekavalta. Käyttöliittymät tulevat paremmin tutuksi ohjelmoinnin harjoitustyössä.

Ikkunan luonti

Seuraava ohjelma luo ikkunan ja piirtää siihen tekstiä:

import java.awt.*;
import javax.swing.*;

public class Tervehtija extends JPanel {

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
	// tähän tulee grafiikan tuottava koodi
        g.drawString("Hei hei!", 80, 30);
    }

    public static void main(String[] args) {
        JFrame ikkuna = new JFrame();
        ikkuna.setTitle("Tervehtijä"); // ikkunan otsikko
        ikkuna.setSize(320, 200);      // ikkunan koko
        ikkuna.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        ikkuna.setVisible(true);
	Tervehtija ohjelma = new Tervehtija();
        ikkuna.getContentPane().add(ohjelma);
    }
}

Ohjelman suoritus näyttää seuraavalta:

Käyttöliittymät tuovat aina mukanaan jonkin verran "ylimääräistä" koodia, josta ei tarvitse käytännössä välittää. Tärkeät kohdat main-metodissa ovat rivit, jotka määrittävät ikkunan otsikon (Tervehtijä) sekä ikkunan koon (320 x 200 pikseliä). Lisäksi toiseksi viimeisellä rivillä esiintyy luokan nimi.

Ikkunaan piirtäminen tapahtuu metodissa paint. Tämä metodi aktivoituu aina silloin, kun ikkunan sisältö täytyy piirtää uudestaan. Metodin ensimmäinen rivi super.paint(g) käytännössä tyhjentää ikkunan piirtämistä varten. Tämän jälkeen tulevat omat piirtokomentomme. Tässä tapauksessa komento drawString piirtää halutun merkkijonon haluttuun paikkaan.

Piirtäminen

Seuraava ohjelma piirtää ikkunaan erilaisia kuvioita:

import java.awt.*;
import javax.swing.*;

public class Piirtaja extends JPanel {

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.setColor(Color.RED);
        g.drawLine(20, 30, 200, 140);
        g.drawRect(60, 40, 140, 90);
        g.setColor(Color.BLUE);
        g.drawOval(160, 10, 90, 90);
        g.setColor(Color.GREEN);
        g.fillRect(30, 70, 70, 80);
        g.fillOval(220, 100, 40, 30);
    }

    public static void main(String[] args) {
        JFrame ikkuna = new JFrame();
        ikkuna.setTitle("Piirtäjä");
        ikkuna.setSize(320, 200);
        ikkuna.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        ikkuna.setVisible(true);
	Piirtaja ohjelma = new Piirtaja();
        ikkuna.getContentPane().add(ohjelma);
    }
}

Ohjelman suoritus näyttää seuraavalta:

Tässä ovat käytössä seuraavat komennot:

Näppäimistön käsittely

Jos ohjelman on tarkoitus tunnistaa, kun käyttäjä painaa näppäimistön nappia, sen täytyy toteuttaa rajapinta KeyListener. Tämän jälkeen metodi keyPressed aktivoituu aina, kun käyttäjä painaa nappia. Seuraava ohjelma tulostaa komentoriville käyttäjän painamat napit.

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Nappaimisto extends JPanel implements KeyListener {
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
    }

    public void keyTyped(KeyEvent e) {
    }

    public void keyPressed(KeyEvent e) {
        int nappi = e.getKeyCode();
        System.out.println("Painoit nappia " + nappi);
    }

    public void keyReleased(KeyEvent e) {
    }

    public static void main(String[] args) {
	JFrame ikkuna = new JFrame();
        ikkuna.setTitle("Näppäimistön käsittely");
        ikkuna.setSize(320, 200);
        ikkuna.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        ikkuna.setVisible(true);
        Nappaimisto ohjelma = new Nappaimisto();
        ikkuna.getContentPane().add(ohjelma);
        ikkuna.addKeyListener(ohjelma); // uusi rivi!
    }
}

Esimerkiksi jos käyttäjä painaa ensin nappia A, sitten nappia Shift ja lopuksi nappia Esc, ohjelma tulostaa seuraavasti:

Painoit nappia 65
Painoit nappia 16
Painoit nappia 27

Huomaa ohjelman tulostus "tavanomaisessa" paikassa NetBeans-ikkunan alareunassa.

Siis esimerkiksi napin A koodi on 65. Helpoin tapa selvittää eri nappien koodit on käyttää yllä olevaa ohjelmaa.

Esimerkki: Näppäimistöohjaus

Seuraavassa esimerkissä käyttäjä voi ohjata näppäimistöllä ikkunassa liikkuvaa neliötä.

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Ohjaus extends JPanel implements KeyListener {
    int nelionX = 100;
    int nelionY = 100;

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.drawRect(nelionX, nelionY, 50, 50);
    }

    public void keyPressed(KeyEvent e) {
        int nappi = e.getKeyCode();
        if (nappi == 37) { // vasemmalle
            nelionX = nelionX - 10;
        }
        if (nappi == 38) { // ylös
            nelionY = nelionY - 10;
        }
        if (nappi == 39) { // oikealle
            nelionX = nelionX + 10;
        }
        if (nappi == 40) { // alas
            nelionY = nelionY + 10;
        }
        repaint();
    }

    public void keyTyped(KeyEvent e) {
    }

    public void keyReleased(KeyEvent e) {
    }

    public static void main(String[] args) {
	JFrame ikkuna = new JFrame();
        ikkuna.setTitle("Näppäimistöohjaus");
        ikkuna.setSize(320, 200);
        ikkuna.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        ikkuna.setVisible(true);
        Ohjaus ohjelma = new Ohjaus();
        ikkuna.getContentPane().add(ohjelma);
        ikkuna.addKeyListener(ohjelma);
    }
}

Muuttujissa nelionX ja nelionY on neliön sijainti ruudulla. Neliö piirretään muuttujien perusteella oikeaan paikkaan metodissa paint. Näppäimistön käsittelyssä sijainti muuttuu, jos käyttäjä painaa nuolinäppäimiä. Huomaa komento repaint näppäimistön käsittelyn lopussa: se piirtää ikkunan uudestaan.

Hiiren käsittely

Hiiren käsitteleminen tapahtuu hyvin samaan tapaan kuin näppäimistön käsitteleminen. Tällä kertaa toteutettava rajapinta on MouseListener. Seuraava ohjelma tulostaa komentoriville, mistä kohdasta ikkunaa ja millä napilla käyttäjä painaa hiirellä.

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Hiiri extends JPanel implements MouseListener {

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
    }

    public void mouseClicked(MouseEvent e) {
        int x = e.getX();
        int y = e.getY();
        int nappi = e.getButton();
        System.out.println("Kohta: (" + x + ", " + y + ")");
        System.out.println("Nappi: " + nappi);
    }

    public void mousePressed(MouseEvent e) {
    }

    public void mouseReleased(MouseEvent e) {
    }

    public void mouseEntered(MouseEvent e) {
    }

    public void mouseExited(MouseEvent e) {
    }

    public static void main(String[] args) {
        JFrame ikkuna = new JFrame();
        ikkuna.setTitle("Hiiren käsittely");
        ikkuna.setSize(320, 200);
        ikkuna.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        ikkuna.setVisible(true);
        Hiiri ohjelma = new Hiiri();
        ikkuna.getContentPane().add(ohjelma);
        ikkuna.addMouseListener(ohjelma);
    }
}

Esimerkiksi jos käyttäjä painaa kahdesti vasemmalla näppäimellä ja kerran oikealla näppäimellä, tulostus voi olla seuraava:

Kohta: (45, 70)
Nappi: 1
Kohta: (272, 146)
Nappi: 1
Kohta: (98, 156)
Nappi: 3

Viesti-ikkunat

Seuraavat lyhyet komennot ovat hyödyllisiä:

JOptionPane.showMessageDialog(this, "Hei hei!");

Tämä komento näyttää halutun viestin pikkuikkunassa:

String nimi = JOptionPane.showInputDialog("Anna nimesi:");

Tämä komento kysyy käyttäjältä jonkin asian muuttujaan:

Ohjelman sulkeminen

Joskus ohjelman pitäisi sulkeutua tietyn asian tapahtuessa. Tähän riittää seuraava komento:

System.exit(0);

Ajastin

Ajastin (Timer) aktivoi metodin actionPerformed säännöllisin väliajoin. Ajastimen määrittelyssä ilmoitetaan millisekunnissa, kuinka usein metodi tulee aktivoida. Nyt ohjelman tulee toteuttaa rajapinta ActionListener. Seuraava ohjelma tulostaa komentoriville viestin sekunnin välein.

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Ajastin extends JPanel implements ActionListener {

    public Ajastin() {
        Timer ajastin = new Timer(1000, this);
        ajastin.start();
    }

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
    }

    public void actionPerformed(ActionEvent e) {
        System.out.println("Aikaa kului sekunti");
    }

    public static void main(String[] args) {
        JFrame ikkuna = new JFrame();
        ikkuna.setTitle("Ajastin");
        ikkuna.setSize(320, 200);
        ikkuna.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        ikkuna.setVisible(true);
        Ajastin ohjelma = new Ajastin();
        ikkuna.getContentPane().add(ohjelma);
    }
}

Esimerkiksi kolmen sekunnin kuluttua tulostus on ollut seuraava:

Aikaa kului sekunti
Aikaa kului sekunti
Aikaa kului sekunti

Tässä ajastin määritellään luokan konstruktorissa. Konstruktoriin voi yleensäkin laittaa koodia, joka suoritetaan ohjelman käynnistyessä.

Esimerkki: Liikkuva neliö

Seuraava ohjelma toteuttaa ajastimen avulla animaation, jossa neliö liikkuu ikkunan halki.

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class LiikkuvaNelio extends JPanel implements ActionListener {
    int nelionX = 100;
    int nelionY = 100;

    public LiikkuvaNelio() {
        Timer ajastin = new Timer(50, this);
        ajastin.start();
    }

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.drawRect(nelionX, nelionY, 50, 50);
    }

    public void actionPerformed(ActionEvent e) {
        nelionX = nelionX + 1;
        repaint();
    }

    public static void main(String[] args) {
        JFrame ikkuna = new JFrame();
        ikkuna.setTitle("Liikkuva neliö");
        ikkuna.setSize(320, 200);
        ikkuna.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        ikkuna.setVisible(true);
        LiikkuvaNelio ohjelma = new LiikkuvaNelio();
        ikkuna.getContentPane().add(ohjelma);
    }
}

Esimerkki: Pomppiva pallo

Seuraava ohjelma toteuttaa ajastimen avulla animaation, jossa ikkunassa pomppii ikkunan reunoista kimpoileva pallo.

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class PomppivaPallo extends JPanel implements ActionListener {
    int palloX = 100;
    int palloY = 100;
    int muutosX = 20;
    int muutosY = 20;

    public PomppivaPallo() {
        Timer ajastin = new Timer(50, this);
        ajastin.start();
    }

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.fillOval(palloX, palloY, 20, 20);
    }

    public void actionPerformed(ActionEvent e) {
        palloX = palloX + muutosX;
        palloY = palloY + muutosY;
        if (palloX <= 0 || palloX >= 280) {
            muutosX = -muutosX;
        }
        if (palloY <= 0 || palloY >= 160) {
            muutosY = -muutosY;
        }
        repaint();
    }

    public static void main(String[] args) {
        JFrame ikkuna = new JFrame();
        ikkuna.setTitle("Pomppiva pallo");
        ikkuna.setSize(320, 200);
        ikkuna.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        ikkuna.setVisible(true);
        PomppivaPallo ohjelma = new PomppivaPallo();
        ikkuna.getContentPane().add(ohjelma);
    }
}