← Back to list

The Dangerous Thing About “Basic” Java Questions

The question sounds familiar. The follow-up is where most interviews change completely.

Sagar Yadav in Javarevisited · 2026-06-02 16:00 · 1 claps · 7.0 min read
#java #interview #interview-questions #oops-concepts #concurrency
Open on Medium ↗

The Dangerous Thing About “Basic” Java Questions

The question sounds familiar. The follow-up is where most interviews change completely.

Every Java developer has seen these questions before.

“What is Singleton?” “How does HashMap work?” “Why are Strings immutable?”

At first, they sound almost insulting. You’ve worked on production systems. Handled outages. Written microservices. Deployed to Kubernetes. And now someone is asking you what a HashMap is.

But here’s the trap.

Credits: Pexels

Credits: Pexels

“Explain Singleton.”

Most developers answer this in under twenty seconds.

“Singleton ensures only one instance of a class exists.”

Done. Next question.

Except the follow-up questions haven’t started yet.

public class Singleton {
    private static Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

Is this thread-safe? No. Two threads can both see instance == null simultaneously and both create instances. So you add synchronized. But now every call to getInstance() acquires a lock — even after the instance exists — which is unnecessary overhead.

So you try double-checked locking:

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
    }
    return instance;
}

Still broken without volatile. Here's why: the JVM can reorder instructions. When instance = new Singleton() executes, three things happen — allocate memory, initialize the object, assign the reference. The JVM is allowed to reorder that to: allocate memory, assign the reference, initialize the object. Now a second thread can read a non-null instance that isn't fully initialized yet. Adding volatile on the field prevents this reordering.

Then comes the next question: what happens during serialization? If your Singleton implements Serializable, deserializing it creates a new instance — bypassing your constructor entirely. You need to implement readResolve() to return the existing instance.

And then: can reflection break it? Yes. Constructor.setAccessible(true) can bypass private constructors entirely. The only Singleton implementation that's truly reflection-safe is the enum-based one:

public enum Singleton {
    INSTANCE;
}

The enum approach is thread-safe by the JVM spec, serialization-safe, and reflection-safe. Most developers who’ve only read about Singleton don’t know why. Most developers who’ve debugged a Singleton in a concurrent system do.

The final turn: would you even use Singleton directly in a Spring Boot application? Spring manages bean lifecycle, and @Component or @Bean with default scope gives you a singleton managed by the container — without any of the above problems. In Spring Boot, reaching for a hand-rolled Singleton often signals someone who hasn't fully understood the framework they're using.

“How Does HashMap Work?”

This question has ended more “strong Java resumes” than most people would admit.

The standard answer — “stores key-value pairs using hashing” — is the warm-up.

The real discussion starts when you go into what actually happens:

map.put("key", "value");

HashMap uses the key’s hashCode() to decide which bucket the entry should go into. If multiple keys land in the same bucket (collision), Java stores them together using a linked list or a balanced tree for faster lookup.

During get(), HashMap computes the hash again, finds the correct bucket, and then uses equals() to identify the exact key inside that bucket.

The load factor and resize threshold matter here. The default load factor is 0.75 — when the map is 75% full, it resizes to a new array roughly double the size and rehashes every entry.Now comes the production angle. What if your hashCode() implementation returns a constant?

@Override
public int hashCode() {
    return 42; // please don't do this
}

Every key lands in the same bucket. Your O(1) lookup quietly becomes O(n). Under Java 8's tree conversion, it eventually becomes O(log n), but you're now paying tree overhead for every operation on what was supposed to be a hash map. HashMap itself doesn't throw exceptions, it just gets progressively slower as more entries pile into the same bucket.

The mutable key problem is subtler. If you use an object as a key and mutate the fields that contribute to its hashCode() after insertion, the entry becomes unreachable. The key is stored in a bucket based on its original hash. After mutation, hashCode() returns a different value, pointing to a different bucket. The entry is still in the map — you just can't find it through normal operations. Memory leak with no error message.

The concurrent access problem: HashMap is not thread-safe. Concurrent puts can cause an infinite loop in Java 6 and earlier (during resize, the linked list can form a cycle). In Java 8+ the behavior is "just" undefined — you might lose data, see stale reads, or get a ConcurrentModificationException. If you need concurrent access, ConcurrentHashMap uses segment-level locking (Java 7) or compare-and-swap operations (Java 8+) to provide thread safety without locking the entire map.

“Why Are Strings Immutable?”

The textbook answer covers security and thread safety. Both are correct. Neither is complete.

The deeper reason String immutability matters is the String Pool. The JVM maintains a pool of String literals. When you write:

String a = "hello";
String b = "hello";

Both a and b point to the same object in the pool. This is possible only because Strings are immutable — if one variable could change the value, it would change it for everyone pointing to that object.

This also enables safe use of Strings as HashMap keys. Their hashCode can be cached on first computation and reused forever, because the underlying content can never change.

The concatenation problem is where immutability has a real performance cost:

