Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Top 25 - Part 2
JAVA

Top 25 - Part 2

Practice the remaining 10 questions from the top 25 Java programming interview questions.

1. What will be printed by this code?

Stream.of("apple", "banana", "cherry")
      .filter(s -> s.length() > 5)
      .peek(s -> System.out.print("1:" + s + " "))
      .filter(s -> s.startsWith("b"))
      .peek(s -> System.out.print("2:" + s + " "))
      .findFirst();

Output: 1:banana 2:banana

This question is about how streams actually process elements, and the answer surprises people who imagine pipelines working like spreadsheets — one whole stage at a time.

Java streams process elements horizontally, not vertically. The stream does not run the first filter over every element, then the second filter over every element. Instead, it takes one element and pushes it through the entire chain before moving to the next. This is called vertical or lazy processing.

The stream reads apple. The first filter asks: is its length greater than 5? No, apple is 5 characters — it fails and is dropped immediately. Nothing is printed for it.

Next, banana. Length 6, so it passes the first filter. The first peek fires, printing 1:banana . Then the second filter asks: does it start with b? Yes. The second peek fires, printing 2:banana . Now the pipeline reaches the terminal operation findFirst().

Here is the second key idea: findFirst() is a short-circuiting operation. The moment it finds a first matching element, the whole pipeline stops. It does not go looking for more. cherry is never examined at all.

That is why the output is exactly 1:banana 2:banana — and why a “wait, where is cherry?” reaction means you understand the mechanics. One element, fully processed, then done.

2. What is the key difference between fail-fast and fail-safe iterators?

Answer: Fail-fast iterators throw ConcurrentModificationException when the collection is structurally modified during iteration. Fail-safe iterators iterate over a snapshot or copy, so they never throw — but they may not reflect the latest changes.

Both are about the same danger: modifying a collection while iterating over it. The two families answer that danger in opposite ways.

Fail-fast iterators — found on ArrayList, HashMap, and other regular collections — watch the collection’s modification counter (modCount). Every structural change bumps it. The iterator records the counter when it starts, and on each step checks whether the collection has moved on. If it has, the iterator concludes that someone modified the collection behind its back and throws ConcurrentModificationException. The behavior is fail-fast because the iterator fails immediately rather than producing unpredictable results.

Fail-safe iterators — found on CopyOnWriteArrayList, ConcurrentHashMap, and other concurrent collections — sidestep the problem entirely by iterating over a snapshot or a stable internal structure. CopyOnWriteArrayList, for example, iterates over the array as it existed when iteration began. Concurrent modifications create a new copy; the iterator keeps walking the old one. No exception, but the iterator does not see changes made after it started.

The trade-off is the natural one: fail-fast gives you protection at the cost of exceptions; fail-safe gives you stability at the cost of not seeing live updates. When you genuinely need to modify a collection during iteration, the modern Java answer is removeIf() or iterating with an explicit Iterator.remove() — both of which stay in sync with the collection.

3. What happens if two distinct keys produce the SAME hash code in a HashMap?

Answer: Both key-value pairs are stored in the same bucket, chained together as a linked list (or a Red-Black tree in heavy-collision cases), and equals() is used to tell the keys apart.

A HashMap works by hashing each key to an array index — the “bucket.” Two keys landing in the same bucket is a collision, and it is completely normal. The map does not panic or overwrite; it stores both entries in that one bucket.

The bucket holds a chain of entries. When a new key arrives, the HashMap computes its hash, jumps to the bucket, and then walks the chain. At each entry it checks equals(): is this new key the same as an existing key? If yes — same key, same hash — the value is replaced. If no — a distinct key that merely collides — the new entry is appended to the chain.

So the two properties work together: hashCode() decides which bucket to search, and equals() decides which entry within the bucket matches. A good hashCode() distributes keys so buckets stay short; a bad one (all keys returning the same hash) piles everything into one bucket, degrading lookup from O(1) to O(n).

When a bucket grows long enough — 8 entries with a table capacity of at least 64 — Java 8 converts that chain into a Red-Black tree to keep lookups at O(log n). But the principle is unchanged: collisions share a bucket, and equals() is the arbiter inside it.

