Porovnávání v Javě

Tomáš Pitner, Radek Ošlejšek

Porovnávání a pořadí (uspořádání)

  • Obecně rozlišujeme, zda chceme zjišťovat shodnost (rovnost, ekvivalenci):

    • mezi dvěma primitivními hodnotami

    • mezi dvěma objekty

  • U primitivních hodnot jsou rovnost i uspořádání určeny napevno a nelze je změnit.

  • U objektů lze porovnání i uspořádání programově určovat.

Rovnost primitivních hodnot

  • Rovnost primitivních hodnot zjišťujeme pomocí operátorů:

    • == (rovná se)

    • != (nerovná se)

  • U integrálních (celočíselných) typů funguje bez potíží.

  • U čísel floating-point (double, float) je třeba porovnávat s určitou tolerancí.

  • U FP navíc existují hodnoty jako +0.0 vedle -0.0 a tyto by měly být rovny.

1 == 1 // true
1 == 2 // false
1 != 2 // true

1.000001 == 1.000001 // true
1.000001 == 1.000002 // false
Math.abs(1.000001 - 1.000002) < 0.001 // true

Porovnávání metodami třídy Objects

  • java.util.Objects je poměrně nová utilitní třída (obsahující jen statické metody) od Javy 8

  • mimo jiné obsahuje metody Objects.equals(o1, o2) na porovnávání objektů

  • i ve variantě deep - hluboké porovnání - pro struktury

public static boolean equals(Object a, Object b)

public static boolean deepEquals(Object a, Object b)

Uspořádání primitivních hodnot

  • Uspořádání primitivních hodnot funguje pomocí operátorů <, , >=, >

  • U primitivních hodnot nelze koncept uspořádání ani rovnosti programově měnit.

Uspořádání není definováno na typu boolean, tj. neplatí false < true!
1.000001 <= 1.000002  // true

Jak chápat rovnost objektů

Identita objektů, ==

vrací true při rovnosti odkazů, tj. když oba odkazy ukazují na tentýž objekt

Rovnost obsahu, metoda equals

vrací true při obsahové ekvivalenci objektů, což musí být explicitně nadefinované

Příklad ==

Person pepa1 = new Person("Pepa");
Person pepa2 = new Person("Pepa");
Person pepa3 = pepa1;

pepa1 == pepa2; // false
pepa1 == pepa3; // true

Porovnávání objektů pomocí ==

  • Porovnáme-li dva objekty prostřednictvím operátoru == dostaneme rovnost jen v případě, jedná-li se o dva odkazy na tentýž objekt.

  • Jedná-li se o dva byť obsahově stejné objekty, ale existující samostatně, pak == vrátí false.

Objekty jsou identické = jedná se o jeden objekt = odkazy obsahují stejnou adresu objektu.

Porovnávání objektů dle obsahu

  • Dva objekty jsou rovné (rovnocenné), mají-li stejný obsah.

  • Na zjištění rovnosti se použije metoda equals, kterou je potřeba překrýt.

  • Pro nadefinování rovnosti bude hlavička metody vždy vypadat následovně:

Metoda equals

@Override
public boolean equals(Object o)
  • Parametrem je objekt typu Object.

  • Jestli parametr není objekt typu <class-name>, obvykle je potřeba vrátit false.

  • Pak se porovnají jednotlivé vlastnosti objektů a jestli jsou stejné, metoda vráti true.

Příklad s equals: komplexní číslo

Dvě komplexní čísla jsou stejná, když mají stejnou reálnou i imaginární část.

public class ComplexNumber {
  private int real, imag;
  public ComplexNumber(int r, int i) {
    real = r; imag = i;
  }
  @Override
  public boolean equals(Object o) {
    if (this.getClass() != o.getClass()) return false;

    ComplexNumber that = (ComplexNumber) o;
    return this.real == that.real
        && this.imag == that.imag;
  }
}

Mírně odlišné equals

public class ComplexNumber {
  private int real, imag;
  public ComplexNumber(int r, int i) {
    real = r; imag = i;
  }
  @Override
  public boolean equals(Object o) {
    // instanceof type pattern, we'll see later
    if (o instanceof ComplexNumber that)
      return this.real == that.real
        && this.imag == that.imag;
    else return false;
  }
}

Porovnávání objektů — osoba I

Popis kódu na následujícím slajdu:

  • Dvě osoby budou stejné, když mají stejné jméno a rok narození.

  • Rovnost nemusí obsahovat porovnání všech atributů (porovnání age je zbytečné, když máme yearBorn).

  • String je objekt, proto pro porovnání musíme použít metodu equals.

  • Klíčové slovo instanceof říká "mohu pretypovat na daný typ".

