Ako správne porovnávať objekty v Jave

V objektovo orientovanom programovacom jazyku ako je Java, patrí porovnávanie objektov medzi sebou, alebo porovnávanie s inými primitívnymi typmi k často vykonávaným základným úkonom. Z praxe, ale vieme, že hlavne začínajúci programátori majú problém správne porovnávať dáta medzi sebou a kedy vlastne používať operátory rovná sa (==) a nerovná sa (!=) a kedy metódy equals(), prípadne compareTo() je pre nich pomerne mätúce.

Porovnávanie objektov je proces kontroly, či sú dva objekty rovnaké alebo nie a to na základe dát alebo referencií.  V Jave sa objekty vytvárajú z tried a každý objekt má svoj vlastný súbor dát a zadefinovaného správania. Porovnávať však môžeme, aj či sa objekty  vyskytujú na rovnakom mieste v pamäti, a ak áno, nejde o dva objekty, ale o jeden ten istý.

V tomto článku si ukážeme ako správne porovnávať objekty v Jave, či už na základe dát v objektoch, alebo ich odkazov (referencií) a to na praktických príkladoch. Dozvieš sa aj, ako je implementovaná a funguje metóda equals() a spomenieme aj význam implementácie metódy hashCode() pre efektívne porovnávanie objektov pomocou hash algoritmu.

Porovnávanie objektov s primitívnymi typmi

V Jave primitívne typy ako int, double, boolean atď. nie sú objekty, ale základné dátové typy. Pri porovnávaní objektov s primitívnymi typmi Java automaticky konvertuje zodpovedajúci wrapper objekt triedy na jeho primitívny typ pomocou rozširujúcej primitívnej konverzie (§5.1.2). Tá má špecifikované nasledujúce pravidlá:

  • Ak je jeden z operandov typu double, druhý sa skonvertuje na double.
  • V opačnom prípade, ak je jeden z operandov typu float, druhý sa skonvertuje na float.
  • V opačnom prípade, ak je jeden z operandov typu long, druhý sa skonvertuje na long.
  • V opačnom prípade sa oba operandy skonvertujú na typ int.

Príklad:

Integer I = new Integer(5);
int i = 5;

System.out.println(I == i);
// => true

System.out.println(I.equals(i));
// => true

Ak môžeme vidieť porovnávanie objektu s jeho primitívnym typom, či už prostredníctvom operátoru == alebo equals je ekvivalentné a pokiaľ sú hodnoty rovnaké, tak výsledkom je booleovská hodnota true.

Porovnávanie objektov na základe referencií

Identita premennej (tiež nazývaná referenčná rovnosť ) je definovaná referenciou (odkazom), ktorú si drží. Ak majú dve premenné rovnakú referenciu, sú totožné . Toto sa kontroluje pomocou operátora ==.

Dôležité je si zapamätať, že operátor == sa v Jave používa na porovnanie referencií dvoch objektov, nie ich obsahov. Kontroluje teda, či oba odkazy ukazujú na presne ten istý objekt v pamäti (resp. jeho adresu).

Príklad:

String s1 = new String("Java");
String s2 = new String("Java");
System.out.println(s1 == s2);
// => false

V tomto príklade máme v oboch reťazcoch rovnaký text, ale keďže porovnávame referencie (odkazy do pamäte na dve rôzne inštancie), výsledkom je pochopiteľne booleovská hodnota false.

Príklad:

String s1 = new String("Java");
String s2 = s1;
System.out.println(s1 == s2);
// => true

Ak však namiesto vytvorenia inštancie do druhého reťazca skopírujeme referenciu na prvý reťazec pri porovnávaní referencií je výsledkom true.

Porovnávanie objektov na základe hodnôt

Rovnosť premennej je definovaná hodnotou, na ktorú odkazuje. Ak dve premenné odkazujú na rovnakú hodnotu, sú rovnaké . Toto sa kontroluje v Jave pomocou metódy equals(). Je súčasťou triedy Object, ktorá je rodičovskou triedou všetkých tried v jazyku Java. To znamená, že môžeme v Jave používať equals na porovnanie obsahu dát akýchkoľvek dvoch objektov.

Príklad:

String s1 = new String("Java");
String s2 = new String("Java");
System.out.println(s1.equals(s2));
// => true

Príklad demonštruje porovnávanie 2 rôznych inštancií typu String s rovnakým textom. Výsledok volania equals() vráti booleovskú hodnotu pravda. Ak by sme mali v predchádzajúcom príklade skopírovanú referenciu na ten istý objekt, výsledkom porovnania hodnôt toho istého objektu je rovnako pravdivý.

Príklad:

String s1 = new String("Java");
String s2 = s1;
System.out.println(s1.equals(s2));
// => true

Do teraz sme používali v príkladoch jednoduché objekty, ktoré nám autori Javy pripravili v knižniciach. Teraz si skúsime vytvoriť vlastný objekt a pozrieme sa metódu equals() podrobnejšie.

Príklad:

Trieda Shape

package pack;

public class Shape {
    String colour;

    public Shape(String colour) {
        this.colour = colour;
    }

    public String getColour() {
        return colour;
    }

    public void setColour(String colour) {
        this.colour = colour;
    }
}

Trieda Circle

package pack;

public class Circle extends Shape {
    double radius;

    public Circle (String colour, double radius) {
        super(colour);
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        this.radius = radius;
    }
}

Vytvorili sme si triedu Circle, ktorá dedí od rodičovskej triedy Shape. Teraz si vytvoríme dve inštancie s rovnakými dátami a porovnáme si ich referencie a ich hodnoty pomocou metódy equals.