String result = "";
for (int i = 0; i < 10000; i++) {
    result = result + i;
}

Each + operation creates a new String object. After 10,000 iterations, you've created 10,000 intermediate String objects, most of which immediately become garbage. The JVM's garbage collector will handle them, but the allocation pressure is real.

StringBuilder solves this by maintaining a mutable character array internally. The compiler actually converts simple string concatenation to StringBuilder operations automatically — but not inside loops, which is why this pattern still causes problems.

The concurrency angle: immutable objects are inherently thread-safe. No synchronization needed, no visibility problems, no possibility of one thread seeing a partially-constructed state. This is why effective Java recommends preferring immutable objects wherever possible, and why the JVM itself makes String immutable rather than leaving it to developers.

“equals() and hashCode() — Why Do You Always Override Both?”

Almost everybody knows the rule: override both or override neither. Far fewer understand what breaks when they don’t.

The contract: if two objects are equal according to equals(), they must have the same hashCode(). The reverse is not required — two objects can have the same hash without being equal (collision is expected). But the forward direction is mandatory.

Here’s what breaks when you override equals() without hashCode():

Set<Employee> set = new HashSet<>();
Employee e1 = new Employee("Alice", 1);
set.add(e1);
Employee e2 = new Employee("Alice", 1);
System.out.println(set.contains(e2)); // false — despite equals() returning true

HashSet uses hashCode() to find the right bucket, then uses equals() to confirm identity. If e1 and e2 are equal but have different hash codes (because you inherited Object.hashCode()), they land in different buckets. The contains() check looks in the wrong bucket and finds nothing.

The mutable key problem extends directly from here. If you use an object as a HashMap key and then mutate the fields that contribute to hashCode(), you've effectively lost the entry in the map.

JPA entities make this interesting. The default behavior for JPA entities inherits Object.equals() and Object.hashCode(), which use object identity (memory address). This means two different instances representing the same database row are not equal. In a Set<Entity>, you can end up with duplicate entries representing the same row. Most JPA documentation recommends using the database ID for equality, but only after it's been assigned — null IDs during persist before flush create their own edge cases.

“volatile vs synchronized — What’s Actually the Difference?”

The first-level answer is easy: volatile guarantees visibility, synchronized guarantees both visibility and atomicity.

The places where this distinction breaks things:

private volatile int count = 0;
public void increment() {
    count++; // still not thread-safe
}

count++ is not atomic. It's a read-modify-write operation: read the current value, add one, write the new value. volatile ensures every read sees the latest written value, but two threads can both read the same value, both compute the same incremented result, and both write it — losing one increment.

Atomicity requires either synchronized or AtomicInteger.

Instruction reordering is the subtler problem. The JVM and CPU are allowed to reorder instructions for performance, as long as single-threaded semantics are preserved. volatile creates a memory barrier — it prevents reordering across the volatile read/write. This is why the double-checked locking pattern requires volatile on the Singleton instance field, and why removing it creates a race condition that's nearly impossible to reproduce in testing but real in production.

synchronized blocks also establish happens-before relationships. Everything that happens before a thread releases a lock is visible to any thread that subsequently acquires the same lock. This is the formal guarantee that makes synchronized code predictable.

The practical question: when do you choose volatile over synchronized? Use volatile for simple flags or single-variable state that's written by one thread and read by many, where the operation is a simple assignment (not read-modify-write). Use synchronized when you need atomicity across multiple operations or read-modify-write on the same variable. Use AtomicInteger, AtomicReference, and friends from java.util.concurrent.atomic when you need atomic operations without the overhead of synchronized.

The Pattern Underneath All of It

These questions don’t look dangerous. That’s what makes them dangerous.

The interviewer already knew you could define Singleton. The follow-up questions are checking whether you understand what breaks, why it breaks, and what the production consequences are when it does. Whether your understanding of HashMap goes beyond O(1) average case to the conditions under which that guarantee disappears. Whether you know why Strings were designed the way they were, not just that they were. Whether you understand concurrency at the level of what the JVM actually guarantees, not just what the keyword implies.

That’s the real test. Not definition recall. Depth under pressure.

If you want to practice these kinds of questions in a structured setting — both sides of the table — PracHub is built specifically to make technical interview practice more realistic and transparent.

Part of a series on Java development, production engineering, and the interview questions that reveal how engineers actually think. Earlier posts cover JVM internals, Spring Boot production behavior, Hibernate traps, and concurrency problems that only appear under load.


메타데이터
post_id
5c497b872faf
slug
the-dangerous-thing-about-basic-java-questions-5c497b872faf
url
https://medium.com/javarevisited/the-dangerous-thing-about-basic-java-questions-5c497b872faf
canonical_url
https://medium.com/javarevisited/the-dangerous-thing-about-basic-java-questions-5c497b872faf
author_url
https://medium.com/@sagaryadav733
status
ok
fetched_at
2026-06-23 17:05:31