In Java, the equals() method decides whether two objects represent the same value, and the hashCode() method returns an int that hash-based collections use to find an object without scanning every element. Both methods come from Object, and a class that overrides equals() must also override hashCode() so that equal objects return equal hash codes.
We override the two methods for value classes such as a song, a money amount or a coordinate, where two separate objects with the same data should count as one. The two methods decide how the objects behave as HashMap keys, in a HashSet and in calls such as List.contains().
The following example uses a Song class that overrides both methods, which we write in section 4.
Song first = new Song("Hello", "Adele");
Song second = new Song("Hello", "Adele");
boolean sameObject = first == second; // false
boolean equal = first.equals(second); // true
boolean sameHash = first.hashCode() == second.hashCode(); // true
Set<Song> playlist = new HashSet<>(List.of(first, second));
int size = playlist.size(); // 1
Notice that == still returns false, because it compares references, while the HashSet keeps one song. We start with the default behavior and the contract, look at the lookup inside a HashSet, and write correct implementations by hand, with records and with tools.
1. Default equals() and hashCode() From Object
Every class inherits both methods from Object. The default equals() returns true only when both references point to the same object, so it behaves like ==. The default hashCode() returns an identity hash code, which is a number the JVM assigns to each object and keeps for the object’s lifetime.
The identity hash code is not the memory address of the object. The Object.hashCode() Javadoc only promises distinct integers for distinct objects “as far as is reasonably practical”. HotSpot generates the number when it is first requested and stores it in the object header, so the value stays the same after the garbage collector moves the object.
class PlainSong {
private final String title;
private final String artist;
PlainSong(String title, String artist) {
this.title = title;
this.artist = artist;
}
}
PlainSong one = new PlainSong("Hello", "Adele");
PlainSong two = new PlainSong("Hello", "Adele");
boolean sameValue = one.equals(two); // false
boolean itself = one.equals(one); // true
boolean text = new String("Hello").equals("Hello"); // true
We can see that two PlainSong objects with the same data are not equal. Classes such as String, Integer, LocalDate and List override both methods to compare their content, which is why the last line returns true.
| Method | Default in Object | Typical override |
|---|---|---|
| equals(Object) | Same reference (this == obj) | Compares the fields that define the value |
| hashCode() | Identity hash code, different for almost every object | Combines the hash codes of the same fields |
| == operator | Same reference | Cannot be overridden |
2. The equals() and hashCode() Contract
Collections such as HashMap, HashSet and ArrayList assume that every class follows the rules in the equals() and hashCode() Javadoc. A class that breaks one of the rules does not fail with an exception. It returns wrong answers from contains(), get() and remove().
| Rule | Meaning for non-null x, y and z |
|---|---|
| Reflexive | x.equals(x) returns true |
| Symmetric | x.equals(y) returns the same result as y.equals(x) |
| Transitive | x.equals(y) and y.equals(z) together mean x.equals(z) |
| Consistent | Repeated calls return the same result while the compared fields do not change |
| Null | x.equals(null) returns false and never throws an exception |
| Stable hash | hashCode() returns the same value during one run while the compared fields do not change |
| Equal objects, equal hashes | x.equals(y) requires x.hashCode() == y.hashCode() |
| Collisions allowed | Unequal objects may return the same hash code |
Equal objects must return equal hash codes, but equal hash codes do not mean equal objects. An int has about 4 billion values, so different objects can share a hash code. Such a collision is legal, and it only makes the lookup a little slower.
int aa = "Aa".hashCode(); // 2112
int bb = "BB".hashCode(); // 2112
boolean sameText = "Aa".equals("BB"); // false
The hash code may also change between two runs of the same app, so we never store it in a database or a file as an identifier.
3. Why Overriding Only equals() Breaks a HashSet
A HashSet is backed by a HashMap, which keeps its entries in an array of buckets. To find an object, the map first calls hashCode() and spreads the result to pick a bucket. Inside that bucket, it compares the stored hash values and calls equals() only for entries with the same hash, which is how a HashMap works internally.

