Skip to main content

Command Palette

Search for a command to run...

Series of Questions in Backend Developer Interviews Part 3

Modern backend interviews are never mere quizzes; they probe how deeply a developer understands mechanisms beneath familiar APIs. The following sections walk through five classic but deceptively difficult questions. We will explore them with narr...

Published
6 min read
Series of Questions in Backend Developer Interviews Part 3
T

I am Tuanh.net. As of 2024, I have accumulated 8 years of experience in backend programming. I am delighted to connect and share my knowledge with everyone.

1. Deep Copy vs Shallow Copy – What Really Happens?

Developers often say “shallow copy copies reference, deep copy copies data,” but this shortcut hides the subtlety that explains bugs. Shallow copying is like duplicating a house key rather than building another house—both keys open the same door, so changing one interior affects both “owners”. A deep copy, on the other hand, builds a new house, furniture included.

Consider a simple Java model:

class Address {
String city;
Address(String city) { this.city = city; }
}

class Person implements Cloneable {
String name;
Address address;

Person(String name, Address address) {
this.name = name;
this.address = address;
}

@Override
protected Person clone() throws CloneNotSupportedException {
return (Person) super.clone(); // shallow copy
}
}

If you clone and mutate:

Person p1 = new Person("Alice", new Address("Paris"));
Person p2 = p1.clone();
p2.address.city = "London";
System.out.println(p1.address.city); // prints "London"

Both persons now mysteriously moved to London. Why? Because shallow copy duplicated only object references.

Deep copy explicitly creates new inner objects:

@Override
protected Person clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.address = new Address(this.address.city);
return cloned;
}

Suddenly, mutations become isolated.Future interview follow-ups often ask: “When is deep copying overkill?” The answer lies in cost—deep copies recursively clone object graphs, impacting latency, memory overhead, and GC pressure. Shallow copies are cheap but dangerous in mutable models, especially in caching and persistence layers.

Illustration idea: show two stick figures sharing one house (shallow) vs two houses on separate land plots (deep).

2. Why is HashMap Not Thread-Safe? How Does ConcurrentHashMap Solve It?

HashMap is a performance beast in single-threaded cases but behaves like a toddler left with Sharpies in a white living room when exposed to concurrency. Two issues arise: structural mutation races and infinite rehashing loops. When multiple threads resize the table, circular linked lists can form, leading to 100% CPU infinite loops. HashMap makes no concurrency guarantees because it trades safety for raw throughput.

Enter ConcurrentHashMap—not merely “a synchronized HashMap” but a reengineered structure. In earlier Java versions, it used segmentation (lock striping); in modern versions it employs CAS (Compare-And-Set) operations, volatile memory barriers, and fine-grained locking on bucket chains. Reads are mostly lock-free, writes are minimally locked, preventing global lock contention.

ConcurrentHashMap<string, integer=""> map = new ConcurrentHashMap<>();
map.put("A", 1);
map.computeIfAbsent("B", k -> 2);
</string,>

Unlike synchronized wrappers, ConcurrentHashMap exposes atomic functions like compute, putIfAbsent, and merge, enabling safe composite operations without external locks.

If you’ve ever chased a “random infinite loop in production” caused by HashMap under concurrency, you understand why interviewers value this question—it reveals whether you grasp memory races beyond buzzwords.

3. What Rules Must equals() and hashCode() Satisfy?

A surprising number of senior candidates still fail this one, mostly because equals/hashCode violations rarely explode loudly—they silently corrupt HashMaps and sets.

The contract:

  1. If two objects are equal (equals() returns true), their hashCode() must be identical.
  2. hashCode() must be stable during an object’s lifetime inside a hash map.
  3. Equal objects must remain equal across repeated comparisons.

Breaking these rules leads to missing entries, ghost duplicates, or unremovable elements.

Example:

class User {
String email;
User(String email) { this.email = email; }

@Override
public boolean equals(Object o) {
return o instanceof User && ((User) o).email.equals(this.email);
}
// Missing hashCode override — violation!
}

HashMap stores based on hashCode then checks equality. If hashCode() differs for identical emails, lookup fails even though equality is satisfied. This problem becomes dramatic when keys are mutable—changing fields modifies their hash bucket location, meaning the map “loses” them forever.

Best practice?Make keys immutable (like String), derive hashCode from stable fields, and always override equals and hashCode together.

You may imagine HashMap as a post office: equals() decides which letter is yours, hashCode() decides which mailbox it gets stored in. If your address changes mid-delivery, chaos ensues.

4. Why Must We Be Careful When Overriding compareTo()?

compareTo() defines natural ordering. But a broken compareTo introduces either inconsistent sorting or runtime exceptions buried deep inside trees like TreeMap or PriorityQueue.

The hidden rule many ignore: compareTo() must agree with equals(). If a.compareTo(b) == 0 but a.equals(b) returns false, sorted structures behave unpredictably.

Example of catastrophe:

class Version implements Comparable<version> {
int major;
Version(int major) { this.major = major; }

@Override
public int compareTo(Version o) {
return this.major - o.major; // naive but okay
}

@Override
public boolean equals(Object o) {
return false; // intentionally wrong for illustration
}
}
</version>

TreeSet refuses to store “duplicate equals() objects” but compareTo() thinks duplicates exist, so one entry gets silently dropped.

Worse yet, comparator logic must be transitive (A>B, B>C, then A>C). Breaking this can send sorting algorithms into infinite loops or cause unstable ordering—imagine trying to sort socks where two socks insist on mutually contradictory rankings.

5. Why Was finalize() Removed?

finalize() was meant to clean up non-managed resources, but it grew into a horror film monster nobody could predict. JVM scheduling is non-deterministic—finalizers may never execute, execute twice, execute after object resurrection, or delay GC cycles dramatically. Worse, attackers exploited finalizers to resurrect objects, bypassing security invariants.

Instead, Java moved toward:

  • try-with-resources
  • AutoCloseable
  • reference queues
  • cleaners (java.lang.ref.Cleaner)

These mechanisms are explicit, deterministic, and safe. Finalization was deprecated and retired because it violated reliability principles interviewers cherish: predictability, GC friendliness, and secure lifecycle management.

If finalize() were a co-worker in a tech team, it would be the guy who sometimes shows up late, sometimes duplicates work, occasionally revives deleted tickets, and occasionally decides not to turn up at all—not exactly the colleague you build mission-critical pipelines around.

Conclusion

This set of backend interview questions hides surprisingly deep reasoning beneath short phrases. They test how much you understand memory semantics, API contracts, concurrency pitfalls, and lifecycle management.If you want me to expand this series into more questions—or rewrite as PDF, slides, narrative voiceover, or animated diagrams—comment below what you want next and we’ll architect it together.

Read more at : Series of Questions in Backend Developer Interviews Part 3

More from this blog

T

tuanh.net

540 posts

Are you ready to elevate your Java, OOP, Spring, and DevOps skills? Look no further!