Lessons
1 · Java Foundations
2 · Object-Oriented Java
3 · Core Java
4 · Collections and Generics
5 · Functional Java
6 · The JVM and Concurrency
7 · Tools and Craft
- 21File I/O and Serialization
- 22Debugging: Errors, Stack Traces, the Debugger and Logging
- 23Maven: What It Does and How to Use It
- 24Git for Java Projects: Commits, Branches, Merging and Pull Requests
- 25Testing with JUnit and Mockito: Unit Tests, Mocks and Their Limits
- 26Project 1: Build an Expense Tracker CLI in Plain Java
8 · Databases and JDBC
9 · Web and Spring
10 · Persistence with JPA
11 · Secure, Tested, Production-Ready
- 36Security Fundamentals and Spring Security: Passwords, JWT, Access Control, CORS and Secrets
- 37Testing Spring Boot Applications: Slice Tests, MockMvc, Real Databases and the Test Pyramid
- 38Production Practices: Configuration, Logging, Docker and Health Checks for Spring Boot
- 39Project 3: The Job Application Tracker API, Assembled and Reviewed
Lesson 7 · Core Java
equals, hashCode, toString and the Object Contract
Why two objects that look identical are not equal by default, what == really compares, how to write equals and hashCode correctly, why mutable keys break HashSet and HashMap, and how immutability makes all of this safer. Includes the Integer cache surprise and the contracts interviewers ask about.
For
Learners who can write classes and want to understand how Java compares, prints and hashes objects, which collections and frameworks depend on.
Level
Before you start
Classes, Objects and Encapsulation (article 4) and Inheritance, Overriding and Overloading (article 5).
On this page
Key takeaways
- The == operator compares references for objects, so it asks whether two variables point to the same object. equals asks whether two objects are equal in value, but the default implementation is identical to ==.
- If you override equals you must override hashCode so that equal objects have equal hash codes, otherwise HashMap and HashSet will misbehave.
- A correct equals is reflexive, symmetric, transitive, consistent and returns false for null. Use Objects.equals and Objects.hash to write them safely.
- Mutating an object after using it as a hash key can make it impossible to find. Prefer immutable key classes, with private final fields, no setters and defensive copies.
- Override toString so objects print usefully in logs and the debugger, and never include secrets in it.
A shop stores customers in a HashSet. A clerk adds "Asha, 9876543210" twice, by accident, and the set happily holds both. Later a search for her in a HashMap fails, even though she is definitely in there. The objects look identical when printed, and Java insists they are different.
These bugs come from not knowing one fact: what Java means by "the same" depends on the class, and collections rely on the class getting it right. This article explains the three methods at the heart of that: equals, hashCode and toString.
What you will learn
#- The difference between identity and equality.
- What the default
equals,hashCodeandtoStringdo. - How to write correct
equalsandhashCode, and the contract behind them. - What goes wrong when you break the contract, or mutate a key.
- How immutability avoids a whole family of problems.
The problem: two kinds of "same"
#There are two different questions you can ask about two objects:
- Identity: are these two variables referring to the very same object?
- Equality: do these two objects represent the same value?
Two bank notes of ₹100 are equal in value but are different physical notes. For objects, Java's == answers the identity question, and the equals method is meant to answer the equality question.
// File: Main.javapublic class Main { public static void main(String[] args) { String a = new String("java"); String b = new String("java"); String c = a; System.out.println(a == b); // different objects System.out.println(a.equals(b)); // same characters System.out.println(a == c); // same object Integer x = 127; Integer y = 127; Integer p = 128; Integer q = 128; System.out.println(x == y); System.out.println(p == q); System.out.println(p.equals(q)); }}falsetruetruetruefalsetruea == b is false, because new created two objects, and a.equals(b) is true, because String overrides equals to compare characters. The Integer lines are the famous surprise: 127 == 127 is true but 128 == 128 is false. Autoboxing Integer x = 127; calls Integer.valueOf, which is specified to cache objects for values from -128 to 127, so x and y are the same object, while 128 creates separate objects. The lesson: never compare objects with == to ask whether they are equal. Use equals. Reserve == for primitives, for enums, and for deliberate identity checks such as x == null.
What Object gives you by default
#Every class inherits three methods from java.lang.Object:
Method | Default behaviour | Usually you want |
|---|---|---|
| Same as | Value equality, if the class represents a value |
| A number derived from the object's identity | A number derived from the same fields as |
|
| A readable description of the state |
The default toString is useless in logs. You can see its shape without printing a changing hash:
// File: Main.javapublic class Main { static class Plain { } static class Friendly { private final String name; private final int age; Friendly(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Friendly[name=" + name + ", age=" + age + "]"; } } public static void main(String[] args) { System.out.println(new Plain().toString().startsWith("Main$Plain@")); System.out.println(new Friendly("Asha", 30)); }}trueFriendly[name=Asha, age=30]System.out.println(object) calls toString() for you, and so does string concatenation. A good toString includes the class name and the important fields, helps whoever reads a log or a debugger, and never includes secrets such as passwords or tokens (they end up in log files). Keep it simple, since it is for people, not for parsing.
Writing equals: the contract
#When a class represents a value (a point, an amount of money, an email address, an ISBN), two instances with the same value should be equal. You then override equals. The documentation of Object.equals states a contract, and every override must honour it:
Property | Meaning |
|---|---|
Reflexive |
|
Symmetric |
|
Transitive | If |
Consistent | Repeated calls give the same answer unless the objects changed |
Non-null |
|
Here is a correct implementation for a small value class, using the modern pattern-matching form of instanceof:
// File: Point.javaimport java.util.Objects;public final class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof Point other)) { // also false when o is null return false; } return x == other.x && y == other.y; } @Override public int hashCode() { return Objects.hash(x, y); } @Override public String toString() { return "Point(" + x + ", " + y + ")"; }}The steps are always the same: return true for the same reference, return false if the argument is not the right type (an instanceof test is also false for null), then compare the significant fields. Use == for primitive fields and Objects.equals(a, b) for reference fields, because it handles null safely.
Three traps deserve a warning:
- Signature. The method must be
equals(Object). If you writeequals(Point other), you have overloaded it, not overridden it, and collections will still call the original.@Overridemakes the compiler catch this. - Inheritance. If a subclass adds fields that participate in equality, symmetry is hard to preserve (a
Pointand aColorPointcannot be equal both ways unless the colour is ignored). Making the classfinal, as here, or using composition avoids the problem. - Floating-point fields. Do not compare
doublevalues with==inequalsif they come from calculations. UseDouble.compare.
hashCode: the second half of the contract
#hashCode returns an int that hash-based collections use to decide where to look for an object. Article 12 explains the machinery. For now, the contract that matters is:
If two objects are equal according to
equals, they must have the samehashCode.
The converse is not required: two unequal objects may share a hash code (a collision), and good hash functions simply make that unlikely. The result must also be consistent: the same object must return the same value while its significant fields do not change.
What happens when you break the rule? A class that overrides equals but not hashCode inherits an identity-based hash code, so two equal objects will usually have different hash codes.
// File: Main.javaimport java.util.HashSet;import java.util.Objects;import java.util.Set;public class Main { static class BrokenPoint { final int x; final int y; BrokenPoint(int x, int y) { this.x = x; this.y = y; } @Override public boolean equals(Object o) { return o instanceof BrokenPoint p && x == p.x && y == p.y; } // hashCode() forgotten } static class GoodPoint { final int x; final int y; GoodPoint(int x, int y) { this.x = x; this.y = y; } @Override public boolean equals(Object o) { return o instanceof GoodPoint p && x == p.x && y == p.y; } @Override public int hashCode() { return Objects.hash(x, y); } } public static void main(String[] args) { Set<BrokenPoint> broken = new HashSet<>(); broken.add(new BrokenPoint(1, 2)); System.out.println("broken contains equal point: " + broken.contains(new BrokenPoint(1, 2))); broken.add(new BrokenPoint(1, 2)); System.out.println("broken size after adding a duplicate: " + broken.size()); Set<GoodPoint> good = new HashSet<>(); good.add(new GoodPoint(1, 2)); System.out.println("good contains equal point: " + good.contains(new GoodPoint(1, 2))); good.add(new GoodPoint(1, 2)); System.out.println("good size after adding a duplicate: " + good.size()); }}broken contains equal point: falsebroken size after adding a duplicate: 2good contains equal point: truegood size after adding a duplicate: 1The first set did exactly the bug from the start of this article: it does not find an equal object, and it accepts a duplicate. (With the identity-based default, the broken results are expected with overwhelming probability, because two objects almost never share a hash code by chance, but it is the contract violation, not luck, that you should rely on.) The second set behaves correctly, because equal objects hash equally.
Objects.hash(x, y) is the simplest correct way to compute a hash from the same fields as equals. It is not the fastest, and for most classes it is fine. IDEs can also generate equals and hashCode, and records (article 9) generate them for you.
The danger of mutable keys
#The contract says that the hash code must be consistent while the object is unchanged. What if the object is changed while it sits in a hash-based collection?
// File: Main.javaimport java.util.HashSet;import java.util.Objects;import java.util.Set;public class Main { static class MutablePoint { int x; int y; MutablePoint(int x, int y) { this.x = x; this.y = y; } @Override public boolean equals(Object o) { return o instanceof MutablePoint p && x == p.x && y == p.y; } @Override public int hashCode() { return Objects.hash(x, y); } } public static void main(String[] args) { Set<MutablePoint> set = new HashSet<>(); MutablePoint p = new MutablePoint(1, 2); set.add(p); System.out.println("before: " + set.contains(p)); p.x = 99; // changes the hash code while p is inside the set System.out.println("after change: " + set.contains(p)); System.out.println("size: " + set.size()); }}before: trueafter change: falsesize: 1After the mutation, set.contains(p) is false for the very object that is in the set, because the set looks in the bucket for the new hash code, and the object is stored under the old one. The object is still counted in size, so you have an element that is in the collection and unreachable by lookup. Never mutate an object in a way that changes its equals or hashCode while it is a key in a map or an element in a set. The robust solution is to make such classes immutable.
Immutability
#An immutable object cannot change after it is created. String, Integer, BigDecimal and LocalDate are immutable, which is a large part of why they are safe to use as map keys and to share between parts of a program.
To make your own class immutable:
- Declare the class
final(or ensure no subclass can add mutable state). - Make all fields
private final. - Provide no methods that modify state. If you need a "changed" version, return a new object.
- Initialise everything in the constructor.
- Copy mutable inputs and outputs. If a field holds a mutable object (a
List, an array, aDate), copy it on the way in and expose a copy or an unmodifiable view on the way out. These are defensive copies.
// File: Main.javaimport java.util.ArrayList;import java.util.List;public class Main { static final class Playlist { private final String name; private final List<String> songs; Playlist(String name, List<String> songs) { this.name = name; this.songs = List.copyOf(songs); // defensive copy in: unmodifiable snapshot } List<String> songs() { return songs; // safe to return: it is unmodifiable } Playlist withSong(String song) { // "change" by creating a new object List<String> more = new ArrayList<>(songs); more.add(song); return new Playlist(name, more); } } public static void main(String[] args) { List<String> original = new ArrayList<>(List.of("Intro", "Verse")); Playlist playlist = new Playlist("Study", original); original.add("Hidden track"); // the caller changes their own list System.out.println(playlist.songs()); // the playlist is unaffected Playlist bigger = playlist.withSong("Outro"); System.out.println(playlist.songs() + " vs " + bigger.songs()); try { playlist.songs().add("Sneaky"); } catch (UnsupportedOperationException e) { System.out.println("cannot modify the playlist from outside"); } }}[Intro, Verse][Intro, Verse] vs [Intro, Verse, Outro]cannot modify the playlist from outsideWithout the defensive copy, the line original.add("Hidden track") would have changed the playlist from the outside, breaking its supposed immutability. List.copyOf (Java 10) makes an unmodifiable snapshot, and List.of creates unmodifiable lists too.
Immutability costs you something: every change creates a new object, which can matter for large structures. In return you get simpler reasoning (an object's state cannot surprise you), safe sharing, automatic thread safety (there is nothing to synchronise, which article 19 uses), and safe use as keys. A sound default for value-like classes is to make them immutable, and to make entities with identity and a lifecycle (an account, an order) mutable but carefully encapsulated.
Utility methods worth knowing
#The java.util.Objects class has helpers that remove boilerplate:
Objects.equals(a, b): null-safe equality.Objects.hash(...): a hash code over several values.Objects.requireNonNull(value, "message"): fail fast with a clear message.Objects.toString(obj, "default"): a null-safe string.
Common mistakes
#- Using
==on objects to test for equality, especiallyStringand boxed numbers. - Overriding
equalswithouthashCode. - Writing
equals(Point)and silently overloading. - Comparing the wrong things: including a field in
hashCodethat is not inequalsis fine, but the reverse breaks the contract. - Mutating a hash key.
- Returning a mutable internal field (a list or array) directly, which breaks encapsulation.
- Putting secrets in
toString. - Relying on a
hashCodevalue being the same between runs or JVM versions. It is not specified.
Exercises
#Exercise 1
Predict
#What do new String("a") == new String("a") and "a".equals(new String("a")) evaluate to, and why?
Exercise 2: Implement
#Write a final class Money with long paise and String currency, with correct equals, hashCode and toString. Show that two Money objects with the same values are equal and can be found in a HashSet.
Exercise 3: Find the bug
#A class defines public boolean equals(Employee other) { return id == other.id; } and hashCode based on id. A HashSet<Employee> still accepts two employees with the same id. Why?
Exercise 4: Spot the contract violation
#A class's equals compares System.currentTimeMillis() with a stored time. Which property of the contract does this break, and what could go wrong in a HashMap?
Exercise 5: Design
#You are modelling a User that has a mutable email address and a database-assigned id that is null until saved. What would you use for equals and hashCode, and what are the risks?
Show solutionHide solution
The first is false: new creates two distinct objects, and == compares references. The second is true: String.equals compares the characters. The same pair of facts is the foundation of every interview question on "== versus equals".
Exercise 2
Show solutionHide solution
import java.util.Objects;public final class Money { private final long paise; private final String currency; public Money(long paise, String currency) { this.paise = paise; this.currency = Objects.requireNonNull(currency); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Money other)) return false; return paise == other.paise && currency.equals(other.currency); } @Override public int hashCode() { return Objects.hash(paise, currency); } @Override public String toString() { return paise + " paise " + currency; }}Adding new Money(500, "INR") to a HashSet and then calling contains(new Money(500, "INR")) returns true, and adding it a second time leaves the size unchanged. Both methods use the same fields (paise and currency), and the fields are final, so the hash code can never change.
Exercise 3
Show solutionHide solution
The method takes an Employee parameter, so it overloads equals(Object) instead of overriding it. The HashSet calls equals(Object), which is still the inherited identity version, so two distinct objects are never equal. The @Override annotation would have produced a compile error and exposed the mistake. The fix is public boolean equals(Object o) with a type check.
Exercise 4
Show solutionHide solution
It breaks consistency: repeated calls on unchanged objects give different answers, because the result depends on the current time. In a HashMap, an entry could be inserted and then never found, because later comparisons would say the key is no longer equal to anything. equals and hashCode must depend only on the object's own state, not on outside factors like the clock, randomness or a database.
Exercise 5
Show solutionHide solution
The email can change, so it is a bad basis for hashCode: a changed email would make the object unfindable in hash collections. The database id is stable once assigned, but it is null before the user is saved, so two unsaved users would compare equal if you only used the id with a naive implementation. Common approaches: base equals on the id only when it is non-null (and treat unsaved entities as equal only to themselves), and use a constant or class-based hashCode so it does not change when the id is assigned; or, if there is a natural immutable business key (a unique username that never changes), use that. The risks are the ones in this article: a changing hash and a broken contract. Article 34 returns to this for JPA entities.
Where to go next
#- Next: Strings, StringBuilder, Text Blocks and Time applies immutability to the most used class in Java.
- To see why hash codes matter: Maps and How HashMap Works.
- For a shortcut that writes equals, hashCode and toString for you: Enums, Records and Sealed Types.
Sources & further reading
- Technology
- Software Engineering
- #interview preparation
- #Java
- #Java backend
- #Java basics
- #Java interview
- #JPA
- #learn Java
- #OOP
- #Spring Boot
- #SQL
Connected lessons
References
Linked from 5 guides
Other pages in the library that point here.
Keep reading
More in Technology →Guide · Technology
Java Essentials for DSA
Guide · Technology
Heaps and Priority Queues
Course · Technology
DSA From Zero to Job-Ready: A Practical Guide with Java

