Nezapomínej na hashCode()

Pokud ve své třídě překrýváte (override) metodu equals(Object), nikdy nezapomínejte překrýt i metodu hashCode(). Tuto poučku jistě každý slyšel mnohokrát, ale možná ne každý tuší proč. Shodou okolností jsem před několika dni narazil na produkční problém způsobený porušením tohoto principu.

Proč je to tedy důležité?

Pokud překryjeme pouze metodu equals, porušujeme tím kontrakt metody hashCode() “...If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.”, což má za následek nepředvídatelné chování všech na hashi založených kolekcí (např. HashMap, potažmo na ní závislý HashSet).

Jaké jsou důsledky?

Objekty v těchto kolekcích jsou identifikovány na základě jejich hashů. Může se tedy snadno stát, že objekt do kolekce vložíme, ale již jej nebudeme schopni získat nazpět, protože jej algoritmus bude hledat na špatném místě.

Příklad

Mějme primitivní třídu MyNumber, která má jednu vlastnost int number. Překrývá pouze metodu equals a to na základě vlastnosti number.
public class MyNumber {
	
	private int number;

	public MyNumber(int number) {
		this.number = number;
	}
	
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		MyNumber other = (MyNumber) obj;
		if (number != other.number)
			return false;
		return true;
	}
	
	// no hashCode() !
	
	@Override
	public String toString() {
		return "MyNumber [number=" + number + "]";
	}
	
	public static void main(String[] args) {
		Set<MyNumber> numbersSet = new HashSet><>;
		numbersSet.add(new MyNumber(10));
		// spravne by mel Set obsahovat pouze 1 instanci
		numbersSet.add(new MyNumber(10)); 
		// !! 2 stejne objekty v Setu
		System.out.println(numbersSet);
		// vraci false - (protoze hleda dle hashe)
		System.out.println("contains ? " + numbersSet.contains(new MyNumber(10))); 
	}

}

Správnou hashCode() metodu dnes umí vygenerovat každé IDE, i tak je ale potřeba dát si pozor na následující věci:

  • Hash musí být počítán pouze z neměnných (immutable) vlastností objektu. V opačném případě hrozí, že se hash během životního cyklu objektu změní, což se nesmí nikdy stát - nebyli bychom pak schopni získat daný objekt z kolekce nazpět.
  • Hash nesmí být počítán z vlastností, které nejsou použity v equals. Jinak opět hrozí, že dva stejné objekty (dle metody equals), budou mít rozdílné hashe.
  • Není nutné, aby nestejné objekty vracely i rozdílné hashe, nicméně z hlediska výkonnosti je to vhodné.

Článek obsahuje 2 komentáře

  • v6ak

    1
    „Hash musí být počítán pouze z neměnných (immutable) vlastností objektu.“ – samotný konkrakt Object.equals(Object) to nevyžaduje: https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#equals-java.lang.Object-

    Na druhou stranu je celkem rozumné se toho z několika důvodů držet, zejména kvůli kolekcím, které s tím počítají, což jsou snad všechny mapy a množiny s rozumnou složitostí.

    Paradoxně se zrovna v kolekcích toto nedodržuje a equals se počítá i z mutable věcí: https://docs.oracle.com/javase/8/docs/api/java/util/Set.html#equals-java.lang.Object-

    Jinak psaní equals a hashCode lze přenechat nejen IDE, ale i knihovně (Lombok) nebo jazyku (Scala). Málokdy je potřeba napsat vlastní equals.
  • Jan Verner

    2
    Díky za doplnění.