Learn Java

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 Updated 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.

By the end of this lesson
  • Name what every class inherits from Object, and say what equals, hashCode and toString do before you override them.
  • Write an equals method in four steps, comparing primitive fields with == and object fields with Objects.equals.
  • Check an equals method against the five rules of its contract.
  • Explain why a HashSet keeps duplicates when hashCode is missing, and fix it with Objects.hash.
  • Keep objects findable by never changing the fields their equals uses while a set holds them.
  • Write a useful toString, and choose between ==, equals, a record and generated code.
From before
You know from the references lesson that == 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:

MethodWhat Object's version doesWhen 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.
Object has a few more methods, such as wait and notify for threads. This lesson needs only these four.

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.

Defaults.java
1public class Main {
2 public static void main(String[] args) {
3 Book a = new Book("Dune", "Frank Herbert", 1965);
4 Book b = new Book("Dune", "Frank Herbert", 1965);
5 Book c = a;
6
7 System.out.println(a == b);
8 System.out.println(a.equals(b));
9 System.out.println(a.equals(c));
10 System.out.println(a.hashCode() == b.hashCode());
11 System.out.println(a.hashCode() == c.hashCode());
12 }
13}
14
15class Book {
16 private final String title;
17 private final String author;
18 private final int year;
19
20 Book(String title, String author, int year) {
21 this.title = title;
22 this.author = author;
23 this.year = year;
24 }
25}
Prints
›false
›false
›true
›false
›true

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:

DefaultToString.java (part of main)
read along
1Book a = new Book("Dune", "Frank Herbert", 1965);
2Book b = new Book("Dune", "Frank Herbert", 1965);
3
4System.out.println(a); // something like Book@1b6d3586
5System.out.println(b); // another number, such as Book@4554617c

After 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 idea

Every class inherits equals, hashCode and toString from Object. Until you override them, all three are about identity: equals means the same object, hashCode is that object's own number, and toString prints 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:

  1. 1If o is this very object, return true. An object always equals itself, so this line can answer without looking at a single field: if (this == o) return true;
  2. 2If o is null or not a Book, return false. instanceof gives false for null, so one line handles both cases: if (!(o instanceof Book)) return false;
  3. 3Cast o to Book so that you can read its fields. Step 2 already made sure the cast cannot fail: Book other = (Book) o;
  4. 4Compare the fields that make two books the same. Use == for a primitive, such as the int year, and Objects.equals for an object, such as a String.

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.

BookEquals.java
1import java.util.ArrayList;
2import java.util.Objects;
3
4public class Main {
5 public static void main(String[] args) {
6 Book a = new Book("Dune", "Frank Herbert", 1965);
7 Book b = new Book("Dune", "Frank Herbert", 1965);
8 Book newer = new Book("Dune", "Frank Herbert", 1984);
9
10 System.out.println(a.equals(b));
11 System.out.println(a.equals(newer));
12 System.out.println(a.equals(null));
13 System.out.println(a.equals("Dune"));
14
15 ArrayList<Book> wishlist = new ArrayList<>();
16 wishlist.add(a);
17 System.out.println(wishlist.contains(b));
18 }
19}
20
21class Book {
22 private final String title;
23 private final String author;
24 private final int year;
25
26 Book(String title, String author, int year) {
27 this.title = title;
28 this.author = author;
29 this.year = year;
30 }
31
32 @Override
33 public boolean equals(Object o) {
34 if (this == o) return true; // 1. the very same object
35 if (!(o instanceof Book)) return false; // 2. null, or not a Book
36 Book other = (Book) o; // 3. look at it as a Book
37 return Objects.equals(title, other.title) // 4. compare the fields
38 && Objects.equals(author, other.author)
39 && year == other.year;
40 }
41}
Prints
›true
›false
›false
›false
›true

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:

Book.java (does not compile)
read along
1class Book {
2 private final String title;
3 private final int year;
4
5 Book(String title, int year) {
6 this.title = title;
7 this.year = year;
8 }
9
10 @Override // error: method does not override or implement a method from a supertype
11 public boolean equals(Book other) {
12 return title.equals(other.title) && year == other.year;
13 }
14}

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:

Book.java (the equals method)
read along
1@Override
2public boolean equals(Object o) {
3 if (this == o) return true;
4 if (!(o instanceof Book other)) return false; // steps 2 and 3 together
5 return Objects.equals(title, other.title)
6 && Objects.equals(author, other.author)
7 && year == other.year;
8}

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.

Put the lines in orderCoin.java
Put the lines back in order: a 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:

