1. What will be the output of the following code?
String s1 = "Java";
String s2 = new String("Java");
String s3 = s2.intern();
System.out.println((s1 == s2) + " " + (s1 == s3));
Output: false true
The trick here is understanding where string objects actually live in memory, because == compares references, not contents.
When you write String s1 = "Java" with a literal, the JVM looks in a special area called the String Constant Pool. If the literal already exists there, s1 points to that pool object. Literals are reused — two literals with the same value always share the same pool object.
When you write new String("Java"), that is different. The new keyword forces the creation of a brand-new object on the heap, even though the value is the same as the pool object. So s1 and s2 are two separate objects that happen to hold the same characters. That is why s1 == s2 is false — their references point to different places.
The intern() method is the bridge between the two worlds. It looks in the String Pool for an object with the same content. If it finds one, it returns that pool reference. Since s1 already lives in the pool, s2.intern() hands back the very same object s1 points to. So s1 == s3 is true.
The lesson for interviews: == on strings checks whether two references point to the same object. To compare actual content, always use .equals(). String literals get pooled automatically, but strings created with new do not — unless you call intern().
Answer:
false true
The trick here is understanding where string objects actually live in memory, because == compares references, not contents.
When you write String s1 = "Java" with a literal, the JVM looks in a special area called the String Constant Pool. If the literal already exists there, s1 points to that pool object. Literals are reused — two literals with the same value always share the same pool object.
When you write new String("Java"), that is different. The new keyword forces the creation of a brand-new object on the heap, even though the value is the same as the pool object. So s1 and s2 are two separate objects that happen to hold the same characters. That is why s1 == s2 is false — their references point to different places.
The intern() method is the bridge between the two worlds. It looks in the String Pool for an object with the same content. If it finds one, it returns that pool reference. Since s1 already lives in the pool, s2.intern() hands back the very same object s1 points to. So s1 == s3 is true.
The lesson for interviews: == on strings checks whether two references point to the same object. To compare actual content, always use .equals(). String literals get pooled automatically, but strings created with new do not — unless you call intern().
2. What is the value of result after executing this code snippet?
int x = 5;
int result = x++ + ++x * x--;
Output: 54
This question tests three things at once: operator precedence, post-increment, and pre-increment. The key is to keep two separate things straight — what the operator evaluates to in the expression, and what the variable becomes afterward.
Let’s work through it one piece at a time. In x++, the post-increment evaluates to the current value of x, which is 5, and then increments x to 6. So the first operand contributes 5.
Next comes ++x. The pre-increment increments first, making x equal to 7, and then evaluates to that new value. So the second operand contributes 7.
Then x--. The post-decrement evaluates to the current value of x, which is 7, and then decrements x back to 6. So the third operand contributes 7.
Now apply precedence. Multiplication binds tighter than addition, so the * happens first: 7 * 7 = 49. Then the addition: 5 + 49 = 54.
A common mistake is to evaluate the increments out of order or to forget that the value a post-increment produces is the old value. If you track each operand’s contribution and the variable’s changes separately, the arithmetic falls out cleanly.
Answer:
54
This question tests three things at once: operator precedence, post-increment, and pre-increment. The key is to keep two separate things straight — what the operator evaluates to in the expression, and what the variable becomes afterward.
Let’s work through it one piece at a time. In x++, the post-increment evaluates to the current value of x, which is 5, and then increments x to 6. So the first operand contributes 5.
Next comes ++x. The pre-increment increments first, making x equal to 7, and then evaluates to that new value. So the second operand contributes 7.
Then x--. The post-decrement evaluates to the current value of x, which is 7, and then decrements x back to 6. So the third operand contributes 7.
Now apply precedence. Multiplication binds tighter than addition, so the * happens first: 7 * 7 = 49. Then the addition: 5 + 49 = 54.
A common mistake is to evaluate the increments out of order or to forget that the value a post-increment produces is the old value. If you track each operand’s contribution and the variable’s changes separately, the arithmetic falls out cleanly.
3. What will happen when compiling and executing the following program?
public class Test {
public static void main(String[] args) {
try {
return;
} finally {
System.out.println("Finally Block");
}
}
}
Output: Finally Block is printed, then the method returns.
The finally block has one ironclad guarantee in Java: it always runs, no matter how the try block exits. Whether the code returns normally, throws an exception, or even calls System.exit() from a nested location that doesn’t kill the JVM, the finally block gets its turn.
In this program, the return statement inside try signals that the method wants to end. But before the method can actually hand control back to the caller, the JVM checks for a finally block. It finds one, executes it — printing Finally Block — and only then completes the return.
So the flow is: the try block hits return, the JVM pauses that return, runs the finally block, and then the return happens. The println executes first.
This guarantee is why finally is the natural home for cleanup work like closing files, releasing database connections, or releasing locks. You can rely on it running regardless of how the surrounding block exits. Interviewers also like to point out that this means code in finally can override an earlier return, which is the trap in the next question.
Answer:
Finally Block is printed, then the method returns.
The finally block has one ironclad guarantee in Java: it always runs, no matter how the try block exits. Whether the code returns normally, throws an exception, or even calls System.exit() from a nested location that doesn’t kill the JVM, the finally block gets its turn.
In this program, the return statement inside try signals that the method wants to end. But before the method can actually hand control back to the caller, the JVM checks for a finally block. It finds one, executes it — printing Finally Block — and only then completes the return.
So the flow is: the try block hits return, the JVM pauses that return, runs the finally block, and then the return happens. The println executes first.
This guarantee is why finally is the natural home for cleanup work like closing files, releasing database connections, or releasing locks. You can rely on it running regardless of how the surrounding block exits. Interviewers also like to point out that this means code in finally can override an earlier return, which is the trap in the next question.
4. What is the output of the following method invocation?
public class Overload {
static void print(Object o) { System.out.print("Object "); }
static void print(String s) { System.out.print("String ");
}
public static void main(String[] args) {
print(null);
}
}
Output: String
When you call an overloaded method, Java has to decide which version to invoke. The rule it follows is called most specific method selection.
Here there are two candidates: print(Object) and print(String). The argument is null, and here is the subtlety — null is compatible with both. It can be treated as an Object reference, and it can also be treated as a String reference, because String is a subclass of Object.
When multiple overloads are applicable, Java picks the one whose parameter type is the most specific — the type that is the closest in the inheritance hierarchy. Since String is a subclass of Object, String is more specific than Object. So print(String) wins, and the output is String .
If you want to force the Object version, you would have to cast: print((Object) null). That explicit cast removes the ambiguity and tells the compiler exactly which overload you mean.
This question is a favorite because it combines two classic Java topics — overloading resolution and the fact that null fits any reference type. The rule to remember: Java prefers the most specific applicable method.
Answer:
String
When you call an overloaded method, Java has to decide which version to invoke. The rule it follows is called most specific method selection.
Here there are two candidates: print(Object) and print(String). The argument is null, and here is the subtlety — null is compatible with both. It can be treated as an Object reference, and it can also be treated as a String reference, because String is a subclass of Object.
When multiple overloads are applicable, Java picks the one whose parameter type is the most specific — the type that is the closest in the inheritance hierarchy. Since String is a subclass of Object, String is more specific than Object. So print(String) wins, and the output is String .
If you want to force the Object version, you would have to cast: print((Object) null). That explicit cast removes the ambiguity and tells the compiler exactly which overload you mean.
This question is a favorite because it combines two classic Java topics — overloading resolution and the fact that null fits any reference type. The rule to remember: Java prefers the most specific applicable method.
5. Which output is produced by the following code?
public class ExceptionTest {
static int getValue() {
try {
return 10;
} finally {
return 20;
}
}
public static void main(String[] args) {
System.out.println(getValue());
}
}
Output: 20
This is the trap promised in question 3. A return inside a finally block doesn’t just run before the try’s return — it replaces it.
Here’s the sequence. The try block executes return 10, which says “the value of this method is 10.” But before that value is handed back, the JVM must run the finally block. Inside finally, there is another return 20. That new return discards the pending 10 and replaces it with 20. The method returns 20.
The same rule applies to exceptions. If a try block throws an exception but the finally block contains a return, the return swallows the exception entirely — the caller never sees it. That silent swallowing is exactly why writing return inside finally is considered a bad practice.
The Java Language Specification is explicit: a finally block can abort whatever the try or catch was about to do, whether that was a return value or a thrown exception.
For interviews, remember the asymmetry: code in finally always runs, and a return in finally takes over completely. To return a meaningful value safely, compute it in the try and let the finally block only do cleanup — never return from it.
Answer:
20
This is the trap promised in question 3. A return inside a finally block doesn’t just run before the try’s return — it replaces it.
Here’s the sequence. The try block executes return 10, which says “the value of this method is 10.” But before that value is handed back, the JVM must run the finally block. Inside finally, there is another return 20. That new return discards the pending 10 and replaces it with 20. The method returns 20.
The same rule applies to exceptions. If a try block throws an exception but the finally block contains a return, the return swallows the exception entirely — the caller never sees it. That silent swallowing is exactly why writing return inside finally is considered a bad practice.
The Java Language Specification is explicit: a finally block can abort whatever the try or catch was about to do, whether that was a return value or a thrown exception.
For interviews, remember the asymmetry: code in finally always runs, and a return in finally takes over completely. To return a meaningful value safely, compute it in the try and let the finally block only do cleanup — never return from it.
6. What is the printed result of comparing these wrapper objects?
Integer a = 127;
Integer b = 127;
Integer c = 128;
Integer d = 128;
System.out.println((a == b) + " " + (c == d));
Output: true false
Two things collide here: autoboxing and reference comparison.
When you assign an int literal to an Integer, Java silently boxes it — it creates an Integer object. The question is whether a and b end up referencing the same object or two different ones, because == on objects compares references.
The answer is that Java caches Integer objects for values in a specific range: -128 to 127. This is the IntegerCache. When autoboxing a value inside that range, the JVM reuses a cached object instead of creating a new one. So 127 autoboxes to the same cached Integer for both a and b, making a == b true.
But 128 is outside the cache range. Autoboxing 128 creates a fresh Integer object each time, so c and d are two distinct objects. Comparing their references gives false, even though their values are equal.
The rule generalizes: == on wrapper objects compares references, not values, and only the cached range (-128 to 127) is guaranteed to share objects. Outside that range, two Integer values may or may not be the same object depending on the JVM. To compare values reliably, use .equals() — or unbox with .intValue().
Answer:
true false
Two things collide here: autoboxing and reference comparison.
When you assign an int literal to an Integer, Java silently boxes it — it creates an Integer object. The question is whether a and b end up referencing the same object or two different ones, because == on objects compares references.
The answer is that Java caches Integer objects for values in a specific range: -128 to 127. This is the IntegerCache. When autoboxing a value inside that range, the JVM reuses a cached object instead of creating a new one. So 127 autoboxes to the same cached Integer for both a and b, making a == b true.
But 128 is outside the cache range. Autoboxing 128 creates a fresh Integer object each time, so c and d are two distinct objects. Comparing their references gives false, even though their values are equal.
The rule generalizes: == on wrapper objects compares references, not values, and only the cached range (-128 to 127) is guaranteed to share objects. Outside that range, two Integer values may or may not be the same object depending on the JVM. To compare values reliably, use .equals() — or unbox with .intValue().
7. What happens when you attempt to run a class where main is NOT declared as static?
Answer: The code compiles fine, but running it throws a runtime error: Main method is not static in class Test, please define the main method as: public static void main(String[] args).
The surprising part is that this is not a compile-time error. The Java compiler happily compiles a class whose main method is an instance method. It is only the JVM at launch time that strictly requires the exact signature public static void main(String[] args).
So the failure happens at runtime, not at compile time. When you run java Test, the JVM looks for a method matching that exact signature. If it finds main but it isn’t static, it reports an error telling you the main method must be static. If it finds no usable main at all, you get Error: Main method not found.
Why does the JVM require static? Because main is the entry point — the JVM starts the program before any objects exist. There is no instance to call a method on, so main must be static, callable directly from the class itself.
The signature has to be exact, too: public (so the JVM can access it), static (no instance needed), void (the JVM doesn’t expect a return value), and String[] args (to receive command-line arguments). Miss any piece, and the launch fails at runtime.
Answer:
The code compiles fine, but running it throws a runtime error: Main method is not static in class Test, please define the main method as: public static void main(String[] args).
The surprising part is that this is not a compile-time error. The Java compiler happily compiles a class whose main method is an instance method. It is only the JVM at launch time that strictly requires the exact signature public static void main(String[] args).
So the failure happens at runtime, not at compile time. When you run java Test, the JVM looks for a method matching that exact signature. If it finds main but it isn’t static, it reports an error telling you the main method must be static. If it finds no usable main at all, you get Error: Main method not found.
Why does the JVM require static? Because main is the entry point — the JVM starts the program before any objects exist. There is no instance to call a method on, so main must be static, callable directly from the class itself.
The signature has to be exact, too: public (so the JVM can access it), static (no instance needed), void (the JVM doesn’t expect a return value), and String[] args (to receive command-line arguments). Miss any piece, and the launch fails at runtime.
8. 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.
Answer:
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.
9. 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.
Answer:
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.
10. 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.
Answer:
-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.
11. 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.
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.
12. 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.
Answer:
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.
13. 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.
Answer:
[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.
14. 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.
Answer:
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.
15. 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.
Answer:
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.
16. 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.
Answer:
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.
17. 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().
Answer:
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().
18. 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.
Answer:
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.
19. 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.
Answer:
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.
20. What is the output of the following arithmetic code?
System.out.println(Math.min(Double.MIN_VALUE, 0.0d));
Output: 0.0
This is the kind of question that punishes assumptions. Most people read MIN_VALUE and think “most negative number” — the integer intuition. Doubles don’t work that way.
For integers, Integer.MIN_VALUE is indeed the most negative value, around -2.1 billion. For double, the situation is different. A double can hold numbers with wildly different magnitudes, so “minimum” is defined as the smallest positive non-zero value the type can represent — about 4.9 × 10^-324, a number so close to zero it’s practically nothing.
So Double.MIN_VALUE is positive. And Math.min(a, b) returns the smaller of the two arguments. Since 4.9 × 10^-324 is greater than 0.0, the smaller value is 0.0.
The answer prints 0.0, not Double.MIN_VALUE. The lesson: in Java, Double.MIN_VALUE is the smallest positive double, not the most negative one.
Answer:
0.0
This is the kind of question that punishes assumptions. Most people read MIN_VALUE and think “most negative number” — the integer intuition. Doubles don’t work that way.
For integers, Integer.MIN_VALUE is indeed the most negative value, around -2.1 billion. For double, the situation is different. A double can hold numbers with wildly different magnitudes, so “minimum” is defined as the smallest positive non-zero value the type can represent — about 4.9 × 10^-324, a number so close to zero it’s practically nothing.
So Double.MIN_VALUE is positive. And Math.min(a, b) returns the smaller of the two arguments. Since 4.9 × 10^-324 is greater than 0.0, the smaller value is 0.0.
The answer prints 0.0, not Double.MIN_VALUE. The lesson: in Java, Double.MIN_VALUE is the smallest positive double, not the most negative one.
21. What will be the output of this code?
boolean b1 = true;
boolean b2 = false;
System.out.println(b1 | b2 & b2);
Output: true
Two things are being tested here: operator precedence, and the difference between the bitwise operators on booleans.
Precedence first. In Java, & binds more tightly than |. So b1 | b2 & b2 is evaluated as b1 | (b2 & b2), not as (b1 | b2) & b2. Getting this grouping backwards gives the wrong answer.
Now evaluate b2 & b2. b2 is false, so false & false is false.
Then the outer operation: b1 | false. b1 is true, so true | false is true.
The output is true.
One more thing worth noting for interviews: |, &, and ^ work on booleans as non-short-circuiting logical operators — they always evaluate both sides. The short-circuiting twins || and && evaluate the right side only when needed. Here it doesn’t change the result, but it’s a classic follow-up question.
Answer:
true
Two things are being tested here: operator precedence, and the difference between the bitwise operators on booleans.
Precedence first. In Java, & binds more tightly than |. So b1 | b2 & b2 is evaluated as b1 | (b2 & b2), not as (b1 | b2) & b2. Getting this grouping backwards gives the wrong answer.
Now evaluate b2 & b2. b2 is false, so false & false is false.
Then the outer operation: b1 | false. b1 is true, so true | false is true.
The output is true.
One more thing worth noting for interviews: |, &, and ^ work on booleans as non-short-circuiting logical operators — they always evaluate both sides. The short-circuiting twins || and && evaluate the right side only when needed. Here it doesn’t change the result, but it’s a classic follow-up question.
22. What will be printed by this code?
List<String> list = List.of("anna", "bob", "alex");
long count = list.stream()
.filter(s -> s.startsWith("a"))
.count();
System.out.println(count);
Output: 2
The stream pipeline has two stages: an intermediate filter and a terminal count.
filter(s -> s.startsWith("a")) keeps only the elements that start with 'a'. Walking the list: "anna" starts with a — kept. "bob" does not — dropped. "alex" starts with a — kept. That leaves two elements.
The terminal operation count() then returns the number of elements left in the stream, which is 2.
Note that count() returns a long, and that the intermediate filter doesn’t run until a terminal operation is invoked — streams are lazy. But for counting purposes, that laziness is invisible: the result is simply 2.
Answer:
2
The stream pipeline has two stages: an intermediate filter and a terminal count.
filter(s -> s.startsWith("a")) keeps only the elements that start with 'a'. Walking the list: "anna" starts with a — kept. "bob" does not — dropped. "alex" starts with a — kept. That leaves two elements.
The terminal operation count() then returns the number of elements left in the stream, which is 2.
Note that count() returns a long, and that the intermediate filter doesn’t run until a terminal operation is invoked — streams are lazy. But for counting purposes, that laziness is invisible: the result is simply 2.
23. What will be the output of this code?
public class StringPool {
public static void main(String[] args) {
String s1 = "Java";
String s2 = "Ja" + "va";
System.out.println(s1 == s2);
}
}
Output: true
This is the mirror image of the "He" + new String("llo") question — and it’s the contrast that makes both of them click.
The difference is that "Ja" + "va" is a constant expression. Both operands are string literals, and the compiler is allowed to evaluate the concatenation at compile time. It folds the two literals into a single literal: "Java".
Now s1 = "Java" and the folded s2 — which is also "Java" — both point to the same entry in the String Constant Pool. The runtime resolves the same literal to the same pool object. So s1 == s2 compares two references to one object and returns true.
The general rule, stated cleanly: == on strings is true when both sides are compile-time constants resolving to the same pooled literal. It breaks the moment anything is built at runtime — new String(...), concatenation involving a variable or method call. The reliable comparison for content is always .equals(). This pair of questions is the classic demonstration of exactly where that line falls.
Answer:
true
This is the mirror image of the "He" + new String("llo") question — and it’s the contrast that makes both of them click.
The difference is that "Ja" + "va" is a constant expression. Both operands are string literals, and the compiler is allowed to evaluate the concatenation at compile time. It folds the two literals into a single literal: "Java".
Now s1 = "Java" and the folded s2 — which is also "Java" — both point to the same entry in the String Constant Pool. The runtime resolves the same literal to the same pool object. So s1 == s2 compares two references to one object and returns true.
The general rule, stated cleanly: == on strings is true when both sides are compile-time constants resolving to the same pooled literal. It breaks the moment anything is built at runtime — new String(...), concatenation involving a variable or method call. The reliable comparison for content is always .equals(). This pair of questions is the classic demonstration of exactly where that line falls.
24. 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.
Answer:
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.
25. 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.
Answer:
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.
26. 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.
Answer:
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.
27. 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.
Answer:
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.
28. 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.
Answer:
-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.
29. What will be printed by this snippet?
public class ThreadTest {
public static void main(String[] args) throws Exception {
Thread t = new Thread(() -> System.out.print("Run "));
t.run();
System.out.print("Main ");
}
}
Output: Run Main
The trap is mistaking .run() for .start(). They are completely different.
t.start()creates a new thread and executesrun()on it, asynchronously. The order of output would then be nondeterministic.t.run()is just a normal method call. It invokes therun()body directly on the current thread — the main thread. No new thread is spawned.
So the code runs purely sequentially: run() executes its lambda, printing Run , then control returns to main, which prints Main . The output is deterministically Run Main .
The interview lesson: calling run() directly does not start a thread. It’s a plain synchronous call that runs the task on the calling thread. Only start() launches real concurrency.
Answer:
Run Main
The trap is mistaking .run() for .start(). They are completely different.
t.start()creates a new thread and executesrun()on it, asynchronously. The order of output would then be nondeterministic.t.run()is just a normal method call. It invokes therun()body directly on the current thread — the main thread. No new thread is spawned.
So the code runs purely sequentially: run() executes its lambda, printing Run , then control returns to main, which prints Main . The output is deterministically Run Main .
The interview lesson: calling run() directly does not start a thread. It’s a plain synchronous call that runs the task on the calling thread. Only start() launches real concurrency.
30. What will be the output of this code?
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
for (int i = 0; i < list.size(); i++) {
list.remove(i);
}
System.out.println(list);
Output: [B]
The bug hides in the fact that remove(i) shifts elements left, and the loop counter is still climbing.
Walk through it. The list starts as [A, B].
- i = 0: condition
0 < 2is true.list.remove(0)removes"A". The array shifts left:"B"moves to index 0. The list is now[B], size 1. - i = 1: condition
1 < 1is false. The loop ends immediately.
So "B" survives at index 0, never examined. The list prints [B].
The general pattern this illustrates: removing elements from a collection while iterating with an index loop skips elements, because each removal shifts the remaining ones into positions the loop has already passed. The correct approaches are iterating from the end (i = size-1; i >= 0; i--), using an Iterator with remove(), or list.removeIf(...).
Answer:
[B]
The bug hides in the fact that remove(i) shifts elements left, and the loop counter is still climbing.
Walk through it. The list starts as [A, B].
- i = 0: condition
0 < 2is true.list.remove(0)removes"A". The array shifts left:"B"moves to index 0. The list is now[B], size 1. - i = 1: condition
1 < 1is false. The loop ends immediately.
So "B" survives at index 0, never examined. The list prints [B].
The general pattern this illustrates: removing elements from a collection while iterating with an index loop skips elements, because each removal shifts the remaining ones into positions the loop has already passed. The correct approaches are iterating from the end (i = size-1; i >= 0; i--), using an Iterator with remove(), or list.removeIf(...).
31. What will be printed?
public class IntTest {
public static void main(String[] args) {
int a = 10;
int b = 20;
System.out.println("Result: " + a + b);
}
}
Output: Result: 1020
String concatenation with + is evaluated left to right, and the moment a String is in the chain, everything that follows is converted to a string too.
The expression is "Result: " + a + b. The + operator associates left to right:
"Result: " + a— one operand is aString, soa(int 10) is converted to the string"10". The result is"Result: 10"."Result: 10" + b— the left operand is again aString, sob(int 20) becomes"20". The result is"Result: 1020".
There is no arithmetic addition at all, because by the time b is reached, the left operand is already a string.
The contrast: System.out.println(a + b) would print 30 — no string present, so plain integer addition. But the prefix "Result: " changes everything. If the intent were arithmetic, the parenthesization "Result: " + (a + b) would be required.
Answer:
Result: 1020
String concatenation with + is evaluated left to right, and the moment a String is in the chain, everything that follows is converted to a string too.
The expression is "Result: " + a + b. The + operator associates left to right:
"Result: " + a— one operand is aString, soa(int 10) is converted to the string"10". The result is"Result: 10"."Result: 10" + b— the left operand is again aString, sob(int 20) becomes"20". The result is"Result: 1020".
There is no arithmetic addition at all, because by the time b is reached, the left operand is already a string.
The contrast: System.out.println(a + b) would print 30 — no string present, so plain integer addition. But the prefix "Result: " changes everything. If the intent were arithmetic, the parenthesization "Result: " + (a + b) would be required.
32. What will be the printed output?
public class TernaryTest {
public static void main(String[] args) {
System.out.println(true ? 1 : 2.0);
}
}
Output: 1.0
The ternary operator a ? b : c produces a single result whose type must accommodate both branches. When the branches have different types, Java performs numeric promotion to find a common type.
Here the branches are 1 (an int) and 2.0 (a double). The common type is double — every int can be widened to a double. So the ternary’s type is double, the chosen branch value 1 is promoted to 1.0, and println prints 1.0.
The output is 1.0, not 1 — a subtle promotion that changes the printed form. The lesson: the ternary’s type is the common type of both branches, not the type of the branch actually selected at runtime.
Answer:
1.0
The ternary operator a ? b : c produces a single result whose type must accommodate both branches. When the branches have different types, Java performs numeric promotion to find a common type.
Here the branches are 1 (an int) and 2.0 (a double). The common type is double — every int can be widened to a double. So the ternary’s type is double, the chosen branch value 1 is promoted to 1.0, and println prints 1.0.
The output is 1.0, not 1 — a subtle promotion that changes the printed form. The lesson: the ternary’s type is the common type of both branches, not the type of the branch actually selected at runtime.
33. What will be the output of this snippet?
public class EqualsTest {
public static void main(String[] args) {
int[] arr1 = {1, 2, 3};
int[] arr2 = {1, 2, 3};
System.out.println(arr1.equals(arr2));
}
}
Output: false
The trap is assuming arrays behave like other objects. They don’t override equals().
arr1.equals(arr2) calls the inherited Object.equals(), whose default implementation is simply reference equality: arr1 == arr2. The two arrays are distinct objects with different identities, even though their contents match. So the result is false.
If the code had wanted to compare contents, the correct call is Arrays.equals(arr1, arr2), which compares element by element and returns true for these two arrays. (For nested arrays, Arrays.deepEquals is needed.)
The interview point: arrays inherit Object.equals() and are compared by reference, not by content. Content comparison requires Arrays.equals.
Answer:
false
The trap is assuming arrays behave like other objects. They don’t override equals().
arr1.equals(arr2) calls the inherited Object.equals(), whose default implementation is simply reference equality: arr1 == arr2. The two arrays are distinct objects with different identities, even though their contents match. So the result is false.
If the code had wanted to compare contents, the correct call is Arrays.equals(arr1, arr2), which compares element by element and returns true for these two arrays. (For nested arrays, Arrays.deepEquals is needed.)
The interview point: arrays inherit Object.equals() and are compared by reference, not by content. Content comparison requires Arrays.equals.
34. What will be the printed result?
public class MathTest {
public static void main(String[] args) {
System.out.println(10 / 4);
}
}
Output: 2
Both 10 and 4 are int literals, and the / operator on two integers performs integer division — the fractional part is truncated (not rounded). 10 / 4 is 2, with the remainder 2 discarded.
The output prints 2, not 2.5 and not 2.0.
To get a fractional result, at least one operand must be a floating-point type: 10 / 4.0 or 10.0 / 4 would produce 2.5. The interview point is the classic one — know when integer division applies, because silent truncation is a common source of off-by-one bugs in real code.
Answer:
2
Both 10 and 4 are int literals, and the / operator on two integers performs integer division — the fractional part is truncated (not rounded). 10 / 4 is 2, with the remainder 2 discarded.
The output prints 2, not 2.5 and not 2.0.
To get a fractional result, at least one operand must be a floating-point type: 10 / 4.0 or 10.0 / 4 would produce 2.5. The interview point is the classic one — know when integer division applies, because silent truncation is a common source of off-by-one bugs in real code.
35. What will be printed by this code?
public class BoolAssign {
public static void main(String[] args) {
boolean b = false;
if (b = true) {
System.out.println("TRUE");
} else {
System.out.println("FALSE");
}
}
}
Output: TRUE
The condition uses the assignment operator = instead of the equality operator ==. That’s not a typo — it’s the whole question.
b = true is an assignment expression. It assigns true to b and evaluates to the assigned value, which is true. Since the condition of the if is true, the then-branch executes and prints TRUE.
This compiles and runs normally because b is a boolean and the assignment’s value is a boolean — legal as a condition. (The infamous bug version is if (x = 1) with an int, which would not compile in Java — another reason Java is stricter than C here.)
The lesson: = assigns and evaluates to the assigned value; == compares. In conditions, = is almost always a mistake — one reason many style guides mandate if (true == b) or the Yoda style, so a missing = produces a compile error instead of silent wrong behavior.
Answer:
TRUE
The condition uses the assignment operator = instead of the equality operator ==. That’s not a typo — it’s the whole question.
b = true is an assignment expression. It assigns true to b and evaluates to the assigned value, which is true. Since the condition of the if is true, the then-branch executes and prints TRUE.
This compiles and runs normally because b is a boolean and the assignment’s value is a boolean — legal as a condition. (The infamous bug version is if (x = 1) with an int, which would not compile in Java — another reason Java is stricter than C here.)
The lesson: = assigns and evaluates to the assigned value; == compares. In conditions, = is almost always a mistake — one reason many style guides mandate if (true == b) or the Yoda style, so a missing = produces a compile error instead of silent wrong behavior.
36. What will be printed by this code?
public class StrNull {
public static void main(String[] args) {
String str = null;
System.out.println(str + " Java");
}
}
Output: null Java
This seems like it should throw NullPointerException — calling a method on null. But string concatenation has special handling.
str + " Java" is string concatenation. The JVM compiles it (loosely) into something like new StringBuilder().append(str).append(" Java").toString(). The key is StringBuilder.append(Object): when the argument is null, it appends the literal string "null" rather than throwing.
So the concatenation produces the string "null Java", and println prints null Java.
The lesson: concatenating a null reference doesn’t throw — it renders as the text "null". This is a frequent source of confusion (and of “null” appearing in logs you expected to be empty).
Answer:
null Java
This seems like it should throw NullPointerException — calling a method on null. But string concatenation has special handling.
str + " Java" is string concatenation. The JVM compiles it (loosely) into something like new StringBuilder().append(str).append(" Java").toString(). The key is StringBuilder.append(Object): when the argument is null, it appends the literal string "null" rather than throwing.
So the concatenation produces the string "null Java", and println prints null Java.
The lesson: concatenating a null reference doesn’t throw — it renders as the text "null". This is a frequent source of confusion (and of “null” appearing in logs you expected to be empty).
37. What will be printed by this code?
public class ListTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add(0, "B");
System.out.println(list);
}
}
Output: [B, A]
List.add(index, element) is an insertion, not an overwrite. It puts the new element at the given index and shifts every existing element from that index onward one position to the right.
Walk through it. The list starts [A]. Then list.add(0, "B") inserts "B" at index 0. The existing "A" shifts from index 0 to index 1.
The final list is [B, A], and that’s what prints.
The lesson: add(0, x) inserts at the front and shifts everything else right; it does not replace the element at index 0. Contrast with set(index, element), which overwrites in place.
Answer:
[B, A]
List.add(index, element) is an insertion, not an overwrite. It puts the new element at the given index and shifts every existing element from that index onward one position to the right.
Walk through it. The list starts [A]. Then list.add(0, "B") inserts "B" at index 0. The existing "A" shifts from index 0 to index 1.
The final list is [B, A], and that’s what prints.
The lesson: add(0, x) inserts at the front and shifts everything else right; it does not replace the element at index 0. Contrast with set(index, element), which overwrites in place.
Premium Content
Unlock Core Java Basics and all premium lessons with a subscription.
From ₹199.99/year — See plans