Two Riyas in the attendance set
Kabir builds an attendance tracker. Each scan of an ID card creates a Student object and adds it to a HashSet, which is supposed to ignore duplicates. Riya scans her card twice. The set now says 2 students are present, and printing it shows gibberish like Student@4e25154f.
I scanned once, then again because the beep was soft. Now the dashboard says two students are present, and it doesn't even show my name!
Your two scans made two Student objects with the same roll number. To Java they're different objects, because nobody told the class what “same student” means. That's the job of equals().
And the Student@4e25154f?
That's the default toString() from the Object class: class name, @, and the hash code in hex. Every class inherits it. Override it to print something useful.
So I just write equals, and the set will notice the duplicate?
Not yet. A HashSet first uses hashCode() to pick a bucket, and only then calls equals. If equal objects can land in different buckets, equals never even runs. The rule is: override equals and hashCode together. Let's see why.
What is the Object class?
java.lang.Object is the root of the class hierarchy: every class, including arrays, directly or indirectly extends it. A class with no extends clause extends Object implicitly. Object defines 11 methods inherited by all classes, including toString() (a text form of the object), equals(Object) (logical equality, by default the same as ==) and hashCode() (an integer used by hash-based collections). If a class overrides equals, it must override hashCode so that equal objects have equal hash codes.
Object is the parent of everything. It gives every object a default way to print itself, compare itself and produce a hash number. For your own classes, you usually replace those defaults with ones that look at the fields.
Words you'll meet in this lesson
@Override.a == b: are these the very same object?a.equals(b): do they mean the same thing (same roll number)?int summarising an object, used to choose a bucket in HashMap/HashSet.The 11 methods every object inherits
| Method | What the default does | Override? |
|---|---|---|
toString() | Returns ClassName@hexHashCode | Almost always |
equals(Object o) | this == o (same object only) | For value-like classes |
hashCode() | A number derived from the object's identity | Whenever you override equals |
getClass() | Returns the run-time class (final, can't override) | No |
clone() | Field-by-field copy (protected; needs Cloneable) | Rarely; prefer copy constructors |
wait() ×3, notify(), notifyAll() | Thread coordination (Lesson 12.5), all final | No |
finalize() | Called by GC; deprecated (for removal since Java 18) | Never |
Exam tip: the equals–hashCode contract: (1) if a.equals(b) is true, then a.hashCode() == b.hashCode() must be true; (2) equal hash codes do not require equals to be true (that's a collision, and it's allowed); (3) hashCode must return the same value each time while the fields used in equals don't change. Also: equals must be reflexive, symmetric, transitive, consistent, and x.equals(null) must be false.
The three methods, default vs overridden
toString()
What println(obj) and "…" + obj show. Default: Student@4e25154f.
equals()
“Do these mean the same thing?” Default: only if they're the same object.
contains, remove, indexOfhashCode()
A number that picks the bucket. Equal objects must give the same number.
HashMap, HashSet1. toString(): from gibberish to useful
class Student {
String roll, name;
Student(String roll, String name) { this.roll = roll; this.name = name; }
}
public class Main {
public static void main(String[] args) {
Student s = new Student("21CS07", "Riya");
System.out.println(s); // calls s.toString() from Object
}
}class Student {
String roll, name;
Student(String roll, String name) { this.roll = roll; this.name = name; }
@Override
public String toString() {
return "Student[" + roll + ", " + name + "]";
}
}
public class Main {
public static void main(String[] args) {
Student s = new Student("21CS07", "Riya");
System.out.println(s);
System.out.println("Present: " + s); // string concatenation also calls toString()
}
}2. equals(): same object vs same meaning
import java.util.Objects;
class Student {
String roll, name;
Student(String roll, String name) { this.roll = roll; this.name = name; }
@Override
public boolean equals(Object o) {
if (this == o) return true; // same object: equal
if (!(o instanceof Student)) return false; // null or another type
Student other = (Student) o;
return Objects.equals(roll, other.roll); // same roll number = same student
}
}
public class Main {
public static void main(String[] args) {
Student a = new Student("21CS07", "Riya");
Student b = new Student("21CS07", "Riya");
System.out.println(a == b); // two objects
System.out.println(a.equals(b)); // same student
System.out.println(a.equals(null));
}
}Common mistake: writing public boolean equals(Student o). That overloads instead of overriding (the parameter must be Object), so collections still call Object's version. @Override catches it: javac reports method does not override or implement a method from a supertype.
3. hashCode(): keep the contract
import java.util.HashSet;
import java.util.Objects;
class Student {
String roll, name;
Student(String roll, String name) { this.roll = roll; this.name = name; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Student)) return false;
return Objects.equals(roll, ((Student) o).roll);
}
@Override
public int hashCode() {
return Objects.hash(roll); // the same field(s) as equals
}
}
public class Main {
public static void main(String[] args) {
HashSet<Student> present = new HashSet<>();
present.add(new Student("21CS07", "Riya"));
present.add(new Student("21CS07", "Riya")); // second scan
present.add(new Student("21CS12", "Kabir"));
System.out.println(present.size());
}
}Remove the hashCode method and run it again: the size becomes 3. The two Riya objects get different identity hash codes, land in different buckets, and the set never compares them with equals.
Which fields? A quick guide
| Class | “Same” means | Use in equals and hashCode |
|---|---|---|
Student | same roll number | roll |
Point | same x and y | x, y |
BankAccount | same account number | accountNo (not the changing balance) |
Money | same amount and currency | paise, currency |
Rule of thumb: generate these three methods with your IDE (or use a record, Lesson 14.5, which writes them for you). Use the same fields in equals and hashCode, prefer fields that never change, and always add @Override.
How a HashSet decides “already there”
Call hashCode()
present.add(new Student("21CS07", "Riya"));HashSetis backed by aHashMap. It callsstudent.hashCode()and mixes the bits to get a number.Pick a bucket
The number is reduced to a bucket index (with 16 buckets, roughlyhash & 15). Only objects in that bucket are candidates for “the same”.Compare with equals() inside the bucket
For each object already in the bucket, it checks the stored hash first, then callsequals. Different hash codes meanequalsis never called.Equal one found: don't add
addreturnsfalseand the set is unchanged. This only works if equal students produce equal hash codes, which is whyhashCodemust use the same fields asequals.No equal one: add a new entry
The object goes into the bucket. Two unequal objects may share a bucket (a collision, e.g."Aa"and"BB"both hash to 2112). That's allowed; it just makes lookups a little slower.
The duplicate scan: without and with hashCode()
Left: the attendance code and what size() reports. Right: the buckets inside the HashSet.
- Without
hashCode(): the first Student is added.addreturns true. - The second Student has the same roll, and
equalswould say they're equal, yetaddreturns true again. size()is 2. The duplicate got in.- Now
Studentalso overrideshashCode()using the roll number. The first add works as before. - The second add returns false: the set recognised the duplicate.
size()is 1. Same main code; the only change was one method in Student.
- Object's default
hashCode()is an identity number, unrelated to the roll. It sends Riya #1 to bucket 3. - Riya #2 is a different object, so its identity hash is different: bucket 12. The set only compares objects inside the chosen bucket, and bucket 12 is empty.
equals()was never even called. Both objects stay: size 2.- With
hashCode()returningObjects.hash(roll), the hash depends only on the roll number. Riya #1 goes to bucket 1. - Riya #2 has the same roll, so the same hash and the same bucket 1. Now the set calls
equals(), gets true, and refuses to add. - That's the contract at work: equal objects must have equal hash codes, so they meet in the same bucket.
equals, hashCode and toString in real systems
These three methods sit underneath logging, caching, de-duplication and every HashMap key in a codebase.
Watch it with an example: a train seat lookup
A booking system stores who sits where in a HashMap<Seat, String>. The ticket checker creates a new Seat object from the printed ticket and looks it up.
- At booking time,
put(new Seat("S4", 32), "Riya")stores the entry in the bucket chosen by the seat'shashCode(). - Later, the checker's app builds a new
Seatobject from the ticket. It is not the same object as the one used to book. getcalls the new key'shashCode(). Because it's computed from coach and number, it picks the same bucket [9].- Inside that bucket,
equals()compares coach and number: equal. The key matches. - The map returns “Riya”. Without the two overrides,
getwould returnnull. (Simplified: real systems also store date and train number in the key.)
1. Readable logs
Log lines and error messages call toString(). Order[id=9912, total=₹1499] helps support teams; Order@1b6d3586 doesn't. Teams leave secrets like passwords and card numbers out of toString.
2. De-duplicating records
Merging customer lists from two branches, or ignoring a double-tap on “Pay”, relies on a HashSet or HashMap recognising equal objects, by PAN, phone number or transaction ID.
3. Cache keys
A travel site caches fare searches in a map keyed by a SearchKey(from, to, date) object. Without proper equals/hashCode, every search misses the cache and hits the slow pricing system.
4. Unit tests
assertEquals(expected, actual) in JUnit calls equals, and prints both objects with toString when the test fails. Good overrides make test failures easy to read.
Examples are simplified. Persistence frameworks such as JPA have extra advice on equals/hashCode for entities with generated IDs.
An attendance register that counts each student once
import java.util.HashSet;
import java.util.Objects;
class Student {
private final String roll;
private final String name;
Student(String roll, String name) {
this.roll = roll;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Student)) return false;
Student s = (Student) o;
return roll.equals(s.roll);
}
@Override
public int hashCode() {
return Objects.hash(roll);
}
@Override
public String toString() {
return name + " (" + roll + ")";
}
}
public class Main {
public static void main(String[] args) {
HashSet<Student> present = new HashSet<>();
System.out.println(present.add(new Student("21CS07", "Riya")));
System.out.println(present.add(new Student("21CS12", "Kabir")));
System.out.println(present.add(new Student("21CS07", "Riya")));
System.out.println("Present: " + present.size());
System.out.println(present.contains(new Student("21CS12", "Kabir")));
}
}equals(Object o)Parameter type isObject, so it truly overrides. Two students are equal if their rolls match.Objects.hash(roll)Uses the same field asequals, so equal students always share a bucket.toString()Makes printing and logging readable.add(...)returnsfalseThe set found an equal student and kept the first one.
The set calls hashCode and equals for you
This trace is recorded from the real JVM. Watch the frames: each add calls your hashCode(), and for the duplicate, your equals() too.
Worked examples: real problems, solved and traced in memory
Three business classes that need the right Object methods. Watch the JVM call your toString, hashCode and equals from inside Java's own code.
A receipt line that prints itself
Problem statement
An order line has an item name, quantity and unit price in paise. Printing it should give 2 x Notebook @ ₹45.00 = ₹90.00. Override toString().
Line("Notebook", 2, 4500)2 x Notebook @ ₹45.00 = ₹90.00
Approach
- Store money in paise as
long, so there are no rounding errors. - A helper
rupees(long paise)formats paise as ₹ with two decimals. toString()builds the line;printlncalls it automatically.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Find a booking by seat
Problem statement
Bookings are stored in a HashMap<Seat, String>. The checker builds a new Seat("S4", 32) from a ticket and must find the passenger. Override equals and hashCode on coach and number.
put(Seat("S4", 32), "Riya"); get(Seat("S4", 32))S4-32: Riya
Approach
- Both methods use the same two fields:
coachandnumber. putcallshashCodeto choose a bucket.getwith a new, equal Seat finds the same bucket, thenequalsconfirms the key.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Is this refund already in the list?
Problem statement
A refund list holds Money values (paise + currency). Before adding a refund, check whether an equal amount is already pending. ArrayList.contains uses only equals.
pending: ₹250 INR, $5 USD; check ₹250 INR
Already pending: true
Approach
equalscompares bothpaiseandcurrency.containswalks the list and callsequalson each element until one matches.hashCodeis still overridden, as the contract requires, even though a list doesn't use it.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Check yourself
Quick revision
Lesson 10.3 in one screen
Objectis the root of all classes; 11 inherited methods.toString()default:ClassName@hex. Override for printing and logs (leave out secrets).equals()default is==. Override with parameterObject: checkthis == o,instanceof, then compare key fields.hashCode(): override whenever you override equals; use the same fields (Objects.hash(...)).- Contract: equal objects → equal hash codes. Equal hash codes → not necessarily equal (collision).
- HashSet/HashMap: hashCode picks the bucket, equals confirms. Missing hashCode → duplicates, failed lookups.
- Always use
@Override; consider IDE generation or records (Lesson 14.5).
Interview and viva questions
What is the difference between == and equals()?
== compares references for objects (same object in memory) and values for primitives. equals() asks the object whether another object is logically equal; its default is ==, but classes like String, Integer and List override it to compare contents.
Why must you override hashCode() when you override equals()?
Hash-based collections find objects by hash code first. If two equal objects returned different hash codes, a HashSet could store both, and HashMap.get with an equal key would look in the wrong bucket and return null. The contract requires equal objects to have equal hash codes.
instanceof or getClass() in equals?
instanceof lets a subclass object be equal to a parent object, which can break symmetry if the subclass adds fields. getClass() != o.getClass() requires the exact same class and is stricter. Many teams use getClass() for non-final classes and instanceof for final ones; records and IDEs choose for you.
What happens if a field used in hashCode changes after the object is in a HashSet?
The object stays in its old bucket, but lookups now compute a new hash and search a different bucket. contains and remove fail, and the object is effectively lost inside the set. Use immutable fields for equality, or remove the object, change it, and add it back.
Is the default hashCode the memory address?
Not necessarily. It's an identity hash code generated by the JVM (HotSpot typically uses a random-based number stored in the object header). It stays the same for the object's lifetime, even if the GC moves the object.
Practice problems
Override the three methods correctly and see collections behave. The last three are business tasks.
toString for a Product
Give Product a toString() that prints Pen (₹10).
Product("Pen", 10)Pen (₹10)
Show solution and explanation
class Product {
String name;
int price;
Product(String name, int price) { this.name = name; this.price = price; }
@Override
public String toString() {
return name + " (₹" + price + ")";
}
}
public class Main {
public static void main(String[] args) {
System.out.println(new Product("Pen", 10));
}
}How it works: println(Object) calls toString(); our override replaces the default Product@….
Every class is an Object
Store a String, an Integer and an int array in Object variables and print each one's class name with getClass().getSimpleName().
"UPI", 42, new int[3]
String Integer int[]
Show solution and explanation
public class Main {
public static void main(String[] args) {
Object[] things = {"UPI", 42, new int[3]};
for (Object o : things) {
System.out.println(o.getClass().getSimpleName());
}
}
}How it works: every object, even an array, is an Object. getClass() reports the real run-time class.
Hash collision
Show that two different Strings can have the same hash code: print the hash codes of "Aa" and "BB" and whether they're equal.
"Aa", "BB"
2112 2112 false
Show solution and explanation
public class Main {
public static void main(String[] args) {
System.out.println("Aa".hashCode() + " " + "BB".hashCode());
System.out.println("Aa".equals("BB"));
}
}How it works: equal hash codes don't imply equal objects. That's a collision; the contract allows it, and hash tables handle it with equals.
Point equality
Override equals and hashCode in Point(x, y) so that a HashSet stores (1,2) only once.
(1,2), (1,2), (3,4)
2
Show solution and explanation
import java.util.HashSet;
import java.util.Objects;
class Point {
final int x, y;
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)) return false;
Point p = (Point) o;
return x == p.x && y == p.y;
}
@Override
public int hashCode() { return Objects.hash(x, y); }
@Override
public String toString() { return "(" + x + "," + y + ")"; }
}
public class Main {
public static void main(String[] args) {
HashSet<Point> pts = new HashSet<>();
pts.add(new Point(1, 2));
pts.add(new Point(1, 2));
pts.add(new Point(3, 4));
System.out.println(pts.size());
}
}How it works: both methods use x and y. Equal points get equal hashes, meet in one bucket, and equals rejects the duplicate.
getClass() vs instanceof
Employee and its subclass Manager share an id. Show how instanceof and getClass() give different answers when comparing an Employee with a Manager.
Employee(7) vs Manager(7)
true false
Show solution and explanation
class Employee {
int id;
Employee(int id) { this.id = id; }
boolean equalsByInstanceof(Object o) {
return o instanceof Employee && ((Employee) o).id == id;
}
boolean equalsByClass(Object o) {
return o != null && getClass() == o.getClass() && ((Employee) o).id == id;
}
}
class Manager extends Employee {
Manager(int id) { super(id); }
}
public class Main {
public static void main(String[] args) {
Employee e = new Employee(7);
Manager m = new Manager(7);
System.out.println(e.equalsByInstanceof(m));
System.out.println(e.equalsByClass(m));
}
}How it works: a Manager is an Employee, so instanceof accepts it. getClass() requires exactly the same class. Pick one rule and use it consistently so equals stays symmetric.
De-duplicate customers by PAN
Three sign-ups arrive: abcde1234f, ABCDE1234F (with a space) and PQRSX9876L. Normalise the PAN in the constructor and count unique customers with a HashSet.
"abcde1234f", "ABCDE1234F ", "PQRSX9876L"
Unique customers: 2
Show solution and explanation
import java.util.HashSet;
import java.util.Objects;
class Customer {
private final String pan;
Customer(String pan) {
this.pan = pan.trim().toUpperCase();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Customer)) return false;
return pan.equals(((Customer) o).pan);
}
@Override
public int hashCode() { return Objects.hash(pan); }
}
public class Main {
public static void main(String[] args) {
HashSet<Customer> unique = new HashSet<>();
unique.add(new Customer("abcde1234f"));
unique.add(new Customer("ABCDE1234F "));
unique.add(new Customer("PQRSX9876L"));
System.out.println("Unique customers: " + unique.size());
}
}How it works: normalising in the constructor means equal PANs are stored identically, so equals and hashCode agree. The final field also keeps the hash stable.
Masked toString for a card
A Card holds a 16-digit number. Its toString() must show only the last 4 digits, so logs never contain the full number.
"4111222233334421"
Payment failed for Card[Riya, XXXX-XXXX-XXXX-4421]
Show solution and explanation
class Card {
private final String number;
private final String holder;
Card(String number, String holder) {
this.number = number;
this.holder = holder;
}
@Override
public String toString() {
return "Card[" + holder + ", XXXX-XXXX-XXXX-" + number.substring(12) + "]";
}
}
public class Main {
public static void main(String[] args) {
Card c = new Card("4111222233334421", "Riya");
System.out.println("Payment failed for " + c);
}
}How it works: string concatenation calls toString(). Masking there protects every log line that ever prints a Card.
Count orders per customer object
Use Customer objects (equal by phone number) as keys in a HashMap to count orders: Riya orders twice (two separate objects), Kabir once.
Riya 98xxx, Kabir 97xxx, Riya 98xxx
{Riya=2, Kabir=1}Show solution and explanation
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
class Customer {
final String phone, name;
Customer(String phone, String name) { this.phone = phone; this.name = name; }
@Override
public boolean equals(Object o) {
return o instanceof Customer && phone.equals(((Customer) o).phone);
}
@Override
public int hashCode() { return Objects.hash(phone); }
@Override
public String toString() { return name; }
}
public class Main {
public static void main(String[] args) {
Map<Customer, Integer> orders = new LinkedHashMap<>();
Customer[] seen = {
new Customer("9876500001", "Riya"),
new Customer("9876500002", "Kabir"),
new Customer("9876500001", "Riya")
};
for (Customer c : seen) {
orders.put(c, orders.getOrDefault(c, 0) + 1);
}
System.out.println(orders);
}
}How it works: the third object is a different instance, but equals/hashCode on the phone number make it the same key, so Riya's count becomes 2. toString keeps the printed map readable.
Real-world scenario questions (asked in company interviews)
equals/hashCode questions are classics in Java interviews, from freshers to seniors. The best answers connect the contract to a real failure.
The cache that never hits
A fare cache uses HashMap<SearchKey, Fare>. Every search creates new SearchKey(from, to, date). The hit rate is 0% even for repeated searches. What's missing?
The interviewer is checking: hashCode/equals for map keys.
Show a strong answer
SearchKeyuses Object's identity-based equals and hashCode, so a new key object never matches the stored one.- Override both using
from,toanddate; make those fieldsfinalso the hash can't change. - Or declare it as
record SearchKey(String from, String to, LocalDate date), which generates both correctly. - Add a test: two keys with the same values must be equal and give the same hash code.
Avoid: using a concatenated String key like from + to + date without a separator. "AB"+"C" and "A"+"BC" collide.
Duplicate customers after a merge
Two branch databases are merged into a HashSet<Customer>. Customer overrides equals on PAN, but the merged set has thousands of duplicates. Why, and how do you prove the fix?
The interviewer is checking: understanding of how HashSet uses hashCode first.
Show a strong answer
hashCodewasn't overridden, so equal customers usually land in different buckets andequalsis never called.- Override
hashCodewithObjects.hash(pan): the same field as equals. Normalise PAN (upper-case, trimmed) in the constructor so equal PANs really are equal. - Prove it: a unit test with two objects having the same PAN gives set size 1; re-run the merge and compare counts.
Avoid: return 1; as hashCode. It's legal but puts every customer in one bucket and makes the set crawl.
Card numbers in the logs
A security audit finds full card numbers in application logs. The code just logs "Payment failed: " + payment. Where did the card number come from, and what do you change?
The interviewer is checking: toString as a hidden data channel.
Show a strong answer
- String concatenation calls
payment.toString(), which was generated to include every field, card number included. - Rewrite
toStringto include only safe fields and mask sensitive ones (XXXX-XXXX-XXXX-4421). - Add a log-scanning check in CI and purge the affected logs per the incident process.
Avoid: “just don't log payments”. You'll lose the information support needs; log safely instead.
An object lost inside a HashSet
A Ticket is added to a HashSet, then its status changes from OPEN to CLOSED. Afterwards set.contains(ticket) returns false for that very object, and remove doesn't work. Explain.
The interviewer is checking: mutable fields in hashCode.
Show a strong answer
hashCodeincludesstatus. When status changed, the hash changed, but the entry stayed in the bucket chosen by the old hash.containssearches the new bucket and finds nothing.- Base equality on the immutable
ticketIdonly. If equality must include mutable data, remove the object before changing it and re-add it after.
Avoid: blaming HashSet. It relies on the contract that hashCode stays stable while the object is in the set.