A music app gives us a real case. When a user merges two playlists, the app puts all songs into a HashSet to drop duplicates. The following example uses a HalfSong class that overrides equals() but keeps the default hashCode().
class HalfSong {
private final String title;
private final String artist;
HalfSong(String title, String artist) {
this.title = title;
this.artist = artist;
}
@Override
public boolean equals(Object o) {
return o instanceof HalfSong other
&& title.equals(other.title)
&& artist.equals(other.artist);
}
}
Set<HalfSong> merged = new HashSet<>();
merged.add(new HalfSong("Hello", "Adele"));
merged.add(new HalfSong("Hello", "Adele"));
int songCount = merged.size(); // 2
boolean found = merged.contains(new HalfSong("Hello", "Adele")); // false
boolean equalSongs = new HalfSong("Hello", "Adele").equals(new HalfSong("Hello", "Adele")); // true
The two objects are equal, but their identity hash codes differ, so the set never calls equals() on them and keeps both. The merged playlist shows the song twice, and contains() returns false for a song that is in the set. A HashMap with HalfSong keys has the same problem, and get() returns null.
4. Overriding equals() and hashCode() the Right Way
A correct pair of methods uses the same fields in both. In the following example, a song is identified by its title and artist, so equals() compares those two fields and hashCode() combines their hash codes with Objects.hash().
final class Song {
private final String title;
private final String artist;
Song(String title, String artist) {
this.title = Objects.requireNonNull(title);
this.artist = Objects.requireNonNull(artist);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
return o instanceof Song other
&& title.equals(other.title)
&& artist.equals(other.artist);
}
@Override
public int hashCode() {
return Objects.hash(title, artist);
}
@Override
public String toString() {
return title + " by " + artist;
}
}
The method follows four steps that cover the whole contract.
- The this == o check returns true for the same object without comparing fields.
- The pattern matching for instanceof check returns false for null and for other types, and it casts o to Song in the same expression.
- The field comparison uses equals() for objects. For fields that may be null, Objects.equals(a, b) does the null check for us.
- The hashCode() method uses the same two fields, so equal songs always get the same hash.
Song hello = new Song("Hello", "Adele");
Song copy = new Song("Hello", "Adele");
Song other = new Song("Hello", "Lionel Richie");
boolean same = hello.equals(copy); // true
boolean differ = hello.equals(other); // false
boolean nullSafe = hello.equals(null); // false
boolean hashMatch = hello.hashCode() == copy.hashCode(); // true
Map<Song, Integer> plays = new HashMap<>();
plays.put(hello, 12);
Integer count = plays.get(copy); // 12
4.1. instanceof or getClass() in equals()
The instanceof check also accepts subclasses, while getClass() != o.getClass() accepts only the exact class. If a subclass adds a field to equals(), an instanceof check breaks symmetry, because the parent says it equals the child and the child says it does not.
Declaring the class final, as Song does, removes the problem, so instanceof is the simpler choice for value classes. For a class that must stay open for extension, we use getClass(), or we prefer composition over a subclass that adds state.
4.2. Comparing Integer and String Fields With ==
A common bug is comparing wrapper or String fields with == inside equals(). The code passes small tests, because Integer.valueOf() returns cached Integer objects for values from -128 to 127.
Integer smallA = 100;
Integer smallB = 100;
boolean smallSame = smallA == smallB; // true, cached
Integer bigA = 1000;
Integer bigB = 1000;
boolean bigSame = bigA == bigB; // false
boolean bigEqual = Objects.equals(bigA, bigB); // true
An equals() that uses == on an Integer id works for id 100 and fails for id 1000. We compare object fields with equals() or Objects.equals(), and we use == only for primitives such as int and long. For double and float fields, Double.compare() and Float.compare() handle NaN and -0.0 the same way Double.equals() does.
5. Records Generate equals() and hashCode()
A Java record gets equals(), hashCode() and toString() from the compiler, based on all its components. When a class only carries data, a record replaces the whole Song class with one line.
record Track(String title, String artist) {}
Track t1 = new Track("Hello", "Adele");
Track t2 = new Track("Hello", "Adele");
boolean recordEqual = t1.equals(t2); // true
boolean recordHash = t1.hashCode() == t2.hashCode(); // true
String shown = t1.toString(); // "Track[title=Hello, artist=Adele]"
The generated equals() compares each component with Objects.equals(), so an array component is compared by reference, not by content. A record that holds a String[] should override equals() and hashCode() with Arrays.equals() and Arrays.hashCode(), or hold an unmodifiable List instead.
record Album(String name, String[] tracks) {}
record AlbumList(String name, List<String> tracks) {}
Album album1 = new Album("25", new String[] {"Hello"});
Album album2 = new Album("25", new String[] {"Hello"});
boolean albumEqual = album1.equals(album2); // false
boolean listEqual = new AlbumList("25", List.of("Hello")).equals(new AlbumList("25", List.of("Hello"))); // true
6. Changing a Field After Adding the Object to a HashSet
The hash code must stay the same while an object is inside a hash-based collection. If a field used in hashCode() changes, the object stays in its old bucket, while every lookup searches the bucket for the new hash.
Say a playlist editor stores songs in a HashSet and lets the user fix a typo in the title. The following EditableSong class has a setter for the title and uses the title in both methods.
class EditableSong {
private String title;
EditableSong(String title) {
this.title = title;
}
void setTitle(String title) {
this.title = title;
}
@Override
public boolean equals(Object o) {
return o instanceof EditableSong other && title.equals(other.title);
}
@Override
public int hashCode() {
return title.hashCode();
}
}
EditableSong song = new EditableSong("Helo");
Set<EditableSong> library = new HashSet<>();
library.add(song);
song.setTitle("Hello");
boolean stillThere = library.contains(song); // false
boolean removed = library.remove(song); // false
int librarySize = library.size(); // 1
The set still holds the song, but contains() and remove() cannot find it. To change a key, we remove the object, change it and add it again, or we make the class immutable so the problem cannot happen. The immutable class guide and the article on how to design a good HashMap key cover both options.
7. Generating equals() and hashCode() With Tools
Hand-written methods are short with Objects.hash(), but most teams generate them. Every option produces the same contract, so the choice depends on the project.
- IntelliJ IDEA generates both methods with Code | Generate | equals() and hashCode(), and Eclipse with Source | Generate hashCode() and equals(). Both let us pick the fields.
- Lombok generates them at compile time from the @EqualsAndHashCode or @Data annotation, and @EqualsAndHashCode.Exclude leaves a field out.
- Apache Commons Lang offers EqualsBuilder and HashCodeBuilder for code that predates Objects.hash().
- The EqualsVerifier library (version 4.5.2) checks the whole contract in a unit test with EqualsVerifier.forClass(Song.class).verify().
The following example shows the Commons Lang 3.21.0 builders. We call append() once per field in both methods, and the two numbers passed to HashCodeBuilder are an odd seed and an odd multiplier, not field values.
final class CatalogSong {
private final String title;
private final String artist;
CatalogSong(String title, String artist) {
this.title = title;
this.artist = artist;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof CatalogSong other)) {
return false;
}
return new EqualsBuilder()
.append(title, other.title)
.append(artist, other.artist)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37).append(title).append(artist).toHashCode();
}
}
boolean builderEqual = new CatalogSong("Hello", "Adele").equals(new CatalogSong("Hello", "Adele")); // true
boolean builderHash = new CatalogSong("Hello", "Adele").hashCode() == new CatalogSong("Hello", "Adele").hashCode(); // true
When we cannot change a class at all, for example a generated class without getters, EqualsBuilder.reflectionEquals() compares the private fields through reflection.
8. equals() and hashCode() in JPA Entities
JPA entities need extra care, because their id is null until the entity is saved, and Hibernate can return a proxy subclass instead of the entity class. An equals() based on a generated id treats two new entities as equal, and a getClass() check fails against a proxy.
- Use a natural business key that never changes, such as an ISBN or an email address, when the entity has one.
- When only a generated id exists, compare ids only when both are not null, and return a constant from hashCode(), such as a fixed number like 31 (not getClass().hashCode(), which differs for a proxy), so the hash does not change after saving.
- Read fields of the other object through getters, because a proxy loads its state only when a method is called.
The article on Hibernate entity equality and identity walks through these cases with a running example.
9. hashCode() and equals() FAQs
Hash collisions and the identity hash code confuse many developers, so the first two answers deal with them.
9.1. Can two different objects have the same hash code?
Yes. The contract allows it, and “Aa” and “BB” both return 2112, as we saw in section 2. A HashMap still works, because it calls equals() to tell the entries in one bucket apart. Many collisions only make lookups slower.
9.2. Is the default hashCode() the memory address of the object?
No. The Javadoc does not tie the identity hash code to an address, and HotSpot generates the number and stores it in the object header. Objects move during garbage collection, but their identity hash code stays the same. System.identityHashCode(obj) returns the same value even when a class overrides hashCode().
9.3. Why do hashCode() implementations multiply by 31?
The number 31 is an odd prime, so multiplying by it spreads the values of several fields over the int range and keeps the order of fields significant. String.hashCode() and Objects.hash() both use 31. The JIT compiler can also turn 31 * h into a shift and a subtraction.
9.4. Can we use a mutable object as a HashMap key?
Yes, but we should not change the object after putting it in the map. A HashMap puts each entry into a bucket picked from the key’s hash code. If a field used in hashCode() changes, get() searches a different bucket and returns null, even though the entry is still in the map, as section 6 shows.
9.5. Does the == operator call equals()?
No. The == operator compares references for objects and values for primitives, and Java does not allow overriding it. To compare the content of two objects, we call equals(), or Objects.equals(a, b) when either side may be null.
10. Conclusion
The default equals() compares references and the default hashCode() returns an identity hash code, so value classes override both. The contract has one rule that hash-based collections depend on, namely that equal objects return equal hash codes.
When only equals() is overridden, a HashSet keeps duplicates and a HashMap returns null for keys it holds. We use the same fields in both methods, compare objects with Objects.equals(), and keep the fields of hash keys unchanged.
For plain data carriers, a record generates both methods, with one catch for array components. Lombok, the IDE, Commons Lang and EqualsVerifier help in larger codebases. More core topics are listed in the Java tutorial, and the object-oriented programming guide explains where value objects fit.
11. References
- Object.equals() and hashCode() Javadoc, Java 25
- java.util.Objects Javadoc
- Record.equals() Javadoc
- HashMap Javadoc
- EqualsVerifier
Happy Learning !!