Course contents

The Object contract: interview questions & scenarios

Free13 questions · 26 what-if follow-ups

These are the Object-contract questions I ask most, and they are rarely about syntax. A five-line HashSet example or one defensive-copy bug tells me whether you can picture references in memory and know what the collections expect from your classes.

Here's how to use this page: read the question, say your answer out loud first, then open the model answer. The follow-ups are the ones I ask next, so if one stumps you, go back to comparing objects, equals and hashCode and deep vs shallow copy.

Question 1. ConceptWarm-upAsked very often

What's the difference between == and equals() for objects?

Tests identity vs equality, Objects.equals

Show answerHide answer2 follow-ups

Model answer

== compares references: it is true only when both sides point to the same object. No class can override it. equals() compares meaning, as defined by the runtime class of the receiver. Object.equals is identity, so the two behave the same until a class overrides it.

For nullable values, use Objects.equals(a, b): two nulls are equal, one null is not, and no NullPointerException is thrown.

How I’d say it in the interview

"Double-equals asks 'same object?', equals asks 'same value?'. Object's default equals is identity, so it only changes for classes like String, records, or your own value types. If either side can be null, I use Objects.equals."

Signals depth

Mentioning that equals is a virtual call on the receiver, and a.equals(b) throws if a is null.

Red flags

"== compares values and equals compares references" (backwards), or "always use equals, even for enums" without knowing == is correct and null-safe for enums.

Follow-ups interviewers ask next

  1. What if I compare two int[] arrays with equals?

    It is false unless they are the same array object. Arrays do not override equals. Use Arrays.equals for one-dimensional arrays and Arrays.deepEquals for nested object arrays.

    1. What if the arrays are record components?

      The generated record equals calls Objects.equals on the array component, so it still compares array identity. If arrays are part of value semantics, write the methods yourself or wrap the data in an immutable value type.

Question 2. ConceptWarm-upAsked very often

Explain the contract between equals() and hashCode().

Tests equals properties, hashCode rule

Show answerHide answer2 follow-ups

Model answer

equals must be reflexive, symmetric, transitive, consistent, and x.equals(null) must be false. hashCode must stay stable while the fields used by equality do not change, and if two objects are equal, their hash codes must be equal. Unequal objects may share a hash code.

The reason is practical: hash collections choose a bucket from hashCode before they compare candidates with equals.

This diagram is the mental model I want candidates to have before they talk about buckets and collisions.

Fig. 1

The diagram shows how hashCode chooses a HashMap bucket and equals chooses the matching entry inside that bucket.

The diagram shows how hashCode chooses a HashMap bucket and equals chooses the matching entry inside that bucket.

How I’d say it in the interview

"Equals defines when two objects mean the same thing. HashCode must agree with that definition, so equal objects always have equal hashes. Collisions are legal, but they make the bucket search longer."

Signals depth

Saying why overriding equals without hashCode breaks HashMap, and noting that a constant hash code is legal but harmful.

Red flags

"Equal hash codes mean the objects are equal."

Follow-ups interviewers ask next

  1. What if hashCode() returns a constant like 42?

    It is legal, because equal objects still have equal hashes. But every key lands in one bucket, so a HashMap degrades from expected O(1) lookup toward O(n), or O(log n) after treeification when the bucket has at least 8 nodes and the table has at least 64 buckets.

    1. What if equals() reads mutable fields?

      Then the hash and equality result can change after insertion. That is how keys get stranded in hash collections.

Question 3. ConceptCoreAsked very often

Walk me through what HashMap.get(key) does.

Tests hash spreading, bucket index, equals in bucket

Show answerHide answer2 follow-ups

Model answer

First, HashMap computes the key hash, using 0 for a null key, and spreads high bits into low bits with h ^ (h >>> 16). Then it chooses the bucket with (n - 1) & hash, where n is a power of two. Inside that bucket it checks stored hash first, then ==, then key.equals(node.key).

If the bucket has become a tree, the same idea applies through red-black tree lookup.

How I’d say it in the interview

"HashCode picks where to look. Equals picks the matching entry inside that bucket. It does not scan the whole map unless your hashing collapses everything into one bucket."

Signals depth

Knowing why the spread exists: table indexing uses low bits, so hashes that differ only in high bits need to be mixed.

Red flags

"It calls equals on every key in the map."

Follow-ups interviewers ask next

  1. What if a class overrides equals but not hashCode, and I put then get with an equal key?

    get usually returns null. The equal key has a different identity-based hash, so lookup goes to a different bucket or fails the stored-hash check before equals helps.

    1. What if the key is null?

      Java's HashMap allows one null key. It is stored with hash 0; ConcurrentHashMap rejects null keys and values.

