Java hashCode() Method

In the last lesson you learned about the Java equals() method. But the moment you override equals(), you have a hidden job to do.

  • Skip it, and your objects start vanishing inside collections.
  • You add one to a HashSet, ask if it is there, and Java says no.
  • The fix is the hashCode() method, which always travels with equals().

Let’s learn why, and how to write it correctly.

πŸ€” What is hashCode()?

hashCode() returns an int. Every object has one, inherited from Object. That number is called a hash code.

  • It is a quick label that hash-based collections use to sort objects into buckets.
  • HashMap and HashSet do not search every item one by one. That is slow.
  • They group objects into buckets, and the hash code picks the bucket.

Here is the smallest example of calling it.

public class Main {
public static void main(String[] args) {
String name = "Alex";
System.out.println(name.hashCode());
}
}

Output

2043177
  • String already has a good hashCode(), so it hands back a number.
  • Different strings give different numbers.
  • The same string always gives the same number. The same input, the same hash code, every time.

πŸͺ£ Why hash-based collections need it

Picture a row of buckets, each with a number on it. Here is the order a set follows when it searches:

// Rough idea of what HashSet.contains does inside
// 1. Ask the object for its hashCode() -> pick a bucket
// 2. Go to that one bucket only
// 3. Inside the bucket, use equals() to find the exact match
  • hashCode() narrows the search to one bucket. That is what makes it fast.
  • equals() confirms the exact item inside that bucket.
  • The set never looks in the other buckets.
  • So if two equal objects land in different buckets, the set looks in the wrong place and misses the match.

This is the root of every bug in this lesson.

πŸ“œ The equals-hashCode contract

The equals-hashCode contract is a promise your code has to keep.

  • If two objects are equal by equals(), they must return the same hashCode().
  • hashCode() on the same object must return the same number every time, unless its fields change.
  • Two objects with the same hash code do not have to be equal. Sharing a bucket is fine.
If a.equals(b) is true -> a.hashCode() == b.hashCode() must be true

The rule that bites people is the first one. Break it, and collections stop working. Let’s prove it.

πŸ’₯ What breaks if you skip it

This User overrides equals() so two users match on id and email. But it forgets hashCode(), the classic mistake.

import java.util.Objects;
class User {
int id;
String email;
User(int id, String email) {
this.id = id;
this.email = email;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User other = (User) o;
return id == other.id && Objects.equals(email, other.email);
}
// ❌ No hashCode() here. Big problem.
}

Now put one user in a HashSet and ask if an equal user is inside.

import java.util.HashSet;
public class Main {
public static void main(String[] args) {
HashSet<User> users = new HashSet<>();
users.add(new User(1, "alex@mail.com"));
User same = new User(1, "alex@mail.com");
System.out.println("Are they equal? " + users.iterator().next().equals(same));
System.out.println("Does the set contain it? " + users.contains(same));
}
}

Output

Are they equal? true
Does the set contain it? false

The two users are equal, but the set says it does not contain the second one. How?

  • The first user used the default hashCode(), which gave some number.
  • The second user is a new object, so the default gave a different number.
  • Different numbers mean different buckets.
  • The set went to the empty bucket, found nothing, and reported β€œnot here”. It never reached equals().

You overrode equals() but left hashCode() alone, so the two methods disagree.

πŸ› οΈ Writing a good hashCode()

The fix is easy. Override hashCode() and build the number from the same fields equals() uses. The cleanest way is Objects.hash(...), which combines fields into one int:

import java.util.Objects;
class User {
int id;
String email;
User(int id, String email) {
this.id = id;
this.email = email;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User other = (User) o;
return id == other.id && Objects.equals(email, other.email);
}
@Override
public int hashCode() {
return Objects.hash(id, email); // βœ… same fields as equals
}
}

The key detail is which fields go into Objects.hash(...).

  • They are id and email, the exact fields equals() compares.
  • That keeps the two methods consistent.
  • Equal users share id and email, so they get the same hash code. Promise kept.

Run the same test with the fixed class:

import java.util.HashSet;
public class Main {
public static void main(String[] args) {
HashSet<User> users = new HashSet<>();
users.add(new User(1, "alex@mail.com"));
User same = new User(1, "alex@mail.com");
System.out.println("hashCode A: " + users.iterator().next().hashCode());
System.out.println("hashCode B: " + same.hashCode());
System.out.println("Does the set contain it? " + users.contains(same));
}
}

Output

hashCode A: 1843517506
hashCode B: 1843517506
Does the set contain it? true

Both users return the same hash code, so they land in the same bucket. The set runs equals(), finds the match, and says yes.

πŸ” Consistency with the fields in equals

The golden rule: use the same fields in both methods.

  • The fields that decide equality must be the fields that build the hash code. Not more, not fewer.
  • If hashCode() uses a field equals() ignores, you break the contract.
  • Then two objects could be equal yet hash differently, and the vanishing-object bug returns.
