Skip to main content Link Menu Expand (external link) Document Search Copy Copied

Olioiden vertaileminen

Tällä oppitunnilla tutustumme tarkemmin olioiden yhtäsuuruuden ja suuruusjärjestyksen vertailemiseen. Kuten merkkijonoja käsitellessämme huomasimme, olioiden vertailu ==-operaatiolla vertailee, ovatko kaksi oliota samat eikä olioiden sisältöä. Tämän oppitunnin aikana toteutamme omia vertailumetodeja, jotka toimivat myös Javan valmiiden metodien kanssa.


Sisällysluettelo

Oppitunnin videot

Videoiden katsominen edellyttää kirjautumista MS Stream -palveluun Haaga-Helian käyttäjätunnuksellasi ja liittymistä kurssin Teams-ryhmään.

Olioiden vertailu: equals-metodi 38:34

Videolla esiintyvät lähdekoodit

Olion tyypin selvittäminen ja tyyppimuunnokset 15:55

Videolla esiintyvät lähdekoodit

Olioiden järjestäminen ja compareTo-metodi 37:11

Videolla esiintyvät lähdekoodit

Olioiden vertaileminen

Kuten merkkijonoja käsiteltäessä huomasimme, merkkijonojen vertailu ==-operaattorilla vertailee olioviittauksia eikä merkkijonojen sisältöä:

public class OlioidenVertailu {

    public static void main(String[] args) {
        String hauki1 = new String("Hauki");
        String hauki2 = new String("Hauki");

        // vertailee olioviittauksia eikä merkkijonojen sisältöä:
        System.out.println(hauki1 == hauki2); // tulostaa false

        // equals-metodi vertailee merkkijonojen sisältöä:
        System.out.println(hauki1.equals(hauki2)); // tulostaa true
    }
}

Merkkijonoja vertaillaankin aina joko equals- tai equalsIgnoreCase-metodilla. Entä jos haluaisimme vertailla itse kirjoittamamme Tuote-luokan olioita?

public class Tuote {
    private String nimi;

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

Törmäämme Tuote-oliolla samaan ongelmaan, että niiden vertailu yhtäsuuruusoperaattorilla tuottaa tuloksen false:

Tuote maito1 = new Tuote("Maito");
Tuote maito2 = new Tuote("Maito");

// vertailee ovatko oliot samat:
System.out.println(maito1 == maito2); // false

maito1 == maito2-vertailu ei toimi, koska operaatio vertailee ovatko oliot samat, eikä huomioi olioiden sisältöä lainkaan.

Equals-metodi

Merkkijonojen yhteydessä käyttämämme equals-metodi on määritetty Javassa automaattisesti kaikille luokille. Vaikka emme ole määritelleet Tuote-luokkaan equals-metodia, voimme silti kutsua sitä:

// käyttää Javan valmista equals-metodia:
System.out.println(maito1.equals(maito2)); // false

Toisin kuin String-luokan kanssa, equals-metodi tuottaa nyt false, vaikka olioiden sisältö on täysin sama. Tämä johtuu siitä, että equals-metodi toimii oletuksena samalla logiikalla kuin ==:

“The equals method for class Object implements the most discriminating possible equivalence relation on objects; that is, for any non-null reference values x and y, this method returns true if and only if x and y refer to the same object (x == y has the value true).”

Oracle. Java API: Object

Jos haluamme että omien Tuote-olioiden vertailu maito1.equals(maito2) vertailee olioiden sisältöä, voimme toteuttaa oman equals-metodin!

Omaa equals-metodia käsitellään tarkemmin Helsingin yliopiston MOOC-kurssilla, jossa voit perehtyä olioiden samankaltaisuuden vertailuun tarkemmin.

Oman equals-metodin toteuttaminen (edistynyttä sisältöä 🌶️)

Edellä esitetty maito1.equals(maito2)-vertailu ei toimi, koska equals-metodi vertailee oletuksena olioita samalla tavalla kuin ==. Voimme kuitenkin määritellä aivan oman tapamme vertailla Tuote-olioita määrittelemällä oman equals-metodin, joka tarkastaa esimerkiksi tässä tapauksessa onko tuotteella sama nimi kuin toisella tuotteella.

public class Tuote {
    private String nimi;

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

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