Metoda equals musí být reflexivní, symetrická i tranzitivní (javadoc).

Porovnávání objektů — osoba II

public class Person {
  private String name;
  private int yearBorn, age;

  public Person(String n, int yB) {
    name = n; yearBorn = yB; age = currentYear - yB;
  }
  @Override
  public boolean equals(Object o) {
    if (!(o instanceof Person)) return false;

    Person that = (Person) o;
    return this.name.equals(that.name)
        && this.yearBorn == that.yearBorn;
  }
}

Technické zjednodušení instanceof

  • Počínaje Javou 14 lze využít tzv. instanceof pattern.

  • Odkaz na objekt se kromě běhové typové kontroly rovnou přiřadí do person.

if (o instanceof Person person) {
  return this.name.equals(person.name)
    && this.yearBorn == person.yearBorn;
} else {
  return false;
}

Porovnávání objektů — použití

ComplexNumber cn1 = new ComplexNumber(1, 7);
ComplexNumber cn2 = new ComplexNumber(1, 7);
ComplexNumber cn3 = new ComplexNumber(1, 42);
cn1.equals(cn2); // true
cn1.equals(cn3); // false

Person karel1 = new Person("Karel", 1993);
Person karel2 = new Person("Karel", 1993);

karel1.equals(karel2); // true

karel1.equals(cn1); // false
cn2.equals(karel2); // false

Chybějící equals

  • Co když zavolám metodu equals aniž bych ji přepsal?

  • Použije se původní metoda equals ve tříde Object:

  • Původní equals funguje přísným způsobem — rovné jsou jen identické objekty:

public boolean equals(Object obj) {
  return (this == obj);
}

Jak porovnat typ třídy

Je this.getClass() == o.getClass() stejné jako o instanceof Person?

  • Ne! Jestli třída Manager dědí od Person, pak:

manager.getClass() == person.getClass() // false
manager instanceof Person // true
  • Co tedy používat?

    • instanceof porušuje symetrii x.equals(y) == y.equals(x)

    • getClass porušuje tzv. Liskov substitution principle

  • Záleží tedy na konkrétní situaci.

Metoda hashCode

  • Při překrytí metody equals nastává dosud nezmíněný problém.

  • Jakmile překryjeme metodu equals, měli bychom současně překrýt i metodu hashCode.

  • Metoda hashCode je také ve třídě Object, tudíž ji obsahuje každá třída.

@Override
public int hashCode()
  • Metoda vrací celé číslo pro daný objekt tak, aby:

  • pro dva stejné (equals) objekty musí vždy vrátit stejnou hodnotu

  • jinak by metoda měla vracet různé hodnoty

  • není to ani nezbytné a ani nemůže být vždy splněno

  • složité třídy mají více různých objektů než je všech hodnot typu int

Příklad hashCode I

public class ComplexNumber {
  private int real;
  private int imag;
  ...
  @Override
  public boolean equals(Object o) { ... }

  @Override
  public int hashCode() {
    return 31*real + imag;
  }
}

Příklad hashCode II

public class Person {
  private String name;
  private int yearBorn, age;
  ...
  @Override
  public boolean equals(Object o) { ... }

  @Override
  public int hashCode() {
    int hash = name.hashCode();
    hash += 31*hash + yearBorn;
    return hash;
  }
}

Obecný hashCode

Nejlépe je vytvářet metodu následujícím způsobem (31 je prvočíslo):

  @Override
  public int hashCode() {
    int hash = attribute1;
    hash += 31 * hash + attribute2;
    hash += 31 * hash + attribute3;
    hash += 31 * hash + attribute4;
    return hash;
  }

A nebo ji generovat (pokud víte, co to dělá :-))

Proč hashCode

  • Metoda se používá v hašovacích tabulkách, využívá ji například množina HashSet.

  • Při zjištění, jestli se prvek X nachází v množině, metoda vypočítá její haš (hash).

  • Pak vezme všechny prvky se stejným hašem a zavolá equals (haš mohl být stejný náhodou).

  • Jestli má každý objekt unikátní haš, pak je tato operace konstantní.

  • Jestli má každý objekt stejný haš, pak je operace contains lineární!

Jestli se hashCode napíše špatně (nevrací pro stejné objekty stejný haš) nebo zapomene — množina nad danou třídou přestane fungovat!

Uspořádání objektů

  • Budeme probírat později

  • V Javě neexistuje přetěžování operátorů <, ⇐, >, >=

  • Třída musí implementovat rozhraní Comparable a její metodu compareTo

Repl.it demo k porovnávání primitivních hodnot a objektů