Luokkamuuttujat ilmentymämetodeissa
Luokkamuuttujia voidaan tutkia ja niiden arvoa voidaan muuttaa myös luokan ilmentymämetodeissa. Esimerkiksi luokka Opiskelija voitaisiin kirjoittaa myös seuraavasti:
class Opiskelija {
private String nimi;
private int pisteet;
private static int lkm = 0;
public Opiskelija(String nimi) {
this.nimi = nimi;
this.pisteet = 0;
lkm++;
}
public String mikaNimi() {
return nimi;
}
public int mitkaPisteet() {
return pisteet;
}
public int annaLkm() {
return lkm;
}
public boolean asetaPisteet(int uudet) {
if (uudet >= 0 && uudet <= 100 ) {
pisteet = uudet;
return true; // onnistui
}
else return false; // virheelliset
}
public String toString() {
return this.nimi+ "\t\t" + this.pisteet;
}
}
Tällöin metodia annaLkm() ei voi kutsua kirjoittamalla Opiskelija.annaLkm(), vaan metodin annaLkm() on aina kohdistuttava johonkin Opiskelija-olioon, esimerkiksi:
Opiskelja opisk1;
opisk1 = new Opiskelija("Anna Virtanen");
lkm = opisk1.annaLkm();
Aikaisemmin esitetty Opiskelija-luokka, jossa
lkm-kentän arvo selvitettiin luokkametodilla, on kuitenkin paljon selkeämpi.
Luokan lataaminen ja olion luonti
Kun suoritettava ohjelma viittaa ensimmäisen kerran johonkin luokkaan, tämä luokka ladataan muistiin. Luokkamuuttujille annetaan alkuarvot ja mahdolliset staattiset alustuslohkot suoritetaan.
Staattisella alustuslohkolla voi esimerkiksi asettaa staattisiin kenttiin arvoja, joiden ilmaisemiseen alustuslausekkeet eivät riitä.
Staattisella alustuslohkolla ei ole nimeä.
static int[] taulu;
static {
taulu = new int[8];
for (int i = 0; i < taulu.length; i++)
taulu[i] = i * i;
}
Kun suoritettava ohjelma luo olion eli luokan ilmentymän new-operaatiolla,
ilmentymämuuttujat saavat alkuarvonsa ja
tämän jälkeen suoritetaan joku konstruktori.
Konstruktoreista
Konstruktori suoritetaan luokan ilmentymää luotaessa ilmentymämuuttujien alkuavon asettamisen jälkeen.
Jos luokassa ei ole määritelty konstruktoria, suoritetaan parametriton oletuskonstruktori, joka vain käynnistää yliluokan konstruktorin (tarkemmin periytymisen yhteydessä)
Konstruktorin nimi on sama kuin luokan nimi. Konstruktorilla ei ole tyyppiä.
Konstruktori voi viitata sekä ilmentymä- että luokkamuuttujiin.
Konstruktori voi ensimmäisenä toimenaan kutsua toista saman luokan konstruktoria ilmauksella this(mahdolliset parametrit)
public class Varipiste {
private int xKoord;
private int yKoord;
private String vari;
public Varipiste(int x, int y) {
xKoord = x;
yKoord = y;
vari = "valkoinen";
}
public Varipiste(int x, int y, String va){
this(x, y);
vari = va;
}
}
Lopetusmetodi
Lopetusmetodilla (finalizer) voidaan vapauttaa luokan varaamia resursseja. Lopetusmetodi suoritetaan sen jälkeen, kun viimeinenkin viittaus olioon on hävinnyt, mutta ennen itse olion hävittämistä. Ohjelmoijalla ei ole kontrollia tarkkaan ajoitukseen.
Lopetusmetodi on aina muotoa
protected void finalize() throws Throwable {
super.finalize();
// ... muuta koodia
}
Normaalisti automaattinen roskankeruu riittää hoitamaan resurssien vapauttamisen eikä lopetusmetodia tarvita. Automaattinen roskienkerääjä hävittää automaattisesti kaikki ne oliot, joihin ei ole viitettä.
Julkiset kentät
Tähänastisissa esimerkeissä luokkien kentät ovat olleet aina yksityisiä eli private-määriteltyjä. Kenttiin on päässyt suoraan käsiksi vain saman luokan metodeista.
Luokan kenttä voidaan määritellä myös julkiseksi. Tällöin luokan kenttä on käytettävissä suoraan myös luokan ulkopuolelta.
Julkisten kenttien avulla voidaan mm. tehdä rakenne, jota voi käyttää C.n ja Pascalin tietueiden tapaan
public class Henkilo {
public String nimi;
public double pituus;
public int ika;
}
public class HenkiloEsim {
public static void main(String[] args) {
Henkilo pomo, sihteeri, paasihteeri;
pomo = new Henkilo();
pomo.nimi = "Maija";
pomo.pituus = 163;
pomo.ika = 24;
sihteeri = new Henkilo();
sihteeri.nimi = "Pekka";
sihteeri.pituus = 184;
sihteeri.ika = 48;
paasihteeri = sihteeri;
++paasihteeri.ika;
}
}
Tällöin kuitenkin menetetään suuri osa olio-ohjelmoinnin hyödyistä. Yleensä yksityisten kenttien ja aksessorien käyttö johtaa huomattavasti selkeämpään ohjelmaan.
Jos muuttujia määritellään ilman näkyvyydensäätelymäärettä, niin kentät ovat näkyvissä siinä pakkauksessa, johon luokka itse kuuluu.
Pakkaus: ohjelmoija voi määritellä mihin pakkaukseen luokka kuuluu. Samaan pakkaukseen voi kuulua useita luokkia. Jos ohjelmoija ei ole määritellyt pakkausta, luokka kuuluu nimettömään pakkaukseen, joka yleensä tarkoittaa kaikkia luokkia, jotka ovat samassa hakemistossa.
Linkitetyt rakenteet
Ohjelmoidessa joudutaan usein tilanteeseen, jossa etukäteen ei tiedetä käsiteltävien alkioiden määrää. Esimerkiksi: käyttäjä syöttää mielivaltaisen määrän opiskelijoiden nimiä. Kun kaikki nimet on syötetty, ohjelma tulostaa nimet aakkosjärjestyksessä.
Taulukko ei käy, koska opiskelijoiden määrää ei tiedetä etukäteen.
Usein linkitetty lista on kätevä ratkaisu.
Linkitetty lista koostuu alkioista. Kukin alkio sisältää tietoa (esimerkissä viitteen opiskelijan nimeen) ja viitteen listan seuraavaan alkioon.
Listan viimeisen alkioon seuraajaan viittavan kentän arvona on null.
Ensimmäisessä esimerkissä tutkitaan yksinkertaisuuden vuoksi listaa, jonka alkiot sisältävät lukuja. Siinä uusia alkioita voidaan lisätä listan alkuun tai loppuun.
Myöhemmin tutkitaan esimerkkiä, joissa alkiot sisältävät viitteitä opiskelijoiden nimiin ja nimet lisätään listaan niin, että ne ovat aakkosjärjestyksessä.
Kokonaislukuja sisältävä lista
Luokalla Lista on kentät
private int tieto;
private Lista linkki;
joista edellistä käytetään alkiossa olevan kokonaisluvun säilyttämiseen ja jälkimmäinen sisältää viitteen listan seuraavaan alkioon.
Lista kannattaa toteuttaa niin, että listassa on yksi ylimääräinen alkio, ns. tunnussolmu alussa. Tällöin metodia kutsuessa ei tarvitse erikseen tarkistaa, onko lista tyhjä. Metodit laaditaan niin, että ne tavallaan hyppäävät tunnussolmun yli.
Metodit:
Konstruktori luo uuden solmun
public void vieAlkuun(int uusiTieto) lisää parametrina annettavan luvun listan alkuun (tunnussolmun jälkeen)
public void vieLoppuun(int uusiTieto) lisää parametrina annettavan alkion listan loppuun
public int pituus() palauttaa listan pituuden
public int monentenako(int x) kertoo, monentenako parametrina annettu luku on listassa. Jos lukua ei ole listassa, metodi palauttaa arvon -1
public String toString() muodostaa listasta merkkijonoesityksen tulostuksen avuksi. Ei toimi, jos listassa on jonkun ohjelmointivirheen takia sykli
// Tunnussolmullinen linkitetty lista
class Lista {
private int tieto;
private Lista linkki;
public Lista() {
linkki = null;
}
public void vieAlkuun(int uusiTieto) {
Lista uusi;
uusi = new Lista();
uusi.tieto = uusiTieto;
uusi.linkki = this.linkki;
this.linkki = uusi;
}
public void vieLoppuun(int uusiTieto) {
Lista uusi, p;
uusi = new Lista();
uusi.tieto = uusiTieto;
uusi.linkki = null;
p = this;
while (p.linkki != null)
p = p.linkki;
p.linkki = uusi;
}
public int pituus() {
int lkm = 0;
Lista p;
p = this.linkki;
while (p != null) {
lkm ++;
p = p.linkki;
}
return lkm;
}
public int monentenako(int x) {
int lkm = 0;
Lista p;
p = this.linkki;
while (p != null) {
lkm++;
if (p.tieto == x)
return lkm;
p = p.linkki;
}
return -1;
}
public String toString() {
Lista p;
String mjono = "";
p = this.linkki;
while (p != null) {
mjono = mjono + "-> (" + p.tieto + ") ";
p = p.linkki;
}
return mjono + "-||";
}
}
public class ListaEsim {
public static void main(String[] args) {
Lista lista1;
lista1 = new Lista();
System.out.println(lista1);
System.out.println("pituus: " +
lista1.pituus());
lista1.vieAlkuun(7);
System.out.println(lista1);
System.out.println("pituus: " +
lista1.pituus());
lista1.vieAlkuun(40);
System.out.println(lista1);
System.out.println("pituus: " +
lista1.pituus());
lista1.vieLoppuun(41);
System.out.println(lista1);
System.out.println("pituus: " +
lista1.pituus());
System.out.println("Monentenako 7? " +
lista1.monentenako(7));
System.out.println("Monentenako 40? " +
lista1.monentenako(40));
System.out.println("Monentenako 41? " +
lista1.monentenako(41));
System.out.println("Monentenako 3? " +
lista1.monentenako(3));
}
}
Järjestetty lista
Tarkastellaan seuraavaksi esimerkkiä, jossa nimiä säilytetään listassa aakkosjärjestyksessä. Uusi nimi viedään listaan aakkosjärjestyksen mukaiselle paikalleen (paikka on ensin etsittävä.)
Pääohjelma lukee päätteeltä mielivaltaisen määrän nimiä ja tallentaa ne listaan. Lopuksi ohjelma tulostaa syötetyt nimet aakkosjärjestyksessä.
Luokalla nimilista on kentät
private String nimi;
private Nimilista linkki;
Metodeista on otettu mukaan vain ne, joita tarvitaan pääohjelmassa:
konstrukori luo uuden solmun
public void lisaaAlkio(String lisattava) lisaa parametrina annetun alkion listaan oikealle paikalle
public String toString() muodostaa listasta merkkijonoesityksen. Kukin alkio on nyt omalla rivillään.
// Tunnussolmullinen linkitetty lista
class Nimilista {
private String nimi;
private Nimilista linkki;
public Nimilista() {
linkki = null;
}
public void lisaaAlkio(String lisattava) {
Nimilista p, uusi;
p = this;
while(p.linkki != null &&
lisattava.compareTo(p.linkki.nimi) > 0)
p = p.linkki;
uusi = new Nimilista();
uusi.nimi = lisattava;
uusi.linkki = p.linkki;
p.linkki = uusi;
}
public String toString() {
Nimilista p;
String mjono = "\n";
p = this.linkki;
while (p != null) {
mjono = mjono + p.nimi + "\n";
p = p.linkki;
}
return mjono;
}
}
public class NimilistaEsim {
public static void main(String[] args) {
Nimilista nimet;
String rivi;
nimet = new Nimilista();
System.out.println("Anna nimiä, lopeta painamalla ctrl-d");
rivi = Lue.rivi();
while (rivi != null) {
nimet.lisaaAlkio(rivi);
System.out.println("Anna seuraava");
rivi = Lue.rivi();
}
System.out.println("Syötetyt nimet " + nimet);
}
}
Listan käytöstä
Listoja ohjelmoidessa on helppo tehdä virheitä. Ohjelmia kirjoitettaessa on syytä tarkistaa, että metodit selviytyvät oikein myös seuraavista tilanteista:
muutos tapahtuu listan alkuun
muutos tapahtuu listan loppuun
käsiteltävä lista on tyhjä
Lisäksi kannattaa tarkistaa, ettei vahingossa aiheuta listaan syklejä.
Jono ja pino
Linkitettyjen listojen avulla voidaan helposti toteuttaa kaksi yleisesti käytettyä tietorakennetta, jono ja pino.
Jono on tietorakenne, johon saapuneet alkiot käsitellään siinä järjestyksessä kuin ne ovat saapuneetkin. Eli jonoon ensiksi tullut alkio poistetaan siitä ja käsitellään ensimmäisenä, toiseksi tullut toisena jne.
Pino toimii päinvastoin. Lisättävä alkio pannaan pinoon päällimmäiseksi. Kun joku alkio otetaan käsittelyyn, otetaan aina pinon päällimmäinen (viimeisenä lisätty) alkio.
Molemmissa rakenteissa alkioiden lisäyksiä ja poistoja voi olla mielivaltaisessa järjestyksessä (tyhjästä jonosta tai pinosta ei voi kuitenkaan poistaa).
Jonon toteutus
Tyypilliset jonometodit ovat
lisaa (enqueue) lisää jonon loppuun parametrina annettavan alkion
poista (dequeue) poistaa jonosta sen ensimmäisen alkion. Metodi palauttaa arvonaan poistetun arvon tai viitteen siihen.
tyhja (empty) palauttaa arvon true, jos jono on tyhjä, ja muussa tapauksessa arvon false
Jono toteutetaan linkitetyn listan avulla. Koska uudet alkiot lisätään aina listan loppuun, on pidettävä yllä tietoa siitä, missä listan loppu sijaitsee. Muuten koko jono jouduttaisiin käymään läpi aina lisäyksen yhteydessä.
Jonon toteutus on jaettu kahteen luokkaan:
Luokan Jono kenttinä ovat viitteet jonon ensimmäiseen ja viimeiseen alkioon. Luokan metodeina ovat varsinaiset jono-operaatiot lisaa, poista ja tyhja.
Luokka JonoAlkio sisältää yksittäisen alkion tarvitsemat kentät sekä listan käsittelyyn liittyvät metodit.
Jonoa edustavassa listassa ei tarvita tunnussolmua, koska luokan Jono kenttien arvojen avulla voidaan helposti käsitellä tyhjiäkin jonoja.
Seuraavassa esimerkissä on toteutettu merkkijonoja sisältävä jono. Sitä on käytetty pääohjelmassa, joka lukee käyttäjän antamia nimiä mielivaltaisen määrän ja tulostaa ne lopuksi annetussa järjestyksessä.
class Jono {
private JonoAlkio alku, loppu;
public boolean tyhja() {
return alku == null;
}
public void lisaa(String mjono) {
JonoAlkio uusi;
uusi = new JonoAlkio(mjono);
if (alku != null) {
loppu.lisaaSeuraaja(uusi);
loppu = uusi;
}
else {
loppu = uusi;
alku = uusi;
}
}
public String poista() {
String mjono;
if (alku != null) {
mjono = alku.annaTieto();
alku = alku.annaSeuraaja();
return mjono;
}
else
return null;
}
}
class JonoAlkio {
private String tieto;
private JonoAlkio linkki;
public JonoAlkio(String mjono) {
tieto = mjono;
linkki = null;
}
public String annaTieto() {
return tieto;
}
public JonoAlkio annaSeuraaja() {
return linkki;
}
public void lisaaSeuraaja(JonoAlkio alkio) {
linkki = alkio;
}
}
public class JonoEsim {
public static void main(String[] args) {
Jono nimet;
String rivi;
nimet = new Jono();
System.out.println("Anna nimiä, lopeta painamalla ctrl-d");
rivi = Lue.rivi();
while (rivi != null) {
nimet.lisaa(rivi);
System.out.println("Anna seuraava");
rivi = Lue.rivi();
}
System.out.println("Syötetyt nimet ");
while (!nimet.tyhja())
System.out.println(nimet.poista());
}
}
Pino
Pino on tietorakenne, josta voidaan poistaa aina sinne viimeiseksi lisätty alkio.
Tyypilliset pino-operaatiot ovat
push lisää parametrina annetun alkion pinon päällimmäiseksi
pop poistaa pinon päällimmäisen arvon ja palauttaa sen arvon
tyhja palauttaa arvon true, jos pino on tyhjä, ja arvon false muuten
Pino on jonoa helpompi toteuttaa linkitetyn listan avulla. Koska alkioita poistetaan päinvastaisessa järjestyksessä kuin niitä lisätään, voidaan uusi alkio lisätä aina listan ensimmäiseksi ja pop-operaatio voi poistaa listan ensimmäisen alkion. Näin ei tarvita viitettä listan loppuun.
Seuraavassa esimerkissä käytetään kuitenkin Javan valmiin luokan Stack operaatioita, eikä toteuteta pinoa itse.
Esimerkkiohjelma kääntää jokaisen syötteen sanan takaperin.
import java.util.Stack;
public class Takaperin {
public static void main (String[] args) {
Stack sana;
String rivi;
int ind = 0, pit;
System.out.println("Anna rivi");
rivi = Lue.rivi();
pit = rivi.length();
sana = new Stack();
System.out.println("Sanat takaperin");
while (ind < pit) {
while(ind < pit &&
rivi.charAt(ind) != ' ') {
sana.push(new Character(rivi.charAt(ind)));
ind++;
}
while (!sana.empty())
System.out.print(((Character) sana.pop()).charValue());
System.out.print(" ");
ind++;
}
System.out.println();
}
}
Tehtävässä ei ole käytetty kirjaimille tyyppiä char, vaan ne ovat luokan Character olioita, koska Stack-olioon pitää tallentaa viitteitä olioihin.
Luokka ohjelmakirjastona
Luokkaan voi kerätä yleiskäyttöisiä vakioita ja algoritmeja. Esimerkiksi Math-luokka sisältää vaikiot PI ja E sekä joukon matemaattisia funktioita (esim. neliöjuuren laskeminen, potenssiin korotus ja trigonometriset funktiot).
Tällaisesta luokasta ei voi luoda ilmentymiä (olioita).
Kirjastoluokan kirjoittaminen
luokka määritellään final-määreellä, jotta sille ei voisi luoda aliluokkia
luokkaan määritellään yksityinen (private) konstruktori, jotta siitä ei voisi luoda ilmentymiä
kentät ja metodit määritellään luokkamuuttujina ja -metodeina (static)
luokan käyttäjälle tarkoitetut metodit ja vakiot ovat julkisia (public), luokan toteutuksen apuvälineet yksityisiä (private)
public final class Kirjasto {
private Kirjasto() { }
public static final double VAKIO = 12.3;
public static int metodi1(int param) {
...
}
...
private static void apumetodi() {
...
}
}
Periytyminen
Periytyminen on sitä, että luokan piirteitä (kenttiä ja metodeja) siirretään toiselle luokalle.
Esimerkki:
Määritellään ensin luokka Piste, jolla on
kentät x ja y
konstruktori
aksessorit annaX, annaY sekä siirry
metodi toString tulostuksen avuksi
Määrittelyn jälkeen voidaan luoda Piste-olioita, joita voidaan käyttää normaaliin tapaan.
Lisäksi halutaan määritellä luokka VariPiste, jolla on samat ja kentät kuin luokalla Piste, mutta sen lisäksi kenttä vari pisteen väriä varten ja metodi uusiVari värin vaihtamista varten. Lisäksi luokan konstruktorissa ja toString-metodissa on otettu vari-kenttä huomioon.
Luokkaa VariPiste ei tarvitse nyt kirjoittaa alusta alkaen uudelleen, vaan luokka VariPiste voi periä luokan Piste. Jokainen luotava VariPiste-olio on tällöin myös
Piste-olio, johon voidaan käyttää Piste-luokan metodeita.
Antavaa luokkaa (Piste) kutsutaan yliluokaksi (superclass). Saavaa luokkaa (VariPiste) kutsutaan aliluokaksi (subclass). Aliluokkaa kutsutaan joskus myös laajennetuksi (extended) tai johdetuksi (derived) luokaksi.
class Piste {
private int x, y;
// ---- konstruktori -----
public Piste(int xKoord, int yKoord) {
x = xKoord;
y = yKoord;
}
// ----- aksessorit -----
public int annaX() {
return x;
}
public int annaY() {
return y;
}
public void siirry(int dx, int dy) {
x += dx;
y += dy;
}
// ----- muut metodit -----
public String toString() {
return "(" + x + "," + y + ")";
}
}
class VariPiste extends Piste {
// ---- lisäkenttä -----
private String vari;
// ---- konstruktori ----
VariPiste(int xKoord, int yKoord, String vari1) {
super(xKoord, yKoord);
vari = vari1;
}
// ---- lisämetodi -----
public void uusiVari(String vari) {
this.vari = vari;
}
// ---- korvaava metodi ----
public String toString() {
return super.toString() +": " + vari;
}
}
public class VariPisteEsim {
public static void main(String[] args) {
Piste p1, p2;
VariPiste vp1;
p1 = new Piste(3, 4);
p2 = new Piste(-1, -5);
System.out.println(p1 + ", " + p2);
p1.siirry(-2, 3);
System.out.println(p1 + ", " + p2);
vp1 = new VariPiste(2, 4, "musta");
System.out.println(vp1);
vp1.uusiVari("punainen");
System.out.println(vp1);
vp1.siirry(-2, -1);
System.out.println(vp1);
}
}