    @Override
    public boolean equals(Object toinen) {
        // TODO: palaute true tai false riippuen Tuotteen nimestä
        return false;
    }

    @Override
    public String toString() {
        return "Tuote [nimi=" + nimi + "]";
    }
}

Huomaa, että equals-metodi ylikirjoittaa Javan standardikirjaston metodin, minkä vuoksi sen otsikon on oltava täsmälleen samanlainen kuin standardikirjastossa: public boolean equals(Object toinen). Metodin on siis oltava julkinen oliometodi (ei static), joka palauttaa totuusarvon ja saa parametrinaan minkä tahansa toisen olion.

Metodeja korvattaessa on hyvä käytäntö lisätä metodin ylle @Override-annotaatio, joka toimii sekä dokumentaationa metodin korvaamisesta että Java-kääntäjän ohjeena varmistaa, että metodi korvattiin onnistuneesti. Tämä annotaatio on meille tuttu aikaisemmilta oppitunneilta myös toString-metodin yhteydestä.

Huomaa, että metodille annettu Object toinen olio ei välttämättä ole toinen Tuote-olio, vaan se voi olla mikä tahansa olio:

@Override
public boolean equals(Object toinen) {
    // TODO: palaute true tai false riippuen Tuotteen nimestä
    return false;
}

Tyyppimuunnos ja instanceof

Mikäli olion tyypistä ei voida olla varmoja, niiden tyyppi voidaan tarkastaa Javassa instanceof-operaatiolla:

if (toinen instanceof Tuote) {
    // TODO: palaute true tai false riippuen Tuotteen nimestä
    return false;
}

Mikäli yllä toinen instanceof Tuote tuottaa arvon true, tiedämme varmuudella, että annettu vertailtava olio on myös Tuote.

Emme voi kuitenkaan suoraan käsitellä saatua oliota tuotteena tai asettaa sitä muuttujaan, koska Java-kääntäjän näkökulmasta olio on yhä Object-tyyppiä eikä Tuote. Tiedämme kuitenkin varmuudella, että toinen olio on Tuote, joten voimme tehdä ns. tyyppimuunnoksen:

Tuote toinenTuote = (Tuote) toinen;

Tyyppimuunnos ei oikeasti muuta käsiteltävää oliota toisen tyyppiseksi. Se on vain keino kertoa Java-kääntäjälle, että kyseistä arvoa tulee käsitellä tietyn tyyppisenä.

Nyt kun sekä this että toinenTuote ovat Tuote-olioita, voimme vertailla niiden sisältöä toisiinsa:

@Override
public boolean equals(Object toinen) {
    if (toinen instanceof Tuote) {
        Tuote toinenTuote = (Tuote) toinen;
        return this.nimi.equalsIgnoreCase(toinenTuote.nimi);

    } else {
        // ei ollut Tuote-olio, joten palautetaan false
        return false;

    }
}
Tuote maito1 = new Tuote("Maito");
Tuote maito2 = new Tuote("Maito");
Tuote kauramaito = new Tuote("Kauramaito");

System.out.println(maito1.equals(maito2));      // true
System.out.println(maito1.equals(kauramaito));  // false

Oman luokkamme equals toimii nyt aivan kuten Javan valmiiden luokkien metodit, joten sitä voidaan kutsua itse kuten yllä. Se toimii myös automaattisesti Javan valmiiden metodien yhteydessä, kuten seuraavassa kappaleessa “Mihin tarvitsemme olioiden vertailua?” todetaan.

Javan uusimmissa versioissa instanceof-operaatiota on jatkokehitetty siten, että tarkastuksen jälkeen voidaan suoraan määritellä muuttuja, johon automaattisesti sijoitetaan tarkastettu olio, mikäli se läpäisi tarkastuksen. Näin edellä kirjoitettu koodi voidaan kirjoittaa ilman erillistä tyyppimuunnosta:

if (toinen instanceof Tuote t) {
    return this.nimi.equalsIgnoreCase(t.nimi);
}

Yllä t-muuttuja viittaa samaan olioon kuin toinen, mutta muuttujan tyyppi on Tuote eikä Object. Näin t.nimi toimii suoraan if-lohkon sisällä ilman erillisiä tyyppimuunnoksia.

Huom! Yllä esitetty uudempi syntaksi ei välttämättä toimi Viopen Java-versiossa.

Mihin tarvitsemme olioiden vertailua?

Olioiden vertailulle ja equals-metodille on Javan standardikirjastossa paljon muutakin käyttöä kuin kahden muuttujan arvojen vertailu. equals-metodia hyödynnetään mm. etsiessä olioita listoilta:

Tuote leipa = new Tuote("Leipä");
List<Tuote> tuotteet = List.of(new Tuote("Maito"), new Tuote("Piimä"), new Tuote("Leipä"));

boolean sisaltaaLeivan = tuotteet.contains(leipa);
System.out.println(sisaltaaLeivan); // true

contains-metodi kutsuu sisäisesti listan indexOf-metodia, joka selvittää etsittävän olion sijainnin listalla kutsumalla vuorollaan jokaisen listalla olevan olion equals-metodia:

// Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
for (int i = 0, s = size(); i < s; i++) {
    if (o.equals(get(i))) {
        return i;
    }
}

Lähde: AdoptOpenJDK. GitHub.com

Toteuttamamme equals-metodi toimii siis nyt yhdessä contains-metodin sekä indexOf-metodin kanssa ja leipä löytyy listalta. Jos emme olisi toteuttaneet omaa equals-metodia, edellä esitetty koodi ei toimisi, koska kyseessä on kaksi eri oliota.

public class TuoteOhjelma {