RuleIn plain wordsFor books
ReflexiveEverything equals itself.a.equals(a) is true.
SymmetricThe order of the two objects does not matter.If a.equals(b) is true, so is b.equals(a).
TransitiveEquality carries along a chain.If a equals b and b equals c, then a equals c.
ConsistentAsking 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 nullNothing equals null, and asking must not crash.a.equals(null) is false.
The five rules from the documentation of Object.equals, in plain words.

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.

Predict the outputTags.java
1public class Main {
2 public static void main(String[] args) {
3 Tag tag = new Tag("Java");
4
5 System.out.println(tag.equals("java"));
6 System.out.println("java".equals(tag));
7 }
8}
9
10class Tag {
11 private final String name;
12
13 Tag(String name) {
14 this.name = name;
15 }
16
17 @Override
18 public boolean equals(Object o) {
19 if (o instanceof Tag other) {
20 return name.equalsIgnoreCase(other.name);
21 }
22 if (o instanceof String text) {
23 return name.equalsIgnoreCase(text);
24 }
25 return false;
26 }
27}

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.

Quick check
A Weight class says two weights are equal when they differ by 1 kg or less. Which rule of the contract does that break?
Key idea

Collections trust equals to 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:

  1. 1It calls the object's hashCode and turns that number into a bucket index.
  2. 2It looks only in that bucket. There it compares hash codes first, and calls equals only 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.

Shelf.java
1import java.util.HashSet;
2import java.util.Objects;
3
4public class Main {
5 public static void main(String[] args) {
6 Book a = new Book("Dune", "Frank Herbert", 1965);
7 Book b = new Book("Dune", "Frank Herbert", 1965);
8 System.out.println(a.equals(b));
9
10 HashSet<Book> shelf = new HashSet<>();
11 shelf.add(a);
12 shelf.add(b);
13 System.out.println(shelf.size());
14 System.out.println(shelf.contains(new Book("Dune", "Frank Herbert", 1965)));
15 }
16}
17
18class Book {
19 private final String title;
20 private final String author;
21 private final int year;
22
23 Book(String title, String author, int year) {
24 this.title = title;
25 this.author = author;
26 this.year = year;
27 }
28
29 @Override
30 public boolean equals(Object o) {
31 if (this == o) return true;
32 if (!(o instanceof Book)) return false;
33 Book other = (Book) o;
34 return Objects.equals(title, other.title)
35 && Objects.equals(author, other.author)
36 && year == other.year;
37 }
38}
Prints
›true
›2
›false

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 idea

If two objects are equal, they must return the same hash code. So whenever you override equals, override hashCode too, 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:

ShelfFixed.java
1import java.util.HashSet;
2import java.util.Objects;
3
4public class Main {
5 public static void main(String[] args) {
6 Book a = new Book("Dune", "Frank Herbert", 1965);
7 Book b = new Book("Dune", "Frank Herbert", 1965);
8 System.out.println(a.equals(b));
9
10 HashSet<Book> shelf = new HashSet<>();
11 shelf.add(a);
12 shelf.add(b);
13 System.out.println(shelf.size());
14 System.out.println(shelf.contains(new Book("Dune", "Frank Herbert", 1965)));
15 }
16}
17
18class Book {
19 private final String title;
20 private final String author;
21 private final int year;
22
23 Book(String title, String author, int year) {
24 this.title = title;
25 this.author = author;
26 this.year = year;
27 }
28
29 @Override
30 public boolean equals(Object o) {
31 if (this == o) return true;
32 if (!(o instanceof Book)) return false;
33 Book other = (Book) o;
34 return Objects.equals(title, other.title)
35 && Objects.equals(author, other.author)
36 && year == other.year;
37 }
38
39 @Override
40 public int hashCode() {
41 return Objects.hash(title, author, year);
42 }
43}
Prints
›true
›1
›true

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.

equals and hashCode lab

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

return Objects.equals(title, other.title)
&& Objects.equals(author, other.author)
&& year == other.year;

hashCode uses

// not written: Book keeps Object's hashCode

Look up

shelf.contains(new Book("Dune", "Frank Herbert", 1965))
equals saystruestored bookidentity number, picked by the JVMlooked upa different identity number

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.

Quick check
Book's equals compares only title and author. Which hashCode breaks the contract?
ExerciseSeats.java
A cinema books a seat by adding a 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.