Question 4. DebuggingCoreAsked very often

Your service started returning null from a cache lookup after a release that changed a field on the cache key object. Walk me through it.

Tests mutable keys, stranded entries, hash-based lookup

Show answerHide answer2 follow-ups

Model answer

I would first suspect a mutable key. The entry was inserted into a bucket using the old hash. After the field changed, get computed a new hash and searched a different bucket, so it missed. Iteration may still show the entry, which makes this incident confusing.

The fix is to make key fields immutable, base equality on a stable identifier, or remove and reinsert the key around a deliberate mutation. In a cache, I prefer a small immutable key record.

How I’d say it in the interview

"The key moved logically but not physically. HashMap does not re-bucket an entry when a field changes, so the value can be in the map and still unreachable by lookup."

Signals depth

Calling out the stranded-entry leak and checking logs for key mutation between put and get.

Red flags

"HashMap rehashes keys whenever their fields change."

Follow-ups interviewers ask next

  1. What if the same bug happens in a HashSet?

    contains and remove miss for the same reason. The object is still present during iteration, but normal lookup cannot find it.

    1. What if the team says the key is a record?

      Records are shallowly immutable only with respect to their component references. A record containing List or an array can still have mutable contents that affect custom equality, or array identity semantics that surprise people.

Question 5. DebuggingCoreCommonly asked

Your payment sorting job started throwing Comparison method violates its general contract! in prod after a larger batch arrived. Walk me through it.

Tests comparator transitivity, integer overflow, TimSort

Show answerHide answer2 follow-ups

Model answer

I would inspect the comparator for overflow or inconsistent ordering. This common version is broken:

Comparator<Account> byBalance = (a, b) -> a.balance() - b.balance();

If a.balance() is Integer.MIN_VALUE and b.balance() is 1, subtraction wraps and flips the sign. TimSort detects some non-transitive comparators only on certain input shapes, which is why the bug can wait for a large production batch. Use Integer.compare(a.balance(), b.balance()) or Comparator.comparingInt(Account::balance).

How I’d say it in the interview

"The comparator must define a stable order. Subtraction can overflow, so it sometimes says A is before B, B before C, and C before A. I'd replace it with comparingInt and add boundary tests."

Signals depth

Mentioning transitivity, equality consistency for sorted sets, and null-handling if production data contains missing fields.

Red flags

"Balances are never that large" without a guardrail or test.

Follow-ups interviewers ask next

  1. What if the comparator is Comparator.comparing(Account::owner).thenComparing(Account::balance).reversed()?

    reversed() applies to the whole chain, so owners sort Z to A and balances high to low. To reverse only balances, use thenComparing(Account::balance, Comparator.reverseOrder()).

    1. What if some owners are null?

      Pick the rule explicitly: Comparator.comparing(Account::owner, Comparator.nullsLast(String::compareTo)). Silent null crashes are a production smell in ordering code.

Question 6. What does it print?CoreCommonly asked

What does this print, and why?

Tests Comparable consistent with equals, BigDecimal scale

Show answerHide answer2 follow-ups

Model answer

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
System.out.println(new HashSet<>(List.of(a, b)).size()); // 2
System.out.println(new TreeSet<>(List.of(a, b)).size()); // 1

BigDecimal.equals compares value and scale, so 1.0 and 1.00 are not equal. HashSet uses hashCode and equals, so it keeps both. TreeSet uses compareTo, which compares numeric value and returns 0, so it treats the second as a duplicate.

How I’d say it in the interview

"Two, then one. HashSet follows equals, BigDecimal equals includes scale, and TreeSet follows compareTo, which ignores scale. This is the classic natural-ordering trap."

Signals depth

Knowing the money fix: normalize scale at boundaries, such as setScale(2, RoundingMode.HALF_EVEN), or compare with compareTo(...) == 0 when that is the intended rule.

Red flags

"Both print one, they are the same number."

Follow-ups interviewers ask next

  1. What if I use them as keys in a HashMap and a TreeMap?

    The HashMap has two entries. The TreeMap has one, and the second put replaces the value for the compare-equal key.

    1. What if I write my own Comparable class?

      Try to make compareTo consistent with equals. If you cannot, document it loudly and avoid mixing sorted and hashed collection assumptions.

Question 7. ScenarioCoreCommonly asked

Your booking API started showing extra nights in responses after you returned Collections.unmodifiableList(nights). Walk me through it.

Tests unmodifiable views, defensive copies, aliasing

Show answerHide answer2 follow-ups

Model answer

