Operators & control flow: interview questions & scenarios
These are the operator and control-flow questions I keep seeing in Java screens, most of them "what does this print?" snippets, because a tiny piece of code tells an interviewer more than a definition ever could. Each one has the follow-ups that come next when your first answer is good.
Here's how to use this page: read the snippet, answer it out loud first (including why), then open the model answer. The "What if…" chains are where interviewers separate memorised answers from real understanding, so don't skip them.
What does this print?
Tests post-increment, operand stack
Show answerHide answer
Model answer
int count = 0;
for (int k = 0; k < 100; k++) {
count = count++;
}
System.out.println(count);
It prints 0. Each count = count++ compiles to iload (push the old value, 0), iinc (the local becomes 1), istore (pop the saved 0 back into count). The increment happens, then gets overwritten, a hundred times in a row. The fix is count++; on its own or count += 1;.
How I’d say it in the interview
"Zero. Post-increment's value is the old value, and javac loads that onto the operand stack before incrementing the local. The assignment then stores the old value back, so every increment is undone."
Signals depth
Explaining it with the bytecode (iload, iinc, istore) and noting that Java defines this exactly, unlike C, where i = i++ is undefined behaviour.
Red flags
"It prints 100 because the increment happens after the assignment." The increment happens before the assignment; it's the stored value that's stale.
Follow-ups interviewers ask next
What if it's
count = ++countinstead?Then it prints
100. Pre-increment compiles toiincfirst andiloadsecond, so the stack holds the new value.What if it's
int i = 10; i = i++ + i--;?21. Left operand first:i++yields10and setsito11; theni--yields11and setsito10; the sum21is stored, overwriting the10.
What does each line print?
Tests string concatenation, associativity, char promotion
Show answerHide answer
Model answer
System.out.println(1 + 2 + "3" + 4 + 5); // 3345
System.out.println('a' + 'b' + "c"); // 195c
System.out.println("" + 'a' + 'b'); // ab
+ is left-associative. In the first line, 1 + 2 is integer addition (3), then 3 + "3" becomes the string "33", and every later + appends. In the second, 'a' + 'b' is char + char, which promotes to int: 97 + 98 = 195, then "c" is appended. In the third, the leading "" makes the very first + a string concatenation.
How I’d say it in the interview
"Plus groups left to right, and it only becomes string concatenation once one operand is a String. Before that point it's numeric addition, and chars promote to int."
Signals depth
Mentioning that char + char is int arithmetic, and that since Java 9 javac compiles concatenation to an invokedynamic call to StringConcatFactory instead of a StringBuilder chain.
Red flags
Answering 12345 or abc without thinking about where the string starts.
Follow-ups interviewers ask next
What if I write
1 + 2 + "" + (4 + 5)?"39". The parentheses force4 + 5to be integer addition before it's appended.
What does this print, and why isn't it 1?
Tests conditional operator typing, unboxing
Show answerHide answer
Model answer
System.out.println(true ? Integer.valueOf(1) : Double.valueOf(2));
It prints 1.0. When both branches of ?: are convertible to numeric types, JLS 15.25 makes the whole expression a numeric conditional: both operands are unboxed and promoted to their common type, double. The result type is decided at compile time from both branches, regardless of which one runs.
How I’d say it in the interview
"The ternary's type comes from both branches. Integer and Double unbox and promote to double, so I get 1.0, not 1."
Signals depth
Knowing that if one branch is a non-numeric reference type (true ? 1 : "x"), the result is a reference type and you get 1 printed as an Integer.
Red flags
"It prints 1 because the condition is true." That's the runtime story; the type was fixed at compile time.
Follow-ups interviewers ask next
What if one branch is
null?Integer n = null; int v = flag ? 0 : n;If
flagis false, this throwsNullPointerException. The result type isint, sonis unboxed. Fix it by making both branchesIntegeror checking for null first.What if I write
Integer v = flag ? 0 : n;instead?Still a
NullPointerException. The expression's type is stillint(one branch isint, the otherInteger), sonis unboxed before the result is boxed again for the assignment. Writeflag ? Integer.valueOf(0) : nto keep it a reference conditional.
What's the difference between && and &?
Tests short-circuit evaluation, side effects
Show answerHide answer
Model answer
On booleans, both compute logical AND. && short-circuits: if the left side is false, the right side isn't evaluated. & always evaluates both. That matters for guards and side effects:
int n = 0;
boolean r1 = (n++ > 0) && (n++ > 0); // n is 1, r1 false: right side skipped
int m = 0;
boolean r2 = (m++ > 0) & (m++ > 0); // m is 2, r2 false: both sides ran
On integers, & is a bitwise AND (6 & 3 is 2) and && doesn't compile. The same applies to || and |.
How I’d say it in the interview
"Double ampersand short-circuits, so I use it for null guards like s != null && s.isEmpty(). Single ampersand always evaluates both sides on booleans, and on ints it's bitwise."
Signals depth
Pointing out that ^ is the only way to write logical XOR on booleans, and that & on booleans is occasionally used deliberately to avoid a branch, though it's rarely worth it.
Red flags
"They're the same, && is the newer syntax."
Follow-ups interviewers ask next
What if the precedence is involved: does
a || b && cmean(a || b) && c?No.
&&binds tighter than||, so it'sa || (b && c). And ifais true, neitherbnorcis evaluated.
What do these shifts print?
Tests >> vs >>>, shift distance masking
Show answerHide answer
Model answer
System.out.println(-16 >> 2); // -4
System.out.println(-16 >>> 28); // 15
System.out.println(1 << 33); // 2
System.out.println(1L << 33); // 8589934592
>> is an arithmetic shift: it copies the sign bit, so it behaves like division by 4 rounded toward negative infinity. >>> fills with zeros: -16 is 0xFFFFFFF0, and shifting right by 28 leaves 0xF. For int, only the low 5 bits of the distance count, so 1 << 33 is 1 << 1. For long, it's the low 6 bits, so 1L << 33 is what you'd expect.
How I’d say it in the interview
"Right shift keeps the sign, unsigned right shift fills with zeros. And the shift distance is masked to 5 bits for ints and 6 for longs, so shifting an int by 33 is shifting by 1."
Signals depth
Mentioning (low + high) >>> 1 as the overflow-safe midpoint in binary search.
Red flags
Saying 1 << 33 is 0 "because the bit falls off".
Follow-ups interviewers ask next
What if I shift a negative byte with
>>>=?byte b = -16; b >>>= 2;bis-4, not a large positive number. The byte is promoted toint(0xFFFFFFF0), shifted to0x3FFFFFFC, and the compound assignment's implicit cast back tobytekeeps only the low 8 bits,0xFC, which is-4.
One of these lines doesn't compile. Which one, and why does the other?
Tests compound assignment, implicit narrowing cast
Show answerHide answer
Model answer
short s = 10;
s = s + 5; // does NOT compile: int cannot be converted to short
s += 5; // compiles
s + 5 promotes to int, and assigning an int expression to a short needs an explicit cast. Compound assignment is defined by JLS 15.26.2 as s = (short) (s + 5), with s evaluated once, so the cast is built in.
How I’d say it in the interview
"Compound assignment hides a narrowing cast. s += 5 means s = (short)(s + 5), so it compiles; the long form doesn't because s + 5 is an int."
Signals depth
Pointing out the danger side: int x = 0; x += 1.9; compiles and leaves x as 1, silently truncating.
Red flags
"Both compile" or "+= is only shorthand".
Follow-ups interviewers ask next
What if it's
char c = 'A'; c += 1;?It compiles and
cis'B'.c = c + 1would not compile.c++also works, because increment operators include the same implicit cast.
What does this print for "TUE", "FRI" and "MON"?
Tests switch fall-through, default placement
Show answerHide answer
Model answer
switch (day) {
case "MON": System.out.print("M");
default: System.out.print("D");
case "TUE": System.out.print("T");
case "WED": System.out.print("W"); break;
}
"TUE" prints TW, "FRI" prints DTW and "MON" prints MDTW. A classic switch jumps to the matching label and then falls through every later label until a break. default is only the target when nothing matches; its position doesn't make it run last, and it falls through like any other label.
How I’d say it in the interview
"Colon labels fall through. The switch jumps to the matching case and keeps going until a break, and default is another entry point used when nothing matches, wherever it sits."
Signals depth
Saying that arrow labels (case "MON" ->, Java 14) never fall through, which removes this whole class of bugs.
Red flags
Thinking default always runs last, or that a matched case stops at the next label.
Follow-ups interviewers ask next
What if
dayis null?NullPointerException. AStringswitch starts by callingday.hashCode(). Since Java 21 you can add acase nulllabel to handle it explicitly.
How does the JVM execute a switch on a String?
Tests tableswitch, lookupswitch, hashCode/equals
Show answerHide answer
Model answer
The JVM only has two switch instructions, and both work on int keys. tableswitch holds a jump table for a dense range (O(1) index); lookupswitch holds sorted key/offset pairs for sparse keys (the JVM may binary search them). javac picks between them with a space/time cost heuristic.
For a String, javac generates two switches: first a lookupswitch on s.hashCode(), with an equals() call inside each hash bucket to rule out collisions and pick a case index; then a second switch on that index which runs your code. Enums switch on ordinal(), via a synthetic $SwitchMap$ array when the enum is declared in another class.
How I’d say it in the interview
"Dense int cases become a tableswitch, a direct jump table. Sparse ones become a lookupswitch. A String switch is a lookupswitch on the hash code, then equals to confirm, then a second switch on the matched index."
Signals depth
Knowing why equals() is required ("Aa" and "BB" both hash to 2112), and that Java 21 pattern switches use invokedynamic with SwitchBootstraps.typeSwitch.
Red flags
"It compares the strings one by one like an if-else chain."
Follow-ups interviewers ask next
What if two case strings have the same hash code?
Both land in the same
lookupswitchbucket, and javac emits anif (s.equals("Aa")) ... else if (s.equals("BB"))sequence inside that bucket. Correctness is unaffected; you pay one extraequals().What if the enum's constants are reordered after my class was compiled?
The
$SwitchMap$array is built at class initialisation from the currentordinal()values, so your switch still routes correctly. That's exactly why javac uses the indirection instead of hardcoding ordinals.
Rewrite this as a modern switch, and tell me what the compiler now checks for you.
Tests switch expressions, pattern matching, exhaustiveness, dominance
Show answerHide answer
Model answer
sealed interface Payment permits Card, Upi, Cash {}
record Card(String last4, long amountCents) implements Payment {}
record Upi(String vpa, long amountCents) implements Payment {}
record Cash(long amountCents) implements Payment {}
static long feeCents(Payment p) {
return switch (p) {
case Card c when c.amountCents() > 100_000 -> c.amountCents() / 100; // 1%
case Card c -> 30;
case Upi u -> 0;
case Cash(long amount) -> amount > 0 ? 0 : -1;
};
}
The compiler now checks three things. Exhaustiveness: Payment is sealed, so covering Card, Upi and Cash is complete and no default is needed. Dominance: an unguarded case Card c above the guarded one would be a compile error. No fall-through: each arrow arm runs alone.
How I’d say it in the interview
"I'd switch on the sealed interface with type and record patterns. Because it's sealed, the compiler proves I've covered every case, so adding a new payment type breaks the build in exactly the places I need to update."
Signals depth
Saying that if a new permitted subtype shows up at runtime without recompiling this class, the switch throws MatchException (Java 21), and that guarded cases must precede the unguarded case of the same type.
Red flags
Adding a default "to be safe": it silences the exhaustiveness check that makes sealed types valuable.
Follow-ups interviewers ask next
What if I add
record Wallet(...) implements Paymentto the permits list?Every switch over
Paymentwithout adefaultstops compiling until it handlesWallet. That's the feature.What if I put
case Payment x ->as the first label?Compile error: every later label is dominated by it. Labels are matched top to bottom, so the most general pattern must come last.
What does this print, and why didn't it throw?
Tests for-each desugaring, fail-fast iterators, modCount
Show answerHide answer
Model answer
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
if (s.equals("b")) list.remove(s);
}
System.out.println(list);
It prints [a, c] with no exception. The for-each is an Iterator loop. After removing "b", the size is 2 and the iterator's cursor is already 2, so hasNext() (cursor != size) returns false and the loop ends before next() can check modCount. "c" is never visited. Remove "a" instead and you get ConcurrentModificationException on the next next().
How I’d say it in the interview
"The fail-fast check lives in next(). Removing the second-to-last element shrinks the list so hasNext returns false, next is never called, and the bug goes silent: the last element is skipped."
Signals depth
Describing modCount vs expectedModCount, and saying fail-fast is best-effort, never something to rely on for correctness.
Red flags
"Removing inside a for-each always throws."
Follow-ups interviewers ask next
What if I need to remove several elements while iterating?
Use
list.removeIf(s -> ...)(one O(n) pass onArrayList) or an explicitIteratorwithit.remove(). Avoidlist.remove(i)in a forward index loop: it skips the element after each removal.What if the list is a
CopyOnWriteArrayList?No exception: its iterator walks a snapshot of the array taken when the loop started, so removals aren't visible to the loop. Each write copies the whole array, so it only fits read-heavy lists.
You need to stop scanning a 2D grid as soon as you find a match. How do you exit both loops?
Tests labeled break, continue, method extraction
Show answerHide answer
Model answer
A plain break only leaves the inner loop. Use a labeled break, or extract a method and return:
static int[] find(int[][] grid, int target) {
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[r].length; c++) {
if (grid[r][c] == target) return new int[] {r, c};
}
}
return null;
}
With a label in place: search: for (...) { for (...) { if (match) break search; } }. My default is the method with return: it's named, testable and reads top to bottom. I use a label when extraction would drag several mutable locals along.
How I’d say it in the interview
"Break only exits the innermost loop. I'd either label the outer loop and break outer, or, my usual choice, pull the search into a method and return as soon as I find it."
Signals depth
Knowing continue label skips to the next iteration of the outer loop (skipping any code after the inner loop), that labels can sit on plain blocks, and that goto is reserved but unused.
Red flags
Using a boolean found flag checked in both loop conditions and getting the off-by-one wrong, or throwing an exception for control flow.
Follow-ups interviewers ask next
What if the inner loop is a
switchinside awhileand I want to leave thewhile?A bare
breakinside the classic switch only leaves the switch. Label thewhile(loop: while (true) { switch (...) { case X: break loop; } }) or use an arrow switch whose arm callsreturn.
Which of these loops terminate?
Tests overflow, reachability, floating point
Show answerHide answer
Model answer
for (int i = 0; i <= Integer.MAX_VALUE; i++) { } // 1: never ends
for (byte b = 0; b < 200; b++) { } // 2: never ends
for (float f = 0; f != 1.0f; f += 0.1f) { } // 3: never ends
while (false) { System.out.println("x"); } // 4: compile error
int n = 0;
for (int x = 0; x < 5; x++); { n++; } // 5: ends, n is 1
ioverflows fromMAX_VALUEtoMIN_VALUE, which is still<= MAX_VALUE.b < 200promotesbtoint; abytemaxes at127and wraps to-128.0.1fisn't exactly representable; after ten stepsfis1.0000001, so it skips past1.0f.- JLS 14.22 makes the body unreachable, which is a compile error.
if (false)is allowed on purpose. - The stray
;is the loop body. The block runs once afterwards.
How I’d say it in the interview
"Three of them run forever because of int overflow, byte wrap-around and floating-point error. while (false) doesn't compile because the body is unreachable. And the last one is the stray semicolon trap: the loop is empty and the block runs once."
Signals depth
Citing reachability rules, and suggesting < instead of != for floating-point loops, or better, an integer counter.
Red flags
Assuming the compiler or JVM will warn about overflow at runtime.
Follow-ups interviewers ask next
What if loop 1 used
long i?Then it terminates after about 2.1 billion iterations, because a
longcan grow pastInteger.MAX_VALUE. Change the bound toLong.MAX_VALUEand the same overflow trap comes straight back.