Oliot ja viitteet
Jatketaan olioiden ja viitteiden parissa. Oletetaan, että käytössämme on alla oleva henkilöä kuvaava luokka. Henkilöllä on oliomuuttujat nimi, ikä, paino ja pituus, jonka lisäksi se tarjoaa metodin mm. painoindeksin laskemiseen.
public class Henkilo {
private String nimi;
private int ika;
private int paino;
private int pituus;
public Henkilo(String nimi) {
this(nimi, 0, 0, 0);
}
public Henkilo(String nimi, int ika, int pituus, int paino) {
this.nimi = nimi;
this.ika = ika;
this.paino = paino;
this.pituus = pituus;
}
// muita konstruktoreja ja metodeja
public String getNimi() {
return this.nimi;
}
public int getIka() {
return this.ika;
}
public int getPituus() {
return this.pituus;
}
public void vanhene() {
this.ika = this.ika + 1;
}
public void setPituus(int uusiPituus) {
this.pituus = uusiPituus;
}
public void setPaino(int uusiPaino) {
this.paino = uusiPaino;
}
public double painoindeksi() {
double pituusPerSata = this.pituus / 100.0;
return this.paino / (pituusPerSata * pituusPerSata);
}
@Override
public String toString() {
return this.nimi + ", ikä " + this.ika + " vuotta";
}
}
Mitä oikein tapahtuu kun olio luodaan?
Henkilo joan = new Henkilo("Joan Ball");
Konstruktorikutsun new
yhteydessä tapahtuu monta asiaa. Ensin tietokoneen muistista varataan tila oliomuuttujille. Tämän jälkeen oliomuuttujiin asetetaan oletus- tai alkuarvot (esimerkiksi int
-tyyppisten muuttujien arvoksi tulee 0). Lopulta suoritetaan konstruktorissa oleva lähdekoodi.
Konstruktorikutsu palauttaa viitteen olioon. Viite on tieto olioon liittyvien tietojen paikasta.
Muuttujan arvoksi asetetaan siis viite, eli tieto olioon liittyvien tietojen paikasta. Yllä oleva kuva paljastaa myös sen, että merkkijonot — kuten henkilömme nimi — ovat myös olioita.
Viittaustyyppisen muuttujan arvon asettaminen kopioi viitteen
Lisätään ohjelmaan Henkilo
-tyyppinen muuttuja ball
ja annetaan sille alkuarvoksi joan
. Mitä nyt tapahtuu?
Henkilo joan = new Henkilo("Joan Ball");
System.out.println(joan);
Henkilo ball = joan;
Lause Henkilo ball = joan;
luo uuden henkilömuuttujan ball
, jonka arvoksi kopioidaan muuttujan joan
arvo. Tämä saa aikaan sen, että ball
viittaa samaan olioon kuin joan
.
Tarkastellaan samaa esimerkkiä hieman pidemmälle.
Henkilo joan = new Henkilo("Joan Ball");
System.out.println(joan);
Henkilo ball = joan;
ball.vanhene();
ball.vanhene();
System.out.println(joan);
Joan Ball, ikä 0 vuotta Joan Ball, ikä 2 vuotta
Joan Ball — eli henkilöolio, johon viite muuttujassa joan
osoittaa — on alussa 0-vuotias. Tämän jälkeen muuttujaan ball
asetetaan (eli kopioidaan) muuttujan joan
arvo. Henkilöoliota ball
vanhennetaan kaksi vuotta ja sen seurauksena Joan Ball vanhenee!
Olion sisäinen tila ei kopioidu muuttujan arvoa asetettaessa. Lauseessa Henkilo ball = joan;
ei luoda uutta oliota — muuttujan ball arvoksi asetetaan kopio muuttujan joan arvosta, eli viite olioon.
Seuraavassa esimerkkiä on jatkettu siten, että joan
-muuttujaa varten luodaan uusi olio, jonka viite asetetaan muuttujan arvoksi. Muuttuja ball
viittaa yhä aiemmin luotuun olioon.
Henkilo joan = new Henkilo("Joan Ball");
System.out.println(joan);
Henkilo ball = joan;
ball.vanhene();
ball.vanhene();
System.out.println(joan);
joan = new Henkilo("Joan B.");
System.out.println(joan);
Tulostuu:
Joan Ball, ikä 0 vuotta Joan Ball, ikä 2 vuotta Joan B., ikä 0 vuotta
Muuttujassa joan
on siis alussa viite yhteen olioon, mutta lopussa sen arvoksi on kopioitu toisen muuttujan viite. Seuraavassa kuva tilanteesta viimeisen koodirivin jälkeen.
Viittaustyyppisen muuttujan arvo null
Jatketaan vielä esimerkkiä asettamalla viittaustyyppisen muuttujan ball
arvoksi null
, eli viite "ei mihinkään". Viitteen "ei mihinkään" (eli null
-viitteen voi asettaa minkä tahansa viittaustyyppisen muuttujan arvoksi.
Henkilo joan = new Henkilo("Joan Ball");
System.out.println(joan);
Henkilo ball = joan;
ball.vanhene();
ball.vanhene();
System.out.println(joan);
joan = new Henkilo("Joan B.");
System.out.println(joan);
ball = null;
Viimeisen rivin jälkeen ohjelman tila on seuraavanlainen.
Olioon, jonka nimi on Joan Ball, ei enää viittaa kukaan. Oliosta on siis tullut "roska". Java-ohjelmointikielessä ohjelmoijan ei tarvitse huolehtia ohjelman käyttämästä muistista. Javan automaattinen roskienkerääjä käy siivoamassa roskaksi joutuneet oliot aika ajoin. Jos automaattista roskien keruuta ei tapahtuisi, jäisivät roskaksi joutuneet oliot varaamaan muistia ohjelman suorituksen loppuun asti.
Kokeillaan vielä mitä käy kun yritämme tulostaa muuttujaa, jonka arvona on viite "ei mihinkään" eli null
.
Henkilo joan = new Henkilo("Joan Ball");
System.out.println(joan);
Henkilo ball = joan;
ball.vanhene();
ball.vanhene();
System.out.println(joan);
joan = new Henkilo("Joan B.");
System.out.println(joan);
ball = null;
System.out.println(ball);
Joan Ball, ikä 0 vuotta Joan Ball, ikä 2 vuotta Joan B., ikä 0 vuotta null
Viitteen null
tulostus tulostaa "null". Entäpä jos yritämme kutsua ei mihinkään viittaavan olion metodia, esimerkiksi metodia vanhene
:
Henkilo joan = new Henkilo("Joan Ball");
System.out.println(joan);
joan = null;
joan.vanhene();
Tulos:
Joan Ball, ikä 0 vuotta Exception in thread "main" java.lang.NullPointerException at Main.main(Main.java:(rivi)) Java Result: 1
Käy huonosti. Tämä on ehkä ensimmäinen kerta kun näet tekstin NullPointerException. Ohjelmassa tapahtuu virhe, joka liittyy siihen, että olemme kutsuneet ei mihinkään viittaavan muuttujan metodia.
Voimme luvata, että tulet näkemään edellisen virheen vielä uudelleen. Tällöin ensimmäinen askel on etsiä muuttujia, joiden arvona saattaisi olla null
. Virheilmoitus on onneksi myös hyödyllinen: se kertoo millä rivillä virhe tapahtuu. Kokeile vaikka itse!
Olio metodin parametrina
Olemme nähneet että metodien parametrina voi olla alkeis- ja viittaustyyppisiä muuttujia. Koska oliot ovat viittaustyyppisiä muuttujia, voi metodin parametriksi määritellä minkä tahansa tyyppisen olion. Demonstroidaan tätä esimerkillä.
Huvipuiston laitteisiin hyväksytään henkilöitä, joiden pituus ylittää annetun rajan. Kaikissa laitteissa raja ei ole sama. Tehdään huvipuiston laitetta vastaava luokka. Olioa luotaessa konstruktorille annetaan parametriksi laitteen nimi sekä pienin pituus, jolla laitteeseen pääsee.
public class Huvipuistolaite {
private String nimi;
private int alinPituus;
public Huvipuistolaite(String nimi, int alinPituus) {
this.nimi = nimi;
this.alinPituus = alinPituus;
}
public String toString() {
return this.nimi + ", pituusalaraja: " + this.alinPituus;
}
}
Tehdään seuraavaksi metodi, jonka avulla voidaan tarkastaa pääseekö tietty henkilö laitteen kyytiin, eli onko henkilö tarpeeksi pitkä. Metodi palauttaa true
jos parametrina annettu henkilö hyväksytään, false
jos ei.
Alla oletetaan, että henkilöllä metodi ``public int getPituus()`, joka palauttaa henkilön pituuden.
public class Huvipuistolaite {
private String nimi;
private int alinPituus;
public Huvipuistolaite(String nimi, int alinPituus) {
this.nimi = nimi;
this.alinPituus = alinPituus;
}
public boolean paaseeKyytiin(Henkilo henkilo) {
if (henkilo.getPituus() < this.alinPituus) {
return false;
}
return true;
}
public String toString() {
return this.nimi + ", pituusalaraja: " + this.alinPituus;
}
}
Huvipuistolaite-olion metodille paaseeKyytiin
annetaan siis parametrina Henkilo
-olio. Kuten aiemmin, muuttujan arvo — eli tässä viite — kopioituu metodin käyttöön. Metodissa käsitellään kopioitua viitettä ja kutsutaan parametrina saadun henkilön metodia getPituus
.
Seuraavassa testipääohjelma jossa huvipuistolaitteen metodille annetaan ensin parametriksi henkilöolio matti
ja sen jälkeen henkilöolio juhana
:
Henkilo matti = new Henkilo("Matti");
matti.setPaino(86);
matti.setPituus(180);
Henkilo juhana = new Henkilo("Juhana");
juhana.setPaino(34);
juhana.setPituus(132);
Huvipuistolaite hurjakuru = new Huvipuistolaite("Hurjakuru", 140);
if (hurjakuru.paaseeKyytiin(matti)) {
System.out.println(matti.getNimi() + " pääsee laitteeseen");
} else {
System.out.println(matti.getNimi() + " ei pääse laitteeseen");
}
if (hurjakuru.paaseeKyytiin(juhana)) {
System.out.println(juhana.getNimi() + " pääsee laitteeseen");
} else {
System.out.println(juhana.getNimi() + " ei pääse laitteeseen");
}
System.out.println(hurjakuru);
Ohjelma tulostaa:
Matti pääsee laitteeseen Juhana ei pääse laitteeseen Hurjakuru, pituusalaraja: 140
Entäpä jos haluaisimme tietää kuinka moni on päässyt laitteen kyytiin?
Lisätään huvipuistolaitteelle oliomuuttuja, joka pitää kirjaa kyytiin päässeiden henkilöiden lukumäärästä.
public class Huvipuistolaite {
private String nimi;
private int alinPituus;
private int kavijoita;
public Huvipuistolaite(String nimi, int alinPituus) {
this.nimi = nimi;
this.alinPituus = alinPituus;
this.kavijoita = 0;
}
public boolean paaseeKyytiin(Henkilo henkilo) {
if (henkilo.getPituus() < this.alinPituus) {
return false;
}
this.kavijoita++;
return true;
}
public String toString() {
return this.nimi + ", pituusalaraja: " + this.alinPituus +
", kävijöitä: " + this.kavijoita;
}
}
Nyt aiemmin käyttämässämme esimerkkiohjelmassa pidetään kirjaa myös laitteen kävijöiden määrästä.
Henkilo matti = new Henkilo("Matti");
matti.setPaino(86);
matti.setPituus(180);
Henkilo juhana = new Henkilo("Juhana");
juhana.setPaino(34);
juhana.setPituus(132);
Huvipuistolaite hurjakuru = new Huvipuistolaite("Hurjakuru", 140);
if (hurjakuru.paaseeKyytiin(matti)) {
System.out.println(matti.getNimi() + " pääsee laitteeseen");
} else {
System.out.println(matti.getNimi() + " ei pääse laitteeseen");
}
if (hurjakuru.paaseeKyytiin(juhana)) {
System.out.println(juhana.getNimi() + " pääsee laitteeseen");
} else {
System.out.println(juhana.getNimi() + " ei pääse laitteeseen");
}
System.out.println(hurjakuru);
Ohjelma tulostaa:
Matti pääsee laitteeseen Juhana ei pääse laitteeseen Hurjakuru, pituusalaraja: 140, kävijöitä: 1
Olio oliomuuttujana
Oliot voivat sisältää viitteitä olioihin.
Jatketaan henkilöiden parissa ja lisätään henkilölle syntymäpäivä. Syntymäpäivä on luonnollista esittää Paivays
-luokan avulla:
public class Paivays {
private int paiva;
private int kuukausi;
private int vuosi;
public Paivays(int paiva, int kuukausi, int vuosi) {
this.paiva = paiva;
this.kuukausi = kuukausi;
this.vuosi = vuosi;
}
public int getPaiva() {
return this.paiva;
}
public int getKuukausi() {
return this.kuukausi;
}
public int getVuosi() {
return this.vuosi;
}
@Override
public String toString() {
return this.paiva + "." + this.kuukausi + "." + this.vuosi;
}
}
Koska tiedämme syntymäpäivän, henkilön ikää ei tarvitse säilöä erillisenä oliomuuttujana. Henkilön ikä on pääteltävissä syntymäpäivästä. Oletetaan, luokassa Henkilo
on nyt seuraavat muuttujat.
public class Henkilo {
private String nimi;
private Paivays syntymapaiva;
private int paino = 0;
private int pituus = 0;
// ...
Tehdään henkilölle uusi konstruktori, joka mahdollistaa syntymäpäivän asettamisen:
public Henkilo(String nimi, Paivays paivays) {
this.nimi = nimi;
this.syntymapaiva = paivays;
}
Edellisen konstruktorin lisäksi henkilölle voisi luoda myös konstruktorin, missä syntymäpäivä annettaisiin parametrina.
public Henkilo(String nimi, int paiva, int kuukausi, int vuosi) {
this.nimi = nimi;
this.syntymapaiva = new Paivays(paiva, kuukausi, vuosi);
}
Konstruktorin parametrina annetaan erikseen päiväyksen osat (päivä, kuukausi, vuosi), niistä luodaan päiväysolio, ja lopulta päiväysolion viite kopioidaan oliomuuttujan syntymapaiva
arvoksi.
Muokataan Henkilo-luokassa olevaa toString
-metodia siten, että metodi palauttaa iän sijaan syntymäpäivän:
public String toString() {
return this.nimi + ", syntynyt " + this.syntymapaiva;
}
Kokeillaan miten uusittu Henkilöluokka toimii.
Paivays paivays = new Paivays(1, 1, 780);
Henkilo muhammad = new Henkilo("Muhammad ibn Musa al-Khwarizmi", paivays);
Henkilo pascal = new Henkilo("Blaise Pascal", 19, 6, 1623);
System.out.println(muhammad);
System.out.println(pascal);
Muhammad ibn Musa al-Khwarizmi, syntynyt 1.1.780 Blaise Pascal, syntynyt 19.6.1623
Henkilöoliolla on nyt oliomuuttujat nimi
ja syntymapaiva
. Muuttuja nimi
on merkkijono, joka sekin on siis olio, ja muuttuja syntymapaiva
on Päiväysolio.
Molemmat muuttujat sisältävät arvon olioon. Henkilöolio sisältää siis kaksi viitettä. Alla olevassa kuvassa paino ja pituus on jätetty huomiotta.
Pääohjelmalla on nyt siis langan päässä kaksi Henkilö-olioa. Henkilöllä on nimi ja syntymäpäivä. Koska molemmat ovat olioita, ovat ne henkilöllä langan päässä.
Syntymäpäivä vaikuttaa hyvältä laajennukselta Henkilö-luokkaan. Totesimme aiemmin, että oliomuuttuja ika
voidaan laskea syntymäpäivästä, joten siitä hankkiuduttiin eroon.
Samantyyppinen olio metodin parametrina
Jatkamme luokan Henkilo
parissa. Kuten muistamme, henkilöt tietävät syntymäpäivänsä:
public class Henkilo {
private String nimi;
private Paivays syntymapaiva;
private int pituus;
private int paino;
// ...
}
Haluamme vertailla kahden henkilön ikää. Vertailu voidaan hoitaa usealla tavalla. Voisimme esimerkiksi toteuttaa Henkilo-luokkaan metodin public int ikaVuosina()
, jolloin kahden henkilön iän vertailu tapahtuisi tällöin seuraavasti:
Henkilo muhammad = new Henkilo("Muhammad ibn Musa al-Khwarizmi", 1, 1, 780);
Henkilo pascal = new Henkilo("Blaise Pascal", 19, 6, 1623);
if (muhammad.ikaVuosina() > pascal.ikaVuosina()) {
System.out.println(muhammad.getNimi() + " on vanhempi kuin " + pascal.getNimi());
}
Opettelemme tässä hieman "oliohenkisemmän" tavan kahden henkilön ikävertailun tekemiseen.
Teemme luokalle Henkilo metodin boolean vanhempiKuin(Henkilo verrattava)
, jonka avulla tiettyä henkilö-olioa voi verrata parametrina annettuun henkilöön iän perusteella.
Metodia on tarkoitus käyttää seuraavaan tyyliin:
Henkilo muhammad = new Henkilo("Muhammad ibn Musa al-Khwarizmi", 1, 1, 780);
Henkilo pascal = new Henkilo("Blaise Pascal", 19, 6, 1623);
if (muhammad.vanhempiKuin(pascal)) { // sama kun muhammad.vanhempiKuin(pascal)==true
System.out.println(muhammad.getNimi() + " on vanhempi kuin " + pascal.getNimi());
} else {
System.out.println(muhammad.getNimi() + " ei ole vanhempi kuin " + pascal.getNimi());
}
Yllä oleva ohjelma kysyy onko al-Khwarizmi vanhempi kuin Pascal. Metodi vanhempiKuin
palauttaa arvon true
jos olio jonka kohdalla metodia kutsutaan (olio.vanhempiKuin(parametrinaAnnettavaOlio)
) on vanhempi kuin parametrina annettava olio, ja false
muuten.
Käytännössä yllä kutsutaan "Muhammad ibn Musa al-Khwarizmia" vastaavan olion, johon muuttuja muhammad
viittaa, metodia vanhempiKuin
. Metodille annetaan parametriksi "Blaise Pascal" vastaavan olion viite pascal
.
Ohjelma tulostaa:
Muhammad ibn Musa al-Khwarizmi on vanhempi kuin Blaise Pascal
Metodille vanhempiKuin
annetaan parametrina henkilöolio. Tarkemmin sanottuna metodin parametriksi määriteltyyn muuttujaan kopioituu parametrina annettavan muuttujan sisältämä arvo, eli viite olioon.
Metodin toteutus näyttää seuraavalta. Huomaa, että metodi voi palauttaa arvon useammasta kohtaa — alla vertailu on pilkottu useampaan osaan vuoden, kuukauden ja päivän kohdalta:
public class Henkilo {
// ...
public boolean vanhempiKuin(Henkilo verrattava) {
// 1. Verrataan ensin vuosia
int omaVuosi = this.getSyntymapaiva().getVuosi();
int verrattavanVuosi = verrattava.getSyntymapaiva().getVuosi();
if (omaVuosi < verrattavanVuosi) {
return true;
}
if (omaVuosi > verrattavanVuosi) {
return false;
}
// 2. Syntymävuosi on sama, verrataan kuukausia
int omaKuukausi = this.getSyntymapaiva().getKuukausi();
int verrattavanKuukausi = verrattava.getSyntymapaiva().getKuukausi();
if (omaKuukausi < verrattavanKuukausi) {
return true;
}
if (omaKuukausi > verrattavanKuukausi) {
return false;
}
// 3. Syntymävuosi ja kuukausi on sama, verrataan päiviä
int omaPaiva = this.getSyntymapaiva().getPaiva();
int verrattavanPaiva = verrattava.getSyntymapaiva().getPaiva();
if (omaPaiva < verrattavanPaiva) {
return true;
}
return false;
}
}
Mietitään hieman olio-ohjelmoinnin periatteiden abstrahointia. Abstrahoinnin ajatuksena on käsitteellistää ohjelmakoodia siten, että kullakin käsitteellä on omat selkeät vastuunsa. Kun pohdimme yllä esitettyä ratkaisua, huomaamme, että päivämäärien vertailutoiminnallisuus kuuluisi mielummin luokkaan Paivays
luokan Henkilo
-sijaan.
Luodaan luokalle Paivays
metodi public boolean aiemmin(Paivays verrattava)
. Metodi palauttaa arvon true
, jos metodille parametrina annettu päiväys on kyseisen olion päiväyksen jälkeen.
public class Paivays {
private int paiva;
private int kuukausi;
private int vuosi;
public Paivays(int paiva, int kuukausi, int vuosi) {
this.paiva = paiva;
this.kuukausi = kuukausi;
this.vuosi = vuosi;
}
public String toString() {
return this.paiva + "." + this.kuukausi + "." + this.vuosi;
}
// metodilla tarkistetaan onko tämä päiväysolio (`this`) ennen
// parametrina annettavaa päiväysoliota (`verrattava`)
public boolean aiemmin(Paivays verrattava) {
// ensin verrataan vuosia
if (this.vuosi < verrattava.vuosi) {
return true;
}
if (this.vuosi > verrattava.vuosi) {
return false;
}
// jos vuodet ovat samat, verrataan kuukausia
if (this.kuukausi < verrattava.kuukausi) {
return true;
}
if (this.kuukausi > verrattava.kuukausi) {
return false;
}
// vuodet ja kuukaudet samoja, verrataan päivää
if (this.paiva < verrattava.paiva) {
return true;
}
return false;
}
}
Vaikka oliomuuttujat vuosi
, kuukausi
ja paiva
ovat olion kapseloimia (private
) oliomuuttujia, pystymme lukemaan niiden arvon kirjoittamalla verrattava.*muuttujanNimi*
. Tämä johtuu siitä, että private
-muuttujat ovat luettavissa kaikissa metodeissa, jotka kyseinen luokka sisältää. Huomaa, että syntaksi (kirjoitusasu) vastaa tässä jonkin olion metodin kutsumista. Toisin kuin metodia kutsuttaessa, viittaamme olion kenttään, jolloin metodikutsun osoittavia sulkeita ei kirjoiteta.
Metodin käyttöesimerkki:
public static void main(String[] args) {
Paivays p1 = new Paivays(14, 2, 2011);
Paivays p2 = new Paivays(21, 2, 2011);
Paivays p3 = new Paivays(1, 3, 2011);
Paivays p4 = new Paivays(31, 12, 2010);
System.out.println(p1 + " aiemmin kuin " + p2 + ": " + p1.aiemmin(p2));
System.out.println(p2 + " aiemmin kuin " + p1 + ": " + p2.aiemmin(p1));
System.out.println(p2 + " aiemmin kuin " + p3 + ": " + p2.aiemmin(p3));
System.out.println(p3 + " aiemmin kuin " + p2 + ": " + p3.aiemmin(p2));
System.out.println(p4 + " aiemmin kuin " + p1 + ": " + p4.aiemmin(p1));
System.out.println(p1 + " aiemmin kuin " + p4 + ": " + p1.aiemmin(p4));
}
14.2.2011 aiemmin kuin 21.2.2011: true 21.2.2011 aiemmin kuin 14.2.2011: false 21.2.2011 aiemmin kuin 1.3.2011: true 1.3.2011 aiemmin kuin 21.2.2011: false 31.12.2010 aiemmin kuin 14.2.2011: true 14.2.2011 aiemmin kuin 31.12.2010: false
Muunnetaan vielä henkilön metodia vanhempiKuin siten, että hyödynnämme jatkossa päivämäärän tarjoamaa vertailutoiminnallisuutta.
public class Henkilo {
// ...
public boolean vanhempiKuin(Henkilo verrattava) {
if (this.syntymapaiva.aiemmin(verrattava.getSyntymapaiva())) {
return true;
}
return false;
// tai suoraan:
// return this.syntymapaiva.aiemmin(verrattava.getSyntymapaiva());
}
}
Nyt päivämäärän konkreettinen vertailu on toteutettu luokassa, johon se loogisesti (luokkien nimien perusteella) kuuluukin.
Olioiden samankaltaisuuden vertailu (equals)
Opimme merkkijonojen käsittelyn yhteydessä, että merkkijonojen vertailu tulee toteuttaa equals
-metodin avullla. Tämä tapahtuu seuraavasti.
Scanner lukija = new Scanner(System.in);
System.out.println("Syötä kaksi sanaa, kumpikin omalle rivilleen.")
String eka = lukija.nextLine();
String toka = lukija.nextLine();
if (eka.equals(toka)) {
System.out.println("Sanat olivat samat.");
} else {
System.out.println("Sanat eivät olleet samat.");
}
Alkeistyyppisten muuttujien kuten int
kanssa muuttujien vertailu on mahdollista kahden yhtäsuuruusmerkin avulla. Tämä johtuu siitä, että alkeistyyppisten muuttujien arvo sijaitsee "muuttujan lokerossa". Viittaustyyppisten muuttujien arvo on taas osoite viitattavaan olioon, eli viittaustyyppisten muuttujien "lokerossa" on viite muistipaikkaan. Kahden yhtäsuuruusmerkin avulla verrataan "muuttujan lokeron" sisällön yhtäsuuruutta — viittaustyyppisillä muuttujilla vertailu tarkastelisi siis muuttujien viitteiden yhtäsuuruutta.
Metodi equals
on samankaltainen metodi kuin toString
siinä, että se on käytettävissä vaikkei metodia olisi luokkaan määritelty. Metodin oletustoteutus vertaa viitteiden yhtäsuuruutta. Tarkastellaan tätä aiemmin toteuttamamme Paivays
-luokan avulla.
Paivays eka = new Paivays(1, 1, 2000);
Paivays toka = new Paivays(1, 1, 2000);
Paivays kolmas = new Paivays(12, 12, 2012);
Paivays neljas = eka;
if (eka.equals(eka)) {
System.out.println("Muuttujat eka ja eka ovat samat");
} else {
System.out.println("Muuttujat eka ja eka eivät ole samat");
}
if (eka.equals(toka)) {
System.out.println("Muuttujat eka ja toka ovat samat");
} else {
System.out.println("Muuttujat eka ja toka eivät ole samat");
}
if (eka.equals(kolmas)) {
System.out.println("Muuttujat eka ja kolmas ovat samat");
} else {
System.out.println("Muuttujat eka ja kolmas eivät ole samat");
}
if (eka.equals(neljas)) {
System.out.println("Muuttujat eka ja neljas ovat samat");
} else {
System.out.println("Muuttujat eka ja neljas eivät ole samat");
}
Muuttujat eka ja eka ovat samat Muuttujat eka ja toka eivät ole samat Muuttujat eka ja kolmas eivät ole samat Muuttujat eka ja neljas ovat samat
Esimerkkiohjelma näyttää ongelman. Vaikka kahdella päiväyksellä (eka ja toka) on täsmälleen samat oliomuuttujan arvot, ovat ne metodin equals
oletustoteutuksen näkökulmasta toisistaan poikkeavat.
Mikäli haluamme pystyä vertailemaan kahta itse toteuttamaamme oliota equals-metodilla, tulee metodi määritellä luokkaan. Metodi equals määritellään boolean-tyyppisen arvon palauttavana metodina — palautettu arvo kertoo ovatko oliot samat.
Metodi equals
toteutetaan siten, että sen avulla voidaan vertailla nykyistä oliota mihin tahansa muuhun olioon. Metodi saa parametrinaan Object-tyyppisen olion — kaikki oliot ovat oman tyyppinsä lisäksi Object-tyyppisiä. Metodissa ensin vertaillaan ovatko osoitteet samat: jos kyllä, oliot ovat samat. Tämän jälkeen tarkastellaan ovatko olion tyypit samat: jos ei, oliot eivät ole samat. Tämän jälkeen parametrina saatu Object-olio muunnetaan tyyppimuunnoksella tarkasteltavan olion muotoiseksi, ja oliomuuttujien arvoja vertaillaan. Alla vertailu on toteutettu Paivays-oliolle.
public class Paivays {
private int paiva;
private int kuukausi;
private int vuosi;
public Paivays(int paiva, int kuukausi, int vuosi) {
this.paiva = paiva;
this.kuukausi = kuukausi;
this.vuosi = vuosi;
}
public int getPaiva() {
return this.paiva;
}
public int getKuukausi() {
return this.kuukausi;
}
public int getVuosi() {
return this.vuosi;
}
public boolean equals(Object verrattava) {
// jos muuttujat sijaitsevat samassa paikassa, ovat ne samat
if (this == verrattava) {
return true;
}
// jos verrattava olio ei ole Paivays-tyyppinen, oliot eivät ole samat
if (!(verrattava instanceof Paivays)) {
return false;
}
// muunnetaan Object-tyyppinen verrattava-olio
// Paivays-tyyppiseksi verrattavaPaivays-olioksi
Paivays verrattavaPaivays = (Paivays) verrattava;
// jos olioiden oliomuuttujien arvot ovat samat, ovat oliot samat
if (this.paiva == verrattavaPaivays.paiva &&
this.kuukausi == verrattavaPaivays.kuukausi &&
this.vuosi == verrattavaPaivays.vuosi) {
return true;
}
// muulloin oliot eivät ole samat
return false;
}
@Override
public String toString() {
return this.paiva + "." + this.kuukausi + "." + this.vuosi;
}
}
Vastaavan vertailutoiminnallisuuden rakentaminen onnistuu myös Henkilö-olioille. Alla vertailu on toteutettu Henkilo-oliolle, jolla ei ole erillista Paivays-oliota. Huomaa, että henkilöiden nimet ovat merkijonoja (eli olioita), joten niiden vertailussa käytetään equals-metodia.
public class Henkilo {
private String nimi;
private int ika;
private int paino;
private int pituus;
// konstruktorit ja metodit
public boolean equals(Object verrattava) {
// jos muuttujat sijaitsevat samassa paikassa, ovat ne samat
if (this == verrattava) {
return true;
}
// jos verrattava olio ei ole Henkilo-tyyppinen, oliot eivät ole samat
if (!(verrattava instanceof Henkilo)) {
return false;
}
// muunnetaan olio Henkilo-olioksi
Henkilo verrattavaHenkilo = (Henkilo) verrattava;
// jos olioiden oliomuuttujien arvot ovat samat, ovat oliot samat
if (this.nimi.equals(verrattavaHenkilo.nimi) &&
this.ika == verrattavaHenkilo.ika &&
this.paino == verrattavaHenkilo.paino &&
this.pituus == verrattavaHenkilo.pituus) {
return true;
}
// muulloin oliot eivät ole samat
return false;
}
// .. metodeja
}
Olion samankaltaisuus ja listat
Tarkastellaan equals
-metodin käyttöä vielä listojen yhteydessä. Oletetaan, että käytössämme on edellä kuvattu luokka Lintu
, jolle ei ole määritelty equals
-metodia.
public class Lintu {
private String nimi;
public Lintu(String nimi) {
this.nimi = nimi;
}
}
Luodaan lista, johon lisätään lintu. Tarkastellaan tämän jälkeen linnun olemassaoloa listalla.
ArrayList<Lintu> linnut = new ArrayList<>()
Lintu red = new Lintu("Red");
if (linnut.contains(red)) {
System.out.println("Red on listalla.");
} else {
System.out.println("Red ei ole listalla.");
}
linnut.add(red);
if (linnut.contains(red)) {
System.out.println("Red on listalla.");
} else {
System.out.println("Red ei ole listalla.");
}
System.out.println("Mutta!");
red = new Lintu("Red");
if (linnut.contains(red)) {
System.out.println("Red on listalla.");
} else {
System.out.println("Red ei ole listalla.");
}
Red ei ole listalla. Red on listalla. Mutta! Red ei ole listalla.
Yllä olevasta esimerkistä huomaamme, että voimme etsiä listalta omia olioitamme. Aluksi kun lintua ei ole lisätty listalle, sitä ei löydy — lisäämisen jälkeen se löytyy. Kun ohjelmassa red
-olio vaihdetaan uudeksi täysin samansisältöiseksi olioksi, ei se enää vastaa listalla olevaa oliota, ja sitä ei löydy listalta.
Listan contains
-metodi hyödyntää olioiden etsimiseen oliolle määriteltyä equals
-metodia. Yllä olevassa esimerkissä luokalle Lintu
ei tätä metodia ole määritelty, joten täysin samansisältöinen lintu, jonka viite on eri, ei listalta löydy.
Lisätään luokalle Lintu
metodi equals
. Metodi tarkastelee onko olioiden nimet samat — mikäli nimet ovat samat, käsitellään ne samanlaisina.
public class Lintu {
private String nimi;
public Lintu(String nimi) {
this.nimi = nimi;
}
public boolean equals(Object verrattava) {
// jos muuttujat sijaitsevat samassa paikassa, ovat ne samat
if (this == verrattava) {
return true;
}
// jos verrattava olio ei ole Lintu-tyyppinen, oliot eivät ole samat
if (!(verrattava instanceof Lintu)) {
return false;
}
// muunnetaan olio Lintu-olioksi
Lintu verrattavaLintu = (Lintu) verrattava;
// jos olioiden oliomuuttujien arvot ovat samat, ovat oliot samat
return this.nimi.equals(verrattavaLintu.nimi);
/*
// yllä oleva nimen vertailu vastaa alla olevaa
// koodia
if (this.nimi.equals(verrattavaLintu.nimi)) {
return true;
}
// muulloin oliot eivät ole samat
return false;
*/
}
}
Nyt listan contains-metodi tunnistaa samansisältöiset linnut.
ArrayList<Lintu> linnut = new ArrayList<>()
Lintu red = new Lintu("Red");
if (linnut.contains(red)) {
System.out.println("Red on listalla.");
} else {
System.out.println("Red ei ole listalla.");
}
linnut.add(red);
if (linnut.contains(red)) {
System.out.println("Red on listalla.");
} else {
System.out.println("Red ei ole listalla.");
}
System.out.println("Mutta!");
red = new Lintu("Red");
if (linnut.contains(red)) {
System.out.println("Red on listalla.");
} else {
System.out.println("Red ei ole listalla.");
}
Red ei ole listalla. Red on listalla. Mutta! Red on listalla.
Olio metodin paluuarvona
Olemme nähneet metodeja jotka palauttavat totuusarvoja, lukuja ja merkkijonoja. On helppoa arvata, että metodi voi palauttaa minkä tahansa tyyppisen olion.
Seuraavassa esimerkissä on yksinkertainen laskuri, jolla on metodi kloonaa
. Metodin avulla laskurista voidaan tehdä klooni, eli uusi laskurio-olio, jolla on luomishetkellä sama arvo kuin kloonattavalla laskurilla:
public Laskuri {
private int arvo;
// esimerkki useamman konstruktorin käytöstä:
// konstruktorista voi kutsua toista konstruktoria this-kutsulla
// huomaa tosin, että this-kutsun tulee olla konstruktorin ensimmäisellä rivillä.
public Laskuri() {
this(0);
}
public Laskuri(int alkuarvo) {
this.arvo = alkuarvo;
}
public void kasvata() {
this.arvo = this.arvo + 1;
}
public String toString() {
return "arvo: " + arvo;
}
public Laskuri kloonaa() {
// luodaan uusi laskuriolio, joka saa alkuarvokseen kloonattavan laskurin arvon
Laskuri klooni = new Laskuri(this.arvo);
// palautetaan klooni kutsujalle
return klooni;
}
}
Seuraavassa käyttöesimerkki:
Laskuri laskuri = new Laskuri();
laskuri.kasvata();
laskuri.kasvata();
System.out.println(laskuri); // tulostuu 2
Laskuri klooni = laskuri.kloonaa();
System.out.println(laskuri); // tulostuu 2
System.out.println(klooni); // tulostuu 2
laskuri.kasvata();
laskuri.kasvata();
laskuri.kasvata();
laskuri.kasvata();
System.out.println(laskuri); // tulostuu 6
System.out.println(klooni); // tulostuu 2
klooni.kasvata();
System.out.println(laskuri); // tulostuu 6
System.out.println(klooni); // tulostuu 3
Kloonattavan ja kloonin sisältämä arvo on kloonauksen tapahduttua sama. Kyseessä on kuitenkin kaksi erillistä olioa, eli kun toista laskureista kasvatetaan, ei kasvatus vaikuta toisen arvoon millään tavalla.
Vastaavasti myös Tehdas
-olio voisi luoda ja palauttaa uusia Auto
-olioita. Alla on hahmoteltu tehtaan runkoa — tehdas tietää myös luotavien autojen merkin.
public class Tehdas {
private String merkki;
public Tehdas(String merkki) {
this.merkki = merkki;
}
public Auto tuotaAuto() {
return new Auto(this.merkki);
}
}
Muistathan tarkistaa pistetilanteesi materiaalin oikeassa alareunassa olevasta pallosta!