== 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
==compares primitive values, or for objects compares references. Two separatenewobjects are never==.equals(Object)inObjectis justthis == obj. Classes likeString,Integer,LocalDateand everyrecordoverride it to compare contents.- The rules for an
equalsoverride.- 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
nullreturnsfalserather than throwing.
- 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.
- Why hash matters. Hash-based collections look in the bucket for
hashCode()first and only then callequals(). Equal objects with different hashes land in different buckets, soequals()never even gets called.
Example
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 == yistruethanks to theIntegercache (-128..127). With128it's usuallyfalse. Always useequalson wrappers.- String literals are interned, so
"hi" == "hi"istrue, but a string built at runtime is a different object. instanceofinequalslets subclasses be equal to the parent;getClass() != o.getClass()doesn't. Mixing the two across a hierarchy breaks symmetry. Making the classfinalavoids the question.- Arrays don't override
equals. Compare them withArrays.equals.
Common mistakes
- Writing
equals(Money other): that's an overload, not an override.@Overridewould have caught it. - Overriding
equalsbut nothashCode. - Including mutable fields in
hashCodefor 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