    public static void main(String[] args) {
        Tuote maito1 = new Tuote("Maito");
        Tuote maito2 = new Tuote("Maito");

        System.out.println(maito1);
        System.out.println(maito2);

        System.out.println("Vertailuoperaattori: " + (maito1 == maito2)); // false, koska maito1 on eri olio kuin maito2
        System.out.println("equals: " + maito1.equals(maito2)); // palauttaa nyt true, koska toteutimme equals-metodin

        // MUITA HYÖTYJÄ EQUALS-METODISTA:

        List<Tuote> tuotteet = List.of(maito1, new Tuote("Leipä"), new Tuote("Piimä"));

        // Listoilta hakeminen käyttää taustalla equals-metodia:
        boolean onLeipa = tuotteet.contains(new Tuote("Leipä"));

        // Indeksin etsiminen käyttää taustalla equals-metodia:
        int piimaIndeksi = tuotteet.indexOf(new Tuote("Piimä"));

        System.out.println("contains: " + onLeipa); // Toimii! => true
        System.out.println("indexOf: " + piimaIndeksi); // Toimii => 2
    }
}

Tätä aihetta käsitellään tarkemmin Helsingin yliopiston MOOC-kurssilla, jossa voit perehtyä olioiden samankaltaisuuden vertailuun tarkemmin.

Olioiden järjestäminen listalla

Listoja ja taulukoita käsitellessämme olemme huomanneet, että Collections.sort sekä Arrays.sort osaavat automaattisesti järjestää Javan standardikirjaston olioita, kuten merkkijonoja, numeroita ja päivämääriä, sisältäviä listoja. Tällä kerralla perehdymme siihen, miksi omat oliomme eivät asetu automaattisesti järjestykseen, ja miten voimme määritellä oman luokkamme olioille luonnollisen järjestyksen.

Järjestäminen ja Collections.sort

Oletetaan, että meillä on lista, jossa on neljä nimeä sekajärjestyksessä:

List<String> nimet = Arrays.asList("Maija", "Matti", "Arja", "Aatami");

Tiedämme aikaisempien oppituntien perusteella, että Javassa on valmiina tapa vertailla ja järjestellä olioita:

Collections.sort(nimet);

// Nimet ovat nyt aakkosjärjestyksessä:
System.out.println(nimet); // [Aatami, Arja, Maija, Matti]

Nimet oli helppoa laittaa järjestykseen Collections.sort-metodin avulla!

Miten Java järjesti oliot?

Collections.sort osasi järjestää merkkijonot kasvavaan järjestykseen, koska String-luokka on yhteensopiva Comparable-tyypin kanssa. Kaikki Comparable-oliot osaavat vertailla omaa järjestystään suhteessa toiseen saman luokan olioon.

Comparable-tyypin dokumentaatiosta löydämme listan kaikista standardikirjaston luokista, jotka ovat järjestettävissä (kohta All Known Implementing Classes). Collections.sort osaa siis asetella merkkijonojen lisäksi liki 200 muutakin tyyppiä oikeaan järjestykseen automaattisesti.

compareTo-metodi

Kaikkiin Comparable-luokkiin on tehty valmiiksi compareTo-niminen metodi, jonka avulla voidaan vertailla kahden olion keskenäistä järjestystä. Collections.sort kutsuu sisäisesti jokaisen merkkijonon compareTo-metodia ja järjestää arvot vertailujen tulosten mukaan.

Voimme halutessamme myös itse kutsua compareTo-metodia ja tutkia sen tuloksia:

String eka = "Maija";
String toka = "Aatami";

// Kutsutaan String-luokan compareTo-metodia:
int tulos = eka.compareTo(toka);

System.out.println(tulos); // tulostaa 12

compareTo palauttaa aina kokonaisluvun, josta päätellään, kumpi arvoista tulee järjestyksessä ensin:

“Compares this object with the specified object for order. Returns a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.”

Oracle. Java API: Comparable.

Vapaasti suomennettuna, vertailulogiikka toimii seuraavasti:

  • Jos se olio, jonka metodia kutsuttiin on järjestyksessä ensin, compareTo palauttaa negatiivisen luvun.
  • Jos parametrina annettu olio on järjestyksessä ensin, compareTo palauttaa positiivisen luvun.
  • Muussa tapauksessa palautetaan 0.

Edellä oleva rivi "Maija".compareTo("Aatami") palauttaa arvon 12, eli "Maija" on aakkosissa "Aatami":n jälkeen. Seuraavat esimerkit näyttävät kaikki kolme erilaista tulosta:

System.out.println("apple".compareTo("xerox")); // -23 on negatiivinen, eli a tulee ennen x:ää
System.out.println("windows".compareTo("bitcoin")); // 21 on positiivinen, eli w tulee b:n jälkeen
System.out.println("tesla".compareTo("tesla")); // tulos on nolla, eli merkkijonot ovat "yhtäsuuret"

Vertailun tulosta voidaan käyttää kuin mitä tahansa arvoa:

String eka = "Kilo";
String toka = "Gramma";

int tulos = eka.compareTo(toka);

if (tulos < 0) {
    System.out.println(eka + " < " + toka);
} else if (0 < tulos) {
    System.out.println(eka + " > " + toka);
} else {
    System.out.println(eka + " == " + toka);
}

Edellä esitetyillä arvoilla yllä oleva koodi tulostaa Kilo > Gramma.

Omien olioiden järjestäminen

Jos yritämme järjestää oman Tuote-luokan oliot Collections.sort-metodin avulla, saamme seuraavan virheilmoituksen:

The method sort(List<T>) in the type Collections is not applicable for the arguments (List<Tuote>)

Tämä johtuu siitä, että luokkamme ei ole yhteensopiva Comparable-tyypin kanssa. Collections.sort-metodi ei siis pysty vertailemaan olioitamme keskenään.

Onneksi olioiden vertailemiseksi voimme määritellä myös vaihtoehtoisia vertailuoperaatioita.

Vaihtoehtoiset vertailuoperaatiot Collections.sort-metodilla

Merkkijonojen vertailu compareTo-metodilla ei huomioi luonnostaan oikein eri kirjainkokoja tai eri kielissä olevia paikallisia merkkejä (ä, ö, å):

List<String> nesteet = Arrays.asList("maito", "Vesi", "ketsuppi", "bensa", "Limu");
Collections.sort(nesteet);

System.out.println(nesteet); // Väärin: [Limu, Vesi, bensa, ketsuppi, maito]

Onneksi String-luokasta löytyy tähän tarkoitukseen sopiva valmis compareToIgnoreCase-metodi. Kun haluamme käyttää tätä metodia oletusmetodin sijasta, voimme määritellä vertailussa käytettävän metodin Collections.sort-kutsun toisena parametrina:

// kerrotaan, että vertailussa halutaan käyttää String-luokan compareToIgnoreCase-metodia:
Collections.sort(nesteet, String::compareToIgnoreCase);

System.out.println(nesteet); // oikea aakkosjärjestys: [bensa, ketsuppi, Limu, maito, Vesi]

Yllä Collections.sort-metodille annetaan toisena parametrina String::compareToIgnoreCase, joka on ns. metodiviittaus. Metodiviittauksessa esiintyy ensin luokan nimi, sitten kaksi kaksoispistettä :: ja lopuksi metodin nimi. Huomaa, että metodia ei suoriteta omassa koodissa, vaan se annetaan parametrina. Siksi metodin nimen jälkeen ei kirjoiteta sulkuja ().

Metodiviittauksen avulla sort käyttää vertailemiseen antamaamme metodia oletuksena olevaa compareTo-metodia. Lue lisää osoitteesta: https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html

Listan järjesteleminen omilla luokilla (edistynyttä sisältöä 🌶️)

Vaikka oma Tuote-luokkamme ei ollut sellaisenaan yhteensopiva Collections.sort-metodin kanssa, voimme ohittaa tämän ongelman antamalla listan lisäksi vertailuoperaation.

Tutustu Javan Comparator.comparing-metodiin, jonka avulla voit määritellä vertailijan kutsumaan mitä tahansa oman luokkasi metodia olioiden järjestämiseksi: https://www.baeldung.com/java-8-comparator-comparing. Tällä kurssilla sinun kannattaa lukea artikkelista kohta 3.1. Key Selector Variant ja sitä aikaisemmat, mutta ei välttämättä tätä kohtaa pidemmälle.

Comparator.comparing-metodille voidaan antaa metodiviittaus mihin tahansa metodiin, jolloin sort käyttää vertailussa juuri tuon metodin palauttamia arvoja. Voisimme sen avulla esimerkiksi järjestää merkkijonot pituusjärjestykseen vertailemalla merkkijonojen pituuksia, jotka selviävät length()-metodin avulla: Comparator.comparing(String::length):

// tehdään järjestely merkkijonojen length-metodin mukaan:
Collections.sort(nesteet, Comparator.comparing(String::length));

System.out.println(nesteet); // kasvava pituusjärjestys: [Limu, Vesi, bensa, maito, ketsuppi]

Jos haluaisit järjestää Tuote-oliot vastaavasti niiden nimi-arvojen mukaiseen kasvavaan järjestykseen, se onnistuisi antamalla metodiviittaus Tuote::getNimi:

Collections.sort(tuotteet, Comparator.comparing(Tuote::getNimi));