Object class: equals(), hashCode(), toString()

Every class you write secretly extends java.lang.Object, and inherits its methods. Three of them decide how your objects are printed (toString), compared (equals) and found in hash-based collections (hashCode). Their defaults are rarely what you want. You'll learn what each default does, how to override them correctly, and the contract that, when broken, makes a HashSet keep duplicates.

Module 10 · Core Classes⏱ 24 minIntermediate8 practice problems
1 The story

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.

Student #1roll 21CS07 Riya Student #2roll 21CS07 Riya HashSet buckets [3]Student #1 [7] [12]Student #2 size() = 2 ✗ (one Riya, counted twice) println(set) [Student@4e25154f, Student@70dea4e] ??? myphatshala.in
Riya

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!

Arjun Sir

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().

Riya

And the Student@4e25154f?

Arjun Sir

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.

Riya

So I just write equals, and the set will notice the duplicate?

Arjun Sir

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.

2 Definition

What is the Object class?

Formal definition (write this in exams)

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.

In simple words

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

OverrideReplace an inherited method with your own version (Lesson 7.4). Mark it with @Override.
Reference equalitya == b: are these the very same object?
Logical equalitya.equals(b): do they mean the same thing (same roll number)?
Hash codeAn int summarising an object, used to choose a bucket in HashMap/HashSet.
BucketOne slot of a hash table. Objects with the same hash land in the same bucket.
ContractRules a method must follow so other code (like collections) works correctly.

The 11 methods every object inherits

MethodWhat the default doesOverride?
toString()Returns ClassName@hexHashCodeAlmost always
equals(Object o)this == o (same object only)For value-like classes
hashCode()A number derived from the object's identityWhenever 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 finalNo
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.

3 Types and ways to use it

The three methods, default vs overridden

toString()

What println(obj) and "…" + obj show. Default: Student@4e25154f.

printing, logs

equals()

“Do these mean the same thing?” Default: only if they're the same object.

contains, remove, indexOf

hashCode()

A number that picks the bucket. Equal objects must give the same number.

HashMap, HashSet

1. 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
    }
}
Student@4e25154f (the hex number differs on every run and machine)
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()
    }
}
Student[21CS07, Riya] Present: Student[21CS07, Riya]

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));
    }
}
false true false

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());
    }
}
2

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” meansUse in equals and hashCode
Studentsame roll numberroll
Pointsame x and yx, y
BankAccountsame account numberaccountNo (not the changing balance)
Moneysame amount and currencypaise, 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.

4 Step by step

How a HashSet decides “already there”

  1. Call hashCode()

    present.add(new Student("21CS07", "Riya"));
    HashSet is backed by a HashMap. It calls student.hashCode() and mixes the bits to get a number.
  2. Pick a bucket

    The number is reduced to a bucket index (with 16 buckets, roughly hash & 15). Only objects in that bucket are candidates for “the same”.
  3. Compare with equals() inside the bucket

    For each object already in the bucket, it checks the stored hash first, then calls equals. Different hash codes mean equals is never called.
  4. Equal one found: don't add

    add returns false and the set is unchanged. This only works if equal students produce equal hash codes, which is why hashCode must use the same fields as equals.
  5. 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.
5 See it happen

The duplicate scan: without and with hashCode()

Left: the attendance code and what size() reports. Right: the buckets inside the HashSet.

Why HashSet needs hashCode() 6 steps
What you see Student overrides equals() only Student overrides equals() and hashCode() set.add(new Student("21CS07")); set.add(new Student("21CS07")); println(set.size()); Returned / printed add → true add → true (duplicate accepted!) size() = 2 ✗ add → false (already there) size() = 1 ✓ First scan of Riya's card. Second scan, same roll number. The set counts Riya twice. Same code, hashCode() added. The second scan is rejected. One Riya, counted once. myphatshala.in
Behind the screen hashCode() identity: 0x1b6d… identity: 0x4e25… Objects.hash(roll) = 1478787413 → bucket 3 → bucket 12 (different!) → bucket 1 → bucket 1 (same) → equals() → true equals() never called one entry per student [1] [3] [12] Riya #1 Riya #2 Riya #1 Riya #2 ? equals: true → not added Default hashCode is based on identity. Another object, another number. Two buckets, so no comparison happens. Hash now depends only on the roll. Same bucket, so equals() runs and says equal. Equal objects share one bucket. myphatshala.in
6 Real-world use

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.

