Course contents

Operators & control flow: interview questions & scenarios

Free12 questions · 17 what-if follow-ups

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.

Question 1. What does it print?Warm-up

What does this print?

Tests post-increment, operand stack

Show answerHide answer2 follow-ups

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

  1. What if it's count = ++count instead?

    Then it prints 100. Pre-increment compiles to iinc first and iload second, so the stack holds the new value.

    1. What if it's int i = 10; i = i++ + i--;?

      21. Left operand first: i++ yields 10 and sets i to 11; then i-- yields 11 and sets i to 10; the sum 21 is stored, overwriting the 10.

Question 2. What does it print?Warm-up

What does each line print?

Tests string concatenation, associativity, char promotion

Show answerHide answer1 follow-up

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

  1. What if I write 1 + 2 + "" + (4 + 5)?

    "39". The parentheses force 4 + 5 to be integer addition before it's appended.

Question 3. What does it print?Core

What does this print, and why isn't it 1?

Tests conditional operator typing, unboxing

Show answerHide answer2 follow-ups

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

  1. What if one branch is null?

    Integer n = null;
    int v = flag ? 0 : n;
    

    If flag is false, this throws NullPointerException. The result type is int, so n is unboxed. Fix it by making both branches Integer or checking for null first.

    1. What if I write Integer v = flag ? 0 : n; instead?

      Still a NullPointerException. The expression's type is still int (one branch is int, the other Integer), so n is unboxed before the result is boxed again for the assignment. Write flag ? Integer.valueOf(0) : n to keep it a reference conditional.

Question 4. ConceptWarm-up

What's the difference between && and &?

Tests short-circuit evaluation, side effects

Show answerHide answer1 follow-up

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

  1. What if the precedence is involved: does a || b && c mean (a || b) && c?

    No. && binds tighter than ||, so it's a || (b && c). And if a is true, neither b nor c is evaluated.

Question 5. What does it print?Core

What do these shifts print?

Tests >> vs >>>, shift distance masking

Show answerHide answer1 follow-up

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

  1. What if I shift a negative byte with >>>=?

    byte b = -16;
    b >>>= 2;
    

    b is -4, not a large positive number. The byte is promoted to int (0xFFFFFFF0), shifted to 0x3FFFFFFC, and the compound assignment's implicit cast back to byte keeps only the low 8 bits, 0xFC, which is -4.

Question 6. DebuggingCore

One of these lines doesn't compile. Which one, and why does the other?

Tests compound assignment, implicit narrowing cast

Show answerHide answer1 follow-up

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

  1. What if it's char c = 'A'; c += 1;?

    It compiles and c is 'B'. c = c + 1 would not compile. c++ also works, because increment operators include the same implicit cast.

Question 7. What does it print?Core

What does this print for "TUE", "FRI" and "MON"?

Tests switch fall-through, default placement

Show answerHide answer1 follow-up

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

  1. What if day is null?

    NullPointerException. A String switch starts by calling day.hashCode(). Since Java 21 you can add a case null label to handle it explicitly.

Question 8. ConceptSenior

How does the JVM execute a switch on a String?

Tests tableswitch, lookupswitch, hashCode/equals

Show answerHide answer2 follow-ups

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

  1. What if two case strings have the same hash code?

    Both land in the same lookupswitch bucket, and javac emits an if (s.equals("Aa")) ... else if (s.equals("BB")) sequence inside that bucket. Correctness is unaffected; you pay one extra equals().

    1. What if the enum's constants are reordered after my class was compiled?

      The $SwitchMap$ array is built at class initialisation from the current ordinal() values, so your switch still routes correctly. That's exactly why javac uses the indirection instead of hardcoding ordinals.

Question 9. CodingCore

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 answer2 follow-ups

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

  1. What if I add record Wallet(...) implements Payment to the permits list?

    Every switch over Payment without a default stops compiling until it handles Wallet. That's the feature.

    1. 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.

Question 10. What does it print?Core

What does this print, and why didn't it throw?

Tests for-each desugaring, fail-fast iterators, modCount

Show answerHide answer2 follow-ups

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

  1. What if I need to remove several elements while iterating?

    Use list.removeIf(s -> ...) (one O(n) pass on ArrayList) or an explicit Iterator with it.remove(). Avoid list.remove(i) in a forward index loop: it skips the element after each removal.

    1. 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.

Question 11. ScenarioCore

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 answer1 follow-up

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

  1. What if the inner loop is a switch inside a while and I want to leave the while?

    A bare break inside the classic switch only leaves the switch. Label the while (loop: while (true) { switch (...) { case X: break loop; } }) or use an arrow switch whose arm calls return.

Question 12. What does it print?Senior

Which of these loops terminate?

Tests overflow, reachability, floating point

Show answerHide answer1 follow-up

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
  1. i overflows from MAX_VALUE to MIN_VALUE, which is still <= MAX_VALUE.
  2. b < 200 promotes b to int; a byte maxes at 127 and wraps to -128.
  3. 0.1f isn't exactly representable; after ten steps f is 1.0000001, so it skips past 1.0f.
  4. JLS 14.22 makes the body unreachable, which is a compile error. if (false) is allowed on purpose.
  5. 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

  1. What if loop 1 used long i?

    Then it terminates after about 2.1 billion iterations, because a long can grow past Integer.MAX_VALUE. Change the bound to Long.MAX_VALUE and the same overflow trap comes straight back.

Lessons in Operators & control flow