The Object contract: interview questions & scenarios
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.
What's the difference between == and equals() for objects?
Tests identity vs equality, Objects.equals
Show answerHide answer
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
What if I compare two
int[]arrays withequals?It is
falseunless they are the same array object. Arrays do not overrideequals. UseArrays.equalsfor one-dimensional arrays andArrays.deepEqualsfor nested object arrays.What if the arrays are record components?
The generated record
equalscallsObjects.equalson 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.
Explain the contract between equals() and hashCode().
Tests equals properties, hashCode rule
Show answerHide answer
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.
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
What if
hashCode()returns a constant like42?It is legal, because equal objects still have equal hashes. But every key lands in one bucket, so a
HashMapdegrades 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.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.
Walk me through what HashMap.get(key) does.
Tests hash spreading, bucket index, equals in bucket
Show answerHide answer
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
What if a class overrides
equalsbut nothashCode, and Iputthengetwith an equal key?getusually returns null. The equal key has a different identity-based hash, so lookup goes to a different bucket or fails the stored-hash check beforeequalshelps.What if the key is
null?Java's
HashMapallows one null key. It is stored with hash0;ConcurrentHashMaprejects null keys and values.
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 answer
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
What if the same bug happens in a
HashSet?containsandremovemiss for the same reason. The object is still present during iteration, but normal lookup cannot find it.What if the team says the key is a record?
Records are shallowly immutable only with respect to their component references. A record containing
Listor an array can still have mutable contents that affect custom equality, or array identity semantics that surprise people.
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 answer
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
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, usethenComparing(Account::balance, Comparator.reverseOrder()).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.
What does this print, and why?
Tests Comparable consistent with equals, BigDecimal scale
Show answerHide answer
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
What if I use them as keys in a
HashMapand aTreeMap?The
HashMaphas two entries. TheTreeMaphas one, and the second put replaces the value for the compare-equal key.What if I write my own
Comparableclass?Try to make
compareToconsistent withequals. If you cannot, document it loudly and avoid mixing sorted and hashed collection assumptions.
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 answer
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
What if the list contains mutable
NightChargeobjects?List.copyOfcopies the list structure, not the elements. If elements are mutable, the caller can still mutate a sharedNightCharge. Copy the elements too, or make the element type immutable.What if null elements are currently allowed?
List.copyOfthrowsNullPointerException. I would push back on null elements for domain collections, but if you must allow them, useCollections.unmodifiableList(new ArrayList<>(nights))and document the rule.
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 answer
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.
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
What if I call
clone()on anint[][]?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.What if the object graph has cycles?
Naive recursion loops or ends in
StackOverflowError. Keep anIdentityHashMap<Object, Object>from original to copy, register the new copy before recursing, and reuse it when the same original appears again.
Design a Booking class that holds a list of nights and cannot be modified by its callers.
Tests defensive copies, immutability, records
Show answerHide answer
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
What if
nightsitself is null?List.copyOf(null)throwsNullPointerException. If you want a custom message, callObjects.requireNonNull(nights, "nights")first.What if the component were
List<Address>?Then a list copy is not enough if
Addressis mutable. MakeAddressimmutable or copy each address at the boundary.
Should equals use instanceof or getClass()?
Tests symmetry, inheritance, proxies
Show answerHide answer
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
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.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.
Why do many engineers avoid Cloneable? What do you use instead?
Tests Cloneable, copy constructors, final fields
Show answerHide answer
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
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.
What if arrays are involved?
Array
clone()is useful for one-dimensional arrays. For nested arrays, clone each nested array too.
Is Object.hashCode() the memory address of the object?
Tests identity hash, object header, moving GC
Show answerHide answer
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
What if I call
hashCode()on a record?The record gets generated
equalsandhashCodefrom its components via runtime bootstrap machinery. Equal records have equal hashes, but array components are still compared and hashed by array identity.What if I need object identity in a map?
Use
IdentityHashMap, which uses==rather thanequals. It is for special cases like graph traversal bookkeeping, not ordinary business keys.
When would you write Objects.hash(...) and when would you hand-write hashCode()?
Tests hashCode performance, allocation, caching
Show answerHide answer
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
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.
What if all fields are primitives?
Use the wrapper hash helpers, such as
Long.hashCode(id)andBoolean.hashCode(active), then combine them. Avoid boxing in hot code.