Finding a booking by seat 5 steps
Animated example Seat("S4", 32)key used to book Seat("S4", 32)new object from ticket HashMap<Seat, String> [2] [9] [14] S4-32 → Riya hashCode(): coach + number → bucket [9] equals(): S4 = S4, 32 = 32 → same seat get → "Riya" At booking: put(seat, "Riya") into bucket [9] A different object, but the same seat myphatshala.in
INFO Order[id=9912, total=₹1499]

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.

log.info("Saved " + order)→order.toString()→useful log line
same PAN → one

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.

Two records→equals + hashCode on the ID→stored once
key (from,to,date)→ cached fares

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.

new SearchKey(...)→map.get(key)→hit, if equal keys hash equally
assertEquals

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.

assertEquals→expected.equals(actual)→clear failure message

Examples are simplified. Persistence frameworks such as JPA have extra advice on equals/hashCode for entities with generated IDs.

7 Run the code

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")));
    }
}
true true false Present: 2 true
  • equals(Object o)Parameter type is Object, so it truly overrides. Two students are equal if their rolls match.
  • Objects.hash(roll)Uses the same field as equals, so equal students always share a bucket.
  • toString()Makes printing and logging readable.
  • add(...) returns falseThe set found an equal student and kept the first one.
8 Line by line

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.

Example 1E-commerce · receipts

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().

Input
Line("Notebook", 2, 4500)
Expected output
2 x Notebook @ ₹45.00 = ₹90.00

Approach

  1. Store money in paise as long, so there are no rounding errors.
  2. A helper rupees(long paise) formats paise as ₹ with two decimals.
  3. toString() builds the line; println calls 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.

Example 2Railways · lookup

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.

Input
put(Seat("S4", 32), "Riya"); get(Seat("S4", 32))
Expected output
S4-32: Riya

Approach

  1. Both methods use the same two fields: coach and number.
  2. put calls hashCode to choose a bucket.
  3. get with a new, equal Seat finds the same bucket, then equals confirms 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.

Example 3Fintech · money

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.

Input
pending: ₹250 INR, $5 USD; check ₹250 INR
Expected output
Already pending: true

Approach

  1. equals compares both paise and currency.
  2. contains walks the list and calls equals on each element until one matches.
  3. hashCode is 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.

9 Quiz and revision

Check yourself

Quick revision

Lesson 10.3 in one screen

  • Object is 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 parameter Object: check this == 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.

P1Easy

toString for a Product

Give Product a toString() that prints Pen (₹10).

Input
Product("Pen", 10)
Expected output
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));
    }
}
Pen (₹10)

How it works: println(Object) calls toString(); our override replaces the default Product@….

P2Easy

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().

Input
"UPI", 42, new int[3]
Expected output
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());
        }
    }
}
String Integer int[]

How it works: every object, even an array, is an Object. getClass() reports the real run-time class.

P3Easy

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.

Input
"Aa", "BB"
Expected output
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"));
    }
}
2112 2112 false

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.

P4Medium

Point equality

Override equals and hashCode in Point(x, y) so that a HashSet stores (1,2) only once.

Input
(1,2), (1,2), (3,4)
Expected output
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());
    }
}
2

How it works: both methods use x and y. Equal points get equal hashes, meet in one bucket, and equals rejects the duplicate.

P5Medium

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.

Input
Employee(7) vs Manager(7)
Expected output
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));
    }
}
true false

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.

P6Business problem

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.

Input
"abcde1234f", "ABCDE1234F ", "PQRSX9876L"
Expected output
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());
    }
}
Unique customers: 2

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.

P7Business problem

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.

Input
"4111222233334421"
Expected output
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);
    }
}
Payment failed for Card[Riya, XXXX-XXXX-XXXX-4421]

How it works: string concatenation calls toString(). Masking there protects every log line that ever prints a Card.

P8Business problem

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.

Input
Riya 98xxx, Kabir 97xxx, Riya 98xxx
Expected output
{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);
    }
}
{Riya=2, Kabir=1}

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.

Travel · cachingFresher

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
  • SearchKey uses Object's identity-based equals and hashCode, so a new key object never matches the stored one.
  • Override both using from, to and date; make those fields final so 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.

Banking · data qualityJunior

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
  • hashCode wasn't overridden, so equal customers usually land in different buckets and equals is never called.
  • Override hashCode with Objects.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.

Security · loggingJunior

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 toString to 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.

Platform · designMid

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
  • hashCode includes status. When status changed, the hash changed, but the entry stayed in the bucket chosen by the old hash.
  • contains searches the new bucket and finds nothing.
  • Base equality on the immutable ticketId only. 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.