4. What is the value printed by this code?

public class StringTest {
    public static void main(String[] args) {
        String a = "Hello";
        String b = "He" + new String("llo");
        System.out.println(a == b);
    }
}

Output: false

Compare this with the constant-concatenation question from earlier. There, "a" + "b" + "c" was folded at compile time into one pooled literal, and == came out true. Here, the result flips — because the concatenation is not a compile-time constant.

The expression "He" + new String("llo") involves a new expression. The compiler cannot fold that into a literal at compile time; it has to build the result at runtime. At runtime, string concatenation is performed with a StringBuilder (or the equivalent), which produces a brand-new String object on the heap.

So b is a fresh heap object with the content "Hello". Meanwhile a points to the canonical "Hello" in the String Constant Pool — assuming the literal is already there. a and b hold the same characters but are different objects, so a == b compares two different references and returns false.

The general rule to carry into interviews: == on strings is only reliable when you are certain both sides are compile-time constants that resolve to the same pool object. Any runtime construction — new, concatenation with a variable, toString() — produces a distinct object, and == will then be false. Compare content with .equals().

5. Which garbage collector introduced in recent JDKs aims for sub-millisecond maximum pause times on massive heaps?

Answer: ZGC (the Z Garbage Collector).

ZGC is Java’s low-latency garbage collector, designed around one headline goal: keep pause times predictable and below one millisecond, even on enormous heaps spanning from megabytes to terabytes.

The problem with earlier collectors is that they pause application threads to do their work. Even well-tuned collectors like G1 produce pauses that grow as the heap grows, and a multi-second stop-the-world pause on a huge heap is unacceptable for latency-sensitive services.

ZGC attacks this by doing nearly all of its work concurrently — while application threads keep running. It uses techniques like colored pointers and load barriers, which let it mark, relocate, and remap objects without stopping the world for meaningful amounts of time. The result is that pause time stays tiny and roughly constant, regardless of heap size, instead of scaling with it.

For comparison, CMS was the older concurrent collector but has been removed in modern JDKs. ZGC is the modern answer for the same niche: big heaps plus strict latency requirements.

The interview takeaway: when you see “sub-millisecond pauses” and “massive heaps,” the answer is ZGC — the JDK’s concurrency-first, ultra-low-latency collector.

6. What is the purpose of the Producer-Extends, Consumer-Super (PECS) rule in Java generics?

Answer: PECS tells you which wildcard to use: if you only read items from a collection, use ? extends T (producer); if you only write items into a collection, use ? super T (consumer).

The rule exists because wildcards trade away capability in one direction to gain flexibility in another, and using the wrong one causes compile errors that confuse everyone.

? extends T means “some unknown subtype of T.” You can safely read from such a collection — anything you get out is at least a T. But you cannot add to it, because the compiler does not know the exact element type. It could be an Integer list or a Double list, so putting a Number in is not guaranteed safe. That is the “Producer extends” half: if the collection produces values for you to read, use ? extends.

? super T means “some unknown supertype of T.” Here the logic flips. You can safely write a T into it, because whatever the list actually holds is guaranteed to accept a T — a List<Object> certainly accepts an Integer. But you cannot confidently read items as T, because the element could be any supertype. That is the “Consumer super” half: if the collection consumes values you write, use ? super.

A memorable framing: PECS = Producer Extends, Consumer Super. Producers give you things (so they extend), consumers take your things (so they super). Choose the wildcard based on whether you are reading or writing, and the compiler will stop fighting you.

7. What will be the output of this code?

public class Test {
    public static void main(String[] args) {
        int a = 0;
        try {
            a = 1 / 0;
        } catch (ArithmeticException e) {
            a = 2;
        } finally {
            a = 3;
        }
        System.out.println(a);
    }
}

Output: 3

The sequence of execution matters more than the exception itself. Let’s walk it.

Inside the try block, 1 / 0 divides by zero. That throws ArithmeticException, so the assignment a = 1 never happens. Control jumps to the matching catch block, which runs a = 2.

