Luokat ja oliot
Tähän asti luokkaa on käytetty vain pääohjelman ja metodien toteuttamiseen. Tämä on vain pieni osa siitä, mihin luokkia voi käyttää.
Käytännössä käsitellään olentoja, jotka ovat huomattavasti monimutkaisempia kuin mitä yhdellä yksinkertaista tyyppiä olevalla muuttujalla voi esittää.
Miten esimerkiksi kuvailisit pallon tietokoneohjelmassa?
Pallolla on ominaisuuksia:
säde
kimmoisuus
väri
Pallolla voi tehdä erilaisia asioita:
heittää
pompottaa
vierittää
Oliosuuntautuneessa kielessä käsiteltävän olennon ominaisuuksien kuvaamiseen tarvittavat tietorakenteet ja olennolle mahdolliset toimenpiteet kootaan yhteen luokaksi.
Luokan avulla voidaan luoda olioita (esimerkiksi palloja, joilla on eri säde, väri ja kimmoisuus), joita voidaan käsitellä ohjelmassa muuttujien tavoin.
Esimerkki: virvoitusjuoma-automaatti
Tarkastellaan yksinkertaista virvoitusjuoma-automaattia, josta voi ostaa yhdenlaisia limsapulloja.
Mitä tietoja tarvitaan, jotta pystyttäisiin kuvaamaan limsa-automaatin toimintaa?
Automaatissa olevien limsapullojen määrä
Automaatissa olevien rahojen määrä
(Myös limsan hinta on tarpeellinen tieto, mutta se ei muutu yhtä usein kuin kaksi edellistä.)
Mitä toimenpiteitä limsa-automaatille voidaan suorittaa?
Limsapullon osto
Limsapullojen lisäys
Rahojen tyhjennys
Kootaan nämä määrittelyt yhdeksi LimsaAutomaatti-luokaksi. Luokan määrittely vastaa automaatin suunnitelmia (piirustuksia).
Yksien piirustusten avulla voidaan rakentaa monta limsa-automaattia, esimerkiksi yksi sähköosastolle ja toinen rakennusosastolle.
Vastaavasti yhden luokan avulla voidaan luoda monta oliota, esimerkiksi yksi olio kuvaamaan sähköosastolle tulevaa automaattia ja toinen olio kuvaamaan rakennusosastolle tulevaa automaattia.
Olio
on luokan ilmentymä.
Limsa-automaatti, yksinkertaistettu versio
Aloitetaan varsinainen ohjelman kirjoittaminen yksinkertaisemmalla versiolla limsa-automaatista: tämä automaatti antaa limsapullon ilmaiseksi. Rahan käsittelyä ei siis vielä tarvita.
Tarvittavat tiedot:
limsapullojen määrä (kokonaisluku)
Mahdolliset toimenpiteet
automaatti antaa limsan
limsapullojen lisäys
automaatti kertoo siinä olevien limsapullojen määrän
Kun luokan määrittely kirjoitetaan Javalla, aloitetaan ensin luokan otsikolla. Tarvittavien tietojen tallentamiseen käytettävät kentät ilmoitetaan yleensä seuraavaksi:
public class LimsaAutomaatti {
private int limsojenLkm;
limsojenLkm on kenttä, jota voi käyttää kuin mitä tahansa muuttujaa. Siinä pidetään yllä tietoa automaatissa olevien limsapullojen määrästä. Koska limsojenLkm on private, sitä voi käyttää vain luokan LimsaAutomaatti sisällä.
Seuraavaksi määritellään konstruktori, jolla voidaan luoda uusia limsa-automaatteja. Tyypillisesti konstruktorissa annetaan luotavan uuden olion kentille alkuarvoja.
public LimsaAutomaatti() {
limsojenLkm = 0;
}
Sitten määritellään metodeja, joilla voidaan muuttaa ja tutkia olion kenttien arvoja:
Metodi lisaaLimsoja lisää automaattiin limsapulloja:
public void lisaaLimsoja(int maara) {
limsojenLkm = limsojenLkm + maara;
}
Metodi annaLimsa antaa yhden limsapullon. Limsapullon voi antaa vain silloin, jos niitä on automaatissa:
public void annaLimsa() {
if (limsojenLkm >= 1)
limsojenLkm--;
}
Metodilla limsojenMaara saadaan selville, montako limsapulloa on automaatissa.
public int limsojenMaara() {
return limsojenLkm;
}
}
Koko luokka vielä uudelleen ilman ylimääräistä tekstiä:
class LimsaAutomaatti {
private int limsojenLkm;
// ----- konstruktori: ------------
public LimsaAutomaatti() {
limsojenLkm = 0;
}
// ----- aksessorit: -----------
public void lisaaLimsoja(int maara) {
limsojenLkm = limsojenLkm + maara;
}
public void annaLimsa() {
if (limsojenLkm >= 1)
limsojenLkm--;
}
public int limsojenMaara() {
return limsojenLkm;
}
}
LimsaAutomaatti-olioiden luonti ja käyttö
Luokan määrittely vastaa vasta limsa-automaatin piirustuksia. Yhtään limsa-automaattia ei ole vielä syntynyt.
LimsaAutomaatti-olioita luova pääohjelma kirjoitetaan toiseen luokkaan:
public class Limsatesti {
public static void main(String[] args) {
Ensin on määriteltävä muuttujat luotavien limsa-automaattien käsittelyä varten:
LimsaAutomaatti raksanAutom, sahkonAutom;
int jaljella;
Huom! Tämä ei luo vielä yhtään LimsaAutomaatti-oliota. Olemme vain määritelleet kaksi nimeä, joiden avulla pystymme käsittelemään myöhemmin luotavia olioita.
Itse olio luodaan konstruktorin kutsulla:
raksanAutom = new LimsaAutomaatti();
Tämä luo yhden uuden LimsaAutomaatti-olion.
Luokkaan kuuluvia metodeja pystyy kohdistamaan olioon seuraavasti:
raksanAutom.lisaaLimsoja(5);
raksanAutom.annaLimsa();
raksanAutom.annaLimsa();
jaljella = raksanAutom.limsojenMaara();
Koko pääohjelma:
public class Limsatesti {
public static void main(String[] args) {
LimsaAutomaatti raksanAutom, sahkonAutom;
int jaljella;
raksanAutom = new LimsaAutomaatti();
raksanAutom.lisaaLimsoja(5);
raksanAutom.annaLimsa();
raksanAutom.annaLimsa();
jaljella = raksanAutom.limsojenMaara();
System.out.println("Raksan automaatissa on " + jaljella + " limsaa");
sahkonAutom = new LimsaAutomaatti();
sahkonAutom.lisaaLimsoja(3);
sahkonAutom.annaLimsa();
jaljella = sahkonAutom.limsojenMaara();
System.out.println("Sahkon automaatissa on " + jaljella + " limsaa");
raksanAutom.lisaaLimsoja(4);
jaljella = raksanAutom.limsojenMaara();
System.out.println("Raksan automaatissa on " + jaljella + " limsaa");
}
}
Huomaa: pääohjelmasta ei voi suoraan päästä käsiksi kentän limsojenLkm arvoon, vaan kenttää voi käsitellä vain käyttämällä LimsaAutomaatti-luokan tarjoamia metodeja.
Jos luokat LimsaAutomaatti ja Limsatesti ovat samassa tiedostossa, vain jälkimmäinen niistä voi olla public.
Limsa-automaatti: maksun ottava versio
Seuraava versio limsa-automaatista ottaa myös maksun limsoista.
Limsojen lukumäärän lisäksi tarvitaan tieto siitä, paljonko automaatissa on rahaa. Tätä varten tarvitaan toinen kenttä
public class LimsaAutomaatti {
private int limsojenLkm;
private double rahat;
Konstruktoriin lisätään uuden kentän alustus:
public LimsaAutomaatti() {
limsojenLkm = 0;
rahat = 0;
}
Metodi annaLimsa muutetaan nyt metodiksi ostaLimsa, joka saa parametrinaan automaatin käyttäjän antaman rahamäärän. Automaatti antaa limsan vain silloin, jos rahaa on tarpeeksi. Ylimääräistä rahaa ei palauteta.
Metodi tarvitsee tiedon limsapullon hinnasta. Tämä ilmoitetaan muuttujalla HINTA. Määreellä final ilmoitetaan, että muuttujalle voi antaa arvon vain yhden kerran eikä arvoa voi sen jälkeen muuttaa.
public void ostaLimsa(double annettuRaha) {
final double HINTA = 5.0;
if ( limsojenLkm >= 1 &&
annettuRaha >= HINTA) {
rahat = rahat + annettuRaha;
limsojenLkm--;
}
}
Koko luokka ja pääohjelma:
class LimsaAutomaatti {
private int limsojenLkm;
private double rahat;
// ----- konstruktori: ------------
public LimsaAutomaatti() {
limsojenLkm = 0;
rahat = 0;
}
// ----- aksessorit: -----------
public void lisaaLimsoja(int maara) {
limsojenLkm = limsojenLkm + maara;
}
public void ostaLimsa(double annettuRaha) {
final double HINTA = 5.0;
if (limsojenLkm >= 1 && annettuRaha >= HINTA) {
rahat = rahat + annettuRaha; // ei palauta ylimääräisiä
limsojenLkm--;
}
}
public double tyhjennaRahat() {
double markat;
markat = rahat;
rahat = 0;
return markat;
}
public int limsojenMaara() {
return limsojenLkm;
}
public double rahojenMaara() {
return rahat;
}
}
public class Limsatesti2 {
public static void main(String[] args) {
LimsaAutomaatti raksanAutom, sahkonAutom;
int jaljella;
double rahaa, saalis;
raksanAutom = new LimsaAutomaatti();
raksanAutom.lisaaLimsoja(5);
raksanAutom.ostaLimsa(6.0);
raksanAutom.ostaLimsa(4.0);
jaljella = raksanAutom.limsojenMaara();
rahaa = raksanAutom.rahojenMaara();
System.out.println("Raksan automaatissa on " + jaljella + " limsaa ja " +
rahaa + "mk");
sahkonAutom = new LimsaAutomaatti();
sahkonAutom.lisaaLimsoja(3);
sahkonAutom.ostaLimsa(5.0);
jaljella = sahkonAutom.limsojenMaara();
rahaa = sahkonAutom.rahojenMaara();
System.out.println("Sahkon automaatissa on " + jaljella + " limsaa ja " +
rahaa + "mk");
raksanAutom.lisaaLimsoja(4);
saalis = raksanAutom.tyhjennaRahat();
System.out.println("Raksan automaatista saatiin " + saalis + "mk");
jaljella = raksanAutom.limsojenMaara();
rahaa = raksanAutom.rahojenMaara();
System.out.println("Raksan automaatissa on " + jaljella + " limsaa ja " +
rahaa + "mk");
}
}
Viittaus "tähän
olioon": this
Jokaisella luokan ilmentymällä (oliolla) on omat kopionsa luokassa määritellyistä muuttujista.
Edellisessä esimerkissä raksanAutom:lla on omat kopionsa kenttien limsojenLkm ja rahat arvoista ja vastaavasti sahkonAutom:lla omansa.
Tämä on välttämätöntä, koska raksanAutom:n ja sahkonAutom:n kenttien arvot muuttuvat toisistaan riippumatta.
Luokan ohjelmoija voi viitata "juuri käsiteltävään olioon" ilmauksella this.
Esimerkiksi LimsaAutomaatti-luokan konstruktori voidaan kirjoittaa myös
public LimsaAutomaatti() {
this.limsojenLkm = 0;
this.rahat = 0;
}
ja metodi tyhjennaRahat
public double tyhjennaRahat() {
double markat;
markat = this.rahat;
this.rahat = 0;
return markat;
}
Tämä on suositeltava tapa.
Metodi toString
Usein ohjelmassa halutaan tulostaa tietoja olion tilasta, esimerkiksi limsa-automaatin sisältämien limsapullojen ja rahojen määrät.
Tulostaminen voidaan tehdä selvittämällä ensin luokan aksessorien avulla kenttien arvot, kuten edellä on tehty.
Jos luokaan määritellään toString-niminen metodi, voidaan tulostaminen tehdä helpommin antamalla oliomuuttujan nimi suoraan System.out.println:lle parametrina.
Metodin nimen täytyy olla toString ja sen pitää palauttaa String-tyyppinen arvo. Paluuarvona on merkkijono, joka sisältää tietoa olion tilasta
toString-metodi LimsaAutomaatti-luokalle (kirjoitetaan luokan määrittelyn sisään):
String toString() {
return "limsapulloja " + limsojenLkm +
", rahaa " + rahat + "mk";
}
Pääohjelmassa voidaan tällöin tulostaa automaattien tiloja esimerkiksi käskyillä
System.out.println("Raksan automaatti: " + raksanAutom);
System.out.println(sahkonAutom);
Toinen esimerkki: Pikkuvarasto
(Esimerkki muistuttaa Wiklan kirjan PikkuVarastoa, mutta ei ole täsmälleen sama.)
Määritellään luokka, jonka avulla voidaan luoda yksinkertaisia varastoja.
Yhteen varastoon voidaan tallentaa vain yhtä tavaraa tai ainetta, mutta mielivaltainen määrä.
Jokaisella varaston pitää tietää, mitä ainetta se sisältää (String), sekä sisällön määrä (double)
public class Pikkuvarasto {
private double maara;
private String tuote;
Kirjoitetaan kaksi eri konstruktoria. Toinen saa parametreina varastoon tallennettavan tavaran nimen ja määrän, toisella ei ole parametreja. (Konstruktorin kuormittaminen)
public Pikkuvarasto() {
maara = 0.0;
tuote = "(nimetön)";
}
public Pikkuvarasto( double paljonko,
String nimi) {
tuote = nimi;
if (paljonko > 0)
maara = paljonko;
else
maara = 0.0;
}
Metodilla paljonkoOn voidaan selvittää varastossa olevan tavaran määrä:
public double paljonkoOn() {
return maara;
}
ja mikaNimi palauttaa tassa varastossa olevan tuotteen nimen:
public String mikaNimi() {
return tuote;
}
Metodilla vieVarastoon voidaan viedä nykyiseen varastoon lisää tavaraa ja otaVarastosta antaa tavaraa pois:
public void vieVarastoon(double paljonko){
if (paljonko > 0)
maara = maara + paljonko;
}
public double
otaVarastosta(double paljonko) {
if (paljonko <= 0)
return 0;
if (paljonko <= maara) {
maara = maara - paljonko;
return paljonko;
}
else {
paljonko = maara;
maara = 0;
return paljonko;
}
}
Varaston tilan tulostamista varten kirjoitetaan oma metodi:
public String toString() {
return "(" + tuote + ": " + maara + ")";
}
class Pikkuvarasto {
private double maara; // tuotteen määrä >= 0
private String tuote; // tuotteen nimi
// ---- konstruktorit: -------
public Pikkuvarasto() {
maara = 0.0;
tuote = "(nimetön)";
}
public Pikkuvarasto(double paljonko, String nimi) {
tuote = nimi;
if (paljonko > 0)
maara = paljonko;
else
maara = 0.0;
}
// ----- aksessorit: --------
public double paljonkoOn() {
return maara;
}
public String mikaNimi() {
return tuote;
}
public void vieVarastoon(double paljonko) {
if (paljonko > 0)
maara = maara + paljonko;
}
public double otaVarastosta(double paljonko) {
if (paljonko <= 0)
return 0;
if (paljonko <= maara) {
maara = maara - paljonko;
return paljonko;
}
else {
paljonko = maara;
maara = 0;
return paljonko;
}
}
// ----- metodi tulostusta varten
public String toString() {
return "(" + tuote + ": " + maara + ")";
}
}
public class Pikkuvarastotesti {
public static void main(String[] args) {
Pikkuvarasto mehuvarasto, olutvarasto, bensavarasto, varasto;
double saatiin, olutta;
mehuvarasto = new Pikkuvarasto(10, "Mehu");
olutvarasto = new Pikkuvarasto(13.4, "Olut");
bensavarasto = new Pikkuvarasto(90.1, "Bensa");
varasto = new Pikkuvarasto();
System.out.println(mehuvarasto);
System.out.println(olutvarasto);
System.out.println(bensavarasto);
System.out.println(varasto);
System.out.println("Lisätään mehuja");
mehuvarasto.vieVarastoon(25);
System.out.println(mehuvarasto);
System.out.println("Otetaan bensaa");
saatiin = bensavarasto.otaVarastosta(50.0);
System.out.println("Bensaa saatiin " + saatiin);
System.out.println("Otetaan bensaa");
saatiin = bensavarasto.otaVarastosta(80.0);
System.out.println("Bensaa saatiin " + saatiin);
System.out.println("Kaksinkertaistetaan oluen määrä");
olutta = olutvarasto.paljonkoOn();
olutvarasto.vieVarastoon(olutta);
System.out.println(olutvarasto);
}
}
Olio parametrina: luokka Vektori
Usein metodin pitää pystyä käsittelemään kahta oliomuuttujaa yhtä aikaa. Esimerkiksi tehdään luokka kaksiulotteisten vektoreiden esittämistä varten. Tyypillinen vektoreille suoritettava operaatio on pistetulon laskeminen.
Pistetulon laskevan metodin pitää pystyä käsittelemään kahta eri vektoria, koska pistetulo lasketaan kahden vektorin välillä.
Toinen vektoreista annetaan tällöin metodille parametrina samoin kuin mikä tahansa muu parametri.
Vektorin komponenttien kertoimien esittämiseen käytetään kenttiä xKerroin ja yKerroin.
Määritellään konstruktori uusien vektoreiden luomiseen sekä metodit
vektorin pituuden laskemiseen
komponenttien arvojen selvittämiseen
vektorin kertomiseen luvulla
pistetulon laskemiseen
toString tulostuksen avuksi
class Vektori {
private double xKerroin;
private double yKerroin;
// ----- konstruktori: ------------
public Vektori(double x, double y) {
xKerroin = x;
yKerroin = y;
}
// ----- aksessorit: --------------
public double pituus() {
double pit;
pit = Math.sqrt(xKerroin * xKerroin + yKerroin * yKerroin);
return pit;
}
public double annaX() {
return xKerroin;
}
public double annaY() {
return yKerroin;
}
public void kerroLuvulla(double kerroin) {
xKerroin = kerroin * xKerroin;
yKerroin = kerroin * yKerroin;
}
// ----- muita metodeja:
public double pistetulo(Vektori toka) {
double tulo;
tulo = this.xKerroin * toka.annaX() + this.yKerroin * toka.annaY();
return tulo;
}
public String toString() {
String mjono;
if (yKerroin >= 0)
mjono = xKerroin + "i + " + yKerroin + "j";
else
mjono = xKerroin + "i - " + (- yKerroin) + "j";
return mjono;
}
}
public class Vektoritesti1 {
public static void main(String[] args) {
Vektori aVektori, bVektori;
double aPit, bPit, pistetulo;
aVektori = new Vektori(3.0, 4.0);
bVektori = new Vektori(2.0, -1.0);
System.out.println("a = " + aVektori);
System.out.println("b = " + bVektori);
aPit = aVektori.pituus();
bPit = bVektori.pituus();
System.out.println("a:n pituus: " + aPit +", b:n pituus: " + bPit);
pistetulo = aVektori.pistetulo(bVektori);
System.out.println("a:n ja b:n pistetulo: "+ pistetulo);
aVektori.kerroLuvulla(2.5);
System.out.println("a = " + aVektori);
}
}
Viite olioon
Oliomuuttujia käsitellään toisin kuin yksinkertaista tyyppiä (esim int, double, char, boolean) olevia muuttujia
Oliomuuttujan arvo ei ole itse olio (esimerkiksi itse vektori kenttineen), vaan vain tieto siitä, missä muistipaikassa varsinainen olio sijaitsee eli viite olioon.
Jos nyt ohjelmassa on määritelty muuttujat
Vektori a, b, c;
sekä a ja b on alustettu viittaamaan luotuihin vektoreihin
a = new Vektori(4, 5);
b = new Vektori(3, -1);
niin sijoitus
c = a;
ei luo uutta vektoria, vaan vain panee muuttujat a ja c viittaamaan samaan vektoriolioon. Jos a:n viittaamaa vektoria muutetaan, niin c muuttuu samalla:
a.kerroLuvulla(2.5);
sen sijaan muutos muuttujan a arvoon (ei itse vektoriin)
ei muuta c:tä
a = new Vektori(8.5, 3.2);
Toinen seuraus: jos metodi muuttaa sille parametrina annetun olion arvoa, muutos näkyy myös kutsuvassa ohjelmassa.
Toinen pääohjelma , joka käyttää Vektori-olioita
public class Vektoritesti2 {
public static void main(String[] args) {
Vektori aVektori, bVektori, cVektori;
aVektori = new Vektori(3.0, 4.0);
bVektori = new Vektori(2.0, -1.0);
System.out.println("a = " + aVektori);
System.out.println("b = " + bVektori);
cVektori = aVektori;
System.out.println("c = " + cVektori);
System.out.println("a:ta kasvatetaan...");
aVektori.kerroLuvulla(2.5); // myös cVektori kasvaa
System.out.println("a = " + aVektori);
System.out.println("b = " + bVektori);
System.out.println("c = " + cVektori);
}
}
Olio metodin palauttamana arvona
Täydennetään Vektori-luokkaa kahdella uudella metodilla:
kopio tekee aidon kopion nykyisestä vektorista
isoKopio tekee kopion nykyisestä vektorista, mutta kertoo vektorin komponentit samalla parametrina annetulla luvulla.
Molempien metodien paluuarvon tyypin täytyy olla Vektori.
Kumpikin metodi luo uuden Vektori-olion. Sen ne tekevät kutsumalla Vektori-luokan konstruktoria.
Viite uuteen Vektori-olio voidaan nyt palauttaa return-lauseella.
public Vektori kopio() {
Vektori uusi;
uusi = new Vektori( this.xKerroin, this.yKerroin);
return uusi;
}
public Vektori isoKopio(double kerroin) {
Vektori uusi;
double uusiX, uusiY;
uusiX = kerroin * this.xKerroin;
uusiY = kerroin * this.yKerroin;
uusi = new Vektori(uusiX, uusiY);
return uusi;
}
Pääohjelma täydennetyn Vektori-luokan testaamiseen:
public class Vektoritesti3 {
public static void main(String[] args) {
Vektori aVektori, cVektori, dVektori, eVektori;
aVektori = new Vektori(3.0, 4.0);
System.out.println("a = " + aVektori);
cVektori = aVektori;
System.out.println("c = " + cVektori);
dVektori = aVektori.kopio();
System.out.println("d = " + dVektori);
eVektori = aVektori.isoKopio(1.5);
System.out.println("e = " + eVektori);
System.out.println("a:ta kasvatetaan...");
aVektori.kerroLuvulla(2.5); // myös cVektori kasvaa
System.out.println("a = " + aVektori);
System.out.println("c = " + cVektori);
System.out.println("d = " + dVektori);
System.out.println("e = " + eVektori);
}
}