Collections.unmodifiableList is a read-only view, not a snapshot. Callers cannot mutate through that returned reference, but if the original backing list is still reachable, later changes show through the view. That is enough to leak state across requests.

For a defensive boundary, copy the input with List.copyOf(nights) and store that copy. It is unmodifiable, rejects null elements, and no longer tracks changes to the caller's list.

How I’d say it in the interview

"Unmodifiable is not the same as immutable. The wrapper blocks writes through itself, but it still points at a mutable list. For API state I want a snapshot, usually List.copyOf."

Signals depth

Copying before validation in constructors to avoid a check-then-use race on a caller-owned list.

Red flags

"Returning an unmodifiable view makes the object immutable."

Follow-ups interviewers ask next

  1. What if the list contains mutable NightCharge objects?

    List.copyOf copies the list structure, not the elements. If elements are mutable, the caller can still mutate a shared NightCharge. Copy the elements too, or make the element type immutable.

    1. What if null elements are currently allowed?

      List.copyOf throws NullPointerException. I would push back on null elements for domain collections, but if you must allow them, use Collections.unmodifiableList(new ArrayList<>(nights)) and document the rule.

Question 8. ScenarioCoreCommonly asked

Your order copy flow changed the original customer's address after editing the copy. Walk me through shallow versus deep copy.

Tests reference assignment, shallow copy, deep copy, clone

Show answerHide answer2 follow-ups

Model answer

Assignment copies the reference: one object. A shallow copy creates a new outer object but copies field references as-is, so nested mutable objects are shared. A deep copy also copies the mutable nested objects, so editing the copy does not change the original. Object.clone() is shallow unless the class fixes each mutable field after cloning.

Here is the picture I use when a candidate says "copy" without naming what gets shared.

Fig. 1

The diagram contrasts a shallow order copy that shares the Address object with a deep copy that creates a separate Address object.

The diagram contrasts a shallow order copy that shares the Address object with a deep copy that creates a separate Address object.

How I’d say it in the interview

"Assignment shares everything. Shallow copy gives me a new parent but shared children. Deep copy duplicates the mutable children too. clone starts shallow, so I do not treat it as a deep-copy tool."

Signals depth

Saying immutable children such as String, LocalDate and value records do not need copying; the practical target is "deep enough" for mutable state.

Red flags

"clone() makes a deep copy."

Follow-ups interviewers ask next

  1. What if I call clone() on an int[][]?

    You get a new outer array whose elements are the same row arrays. Mutating copy[0][0] changes the original row. Clone each row as well.

    1. What if the object graph has cycles?

      Naive recursion loops or ends in StackOverflowError. Keep an IdentityHashMap<Object, Object> from original to copy, register the new copy before recursing, and reuse it when the same original appears again.

Question 9. CodingCoreCommonly asked

Design a Booking class that holds a list of nights and cannot be modified by its callers.

Tests defensive copies, immutability, records

Show answerHide answer2 follow-ups

Model answer

public record Booking(String guestId, List<LocalDate> nights) {
    public Booking {
        Objects.requireNonNull(guestId);
        nights = List.copyOf(nights);
        if (nights.isEmpty()) {
            throw new IllegalArgumentException("no nights");
        }
    }
}

The record is final, its fields are final, and the compact constructor stores one defensive copy. Since LocalDate is immutable, returning the list from the accessor is safe: callers cannot change the list or the elements.

How I’d say it in the interview

"I'd use a record and make the boundary copy in the compact constructor. The list is unmodifiable and the elements are immutable, so the Booking is safe to share and safe as a map key."

Signals depth

Noting that immutability removes many copy and hash bugs at once, and that updates should create a new Booking.

Red flags

Storing the caller's list directly, or returning a fresh ArrayList from every accessor when the field could have been immutable.

Follow-ups interviewers ask next

  1. What if nights itself is null?

    List.copyOf(null) throws NullPointerException. If you want a custom message, call Objects.requireNonNull(nights, "nights") first.

    1. What if the component were List<Address>?

      Then a list copy is not enough if Address is mutable. Make Address immutable or copy each address at the boundary.

Question 10. DesignSeniorCommonly asked

Should equals use instanceof or getClass()?

Tests symmetry, inheritance, proxies

Show answerHide answer2 follow-ups

Model answer

For value classes, my default is final class or record plus instanceof pattern. If an instantiable superclass uses instanceof and a subclass adds a value field, symmetry breaks: the parent may consider the child equal while the child does not. getClass() preserves symmetry, but it can reject framework proxies and reduce substitutability.

The safest design is not to extend value classes with extra state. Use composition instead.

How I’d say it in the interview

"The real answer is design. Make value types final, or use records. Instanceof is fine there. In open hierarchies, adding state breaks the equals contract, while getClass has proxy and substitutability costs."

