1. What will be the output of this code?
List<String> list = new ArrayList<>(List.of("A", "B", "C"));
for (String s : list) {
if (s.equals("B")) {
list.remove(s);
}
}
Output: ConcurrentModificationException is thrown.
The enhanced for-loop (the for (String s : list) syntax) is sugar that hides an Iterator underneath. When the loop starts, it grabs an iterator over the list. That iterator keeps track of how many times the list has been structurally modified — the modCount.
Removing an element changes the list’s structure. When you call list.remove(s) directly on the ArrayList, it bumps the list’s modCount. But the iterator’s own record of the modification count is now out of date. The next time the iterator checks — which happens on the very next iteration, when it asks “is there another element?” — it sees that the list was modified behind its back and throws ConcurrentModificationException.
The fix is to remove through the iterator itself, which keeps the bookkeeping in sync:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("B")) {
it.remove();
}
}
Or, more modernly, use list.removeIf(s -> s.equals("B")). Both work because the removal goes through the same code that updates the iterator’s state. The interview lesson: modifying a collection’s structure directly while iterating over it — with an enhanced for-loop or an iterator — triggers the fail-fast protection.
2. What is the outcome of compiling and running this program?
public class PassByValue {
static void update(StringBuilder sb) {
sb.append(" World");
sb = new StringBuilder("Java");
}
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Hello");
update(sb);
System.out.println(sb);
}
}
Output: Hello World
Java is strictly pass-by-value. That statement confuses people, so let’s be precise about what it means for objects.
When you pass sb to update(), Java copies the reference value into the parameter. Both the caller’s sb and the method’s parameter sb now point to the same StringBuilder object in memory. There is only one object; there are two references to it.
Because both references point to the same object, calling sb.append(" World") inside the method mutates that shared object. The caller sees the change, because it is looking at the same object — hence Hello World.
The second line is the trick. sb = new StringBuilder("Java") does not change the caller’s reference. It just reassigns the method’s local copy of the reference to a brand-new object. The caller’s sb still points to the original object, now containing Hello World. The new Java StringBuilder is orphaned and gets garbage-collected.
The classic summary: Java passes copies of references for objects. You can modify the object the reference points to, but you cannot swap the caller’s reference for a different object from inside the method. Reassigning the parameter only affects the local copy.
3. Which statement accurately describes the memory region Metaspace in Java 8+?
Answer: Metaspace lives in native memory outside the JVM heap and expands dynamically by default, up to available system memory.
Before Java 8, class metadata — the information about your classes, methods, and fields that the JVM needs to run — was stored in a heap region called PermGen. PermGen had a fixed default maximum size, and the classic failure mode was OutOfMemoryError: PermGen space, when an application loaded more classes than that fixed cap allowed.
Java 8 replaced PermGen with Metaspace. The important differences are where it lives and how it grows:
- Metaspace uses native memory (off-heap), not the JVM heap. User-created object instances still live on the heap; class metadata now lives outside it.
- By default, Metaspace has no fixed maximum — it grows dynamically as needed, bounded only by the available system memory. That eliminates the PermGen-out-of-space failure for ordinary applications.
The fact that Metaspace is native memory also matters for sizing. Because it is off-heap, it does not compete with the heap for the same space — a large heap and a large Metaspace can coexist. If you do want to cap it, you control it explicitly with flags like -XX:MaxMetaspaceSize.
For interviews, the key points: PermGen → Metaspace in Java 8, Metaspace is off-heap native memory, and it grows dynamically rather than hitting a fixed default cap.
4. What does the volatile keyword guarantee when applied to a variable?
Answer: It forces reads and writes to go directly to main memory, establishing visibility and ordering guarantees across threads.
Every CPU has its own cache. When a thread reads and writes a variable, the value can sit in that thread’s cache instead of main memory. Another thread, on another CPU, might keep reading a stale copy — it never sees the update. This is the classic visibility problem of multithreading.
Declaring a variable volatile solves visibility. The JVM is told that this variable is shared, so every read of a volatile variable reads from main memory, and every write goes straight to main memory. No thread is allowed to serve a stale cached copy. If thread A writes a volatile variable, thread B is guaranteed to see the new value on its next read.
volatile also provides an ordering guarantee: it establishes a happens-before relationship. Writes to a volatile variable happen-before subsequent reads of it by other threads, which means any other memory writes made before the volatile write are also visible afterward.
But volatile does not make compound operations atomic. A statement like count++ is really three steps — read, add one, write back. Two threads can interleave those steps and lose updates even with volatile. For that you need synchronization or atomic classes like AtomicInteger. volatile is for flags and simple shared state that are only read and written, never modified in place.
5. What will be the output of this code?
System.out.println((-10 >> 2) + " " + (-10 >>> 2));
Output: -3 1073741821
This question distinguishes the two right-shift operators by how they treat the sign bit.
>> is the signed (arithmetic) right shift. It shifts bits right and fills the new leftmost bits with the sign bit. For a negative number, the sign bit is 1, so 1s are shifted in from the left — the number stays negative. Shifting -10 right by 2 positions keeps the sign, and the result is -3. Mathematically, it is effectively division by 4 (toward negative infinity).
>>> is the unsigned (logical) right shift. It shifts bits right and always fills the new leftmost bits with 0, regardless of sign. For -10, which in two’s complement is a huge bit pattern starting with 1s, shifting in zeros from the left converts it into a large positive number: 1073741821. The sign bit is treated like any other bit — the number stops being negative.
The practical takeaway: use >> when you want to preserve the sign (like dividing a possibly-negative value), and >>> when you want to treat the value as purely positive bits. When you see an odd, huge positive number in a bit-shift output, >>> is almost always the explanation.
6. What is the compile-time result of this code?
List<Number> list = new ArrayList<Integer>();
Answer: Compilation error — ArrayList<Integer> cannot be converted to List<Number>.
The surprise is that this fails, because it feels like it should work. Integer extends Number, so intuitively ArrayList<Integer> should be a List<Number>. It isn’t — and the reason is that generics are invariant.
Invariance means List<Integer> is not a subtype of List<Number>, even though Integer is a subtype of Number. The type parameter is treated exactly, not polymorphically.
Why does Java make this choice? Because it protects you at compile time. If ArrayList<Integer> were assignable to List<Number>, then this would compile:
List<Number> numbers = new ArrayList<Integer>(); // if allowed
numbers.add(3.14); // a Double into a list of Integers
That would let a Double slip into a list the rest of the code believes holds only Integers, and the type safety would be broken. Invariance prevents this whole class of bugs by rejecting the assignment outright.
If you genuinely need subtyping flexibility with generics, you use wildcards. List<? extends Number> means “a list of some type that is a subtype of Number,” and it can safely hold an ArrayList<Integer>. The trade-off is that you cannot add elements to a ? extends collection, because the exact element type is unknown.
7. What will be the output of the following program?
public class Outer {
private int x = 10;
class Inner {
private int x = 20;
void show() {
System.out.println(Outer.this.x);
}
}
public static void main(String[] args) {
Outer.Inner in = new Outer().new Inner();
in.show();
}
}
Output: 10
This is a question about variable shadowing inside nested classes. Both the outer class and the inner class declare a field named x. The inner class’s x shadows the outer’s within the inner class — a plain reference to x inside Inner would get 20.
The key here is the explicit qualification Outer.this.x. Inside an inner class, Outer.this is the special reference that points to the enclosing instance of the outer class. Writing Outer.this.x means “the x field of the outer object” — bypassing the shadow — so it reads the outer’s value, which is 10.
If the code had simply printed x, the answer would have been 20, because the inner class’s own field takes precedence. The entire question hinges on noticing the Outer.this prefix and knowing that it reaches through the shadow to the outer instance.
The construction line is worth understanding too: new Outer().new Inner() first creates an outer instance, then creates an inner instance tied to it. Inner classes always hold a reference to their enclosing instance, which is exactly what makes Outer.this meaningful. Without an outer instance, you cannot even create a non-static inner class.
8. What is the result of executing this snippet?
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.remove(1);
System.out.println(list);
Output: [1]
The trick is that List has two overloaded remove methods, and Java’s overload resolution picks a surprising one for an int.
The two overloads are remove(int index) — which removes the element at a position — and remove(Object o) — which removes the element by value. The argument here is the literal 1, an int. Java prefers the remove(int) overload, because an int matches int index exactly, while matching Object would require autoboxing 1 into an Integer.
So list.remove(1) removes the element at index 1, not the element whose value is 1. The list is [1, 2]; index 0 holds 1, index 1 holds 2. Removing index 1 removes the 2, leaving [1].
To remove the value 1 instead, you would have to box it explicitly: list.remove(Integer.valueOf(1)), or use Integer directly. Then Java picks the Object overload and removes the element equal to 1.
The interview point: remove(int) is index-based; if you want value-based removal of a number, you must pass an Integer so the Object overload wins.
9. Which statement is TRUE regarding interface default methods in Java 8+?
Answer: Default methods let you add new functionality to interfaces while preserving backward compatibility for existing implementing classes.
Before Java 8, interfaces could only declare method signatures. Adding a new method to an interface broke every class that implemented it, because those classes would no longer implement the full contract. For a widely used interface like Collection, that made evolution nearly impossible.
Default methods solved this. A default method has a body written right in the interface:
interface Greeter {
default String greet() {
return "Hello";
}
}
Existing implementing classes instantly inherit this implementation — they keep compiling without any changes. New behavior is added without breaking old code. This is exactly how Java 8 added .stream() to Collection without forcing every existing collection class to be rewritten.
There are important limits. A default method cannot override equals, hashCode, or toString from java.lang.Object — those are reserved for the classes themselves. Interfaces also cannot hold instance state fields; the default method body has no fields to work with beyond constants. And a default method is not mandatory — an implementing class can override it, or ignore it and use the provided body.
The interview takeaway: default methods are the mechanism for interface evolution — add methods to a published interface without breaking implementers.
10. What is the output of this snippet?
String s1 = "a" + "b" + "c";
String s2 = "abc";
System.out.println(s1 == s2);
Output: true
The critical detail is that "a" + "b" + "c" is a compile-time constant expression — all three operands are string literals. The Java compiler evaluates the concatenation during compilation, not at runtime, and folds it into the single literal "abc".
So the compiled bytecode for s1 is effectively s1 = "abc" — identical to s2’s literal. Because both are literals with the same value, they both resolve to the same interned object in the String Constant Pool. Since == on objects compares references, and both references point to that one pooled object, the result is true.
This is different from what happens when at least one operand is not a constant. If the concatenation involved a variable, the compiler cannot fold it — it would build the string at runtime with a StringBuilder, producing a new object, and the == comparison would be false.
String s = "a";
String s3 = s + "b" + "c"; // runtime concat → new object
System.out.println(s3 == "abc"); // false
The interview lesson: constant string expressions are folded and interned at compile time; runtime concatenations are not. == on strings is reliable only when you are sure both sides are compile-time constants pointing at the same pool entry.
11. What happens when a thread calls Thread.interrupt() while sleeping?
Answer: InterruptedException is thrown, and the thread’s interrupted status flag is cleared to false.
Thread.sleep() is a blocking call that responds to interruption. When another thread calls interrupt() on a sleeping thread, the JVM wakes that thread up and throws InterruptedException from within the sleep() call.
Two things happen in that moment. First, the exception propagates, and the thread exits the sleep and resumes running its exception handler. Second, and this is the part interviews love to test, the interrupt flag — the boolean that records “this thread was asked to stop” — is cleared to false as part of the interruption.
That clearing matters for how you write interruption handling. A common pattern is:
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // re-set the flag
return;
}
Because the exception handler sees the flag already cleared, good practice is to restore it by calling interrupt() again, so that any outer code checking Thread.interrupted() or isInterrupted() can still see that an interruption occurred.
The interview point: interruption of a sleeping thread surfaces as InterruptedException, and the interrupt status is automatically reset to false in the process.
12. What will be printed?
public class Base {
public static void main(String[] args) {
Base b = new Sub();
b.greet();
}
private void greet() { System.out.println("Base"); }
}
class Sub extends Base {
public void greet() { System.out.println("Sub"); }
}
Output: Base
The key rule is that private methods are not inherited and cannot be overridden. They are invisible to subclasses, even though the Sub class here declares a method with the same name and signature.
Because greet() in Base is private, it does not participate in polymorphism. The method in Sub is a completely separate, unrelated method — it happens to share a name, but Java does not treat it as an override. A private method in the superclass is simply not part of the subclass’s interface.
Resolution for private methods happens at compile time, using the reference type (static binding). The variable b is declared as type Base, so the compiler binds b.greet() to Base.greet(). The fact that b actually holds a Sub object is irrelevant, because there is no virtual dispatch for a private method.
That is why the output is Base. Had greet() been public (or protected), it would have been overridable, resolved at runtime by the actual object type, and Sub’s version would have printed.
13. What does CompletableFuture.supplyAsync() use by default when no explicit executor is provided?
Answer: It uses the shared ForkJoinPool.commonPool().
CompletableFuture.supplyAsync(Supplier) is a convenience for running a task asynchronously. When you provide only the supplier, the framework still needs somewhere to run it — so it falls back to a default.
That default is the common ForkJoinPool, a process-wide pool shared by all async operations in the JVM. It is the same pool used by parallel streams and other parallel operations. It is sized by default to the number of processor cores, making it efficient for CPU-bound tasks.
There is an overload that takes an explicit executor: supplyAsync(supplier, executor). Passing your own Executor — say a dedicated thread pool sized for your workload — gives you control over thread count, naming, and isolation. That is the recommended approach when your tasks are I/O-bound (which block and tie up pool threads) or when you do not want one long task starving the shared pool.
The interview point: omit the executor, and the task runs on ForkJoinPool.commonPool(). Provide one, and your pool is used instead.
14. Which condition must be met for a custom class to be used in a try-with-resources statement?
Answer: It must implement java.lang.AutoCloseable (or its sub-interface java.io.Closeable).
The try-with-resources statement is the modern way to manage resources that need closing:
try (MyResource r = new MyResource()) {
r.use();
}
The compiler needs to know how to close the resource when the block exits — whether normally or via an exception. The contract that makes that possible is AutoCloseable, which declares the single method void close(). Any class implementing it can be used as a resource in try-with-resources, and the compiler generates the code to call close() automatically.
Closeable is a sub-interface of AutoCloseable, specialized for I/O streams. It throws IOException and is idempotent-friendly, but for the try-with-resources requirement, implementing either one is sufficient.
What you do not need: the class need not extend InputStream, need not be Serializable, and needs no particular constructor visibility beyond what you need to create it. The single condition is the AutoCloseable/Closeable contract, so the compiler can guarantee cleanup.
15. What is the time complexity of basic operations (add, remove, contains) in a TreeSet?
Answer: O(log n).
The answer comes from what is underneath. TreeSet is backed by a TreeMap, and a TreeMap is implemented as a self-balancing Red-Black tree.
The tree keeps elements in sorted order by arranging them in a structure where, from any node, you go left for smaller values and right for larger ones. Because the tree is self-balancing, its height stays proportional to log n rather than degenerating into a long chain. Searching for an element therefore discards roughly half the remaining elements at each level, giving O(log n) for add, remove, and contains.
Compare that to a HashSet, which is backed by a HashMap. A HashSet gives O(1) average for the same operations, because it uses hashing to jump straight to a bucket. But the trade-off is ordering: a HashSet iterates in no particular order, while a TreeSet always iterates in sorted order and offers sorted-related operations like first(), last(), and range views.
The interview answer: choose TreeSet when you need sorted iteration and are happy with O(log n) operations; choose HashSet when speed matters more and order is irrelevant.
Premium Content
Unlock Top 25 - Part 1 and all premium lessons with a subscription.
From ₹199.99/year — See plans