// equals compares: id, email
// hashCode must use: id, email βœ… matching
// hashCode using: id, email, lastLogin ❌ extra field breaks the contract

🧬 The default Object.hashCode()

Before you override it, the default hashCode() is identity-based.

  • It is built from the object itself, roughly its place in memory, not its fields.
  • So two separate objects almost always get different numbers, even with identical fields.

This snippet shows the default on a class that overrides nothing:

class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
}
public class Main {
public static void main(String[] args) {
Point a = new Point(2, 3);
Point b = new Point(2, 3);
System.out.println(a.hashCode());
System.out.println(b.hashCode());
System.out.println(a.hashCode() == b.hashCode());
}
}

Output

1554547125
1846274136
false

Two points with the same x and y, but different hash codes. The default ignores fields. That is why a field-based equals() needs a field-based hashCode() to match it.

Same value every run?

Identity-based hash codes change from run to run, so the exact numbers in your output may differ from the ones shown here. What stays true is the comparison result. Equal objects with a proper hashCode() always match, and the default one usually will not.

⚠️ Common Mistakes

These three trip up almost everyone.

Overriding equals() but not hashCode(). This is the headline bug of the whole lesson. Your objects disappear inside HashSet and HashMap.

// ❌ equals overridden, hashCode left as the default
@Override
public boolean equals(Object o) { /* compares fields */ }
// no hashCode -> contract broken, lookups fail
// βœ… always override both together
@Override
public boolean equals(Object o) { /* compares id, email */ }
@Override
public int hashCode() { return Objects.hash(id, email); }

Using mutable fields in hashCode(). If a field can change after the object is stored in a collection, do not build the hash code from it. The object would move buckets while the collection still looks in the old one.

// ❌ email can change later; the object gets lost in the set
set.add(user);
user.email = "new@mail.com"; // hashCode just changed
set.contains(user); // now false, even though it is the same object
// βœ… base hashCode on a stable field like a database id that never changes
@Override
public int hashCode() { return Objects.hash(id); } // id is fixed for life

Returning a constant. Some people β€œfix” the contract by returning the same number for every object. It is technically legal, because equal objects do return equal hash codes. But it is terrible.

// ❌ every object lands in the same bucket
@Override
public int hashCode() { return 1; }
// βœ… spread objects across buckets using their fields
@Override
public int hashCode() { return Objects.hash(id, email); }
  • When everything returns 1, every object piles into one bucket.
  • The collection then checks them one by one with equals(), the slow search you wanted to avoid.
  • A good hash code spreads objects out so each bucket stays small.

βœ… Best Practices

  • βœ… Override both or neither. The day you write equals(), write hashCode() in the same sitting. They are a pair.
  • βœ… Use the same fields in both. The fields that decide equality must be the fields that build the hash code. Nothing extra.
  • βœ… Reach for Objects.hash(field1, field2). It combines fields into a solid hash code in one line, so you do not write the math by hand.
  • βœ… Prefer stable fields. Build the hash code from values that do not change after the object is stored, like an id, not from values a user can edit.
  • βœ… Let your IDE help. Most editors generate matching equals() and hashCode() from the fields you pick. Use that and the pair stays in sync.
  • βœ… Never return a constant. It keeps the contract but destroys performance. Spread objects across buckets.

🧩 What You’ve Learned

Good work. That was a lot, so let’s recap the hashCode() method.

  • βœ… hashCode() returns an int that hash-based collections use to pick a bucket for an object.
  • βœ… HashMap and HashSet find items fast by jumping to one bucket, then using equals() inside it.
  • βœ… The contract: equal objects must return equal hash codes, and the hash code must stay steady for an unchanged object.
  • βœ… If you override equals() but not hashCode(), objects go missing in sets and maps, even when an equal one was added.
  • βœ… Write a good one with Objects.hash(...) using the same fields as equals().
  • βœ… The default Object.hashCode() is identity-based, so it ignores your fields.
  • βœ… Avoid mutable fields and never return a constant.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What does the equals-hashCode contract require?

    Why: The core rule is that if two objects are equal, they must return the same hashCode. The reverse is not required.

  2. 2

    You override equals() but not hashCode() and add an object to a HashSet. What happens when you check for an equal object?

    Why: The default hashCode gives different numbers for different objects, so the set looks in the wrong bucket and the equals step never runs.

  3. 3

    Which is the cleanest way to build a hashCode from two fields?

    Why: Objects.hash(id, email) combines the same fields used in equals into one solid int.

  4. 4

    What is the default Object.hashCode() based on?

    Why: The default hashCode is identity-based, so two objects with identical fields usually get different numbers.

πŸš€ What’s Next?

You now control how your objects behave inside Java’s collections. Next we step back from the Object class and look at how Java decides who is allowed to see and use your classes, fields, and methods. It is all about keeping the right doors open and the wrong ones closed.

Java Access Modifiers

Share & Connect