1. What will be printed?
public class ScopeTest {
static int x = 10;
public static void main(String[] args) {
int x = 20;
System.out.println(x);
}
}
Output: 20
Two variables are named x here, but they live in different scopes — and the rule is that the inner declaration wins.
There is a static field x = 10, owned by the class. Inside main there is a local variable x = 20. When a local variable has the same name as a field, it shadows the field within its block — it hides it. Inside main, the simple name x refers to the local variable, not the static field.
So System.out.println(x) prints 20.
This compiles fine — shadowing is legal. The distinction is only a source of confusion for readers. If the code wanted the field, it would need to qualify it as ScopeTest.x. The interview point: local variables shadow fields of the same name within their scope.
2. Which interface does TreeSet rely on to maintain sorted order when elements do NOT implement Comparable?
Answer: java.util.Comparator.
A TreeSet keeps its elements sorted at all times. But “sorted” is meaningless without a definition of ordering — some way to say which of two elements comes first. TreeSet gets that ordering from one of two sources:
- Natural ordering — the elements implement
Comparable(for example,Stringand the boxed number types do), andcompareTodefines the order. - A supplied
Comparator— when the elements don’t implementComparable(or you want a different order than natural), you pass aComparatorto theTreeSetconstructor. It then uses that comparator for every insertion, removal, and lookup.
So the answer is Comparator. It’s the mechanism of last resort and of customization: without Comparable on the elements and without a Comparator in the constructor, a TreeSet will throw a ClassCastException the moment it tries to insert an element, because it has no way to order anything.
3. What will be printed?
public class ArrayTest {
public static void main(String[] args) {
int[] arr = new int[5];
System.out.println(arr[0]);
}
}
Output: 0
The trap here is assuming that array elements start out uninitialized or hold garbage. In Java they don’t.
When you allocate an array with new int[5], the JVM initializes every slot to the default value for the element type. For int that default is 0. So arr[0] — and every other slot — holds 0 immediately.
The defaults are type-specific: numeric primitives (int, long, double, …) get 0; boolean gets false; char gets the null character; and references to objects get null.
For int, then, the output is simply 0.
The lesson: Java arrays are always initialized, never left with garbage values — unlike some other languages where reading an uninitialized slot is undefined behavior.
4. What happens when ReentrantLock is acquired multiple times by the same holding thread?
Answer: The lock’s hold count increments by 1 for each acquisition, and execution continues seamlessly. It’s reentrant.
A lock is reentrant if the thread that holds it can acquire it again without deadlocking on itself. ReentrantLock implements exactly that.
Internally the lock tracks a hold count. When the same thread acquires the lock it already holds, the count goes from 1 to 2, and so on — each nested acquisition just bumps the count. The thread proceeds; there is no deadlock, no exception.
The key discipline that makes this work: each lock() must be balanced by a matching unlock(). When the thread has acquired the lock three times, it must unlock it three times before the lock is actually released to other threads. The count has to return to zero.
This reentrancy mirrors synchronized, which is also reentrant — a synchronized method calling another synchronized method on the same object is perfectly legal. The interview point: ReentrantLock is reentrant via a hold count, and balanced acquire/release pairs are required.
5. What is the effect of declaring a parameter final in a method signature?
Answer: It prevents reassigning the parameter variable to another object inside the method body. It does not protect the object’s internals.
final on a parameter is a restriction on the variable, not on the object it references.
The parameter is a local variable initialized with the argument. Marking it final means that variable cannot be reassigned: param = newValue; inside the method body is now a compile-time error. The reference stays pinned to the original argument.
What final does not do is freeze the object. If the argument is mutable, you can still call methods that change its internal state — param.setName(...) is perfectly fine. final only blocks rebinding the reference.
The interview answer: final parameters are non-reassignable references. The referenced object remains fully mutable.
6. What happens if an unhandled checked exception is thrown inside Runnable.run()?
Answer: Compilation error — run() does not declare a throws clause, so checked exceptions must be handled inside it.
This is one of the constraints that shapes how Runnable is used. The interface’s single abstract method is void run(), and its signature includes no throws declaration. When you override run(), you cannot widen that contract: any checked exception thrown by the code inside must be caught or declared within the method — but you can’t declare it on the method itself.
So a checked exception inside run() forces a try/catch (or a wrapping in an unchecked exception). Unchecked exceptions (RuntimeException and its subclasses) are fine — they don’t need to be declared, and an uncaught one will propagate to the thread’s uncaught exception handler.
The practical consequence: Runnable is awkward for code that throws checked exceptions like IOException or InterruptedException. That’s one reason Callable<V> exists — its call() method does declare throws Exception, making it the right tool when you need to propagate checked failures.
The interview answer: Runnable.run() throws nothing, so checked exceptions inside it must be caught locally or wrapped in an unchecked exception — otherwise the code won’t compile.
7. Which keyword is used in Java 17+ to declare a sealed class with restricted inheritance?
Answer: sealed.
A sealed class controls who can extend it — the opposite of an open hierarchy. You declare it with sealed, and you list the allowed subclasses with permits:
public sealed class Shape permits Circle, Square, Triangle { ... }
Only Circle, Square, and Triangle may extend Shape — and each of those must itself be sealed, non-sealed, or final. A sealed class cannot be extended by anything not on the permits list.
Why does this matter? It gives you a closed, exhaustive set of types. Pattern matching (especially with switch expressions) can then reason that all cases are covered, which lets the compiler verify exhaustiveness and removes the need for a default branch.
The interview answer: sealed restricts inheritance; paired with permits to name the allowed subclasses. It’s the tool for closed hierarchies and exhaustive pattern matching.
8. What is the value printed by this reduction stream operation?
int total = Stream.of(1, 2, 3, 4)
.reduce(0, (a, b) -> a + b);
System.out.println(total);
Output: 10
reduce combines all elements of a stream into a single value. The two-argument form takes an identity and a combiner.
The identity is the starting value and the neutral element for the operation. For addition that’s 0. The combiner is a binary function; here (a, b) -> a + b adds two values.
Reduction proceeds by folding: start with 0, combine with 1 → 1; combine with 2 → 3; combine with 3 → 6; combine with 4 → 10. The result is 10.
Two details matter for interviews. First, because an identity is provided, the result is a plain int — no Optional wrapper, no “no elements” case. (The Optional-returning reduce form only exists for the no-identity overload, where an empty stream has no answer.) Second, the identity is a genuine initial value, not an offset — with an identity of 0 and a + combiner, the result is simply the sum.
9. What happens if an Optional contains null and Optional.of(null) is invoked?
Answer: Optional.of(null) throws NullPointerException immediately.
Optional is meant to represent a value that might be absent. But there’s an important asymmetry in its factory methods:
Optional.of(value)requires a non-null value. Passnulland it throwsNullPointerExceptionright away. It asserts “this definitely has a value.”Optional.ofNullable(value)acceptsnull, returningOptional.empty()for it. It is the “maybe” variant.
There is no Optional in existence that contains null as a present value — a present Optional always holds a non-null object. So Optional.of(null) can never produce a useful result; it fails fast.
The rule to remember: if there’s any chance the value can be null, use ofNullable. Use of only when you are certain the value is present — it doubles as a null-check. The interview answer is simply: NullPointerException, because Optional.of rejects null.
10. What will be the output of this pattern matching code (Java 17+)?
Object obj = "Hello";
if (obj instanceof String s) {
System.out.println(s.toUpperCase());
}
Output: HELLO
Before Java 16, instanceof only answered yes or no. If you wanted to use the object as the matched type, you had to cast it yourself:
if (obj instanceof String) {
String s = (String) obj;
...
}
Pattern matching for instanceof removes that boilerplate. The pattern String s does two things at once: it tests whether obj is a String, and — if so — binds obj to the new variable s, which is already typed as String. No explicit cast needed.
Here obj is "Hello", so the test passes, s is bound to the string, and s.toUpperCase() produces "HELLO", which is printed.
The scope of the pattern variable is important: s is in scope only where the pattern is guaranteed to have matched — inside the if block. The code compiles and runs without exception. Output: HELLO.
11. What is the initial default capacity of an ArrayList created via new ArrayList<>()?
Answer: It’s created with an empty array buffer (capacity 0), and the array grows to the default capacity of 10 only on the first element insertion.
ArrayList is backed by an internal array, and it uses lazy initialization. When you write new ArrayList<>(), the constructor points the internal buffer at a shared empty array. No real backing storage of size 10 is allocated at construction time.
The capacity grows to 10 — the default initial capacity — on the first add() call. From there it keeps growing by roughly 1.5× whenever it fills up, copying elements into the larger array.
The distinction is subtle but real, and it’s a classic follow-up: the default capacity is 10, but the initial allocation is empty. new ArrayList<>() doesn’t hand you a 10-slot array; it gives you a lazily-grown structure that starts at 0 and jumps to 10 at first use.
12. What will be printed by this code?
public class StringAppend {
public static void main(String[] args) {
String s = "Hello";
s.concat(" World");
System.out.println(s);
}
}
Output: Hello
The trap is expecting concat to modify the string. Strings are immutable — no method can change the contents of an existing String object.
concat doesn’t mutate s. It builds a brand-new String containing "Hello World" and returns that new object. The original "Hello" is untouched.
The code calls s.concat(" World") but throws the return value away. The reference s still points to the original "Hello" object. So System.out.println(s) prints Hello.
This is the single most important habit with immutable objects: the result of an operation must be captured — s = s.concat(" World") — or the operation is silently discarded. Same principle applies to String.replace, substring, toUpperCase, and friends. The interview answer: Hello, because the new string was created but never assigned back.
13. What is the result of running this block?
public class SwitchTest {
public static void main(String[] args) {
int day = 2;
switch (day) {
case 1: System.out.print("One ");
case 2: System.out.print("Two ");
case 3: System.out.print("Three ");
default: System.out.print("Default");
}
}
}
Output: Two Three Default
Classic switch fall-through. In a traditional switch statement, each case is a jump label, and execution flows downward until it hits a break (or returns). Here there are no break statements at all.
day is 2, so control jumps to case 2, which prints Two . With no break, execution continues straight into case 3, printing Three . Still no break, so it falls into default, printing Default. There are no statements after that, so the switch ends.
The output is Two Three Default.
Fall-through is almost always a bug — which is why Java 14+ introduced the switch expression, where every branch is a value and there’s no fall-through at all. In an expression form, this code’s intent — print one word — would be written with case arms, each an expression, and the accidental cascade disappears.
14. What is guaranteed by the AtomicInteger class?
Answer: Lock-free, thread-safe atomic operations on a single integer, built on hardware Compare-And-Swap (CAS) instructions.
AtomicInteger wraps an int with thread-safety that doesn’t rely on the synchronized keyword or Lock objects. Instead, it uses CAS: a CPU instruction that compares a memory location to an expected value and, only if they match, writes a new value — all in one atomic step. The hardware guarantees the compare-and-swap is indivisible.
Operations like incrementAndGet(), getAndAdd(5), and compareAndSet(expected, updated) run lock-free. Multiple threads can call them concurrently without blocking each other, because each operation is a single atomic primitive — no monitor is held, no thread is parked.
The practical meaning of “guaranteed”: a ++ on a plain int is not atomic (a read-modify-write that can interleave), but incrementAndGet() on an AtomicInteger is, so the count is always correct under contention. The cost is far lower than a lock because there’s no blocking — the trade-off being a modest risk of retry loops under extreme contention.
The interview answer: AtomicInteger gives lock-free, thread-safe atomic mutations via CAS, avoiding the overhead of heavy locks.
15. What is the output of the following operation?
public class TypeCast {
public static void main(String[] args) {
byte b = 120;
b += 10;
System.out.println(b);
}
}
Output: -126
The question is about compound assignment and integer overflow.
First, b += 10 is not the same as b = b + 10. A compound assignment like += implicitly casts the result back to the variable’s type — it’s equivalent to b = (byte) (b + 10). That’s why this compiles: b = b + 10 alone would be a compile error, because b + 10 promotes b to int, and assigning an int to a byte is a lossy narrowing.
Now the arithmetic. 120 + 10 = 130. But byte is an 8-bit signed type, and its range is -128 to 127. 130 is out of range. The cast to byte keeps only the low 8 bits of 130, which — as signed — wraps around: 127, -128, -127, -126… 130 overflows to -126.
The output is -126, the classic example of silent overflow: no exception, no error, just a wrapped value. The lesson is that += hides a narrowing cast, and the onus is on you to know whether the value fits.
Premium Content
Unlock Top 50 - Part 2 and all premium lessons with a subscription.
From ₹199.99/year — See plans