ByteScrollGet the app
☰ Topics
equals-hashcode9 / 200‹›
JAVA / OOP3 minute read

== vs equals() vs the hashCode contract

Medium

== on objects asks "same instance?". equals() asks "same value?", and is == unless a class overrides it. The contract: objects that are equal must return the same hashCode(). Break it and HashMap and HashSet stop finding your objects.

How it works

  1. == compares primitive values, or for objects compares references. Two separate new objects are never ==.
  2. equals(Object) in Object is just this == obj. Classes like String, Integer, LocalDate and every record override it to compare contents.
  3. The rules for an equals override.
    • Reflexive: an object is always equal to itself.
    • Symmetric: if x says it matches y, y has to agree.
    • Transitive: x matches y and y matches z, so x must match z.
    • Consistent: repeated calls give the same answer while no field changes.
    • Null-safe: comparing with null returns false rather than throwing.
  4. hashCode rule. Whenever two objects compare equal, their hash codes have to match. It only goes one way: unequal objects are allowed to share a hash, which is just a collision.
  5. Why hash matters. Hash-based collections look in the bucket for hashCode() first and only then call equals(). Equal objects with different hashes land in different buckets, so equals() never even gets called.
noyesyesnoset.contains(x)bucket =f(x.hashCode())Any entry inbucket withsame hash?false (equalsnever called)x.equals(entry)?truefalse
noyesyesnoset.contains(x)bucket =f(x.hashCode())Any entry inbucket withsame hash?false (equalsnever called)x.equals(entry)?truefalse

Example

Example.javaJava
final class Money {
    private final long cents;
    private final String currency;

    Money(long cents, String currency) { this.cents = cents; this.currency = currency; }

    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money m)) return false;
        return cents == m.cents && currency.equals(m.currency);
    }

    @Override public int hashCode() { return Objects.hash(cents, currency); }
}

Money a = new Money(500, "EUR"), b = new Money(500, "EUR");
boolean same  = a == b;              // false: two instances
boolean equal = a.equals(b);         // true: same value
boolean found = Set.of(a).contains(b); // true, because hashCode agrees

// Same thing in one line
record Price(long cents, String currency) {}

Edge cases

  • Integer x = 127, y = 127; x == y is true thanks to the Integer cache (-128..127). With 128 it's usually false. Always use equals on wrappers.
  • String literals are interned, so "hi" == "hi" is true, but a string built at runtime is a different object.
  • instanceof in equals lets subclasses be equal to the parent; getClass() != o.getClass() doesn't. Mixing the two across a hierarchy breaks symmetry. Making the class final avoids the question.
  • Arrays don't override equals. Compare them with Arrays.equals.

Common mistakes

  • Writing equals(Money other): that's an overload, not an override. @Override would have caught it.
  • Overriding equals but not hashCode.
  • Including mutable fields in hashCode for objects stored as map keys.

Likely follow-up

"What do records generate?" equals, hashCode and toString built from all components, using equals for reference components and the wrapper's compare for primitives, so two records with equal components are equal. One quirk follows: a double component holding NaN equals NaN, while 0.0 and -0.0 don't match.

Get every deep dive in the app

Coming soon to the App StoreComing soon to Google Play