Predict the outputTypo.java
1import java.util.HashSet;
2import java.util.Objects;
3
4public class Main {
5 public static void main(String[] args) {
6 HashSet<Book> shelf = new HashSet<>();
7 Book book = new Book("Dnue", "Frank Herbert"); // a typo
8 shelf.add(book);
9
10 book.setTitle("Dune"); // fix the typo
11 System.out.println(shelf.contains(book));
12 System.out.println(shelf.size());
13
14 shelf.add(book);
15 System.out.println(shelf.size());
16 }
17}
18
19class Book {
20 private String title;
21 private final String author;
22
23 Book(String title, String author) {
24 this.title = title;
25 this.author = author;
26 }
27
28 void setTitle(String title) {
29 this.title = title;
30 }
31
32 @Override
33 public boolean equals(Object o) {
34 if (this == o) return true;
35 if (!(o instanceof Book)) return false;
36 Book other = (Book) o;
37 return Objects.equals(title, other.title)
38 && Objects.equals(author, other.author);
39 }
40
41 @Override
42 public int hashCode() {
43 return Objects.hash(title, author);
44 }
45}

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.

Use objects that cannot change
The safest objects to put in a 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.

Printing.java
1import java.util.ArrayList;
2import java.util.Objects;
3
4public class Main {
5 public static void main(String[] args) {
6 Book dune = new Book("Dune", "Frank Herbert", 1965);
7 System.out.println(dune);
8 System.out.println("Now reading: " + dune);
9
10 ArrayList<Book> shelf = new ArrayList<>();
11 shelf.add(dune);
12 shelf.add(new Book("Emma", "Jane Austen", 1815));
13 System.out.println(shelf);
14
15 Book lent = null;
16 System.out.println("Lent out: " + lent);
17 System.out.println("Lent out: " + Objects.toString(lent, "nothing"));
18 }
19}
20
21class Book {
22 private final String title;
23 private final String author;
24 private final int year;
25
26 Book(String title, String author, int year) {
27 this.title = title;
28 this.author = author;
29 this.year = year;
30 }
31
32 @Override
33 public String toString() {
34 return title + " by " + author + " (" + year + ")";
35 }
36}
Prints
›Dune by Frank Herbert (1965)
›Now reading: Dune by Frank Herbert (1965)
›[Dune by Frank Herbert (1965), Emma by Jane Austen (1815)]
›Lent out: null
›Lent out: nothing

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.

Let Java call toString for you
Writing 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 equals compares.
  • 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:

CallReturnsWhen 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 valuesCounts 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
java.util.Objects: each call works on a null without throwing.
NullSafe.java
1import java.util.Objects;
2
3public class Main {
4 public static void main(String[] args) {
5 String subtitle = null;
6 String title = "Dune";
7
8 System.out.println(Objects.equals(subtitle, title));
9 System.out.println(Objects.equals(subtitle, null));
10 System.out.println(Objects.hashCode(subtitle));
11 System.out.println(Objects.toString(subtitle, "none"));
12 System.out.println(Objects.hashCode(title) == title.hashCode());
13 System.out.println(Objects.hash(title) == title.hashCode());
14 }
15}
Prints
›false
›true
›0
›none
›true
›false

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:

RecordShelf.java
1import java.util.HashSet;
2
3public class Main {
4 public static void main(String[] args) {
5 HashSet<Book> shelf = new HashSet<>();
6 shelf.add(new Book("Dune", "Frank Herbert", 1965));
7 shelf.add(new Book("Dune", "Frank Herbert", 1965));
8
9 System.out.println(shelf.size());
10 System.out.println(shelf.contains(new Book("Dune", "Frank Herbert", 1965)));
11 System.out.println(shelf);
12 }
13}
14
15record Book(String title, String author, int year) { }
Prints
›1
›true
›[Book[title=Dune, author=Frank Herbert, year=1965]]

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== asksequals asksCompare with
int, double, char, booleanSame value?Nothing: primitives have no methods.==
StringSame object?Same characters?equals
Integer and the other wrappersSame object? Small numbers can share one cached object, so the answer depends on the number.Same type and same number?equals
An enum constantSame constant?Same constant?Either. Each constant is a single object.
An arraySame array object?Also same array object: arrays do not override equals.Arrays.equals(a, b)
A recordSame object?Same record type and equal components?equals
Your class without equalsSame object?Same object?Either. Both ask about identity.
Your class with equalsSame object?Whatever its equals compares.equals
For objects, == always asks about identity. equals asks about equality when the class defines it.

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.

Quick check
Either of two 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. equals asks whether two objects hold the same value. For anything that holds a value, such as a string, a record or your own Book, compare with equals.

Sorted collections ask a different question
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.

1
Without overriding anything, what do equals, hashCode and toString do for a class you write?
2
This Point overrides hashCode but not equals. What does the program print, and why?
3
A teammate writes 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?
4
Someone writes an equals that returns Math.random() < 0.5. Which rules of the contract does it break?
5
A 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.

1
Write a class 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.
2
Write a 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.
3
Make a 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.
4
Rewrite the 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.
5
Write a 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#

Spotted a mistake, or a part that did not make sense? Report it by email