Equality & Hashing
== checks identity, equals() checks whatever a class defines as "equal," and hashCode() has to agree with equals() or hash-based collections quietly break — three related but distinct ideas that trip up even experienced developers.
Java gives you two different notions of "equal": == and equals(). Getting them confused, or overriding one without the other, is one of the most common sources of subtle bugs in real Java programs. This topic works through identity versus value equality, the equals/hashCode contract, and the practical consequences of getting it wrong.
== on Primitives vs. References
On primitive types, == compares the actual values directly — 5 == 5 is true because both sides are the same numeric value. On reference types, == compares reference identity: whether both variables point to the exact same object in memory, not whether the objects have equivalent content.
int a = 5, b = 5;
System.out.println(a == b); // true: comparing primitive values
String s1 = new String("hi");
String s2 = new String("hi");
System.out.println(s1 == s2); // false: two distinct String objects
System.out.println(s1.equals(s2)); // true: equals() compares contentequals(): Behavior Defined by the Class
equals(Object) is an ordinary method, inherited from Object, that any class can override. Its behavior is entirely up to that class's implementation — Object's default is identity comparison (same as ==), but a class like String overrides it to compare character sequences, and a custom class can define equals() however makes sense for it. There is no automatic "deep equality" for arbitrary objects; you get exactly what the class's equals() method implements.
class NoOverride { int value; }
class WithOverride {
int value;
WithOverride(int value) { this.value = value; }
@Override
public boolean equals(Object o) {
return (o instanceof WithOverride w) && w.value == this.value;
}
@Override
public int hashCode() { return Integer.hashCode(value); }
}
// new NoOverride().equals(new NoOverride()) -> false (identity, from Object)
// new WithOverride(1).equals(new WithOverride(1)) -> true (value-based, custom)String's == vs. equals() Trap
String literals are often, but not always, the same object due to an internal literal pool the JVM typically maintains — this makes "a" == "a" often true for literals, which misleads many beginners into thinking == works for Strings in general. Strings created with new, read from user input, or built by concatenation at runtime are frequently distinct objects even with identical content, so == becomes unreliable exactly when you'd rely on it most. Always use equals() for String content comparison; never depend on interning behavior for correctness.
String a = "hello";
String b = "hello";
System.out.println(a == b); // often true -- literal pool detail, not a guarantee to rely on
String c = new String("hello");
System.out.println(a == c); // false: c is a distinct object
System.out.println(a.equals(c)); // true: same content -- the correct comparisonThe equals() Contract
A correct equals() override must satisfy several properties, as documented on Object: reflexive (x.equals(x) is always true), symmetric (x.equals(y) implies y.equals(x)), transitive (x.equals(y) and y.equals(z) implies x.equals(z)), consistent (repeated calls return the same result as long as neither object changes), and x.equals(null) must always return false. Violating any of these produces surprising, hard-to-debug behavior in code (and library collections) that assumes the contract holds.
hashCode(): Purpose and Requirements
hashCode() returns an int summarizing an object for fast bucket lookup in hash-based collections like HashMap and HashSet. The critical rule linking it to equals() is: if two objects are equal according to equals(), they must return the same hashCode(). The reverse is not required — unequal objects may (and eventually, by pigeonhole, must) share a hash code; this is a "collision," and hash-based collections are designed to handle it correctly, just with a small performance cost.
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
return (o instanceof Point p) && p.x == x && p.y == y;
}
@Override
public int hashCode() {
return java.util.Objects.hash(x, y); // combines x and y into one hash
}
}What Breaks When You Violate the Contract
If equals() is overridden without overriding hashCode(), two objects can be equal() but land in different hash buckets — a HashSet.contains() or HashMap.get() lookup can then fail to find an entry that is, by equals(), clearly present. This is one of the most common real-world Java bugs, and it typically produces no compiler warning at all, only puzzling runtime behavior.
class Bad {
int id;
Bad(int id) { this.id = id; }
@Override
public boolean equals(Object o) {
return (o instanceof Bad b) && b.id == id;
}
// hashCode() NOT overridden -- still Object's identity-based default
}
import java.util.HashSet;
HashSet<Bad> set = new HashSet<>();
set.add(new Bad(1));
System.out.println(set.contains(new Bad(1))); // false! equals() says they match,
// but hashCode() disagrees, so the
// set never even checks the same bucket.Mutable Fields as Hash-Key Danger
If an object's hashCode() (or equals()) is derived from fields that later change, and that object is already stored as a key in a HashMap or HashSet, the collection can effectively "lose" the entry: it was inserted into a bucket based on the old hash, but a later lookup computes a different hash from the mutated state and looks in the wrong bucket. The entry isn't gone, but it becomes unreachable through normal lookup. The safe practice is to only use immutable fields (or entirely immutable objects) as hash keys.
class MutableKey {
int id;
MutableKey(int id) { this.id = id; }
@Override public boolean equals(Object o) {
return (o instanceof MutableKey k) && k.id == id;
}
@Override public int hashCode() { return Integer.hashCode(id); }
}
import java.util.HashMap;
HashMap<MutableKey, String> map = new HashMap<>();
MutableKey key = new MutableKey(1);
map.put(key, "value");
key.id = 2; // mutating the key after insertion -- dangerous!
System.out.println(map.get(key)); // likely null: lookup hashes to a different bucket nowOverriding equals() and hashCode() Correctly
A typical, correct pattern: check reference identity first as a fast path, then check the argument's type/nullness with instanceof (which also handles null safely, since null instanceof AnyType is always false), then compare the relevant fields. For hashCode(), combine those same fields using java.util.Objects.hash(...) (convenient) or a hand-rolled combination for performance-sensitive code.
import java.util.Objects;
final class Employee {
private final String id;
private final String name;
Employee(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Employee other)) return false;
return id.equals(other.id) && name.equals(other.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}Comparing Reference Types with instanceof
Pattern-matching instanceof (Java 16+) both checks the type and binds a typed variable in one step, which is exactly the shape equals() implementations need. Note that o instanceof Employee other is false when o is null, so a separate null check is unnecessary before it.
Records Get This For Free
Records (Java 16+) automatically generate equals(), hashCode(), and toString() based on their components, using exactly the correct-contract pattern described above. This is one of the strongest reasons to prefer a record over a hand-written class for simple, immutable data carriers — it eliminates an entire category of equals/hashCode bugs by construction.
record Point(int x, int y) { }
// Point(1,2).equals(new Point(1,2)) -> true
// Both have identical hashCode() as well, automatically.Arrays and equals()/hashCode()
Arrays do not override equals() or hashCode() meaningfully — array equals() is identity comparison (same as ==), and array hashCode() is identity-based too. Comparing array contents requires java.util.Arrays.equals(a, b) (or Arrays.deepEquals for nested arrays), and Arrays.hashCode(a) for a content-based hash.
int[] arr1 = {1, 2, 3};
int[] arr2 = {1, 2, 3};
System.out.println(arr1.equals(arr2)); // false: identity comparison
System.out.println(java.util.Arrays.equals(arr1, arr2)); // true: content comparisonEdge Cases
Comparing across unrelated types with equals() should return false rather than throw — a well-written equals() handles any Object argument gracefully, including instances of completely different classes.
Two unequal objects sharing the same hashCode() is legal and expected occasionally (a "collision") — only the reverse (equal objects, different hash codes) is a genuine contract violation.
Floating-point NaN is a special case: Double.NaN != Double.NaN under ==, but Double.valueOf(Double.NaN).equals(Double.valueOf(Double.NaN)) is true, because Double.equals() is specified to treat NaN as equal to itself for consistency with hashCode() and collection use.
Best Practices
Always override equals() and hashCode() together, never just one. Base both on the same set of fields. Prefer immutable fields for anything used in equals()/hashCode(), especially if instances will be used as map keys or set elements. Reach for records when a class is a simple immutable data holder, to get correct equality for free. Use == deliberately only when identity comparison is genuinely what you mean (e.g., checking against a known sentinel object).
Key Takeaways
== compares primitive values directly, or reference identity for objects; it never invokes a class's custom comparison logic.
equals() is an ordinary, overridable method — its behavior (identity, value-based, or something else) is entirely defined by the class.
Whenever equals() is overridden, hashCode() must be overridden too, and both must be based on the same fields.
Equal objects must produce equal hash codes; unequal objects may share a hash code (a collision), which is legal.
Mutating fields used in hashCode()/equals() after an object is stored as a hash-collection key can make it unreachable.
Records generate a correct, contract-respecting equals()/hashCode()/toString() automatically from their components.
String input = scanner.nextLine(); if (input == "quit") { // almost always false for runtime-built Strings System.out.println("Bye"); }
String input = scanner.nextLine(); if (input.equals("quit")) { System.out.println("Bye"); }
== compares reference identity. Strings read at runtime are typically not the same object as a compile-time literal, even with identical content, so == silently fails where equals() is required.
class Id { int value; Id(int value) { this.value = value; } @Override public boolean equals(Object o) { return (o instanceof Id other) && other.value == value; } // hashCode() left as Object's default } class Id { int value; Id(int value) { this.value = value; } @Override public boolean equals(Object o) { return (o instanceof Id other) && other.value == value; } @Override public int hashCode() { return Integer.hashCode(value); } }
Overriding equals() without hashCode() breaks the required contract; equal Id objects can end up with different hash codes and "disappear" from HashSet/HashMap lookups.
HashMap<List<Integer>, String> cache = new HashMap<>(); List<Integer> key = new ArrayList<>(List.of(1, 2, 3)); cache.put(key, "cached result"); key.add(4); // mutating a live hash key! System.out.println(cache.get(List.of(1, 2, 3, 4))); // likely null, not "cached result"
HashMap<List<Integer>, String> cache = new HashMap<>(); List<Integer> key = List.copyOf(List.of(1, 2, 3)); // immutable key cache.put(key, "cached result"); System.out.println(cache.get(List.of(1, 2, 3))); // "cached result", reliably
Using a mutable collection as a hash key and then mutating it after insertion changes its hashCode(), effectively stranding the entry at its old bucket. Use immutable keys whenever an object will live inside a hash-based collection.
What does == compare when applied to two object references?
Reference identity — whether both references point to the exact same object in memory, not whether the two objects have equivalent content.
Why is 'a == b' often true for two String literals but not reliable in general?
The JVM typically maintains an internal pool that can make identical String literals share the same object, but this is an implementation detail, not a language guarantee, and Strings built at runtime (via new, concatenation, or input) are frequently distinct objects even with identical content. equals() should always be used for content comparison.
State the two-way relationship (or lack of one) between equals() and hashCode() required by the contract.
If two objects are equal via equals(), they MUST have the same hashCode(). The converse is not required: two unequal objects may share the same hashCode() (a collision), and hash-based collections are built to tolerate that correctly.
Why can mutating a field used in hashCode() after an object has been inserted into a HashSet cause lookups to fail?
The object was placed into a bucket based on its hash code at insertion time. If the mutation changes that hash code, a later lookup computes a different bucket and won't find the entry there, even though equals() would still report a match if compared directly.
Do arrays override equals() to compare their contents?
No. Array equals() is inherited identity comparison (equivalent to ==). Use java.util.Arrays.equals() (or Arrays.deepEquals() for nested arrays) to compare array contents.
Why do records automatically avoid the classic equals()-without-hashCode() bug?
Records generate both equals() and hashCode() together, automatically, based on the same set of components — there is no way to override just one and forget the other, unlike a hand-written class.
Interview-style: why does Java not provide a universal, automatic 'deep equals' for every object out of the box?
Because 'equal' means different things for different types — identity, value, structural, or business-key equality can all be valid depending on the class's purpose. Java leaves equals() as an overridable method so each class defines equality appropriately for its own semantics, rather than imposing one interpretation on every object.
Write a class Rectangle with width and height fields that implements value-based equals() and a matching hashCode().
import java.util.Objects; final class Rectangle { private final int width, height; Rectangle(int width, int height) { this.width = width; this.height = height; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Rectangle other)) return false; return width == other.width && height == other.height; } @Override public int hashCode() { return Objects.hash(width, height); } } equals() short-circuits on identity, safely rejects non-Rectangle or null arguments via instanceof pattern matching, then compares both fields; hashCode() combines the same two fields with Objects.hash(), keeping the contract intact.
What does this print, and why the apparent inconsistency?
Prints true then false. Integer caches boxed values for -128 to 127 (an implementation detail of Integer.valueOf(), used by autoboxing), so a and b are the same cached object. 200 falls outside that cache range, so c and d are distinct Integer objects, making == false. This is exactly why == should never be used to compare boxed numeric wrapper values -- always use equals() (or unbox to a primitive) instead.
Write a program that adds two 'equal' custom objects to a HashSet and demonstrates the set correctly treats them as one element, given correct equals()/hashCode() overrides.
import java.util.HashSet; import java.util.Objects; final class Coord { final int x, y; Coord(int x, int y) { this.x = x; this.y = y; } @Override public boolean equals(Object o) { return (o instanceof Coord c) && c.x == x && c.y == y; } @Override public int hashCode() { return Objects.hash(x, y); } } public class Main { public static void main(String[] args) { HashSet<Coord> set = new HashSet<>(); set.add(new Coord(1, 1)); set.add(new Coord(1, 1)); // "equal" to the first, per equals()/hashCode() System.out.println(set.size()); // 1 } }
Prints 1. Because Coord correctly overrides both equals() and hashCode() consistently, the HashSet recognizes the second Coord(1, 1) as equal to the first and does not add a duplicate.
What does this print, and what is the class missing?
Prints false, even though the two Item(5) objects are equal() by Item's own definition. Item overrides equals() but not hashCode(), so it still inherits Object's identity-based hashCode(). The two objects almost certainly hash to different buckets, so HashSet.contains() never even compares them with equals(). Adding a matching hashCode() override (based on code) fixes it.
Integer a = 127;
Integer b = 127;
Integer c = 200;
Integer d = 200;
System.out.println(a == b);
System.out.println(c == d);class Item {
int code;
Item(int code) { this.code = code; }
@Override
public boolean equals(Object o) {
return (o instanceof Item i) && i.code == code;
}
}
public class Main {
public static void main(String[] args) {
java.util.HashSet<Item> set = new java.util.HashSet<>();
set.add(new Item(5));
System.out.println(set.contains(new Item(5)));
}
}