Lesson 36Phase 4: Collections and algorithms
equals, hashCode and toString
What it means for two objects to be equal, the contract equals and hashCode must keep, and why a HashSet breaks without it.
By BasithUpdated 45 min
- Object
- equals
- hashCode
- contract
- toString
- Objects.equals
A library app keeps its books in a HashSet, so that no book is listed twice. It adds a Book for Dune by Frank Herbert, 1965, and later adds a second Book object with exactly the same details. The set now holds two. Then the app asks whether Dune is on the shelf, and the set says no. This lesson shows why, and how to fix it: how Java decides that two objects are equal, the two methods that must agree about it, and the method that decides how an object prints.
- Name what every class inherits from
Object, and say whatequals,hashCodeandtoStringdo before you override them. - Write an
equalsmethod in four steps, comparing primitive fields with==and object fields withObjects.equals. - Check an
equalsmethod against the five rules of its contract. - Explain why a
HashSetkeeps duplicates whenhashCodeis missing, and fix it withObjects.hash. - Keep objects findable by never changing the fields their
equalsuses while a set holds them. - Write a useful
toString, and choose between==,equals, a record and generated code.
== asks whether two variables point at the same object, and that your class's equals asks the same thing until you teach it otherwise. Overriding is how you teach it, with @Override as the safety check, and polymorphism gave you instanceof and casts. You wrote a toString in OOP practice, and records wrote all three methods for you. The last lesson, HashMap and HashSet, showed how a hash code picks a bucket.Every class starts with Object#
Every class you write has a parent, even when you leave out extends. Java quietly makes it extend java.lang.Object, as the inheritance lesson mentioned. So every object, whether it is a String, an ArrayList or your own Book, inherits a few methods from Object. These four matter for this lesson:
| Method | What Object's version does | When to override it |
|---|---|---|
equals(Object o) | Returns true only when o is this very object: the same test as ==. | When two separate objects can hold the same value, like two copies of one book. |
hashCode() | Returns a number for this object, called its identity hash code. Java gives separate objects different numbers, as far as it practically can. | Every time you override equals. |
toString() | Returns the class name, an @ sign and the hash code in hexadecimal, such as Book@1b6d3586. | Almost always, so that the object prints as something readable. |
getClass() | Returns the object's real class, such as Book. | Never. It is final, so no class can override it. |
Here is a Book with fields and a constructor, and nothing else. Step through it and watch the memory view: a and b point at two separate objects, and c points at the same object as a.
public class Main { public static void main(String[] args) { Book a = new Book("Dune", "Frank Herbert", 1965); Book b = new Book("Dune", "Frank Herbert", 1965); Book c = a; System.out.println(a == b); System.out.println(a.equals(b)); System.out.println(a.equals(c)); System.out.println(a.hashCode() == b.hashCode()); System.out.println(a.hashCode() == c.hashCode()); }} class Book { private final String title; private final String author; private final int year; Book(String title, String author, int year) { this.title = title; this.author = author; this.year = year; }}It prints false, false, true, false and true. With only Object's methods, a and b are not equal, even though they hold the same title, author and year: the inherited equals asks the same question as ==, and they are two objects. a and c are one object with two names, so equals says true and their hash codes match, while a and b get two different numbers.
Printing one of these books runs the toString it inherited. Java promises nothing about the number in its output, so here it is as read-along code:
Book a = new Book("Dune", "Frank Herbert", 1965);Book b = new Book("Dune", "Frank Herbert", 1965); System.out.println(a); // something like Book@1b6d3586System.out.println(b); // another number, such as Book@4554617cAfter the @ comes the object's hash code written in hexadecimal, which counts in base 16 with the digits 0 to 9 and then a to f. The JVM picks that number when the program runs. Another computer or another Java version can give a different one, and the documentation does not even promise the same number from one run to the next. For that reason, no runnable example on this page prints an object that lacks its own toString.
Key ideaEvery class inherits
equals,hashCodeandtoStringfromObject. Until you override them, all three are about identity:equalsmeans the same object,hashCodeis that object's own number, andtoStringprints the class name and that number.
Writing equals in four steps#
For a library, two Book objects with the same title, author and year describe the same book. You tell Java so by overriding equals. Its header is fixed: public boolean equals(Object o). The parameter is an Object because that is the method you are replacing, and because anything at all, or null, might be passed in.
A good equals does four things, in this order:
- 1If
ois this very object, returntrue. An object always equals itself, so this line can answer without looking at a single field:if (this == o) return true; - 2If
oisnullor not aBook, returnfalse.instanceofgivesfalsefornull, so one line handles both cases:if (!(o instanceof Book)) return false; - 3Cast
otoBookso that you can read its fields. Step 2 already made sure the cast cannot fail:Book other = (Book) o; - 4Compare the fields that make two books the same. Use
==for a primitive, such as theintyear, andObjects.equalsfor an object, such as aString.
Objects.equals(a, b) is a helper from java.util.Objects, so the file needs import java.util.Objects; at the top. It calls a.equals(b) for you, but deals with null first: two nulls count as equal, and a null against a real value counts as different. A book whose author is still null therefore cannot crash the comparison.
Here are the four steps in a real class. Step through the calls and watch where each one stops.
import java.util.ArrayList;import java.util.Objects; public class Main { public static void main(String[] args) { Book a = new Book("Dune", "Frank Herbert", 1965); Book b = new Book("Dune", "Frank Herbert", 1965); Book newer = new Book("Dune", "Frank Herbert", 1984); System.out.println(a.equals(b)); System.out.println(a.equals(newer)); System.out.println(a.equals(null)); System.out.println(a.equals("Dune")); ArrayList<Book> wishlist = new ArrayList<>(); wishlist.add(a); System.out.println(wishlist.contains(b)); }} class Book { private final String title; private final String author; private final int year; Book(String title, String author, int year) { this.title = title; this.author = author; this.year = year; } @Override public boolean equals(Object o) { if (this == o) return true; // 1. the very same object if (!(o instanceof Book)) return false; // 2. null, or not a Book Book other = (Book) o; // 3. look at it as a Book return Objects.equals(title, other.title) // 4. compare the fields && Objects.equals(author, other.author) && year == other.year; }}It prints true, false, false, false and then true. a.equals(b) gets through steps 1 to 3 and finds every field equal. a.equals(newer) reaches step 4 and fails on the year, so here a 1984 edition counts as a different book. a.equals(null) and a.equals("Dune") both stop at step 2, since neither is a Book. The last line is new: an ArrayList's contains asks equals, so the wishlist now finds b, an equal book that is a different object.
One detail in step 4 may surprise you: other.title reads a private field of another object. That is allowed. private means private to the class, so code inside Book can read the fields of any Book.
The parameter must be Object#
It is tempting to write equals(Book other), since only books matter. That method compiles, but it replaces nothing. It sits next to the inherited equals(Object) as a second method with the same name, an overload. Collections such as ArrayList and HashSet only ever call equals(Object), so they keep using Object's version and never ask yours. With @Override above it, the mistake becomes a compile error:
class Book { private final String title; private final int year; Book(String title, int year) { this.title = title; this.year = year; } @Override // error: method does not override or implement a method from a supertype public boolean equals(Book other) { return title.equals(other.title) && year == other.year; }}The message means “you said this method replaces one from a parent, but no parent has a method like it”. Put @Override on every equals, hashCode and toString you write.
A shorter way to write steps 2 and 3#
Since Java 16, the instanceof pattern from sealed types and pattern matching can check the type and give you the cast variable in one line. Many newer codebases write equals like this:
@Overridepublic boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Book other)) return false; // steps 2 and 3 together return Objects.equals(title, other.title) && Objects.equals(author, other.author) && year == other.year;}If o is not a Book, the method returns false. Otherwise other is ready on the next line, already cast. Both versions behave the same, so use the one that reads better to you.
Coin class whose equals follows steps 2 to 4, and a main that tests it. The right order prints true, false and false. (Step 1 is optional and @Override is left off, to keep the puzzle short.)Tap a line, then tap where it should go. Or use the arrows.
The equals contract#
HashSet, HashMap, ArrayList and every other collection call your equals without knowing what is inside it. They trust it to behave like real equality. The documentation of Object.equals spells out what that means as five rules, together called the equals contract: promises a method keeps so that other code can rely on it. For any objects a, b and c that are not null:
| Rule | In plain words | For books |
|---|---|---|
| Reflexive | Everything equals itself. | a.equals(a) is true. |
| Symmetric | The order of the two objects does not matter. | If a.equals(b) is true, so is b.equals(a). |
| Transitive | Equality carries along a chain. | If a equals b and b equals c, then a equals c. |
| Consistent | Asking again gives the same answer, as long as no field that equals uses has changed. | No random numbers, clocks or counters inside equals. |
| Never equal to null | Nothing equals null, and asking must not crash. | a.equals(null) is false. |
Written with the four steps, Book's equals keeps all five rules. Trouble usually starts when an equals tries to be helpful. This Tag, a label for blog posts, treats the plain text "java" as equal to the tag Java, ignoring case.
public class Main { public static void main(String[] args) { Tag tag = new Tag("Java"); System.out.println(tag.equals("java")); System.out.println("java".equals(tag)); }} class Tag { private final String name; Tag(String name) { this.name = name; } @Override public boolean equals(Object o) { if (o instanceof Tag other) { return name.equalsIgnoreCase(other.name); } if (o instanceof String text) { return name.equalsIgnoreCase(text); } return false; }}What do the two lines print?
The first line prints true, because Tag's equals accepts a String. The second prints false: that call runs String's equals, which knows nothing about tags and turns down anything that is not a String. Swapping the two objects changed the answer, so this equals is not symmetric.
It matters because a collection decides which object's equals to call. A List<String> holding "java" claims to contain the tag, while a List<Tag> holding the tag claims not to contain "java". The fix is to let a Tag equal only another Tag.
Weight class says two weights are equal when they differ by 1 kg or less. Which rule of the contract does that break?Key ideaCollections trust
equalsto behave like real equality. If its answer depends on which object goes first, on chance, or on a chain of near misses, a set or a list gives wrong answers without any error.
Why a HashSet needs hashCode#
With a working equals in place, return to the shelf from the start of this lesson. As you saw in HashMap and HashSet, a HashSet looks for an object in two moves:
- 1It calls the object's
hashCodeand turns that number into a bucket index. - 2It looks only in that bucket. There it compares hash codes first, and calls
equalsonly on a stored object whose hash code matches.
Book still inherits hashCode from Object. Decide what the three lines print, then step through the program and watch the buckets in the memory view.
import java.util.HashSet;import java.util.Objects; public class Main { public static void main(String[] args) { Book a = new Book("Dune", "Frank Herbert", 1965); Book b = new Book("Dune", "Frank Herbert", 1965); System.out.println(a.equals(b)); HashSet<Book> shelf = new HashSet<>(); shelf.add(a); shelf.add(b); System.out.println(shelf.size()); System.out.println(shelf.contains(new Book("Dune", "Frank Herbert", 1965))); }} class Book { private final String title; private final String author; private final int year; Book(String title, String author, int year) { this.title = title; this.author = author; this.year = year; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Book)) return false; Book other = (Book) o; return Objects.equals(title, other.title) && Objects.equals(author, other.author) && year == other.year; }}It prints true, then 2, then false. The two books are equal, yet the set keeps both, and it cannot find a third equal book either. The memory view shows why. Object's hashCode gives every book its own identity number, so the two copies land in different buckets, 2 and 6 here, and the book built for contains has a number of its own as well. A real JVM picks its own numbers, so on your computer the buckets may be different ones, but the numbers still differ from book to book. Even when two such books happen to share a bucket, the set compares their hash codes first, sees that they differ, and moves on without calling your equals.
Think of a hash code as the shelf number a library prints on a book's spine. Two copies of the same book must get the same shelf number. Otherwise they end up on different shelves, and a librarian who walks to the shelf for one copy never sees the other.
Key ideaIf two objects are equal, they must return the same hash code. So whenever you override
equals, overridehashCodetoo, and build it from the same fields.
The rule only runs one way. Two books that are not equal may still share a hash code. That is called a collision, and it is allowed: the set keeps both in one bucket and uses equals to tell them apart. A collision costs a little speed, and the answers stay right.
The fix: Objects.hash#
Objects.hash takes the fields that equals compares, as many as you like, and combines their hash codes into one int. Like equals, hashCode must be public, because Object's version is. Add it to Book and run the same main again:
import java.util.HashSet;import java.util.Objects; public class Main { public static void main(String[] args) { Book a = new Book("Dune", "Frank Herbert", 1965); Book b = new Book("Dune", "Frank Herbert", 1965); System.out.println(a.equals(b)); HashSet<Book> shelf = new HashSet<>(); shelf.add(a); shelf.add(b); System.out.println(shelf.size()); System.out.println(shelf.contains(new Book("Dune", "Frank Herbert", 1965))); }} class Book { private final String title; private final String author; private final int year; Book(String title, String author, int year) { this.title = title; this.author = author; this.year = year; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Book)) return false; Book other = (Book) o; return Objects.equals(title, other.title) && Objects.equals(author, other.author) && year == other.year; } @Override public int hashCode() { return Objects.hash(title, author, year); }}Now it prints true, 1 and true. Step through it: every Book with these details gets the hash code -641204184, which picks bucket 0. The second add finds an equal book there and changes nothing, and contains finds it the same way.
Try the rule for yourself in the lab below, which holds the same shelf. Start with no hashCode, then write one from all three fields. Then make equals ignore the year while hashCode still uses it, and look up the 1984 edition.
The set shelf already holds new Book("Dune", "Frank Herbert", 1965). Choose what equals compares and what hashCode uses, then pick a book to look up.
equals compares
hashCode uses
Look up
the stored book where contains looksWithout hashCode the buckets come from numbers the JVM picks, so none are shown.
contains returns false
equals would say true, but the set never asks it. Without hashCode the two books have different identity numbers, and the set only calls equals when the hash codes match.
EQUAL OBJECTS MUST HAVE EQUAL HASH CODES
Broken. Without hashCode, two equal books are still two separate objects, and each keeps its own identity number.
Book's equals compares only title and author. Which hashCode breaks the contract?Seat to a HashSet, and right now the same seat can be booked twice. Give Seat an equals and a hashCode, both built from row and number, so that the program prints 2 seats booked, C7 taken: true and D7 taken: false.Changing an object inside a set#
A HashSet files each object under the hash code it had when it went in. If a field that hashCode uses changes later, the object stays where it was filed, under the old number. In this program a librarian adds a book with a typo in its title and then fixes the typo. This Book has a setTitle method, so its title can change.
import java.util.HashSet;import java.util.Objects; public class Main { public static void main(String[] args) { HashSet<Book> shelf = new HashSet<>(); Book book = new Book("Dnue", "Frank Herbert"); // a typo shelf.add(book); book.setTitle("Dune"); // fix the typo System.out.println(shelf.contains(book)); System.out.println(shelf.size()); shelf.add(book); System.out.println(shelf.size()); }} class Book { private String title; private final String author; Book(String title, String author) { this.title = title; this.author = author; } void setTitle(String title) { this.title = title; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Book)) return false; Book other = (Book) o; return Objects.equals(title, other.title) && Objects.equals(author, other.author); } @Override public int hashCode() { return Objects.hash(title, author); }}What do the three lines print?
It prints false, 1 and 2. When the book went in, its hash code came from the title "Dnue" and picked bucket 5. After the fix, the hash code comes from "Dune" and picks bucket 12, so contains looks in bucket 12 and finds nothing there, even though it was handed the very same object. The set still counts one book. The second add also looks in bucket 12, finds nothing, and files the book again, so one object now sits in the set twice. Open the program full screen and step through it to watch both buckets.
The documentation of Set warns about exactly this: if an element changes in a way that affects equalswhile it is in a set, the set's behavior is not specified. The same goes for the keys of a HashMap.
HashSet, or to use as HashMap keys, are ones that cannot change: strings, numbers, and records or classes whose final fields hold only such values. If an object has to change, remove it from the set, change it, and then add it back. Another way out is to base equals and hashCode on a field that never changes, such as an id number.toString: how an object prints#
Java calls toString for you in several places. System.out.println(x) and string concatenation such as "Now reading: " + x both call x.toString(). A list or a set prints itself by calling toString on each element. And when x is null, printing and concatenation produce the word null instead of crashing.
import java.util.ArrayList;import java.util.Objects; public class Main { public static void main(String[] args) { Book dune = new Book("Dune", "Frank Herbert", 1965); System.out.println(dune); System.out.println("Now reading: " + dune); ArrayList<Book> shelf = new ArrayList<>(); shelf.add(dune); shelf.add(new Book("Emma", "Jane Austen", 1815)); System.out.println(shelf); Book lent = null; System.out.println("Lent out: " + lent); System.out.println("Lent out: " + Objects.toString(lent, "nothing")); }} class Book { private final String title; private final String author; private final int year; Book(String title, String author, int year) { this.title = title; this.author = author; this.year = year; } @Override public String toString() { return title + " by " + author + " (" + year + ")"; }}The first two lines show Dune by Frank Herbert (1965). The list prints as [Dune by Frank Herbert (1965), Emma by Jane Austen (1815)], with the brackets and commas from ArrayList and each book's text from your method. The last two lines are Lent out: null and Lent out: nothing. Objects.toString(x, "nothing") returns x.toString() when x is not null, and its second argument when it is.
lent.toString() yourself, while lent is null, throws a NullPointerException. Printing, concatenation and Objects.toString check for null first, so prefer them.What makes a good toString#
- It shows the values that tell objects apart, usually the same fields that
equalscompares. - It stays on one short line, because it ends up inside lists, log files and error messages.
- It leaves out secrets such as passwords, since whatever it returns may be printed somewhere.
- It is written for people. When a program needs a value, it should call a getter, never cut the text apart.
Objects helpers, records and generated code#
The class java.util.Objects, added in Java 7, holds small static methods that deal with null for you. You have already met two of them:
| Call | Returns | When x is null |
|---|---|---|
Objects.equals(x, y) | x.equals(y) | true if y is null too, otherwise false |
Objects.hash(x, y, ...) | One hash code combining all the values | Counts that value as 0 |
Objects.hashCode(x) | x.hashCode() | 0 |
Objects.toString(x) | x.toString() | The text null |
Objects.toString(x, "none") | x.toString() | The second argument, here none |
import java.util.Objects; public class Main { public static void main(String[] args) { String subtitle = null; String title = "Dune"; System.out.println(Objects.equals(subtitle, title)); System.out.println(Objects.equals(subtitle, null)); System.out.println(Objects.hashCode(subtitle)); System.out.println(Objects.toString(subtitle, "none")); System.out.println(Objects.hashCode(title) == title.hashCode()); System.out.println(Objects.hash(title) == title.hashCode()); }}It prints false, true, 0, none, true and false. Look at the last two lines. Objects.hashCode(title) is the title's own hash code, while Objects.hash(title) is not, since Objects.hash always starts from 1 and multiplies by 31, even for a single value. The documentation warns about this. Either one makes a correct hashCode for a class with one field, but never expect Objects.hash(x) to equal x.hashCode().
Let Java write them: records#
When a class only carries data, a record writes all three methods for you, built from every component, as you saw in records. The whole Book class, equals and hashCode included, becomes one line:
import java.util.HashSet; public class Main { public static void main(String[] args) { HashSet<Book> shelf = new HashSet<>(); shelf.add(new Book("Dune", "Frank Herbert", 1965)); shelf.add(new Book("Dune", "Frank Herbert", 1965)); System.out.println(shelf.size()); System.out.println(shelf.contains(new Book("Dune", "Frank Herbert", 1965))); System.out.println(shelf); }} record Book(String title, String author, int year) { }It prints 1, true and [Book[title=Dune, author=Frank Herbert, year=1965]]. A record's fields are final, so as long as its components cannot change either (strings, numbers, other such records), the trap from the typo example cannot happen to it.
Let your editor write them#
For an ordinary class, IntelliJ IDEA, Eclipse and VS Code with its Java extensions can each write equals, hashCode and toString from the fields you tick. Look for Generate in IntelliJ IDEA, and for the Source menu or Source Action in Eclipse and VS Code. The result looks much like what you wrote by hand on this page, so read it and check it. Generate the methods again whenever you add a field; otherwise the new field is silently left out of equals and hashCode.
Whoever writes them, pick the fields with care. A library member with a membership number is the same member after changing their email address, so equals and hashCode should compare the number alone.
Identity and equality, side by side#
Two questions run through this whole lesson. Identity asks whether two references point at one object, and == answers it. Equality asks whether two objects hold the same value, and equals answers it, in whatever way the class defines. Here is how the two play out for the kinds of values you know:
| Kind of value | == asks | equals asks | Compare with |
|---|---|---|---|
int, double, char, boolean | Same value? | Nothing: primitives have no methods. | == |
String | Same object? | Same characters? | equals |
Integer and the other wrappers | Same object? Small numbers can share one cached object, so the answer depends on the number. | Same type and same number? | equals |
| An enum constant | Same constant? | Same constant? | Either. Each constant is a single object. |
| An array | Same array object? | Also same array object: arrays do not override equals. | Arrays.equals(a, b) |
| A record | Same object? | Same record type and equal components? | equals |
Your class without equals | Same object? | Same object? | Either. Both ask about identity. |
Your class with equals | Same object? | Whatever its equals compares. | equals |
The cached wrapper objects behind that Integer row are explained in wrapper classes and autoboxing. Arrays.equals compares two arrays element by element, which is usually what you meant.
Book variables, a and b, might be null. Which expression safely tells you whether they hold equal books?Key idea
==asks whether two references point at one object.equalsasks whether two objects hold the same value. For anything that holds a value, such as a string, a record or your ownBook, compare withequals.
HashSet and HashMap find things with hashCode and equals. TreeSet and TreeMap keep their contents sorted, so they compare objects with compareTo instead, and your class needs a way to say which of two objects comes first. That is the next lesson, Comparable, Comparator and sorting.Try it
Answer each one in your head or on paper first, then open the answer.
equals, hashCode and toString do for a class you write?Point overrides hashCode but not equals. What does the program print, and why?public boolean equals(Book other) without @Override. In main, a.equals(b) prints true, but wishlist.contains(b) on an ArrayList holding a prints false. Why?equals that returns Math.random() < 0.5. Which rules of the contract does it break?Ticket has a final int id that never changes and a seat field that changes when a customer moves. Two tickets with the same id are the same ticket. Which fields should equals and hashCode use?Practice
Work through these on your own. Each one builds on this page.
Fraction with int fields numerator and denominator, where new Fraction(1, 2) equals new Fraction(2, 4). Reduce each fraction to lowest terms in the constructor, then write equals, hashCode and toString from the reduced fields. Put 1/2, 2/4 and 3/6 into a HashSet and check that its size is 1.Point class with x and y and an equals that uses instanceof, and a ColorPoint extends Point that adds a String color and its own equals that also compares the color. Show that p.equals(cp) and cp.equals(p) disagree. Then switch both classes to the getClass() check and run it again.Member class with a name field and a setName method, with equals and hashCode built from the name. Use members as keys in a HashMap<Member, Integer> that counts each member's loans, change one member's name, and print what get returns for that member. Then fix the design: give Member a final int memberId and base equals and hashCode on it alone.Seat class from the exercise as a record. Count the lines you saved, print one seat to see the toString Java writes, and check that the booking program still prints 2 seats booked.Song class with a title and a length in seconds, and a Playlist class with a name and an ArrayList<Song>. Give both a toString, so that printing a playlist shows something like Road trip: [Intro (58 s), Home (221 s)]. Which toString methods run when you print the playlist?Further reading#
- java.lang.Object in the Java SE 25 API documentation, with the full contracts of equals and hashCode
- java.util.Objects in the Java SE 25 API documentation, the null-safe helpers for equals, hash codes and strings
- Object as a Superclass, the dev.java tutorial on the methods every class inherits
- java.util.Set in the Java SE 25 API documentation, including its warning about changing an element while it is in a set
Spotted a mistake, or a part that did not make sense? Report it by email