Now the crucial part: after the catch block finishes, the finally block runs — unconditionally. It executes a = 3, overwriting the 2 that the catch block just set.

When the finally block completes, the program continues to the println, printing the current value of a, which is 3.

The lesson is the same one that keeps appearing in exception questions: the finally block always runs after the try and catch, and it executes last. Whatever the try or catch did to a variable, a finally block that writes the same variable wins. If you want the catch value to survive, don’t overwrite it in finally.

8. What distinguishes Virtual Threads (Java 21) from traditional Platform Threads?

Answer: Virtual threads are lightweight user-mode threads managed by the JVM, so thousands of them can run on a handful of OS threads with minimal memory overhead.

The old model, now called platform threads, has one thread per OS thread. Each carries a dedicated stack and costs real memory — around a megabyte per thread, plus OS scheduler overhead. That makes “a thread per request” impractical at scale: a server handling 10,000 concurrent connections would need 10,000 heavyweight threads.

Virtual threads decouple the two. They are managed by the JVM, not the OS. Many virtual threads are multiplexed onto a small pool of platform “carrier” threads. When a virtual thread blocks — on an I/O call, for instance — it is unmounted from its carrier, the carrier picks up another virtual thread, and the blocked one resumes later. Blocking stops costing a thread.

The benefits are immediate for the classic Java server pattern: you can write straightforward blocking code — thread per request — and let thousands of virtual threads run on a handful of platform threads. That is the familiar, easy-to-reason-about style, now scalable.

It is important to be precise about what virtual threads do not change. They do not remove the need for synchronization or locks — shared mutable state still needs care. They are not limited to CPU computations; in fact they shine on blocking I/O. The difference is purely about how threads are scheduled and how cheaply you can have many of them.

9. What will be printed?

public class ClassTest {
    static int count = 0;
    ClassTest() { count++; }
    public static void main(String[] args) {
        ClassTest t1 = new ClassTest();
        ClassTest t2 = new ClassTest();
        ClassTest t3 = null;
        System.out.println(count);
    }
}

Output: 2

The trap is the null reference. t3 is declared as ClassTest, but it is assigned null — it does not point to any object.

Declaring a reference variable and assigning null does not create an object. No new, no constructor call, no allocation. The constructor, which increments count, simply never runs for t3.

The first two lines do create objects. new ClassTest() allocates an instance and calls the constructor, bumping count to 1 for t1 and to 2 for t2.

So only two constructor executions happen, and count is printed as 2. The t3 line is pure noise designed to tempt you into counting three instantiations.

The interview point is simple: new is the only thing that calls a constructor. A null reference is just an empty slot — it costs nothing and constructs nothing.

10. What is the behavior of ConcurrentHashMap regarding locks during write operations in modern Java?

Answer: ConcurrentHashMap avoids a global lock. It uses lock-free CAS (Compare-And-Swap) for inserting initial nodes and synchronizes only on the specific bucket’s head node for updates.

The whole point of ConcurrentHashMap is to be safe for concurrent use without paying the price of locking the entire map on every operation — which is exactly what a naively synchronized HashMap would do.

Modern ConcurrentHashMap (Java 8+) achieves this with a two-tier strategy. For first insertion into an empty bucket, it uses CAS — an atomic, lock-free operation that sets the bucket’s head node if the bucket is still empty. No lock is taken at all; the hardware guarantees the atomicity.

For operations that must modify an existing bucket — inserting when the bucket already has entries, replacing a value, removing a node — the map synchronizes only on that bucket’s head node. The synchronized block is scoped to the single bucket being touched. Other threads can freely read and write other buckets at the same time, because their locks are on different objects.

This is what people mean when they say the map “locks per bucket” rather than globally. The concurrency scales with the number of buckets instead of collapsing to one lock. Reads are effectively lock-free against these writes, and two writers on different buckets proceed in parallel.

The interview takeaway: modern ConcurrentHashMap = CAS for empty-bucket inserts + fine-grained locks on individual bucket head nodes for updates. No global lock, no read blocking.

My Private Notes

Notes are auto-saved locally to this device.