Porovnávání v Javě

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

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

Překrytí rovnosti a uspořádání

  • Java neumožňuje měnit chování operátorů. Nelze tak předefinovat chování např. ==

  • Nicméně identitu (rovnost) i uspořádání lze přizpůsobit pomocí překrytí jistých metod

  • Uspořádání se budeme věnovat později, nyní se zaměříme na identitu (rovnost) objektů

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é

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

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

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

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 will 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?

  • ⇒ Neexistuje ideální řešení.

  • Symetrie je většinou důležitější a porušení LSP je trochu sporné (striktní výklad). Proto se u podtříd spíše doporučuje používat getClass(). Záleží ale 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, musíme 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()

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 objekt X nachází v množině, metoda vypočítá jeho haš (hash), který slouží jako klíč

  • Pro hashování musí platit: a = b ⇒ h(a) = h(b), kde a a b jsou záznamy (objekty) a h(x) je hešovací funkce (hashCode).

  • Proto, jakmile změníme význam rovnosti a = b překrytím metody equals, musíme změnit i hašovací funkci hashCode.

Minimální hashCode

  • Splňuje následující implementace podmínky pro hašovací funkci?

    @Override
    public int hashCode() {
      return 0;
    }
- ANO, protože platí, že pro stejné objekty dostaneme stejný haš.
- ALE, dostaneme ho i pro všechny různé objekty
- **=> ukládání takových objektů do hašovacích tabulek by degradovalo výkon na úroveň lineárního vyhledávání**
- Proto by se měla implementace snažit dodržovat i další pravidlo: Rovnoměrné rozložení vracených hodnot s využitím plného rozsahu celých čísel.

Jak docílit optimální implementace hashCode

  • Metoda vrací celé číslo, které:

  • pro dva stejné (equals) objekty musí být vždy stejné

  • pro různé objekty by mělo být pokud možno různé (není to ani nezbytné, respektive to ani nemůže být vždy splněno)

  • Univerzální přístup:

  • Vezmou se atributy, na které se porovnává obsahová shoda v equals. Ostatní se ignorují.

  • Hodnoty atributů se převedou na int. U objektových atributů lze využít toho, že každý objekt má metodu int hashCode()

  • Jednotlivá celá čísla se vhodně zkombinují do finálního čísla.

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

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á :-))

Test pochopení vlastností hašování

  • Splňuje následující implementace podmínky pro porovnání a hašování?

public class Person {
  private String name;

  @Override
  public boolean equals(Object o) {
    return this.hashCode() == o.hashCode();
  }

  @Override
  public int hashCode() {
    return name.hashCode();
  }
}
- NE, protože opačná implikace a = b <= h(a) = h(b) obecně NEPLATÍ. equals je tedy implementována chybně.

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

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

  • 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)

Hašování pomocí třídy Objects

  • Třída nabízí i metody pro výpočet haše z jednoho nebo více objektů

public static int hashCode(Object o)
public static int hash(Object... values)
public class ComplexNumber {
  private int real;
  private int imag;

  @Override
  public boolean equals(Object o) { ... }

  @Override
  public int hashCode() {
    return Objects.hash(Integer.valueOf(real), Integer.valueOf(imag));
  }
}

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