Circle c1 = new Circle("green", 1);
Circle c2 = new Circle("green", 1);

System.out.println(c1 != c2);
// => true

System.out.println(c1.equals(c2));
// => false

Zatiaľ čo, to že sú referencie na dve rôzne inštancie rôzne, to sme očakávali. Prekvapením, ale môže byť, že napriek tomu, že oba objekty obsahujú rovnaké hodnoty atribútov, metóda equals vráti false a teda hodnotovo sú oba objekty rôzne. A to preto, lebo štandardná implementácia equals v triede Object, ktorú zdedí každý objekt porovnáva adresy pamäte objektov, takže funguje rovnako ako operátor ==. Preto musíme túto metódu prepísať, aby sme si zadefinovali, čo znamená rovnosť pre naše objekty. Na to, ale najskôr potrebujeme vedieť akými pravidlami sa riadi metóda equals.

Pravidlá metódy equals()

Predpokladajme, že máme zadefinované referencie x, y, z. Potom by správne implementovaná metóda equals mala mať nasledujúce vlastnosti.

Reflexívnosť: pre každú nenulovú referenčnú hodnotu x by x.equals(x) sa malo vrátiť true.

Symetrickosť: pre všetky nenulové referenčné hodnoty x a y, x.equals(y) by sa malo vrátiť true, vtedy a len vtedy, ak y.equals(x) vráti hodnotu true.

Tranzitívnosť: pre všetky referenčné hodnoty, ktoré nie sú nulové x, y a z, ak x.equals(y) vráti true a y.equals(z) vráti true, potom x.equals(z) by malo vrátiť true.

Konzistentnosť: pre akékoľvek nenulové referenčné hodnoty x a y viacnásobné vyvolanie x.equals(y) za predpokladu, že sa nezmenia žiadne informácie použité pri equals porovnávaní objektov, vrátia konzistentne za sebou stále tie isté true alebo false hodnoty.

Pre akúkoľvek referenčnú hodnotu , ktorá nie je nulová , x.equals(null) by sa mala vrátiť false.

Správna implementácia metódy equals()

Teraz, keď už poznáme pravidlá equals, naimplementujeme si svoju vlastnú verziu equals pre triedu Circle nasledovne.

@Override
public boolean equals(Object o) {
    // self check
    if (this == o) return true;

    // Null check
    if (o == null) return false;

    // Type check and cast
    if (getClass() != o.getClass())
        return false;
    
    Circle circle = (Circle) o;

    // Field comparison
    return Objects.equals(radius,  circle.radius)
            && Objects.equals(colour, circle.colour);
}

Ešte si overíme, že obe inštancie obsahujú rovnaké hodnoty a metóda equals funguje správne a vráti true.

Circle c1 = new Circle("green", 1);
Circle c2 = new Circle("green", 1);
System.out.println(c1.equals(c2));
// => true

V našej implementácii equals najskôr skontrolujeme, či neporovnávame objekt sám so sebou a ak áno výsledkom je true.

Potom skontrolujeme, či referencia na druhý objekt, s ktorým porovnávame prvý objekt je platná (to znamená nenulová) a ak nie, vrátime false. Žiadná inštancia by nemala byť rovná null a týmto riadkom zabezpečíme, že neskôr nedostaneme NullPointerException pri prístupe na atribúty.

Pred samotným porovnaním atribútov ešte otestujeme, že oba porovnávané objekty patria do tej istej triedy. Toto tak isto zabezpečí, že aj keď trieda Circle je oddedená z triedy Shape, porovnávanie týchto tried vráti vždy false.

Metóda hashCode()

Bežnou praxou v Jave je, že ak prepíšeme pôvodnú implementáciu equals(), tak by sme mali prepísať aj metódu hashCode(), keďže obe metódy navzájom spolupracujú, hlavne ak narábame s kolekciami objektov.

Súvislosť medzi oboma metódami je taká, že ak metoda equals určí, že oba objekty sú rovnaké, tak by mali mať vypočítať aj rovnaký hash kód. Bez správnej implementácie hash kódu, používanie kolekcií, ktoré sú na ňom závislé ako sú napr HashSetHashMap nebudú fungovať správne.

Teraz si ukážeme na našej triede Circle, ako by sme mali správne preťažiť metódu hashCode().

@Override
public int hashCode() {
    return Objects.hash(radius, colour);
}

Zhrnutie

V Jave nie je porovnávanie dvoch objektov také jednoduché ako porovnávanie dvoch primitívnych dátových typov. Vyžaduje si hlbšie pochopenie štruktúry objektov ako aj spôsobu ich uloženia v pamäti. Metóda equals() je primárna metóda používaná na porovnanie dvoch objektov v Jave. Keď prepíšeme metódu equals(), musíme prepísať aj metódu hashcode(), aby sme zabezpečili, že dva rovnaké objekty budú mať rovnaký hash kód. Pomocou metód equals() a hashcode() môžeme efektívne porovnávať dva objekty na základe ich vnútorného stavu, a nie na základe ich umiestnenia v pamäti.

Ak si Java programátor a hľadáš prácu, pozri si naše benefity pre zamestnancov a reaguj na najnovšie ponuky práce.

O autorovi

Jozef Wagner

Java Developer Senior

Viac ako 10 rokov programujem v Jave, momentálne pracujem v msg life Slovakia ako Java programátor senior a pomáham zákazníkom implementovať ich požiadavky do poistného softvéru Life Factory. Vo voľnom čase si rád oddýchnem v lese, prípadne si zahrám nejakú dobrú počítačovú hru.

Daj nám o sebe vedieť