Signals depth

Naming the proxy issue and the fact that no equality test rescues an extensible value class that adds state cleanly.

Red flags

"getClass() is always correct" with no mention of proxies, or "instanceof is always correct" with no symmetry discussion.

Follow-ups interviewers ask next

  1. What if it is a JPA entity with a generated id?

    Use a proxy-friendly type check, compare non-null IDs through accessors, and avoid equality that touches lazy collections. Many teams use a constant hash code until the ID is assigned, so the hash does not change after persist.

    1. What if the entity has a natural business key?

      A stable, immutable business key is often cleaner than a generated ID for equality. It must be unique, assigned before persistence, and not editable by users.

Question 11. DesignCoreCommonly asked

Why do many engineers avoid Cloneable? What do you use instead?

Tests Cloneable, copy constructors, final fields

Show answerHide answer2 follow-ups

Model answer

Cloneable is a marker interface with no clone method. It changes the behavior of Object.clone(), which is protected, shallow, skips constructors, and throws a checked CloneNotSupportedException. It also fights final fields because deep-copy repair needs assignment after construction.

I prefer a copy constructor, new Customer(other), or a static factory, Customer.copyOf(other). Validation runs, fields can stay final, and the copy rule is visible in code.

How I’d say it in the interview

"Cloneable is an awkward protocol around a protected shallow copy. I use copy constructors or copyOf factories because they make invariants and deep-copy decisions explicit."

Signals depth

Admitting the trade-off: copy constructors are not polymorphic. In a hierarchy, you may need an overridable copy() method that delegates to constructors.

Red flags

"Implementing Cloneable makes clone() public."

Follow-ups interviewers ask next

  1. What if you need a generic deep copy of any object graph?

    Serialization round trips can work and handle cycles, but every class must participate, transient fields vanish, constructors do not run, and performance is poor compared with explicit copying. I keep that for tests or tooling, not request paths.

    1. What if arrays are involved?

      Array clone() is useful for one-dimensional arrays. For nested arrays, clone each nested array too.

Question 12. ConceptSeniorSometimes asked

Is Object.hashCode() the memory address of the object?

Tests identity hash, object header, moving GC

Show answerHide answer2 follow-ups

Model answer

No. A moving garbage collector can relocate objects, but the identity hash must remain stable. On HotSpot, the identity hash is generated lazily and stored with object-header state, commonly in the mark word or associated monitor metadata. System.identityHashCode(obj) returns that identity hash even when the class overrides hashCode.

Identity hashes are not unique. They are stable for an object during its lifetime, not a database key.

How I’d say it in the interview

"It cannot be the address because the GC can move the object. Think of it as a lazily assigned identity hash stored with the object's header or monitor state."

Signals depth

Mentioning locking interaction and that two live objects can have the same identity hash.

Red flags

"Yes, it is the address, so it is unique."

Follow-ups interviewers ask next

  1. What if I call hashCode() on a record?

    The record gets generated equals and hashCode from its components via runtime bootstrap machinery. Equal records have equal hashes, but array components are still compared and hashed by array identity.

    1. What if I need object identity in a map?

      Use IdentityHashMap, which uses == rather than equals. It is for special cases like graph traversal bookkeeping, not ordinary business keys.

Question 13. DesignSeniorSometimes asked

When would you write Objects.hash(...) and when would you hand-write hashCode()?

Tests hashCode performance, allocation, caching

Show answerHide answer2 follow-ups

Model answer

Objects.hash(a, b, c) is readable, but it is varargs: it creates an Object[] and boxes primitives unless the JIT removes the allocation. For DTOs hashed occasionally, that is fine. For hot keys, hand-write the hash or use a generated implementation that avoids per-call allocation.

For immutable types, caching the hash can help if hashing is frequent and the computation is non-trivial. String does this.

How I’d say it in the interview

"Objects.hash is fine when clarity matters and the object is not a hot key. In a cache key or routing key, I avoid varargs allocation and write the hash explicitly."

Signals depth

Knowing that 31 * h + fieldHash is a convention because 31 is odd and cheap to multiply by, and that records do not use Objects.hash directly.

Red flags

"Hash performance never matters" or "Objects.hash is bad" without naming allocation and boxing.

Follow-ups interviewers ask next

  1. What if the class is mutable?

    Do not cache the hash unless every field used by equality is immutable. A cached hash from old state is another way to break hash collections.

    1. What if all fields are primitives?

      Use the wrapper hash helpers, such as Long.hashCode(id) and Boolean.hashCode(active), then combine them. Avoid boxing in hot code.

Lessons in The Object contract