Java Flow Control: if/else, switch and Loops (1Z0-831)
Flow control is how a Java program decides which statements run and how often: if/else picks a branch, switch statements and switch expressions pick among many cases, while, do-while, for and enhanced for loops repeat code, and break and continue change where a loop or switch goes next. Objective 2.1 of the Java SE 25 Developer exam asks you to create these constructs and to predict exactly what they do, including where they do not compile. This lesson covers the dangling else and boolean conditions, fall-through in colon switches, arrow labels, selector types and case constants, switch expressions with yield, exhaustiveness and result types, null handling, type and record patterns with guards and dominance, every loop form and its scope rules, labelled break and continue, and unreachable code. Every code example was compiled and run on JDK 25.
On this page10 sections
- if/else statements and the dangling else
- Switch statements with case X: labels and fall-through
- Arrow labels: case X -> with no fall-through
- Selector types and case constants
- Switch expressions: yield, exhaustiveness and result type
- null selectors, case null and pattern labels
- Dominance and record patterns
- while and do-while loops
- for loops, enhanced for loops and loop-variable scope
- break, continue, labels and unreachable code
- Predict which if an else belongs to, and which conditions compile, including assignments inside a condition
- Trace colon-form switch statements through fall-through and a default placed anywhere, and write arrow-form switches with multi-constant labels
- Write switch expressions that yield a value from every arm, are exhaustive for their selector, and have the result type you expect
- Identify legal selector types and case constants, and handle null with case null and case null, default
- Use type patterns, when guards and record patterns in case labels, and spot labels that are dominated
- Trace while, do-while, for and enhanced for loops, including increments in conditions, multi-variable for loops and loop-variable scope
- Use break, continue and labels in nested loops and switches, and recognise unreachable code
if/else statements and the dangling else
An if statement runs its body only when its condition is true, and an optional else runs when it is false. Each branch is a single statement; braces turn several statements into one block. When braces are omitted, an else always belongs to the nearest preceding if that has no else yet — indentation means nothing to the compiler.
public class Main {
static String pick(int h) {
String r = "";
if (h >= 6)
if (h < 12)
r += "morning";
else
r += "late";
return r + ";";
}
public static void main(String[] args) {
System.out.println(pick(9) + pick(15) + pick(3));
}
}This prints morning;late;;. The else is indented as if it belonged to if (h >= 6), but it binds to the inner if (h < 12). So for 15 the outer test is true, the inner test is false and late is added; for 3 the outer test is false and, because the outer if has no else of its own, nothing is added at all. Braces around the inner if are the only way to attach the else to the outer one. The same rule explains a statement that is indented as if it were part of an else or else if branch: without braces, only the first statement after else belongs to it, and the next one runs whatever the conditions were.
An else if chain is simply an else whose single statement is another if. The conditions are tested top to bottom and only the first true branch runs; later branches are skipped even when their conditions would also be true. A trailing else runs only when every test failed.
The condition must be boolean
The condition of an if, while, do or for must have type boolean or Boolean. Java never treats a number, a reference or null as true or false. In the table, qty is an int, open a boolean and ok a Boolean.
| Condition | Compiles? | Why |
|---|---|---|
while (qty) | No | int cannot be converted to boolean |
if (qty = 0) | No | the value of an assignment has the variable's type, here int |
if (!qty) | No | ! takes only a boolean operand |
if (open & qty) | No | & and | need two booleans or two integers, not one of each |
if (open = qty < 5) | Yes | < binds tighter than =, so a boolean is assigned and its value tested |
if (ok) | Yes | a Boolean is unboxed |
Assigning a boolean inside a condition is legal and has a side effect: the variable keeps the assigned value after the statement, and when the assignment sits in a loop it runs again on every test, so the variable ends up holding whatever the last test assigned. Writing = where == was meant therefore compiles silently whenever the variable is a boolean.
Unboxing a Boolean that is null throws a NullPointerException. A null check protects only the operands that are evaluated after it in the same short-circuit expression (ok != null && ok is safe, because && skips the right side when the left is false). A separate test of the same variable in a later else if is a new expression with no such protection, so it unboxes the null again.
Switch statements with case X: labels and fall-through
In the traditional (colon) form, a switch statement jumps to the label that matches its selector and then keeps executing every following statement, through later labels and default, until a break or the end of the switch. That is fall-through. If nothing matches and there is no default, the switch does nothing.
public class Main {
public static void main(String[] args) {
for (char c : new char[] {'b', 'z', 'd'}) {
int pts = 0;
switch (c) {
case 'a', 'b':
pts += 1;
case 'c':
pts += 10;
break;
default:
pts += 100;
case 'd':
pts += 1000;
}
System.out.print(pts + " ");
}
}
}This prints 11 1100 1000 . For 'b', execution enters at case 'a', 'b', falls into case 'c' and stops at its break. For 'z', no constant matches, so execution enters at default and falls into case 'd', the last group. For 'd', it enters at case 'd', even though default is written above it. Where default sits never changes which label is chosen — every constant is tried first, wherever it is written — it only changes which statements follow it. The last group needs no break, because the switch simply ends.
To trace a colon switch: find the entry label, then read straight down, ignoring every case and default line, until you reach a break, a return, a continue of an enclosing loop, a thrown exception or the closing brace. Several labels can share one group, either stacked (case 'a': case 'b':) or, since Java 14, as one comma-separated list (case 'a', 'b':).
Arrow labels: case X -> with no fall-through
An arrow label (case X ->) runs exactly one thing — the code to the right of the arrow — and then leaves the switch. There is no fall-through and no break is needed. One label can list several constants separated by commas, and default -> may appear anywhere among constant labels.
public class Main {
static int weight(String size) {
int w = 0;
switch (size) {
case "S", "XS" -> w = 1;
case "M" -> {
int base = 2;
w = base * 3;
}
case "L" -> w += 9;
default -> throw new IllegalArgumentException(size);
}
return w;
}
public static void main(String[] args) {
System.out.println(weight("XS") + " " + weight("M")
+ " " + weight("L"));
try {
weight("XXL");
} catch (IllegalArgumentException e) {
System.out.println("rejected " + e.getMessage());
}
}
}This prints 1 6 9 and then rejected XXL. The right-hand side of an arrow must be one of three things:
- a single expression statement — a method call, an assignment, or an increment or decrement;
- a block in braces, which may hold any statements, including local variable declarations (
baseabove); - a
throwstatement.
A local variable declaration written directly after the arrow is not an expression statement, so it fails with variable declaration not allowed here; wrap it in braces. And when two statements follow an arrow without braces, only the first belongs to the arm. The second sits loose in the switch body, where an arrow switch allows only labels, so compilation fails (case, default, or '}' expected). It never runs "as part of" the arm.
A single switch cannot mix the two label kinds. Using an arrow label and a colon label in the same switch fails with different case kinds used in the switch. Both kinds work in switch statements and in switch expressions.
| Feature | case X: (colon) | case X -> (arrow) |
|---|---|---|
| Falls through to the next label | Yes, until break | Never |
| Several constants per label | Yes (case 1, 2:) | Yes (case 1, 2 ->) |
| Body of a label | Any sequence of statements | One expression statement, one block, or one throw |
| Value in a switch expression | yield | The expression itself, or yield inside a block |
| Scope of a variable declared in a group | From its declaration to the end of the switch block, so later groups can assign and use it | Only that arm's block |
In the colon form, a variable declared in one group is in scope in the groups after it, but it is only initialised on the path that ran its declaration: a later group may assign it and then read it, but reading it there before assigning fails with variable … might not have been initialized.
Selector types and case constants
A switch whose labels are constants accepts a selector of type char, byte, short, int, their wrappers Character, Byte, Short, Integer, String, or an enum type. Any other reference type, such as Object or a sealed interface, needs pattern labels (next sections), or qualified enum constants such as case Level.LOW when an enum type is assignable to the selector type. long, float, double and boolean selectors do not compile in Java SE 25: javac reports that primitive patterns are a preview feature and are disabled by default, and preview features are outside the exam.
Each constant label must be a compile-time constant assignable to the selector's type, and no constant may appear twice. A compile-time constant is a literal, an enum constant, or a constant variable: a final variable of a primitive type or String whose declaration initialises it with a constant expression (literals, operators and other constants — never a method call).
| Label | Legal? | Why |
|---|---|---|
case 100 -> on a short | Yes | 100 fits in a short |
case 40000 -> on a short | No | 40000 does not fit: possible lossy conversion from int to short |
case MAX with local final int MAX = 7; | Yes | a final primitive initialised with a constant in its declaration |
case TAG with static final String TAG = "v" + 2; | Yes | concatenating constants is itself a constant expression |
case cap with final int cap; then cap = 7; | No | not initialised in its declaration, so not a constant: constant expression required |
case TAG with static final String TAG = "v".toUpperCase(); | No | a method call: constant string expression required |
case MARK with static final Character MARK = 'm'; | No | a wrapper is never a constant variable, even when final |
case 3, 5 and later case 5, 7 | No | duplicate case label |
case LOW or case Level.LOW on a Level enum | Yes | enum constants, plain or (since Java 21) qualified |
case Level.LOW on an Object selector | Yes | a qualified enum constant works on any selector type the enum is assignable to |
case LOW on an Object selector | No | the simple name resolves only when the selector's type is the enum itself: cannot find symbol |
On an enum selector, a label must be one of that enum's constants: a String spelling of the name, an ordinal number or the result of a method call such as valueOf is not accepted. A non-final variable is never a constant. A String switch compares with equals(), so case matters: "low" does not match "LOW".
Switch expressions: yield, exhaustiveness and result type
A switch expression is a switch that produces a value, so it can sit on the right of an assignment, in a return or as an argument — and the statement that contains it still ends with a semicolon. Every way through it must either produce a value or throw. An arrow arm produces the value of its expression; a block arm, and every colon group, produces a value with yield.
public class Main {
public static void main(String[] args) {
for (String day : new String[] {"MON", "FRI", "THU"}) {
int hours = switch (day) {
case "FRI" -> 6;
case "SUN" -> 0;
default -> {
int h = 4 * 2;
yield h;
}
};
char band = switch (day) {
case "MON":
System.out.print("[start]");
case "TUE", "WED":
yield 'E';
default:
yield 'L';
};
System.out.println(day + " " + hours + band);
}
}
}Output:
[start]MON 8E
FRI 6L
THU 8LThe second switch shows that colon groups still fall through inside a switch expression: "MON" prints [start] and falls into the group that yields 'E'. yield ends the switch expression with that value.
What cannot leave a switch expression
Control may not jump out of a switch expression other than by yield or throw. return inside an arm fails with attempt to return out of a switch expression; continue of an enclosing loop fails with attempt to continue out of a switch expression; and break fails with attempt to break out of a switch expression. A block arm whose last statement is neither yield nor throw, such as one ending in a bare expression, does not compile. yield is a contextual keyword: it means "produce this value" only inside a switch expression.
Exhaustiveness
A switch expression must cover every possible selector value; otherwise javac reports the switch expression does not cover all possible input values — it never silently yields null. Any of these makes it exhaustive:
- a
defaultlabel (orcase null, default); - an unguarded type pattern of the selector's own type (or a supertype), such as
case String otheron aStringselector orcase Object oon anObjectselector — it matches every non-null value, so nodefaultis needed; - for an enum, a label for every constant;
- for a sealed interface or class, a pattern for every permitted subtype.
public class Main {
static String reply(String answer) {
return switch (answer) {
case "y" -> "accepted";
case String other -> "ignored " + other;
};
}
public static void main(String[] args) {
System.out.println(reply("y") + ", " + reply("maybe"));
}
}This prints accepted, ignored maybe: the constant label is tried first and the unguarded case String other takes everything else. A guarded pattern (case String s when …) never counts towards exhaustiveness, because its guard may be false. The sealed form needs no default either:
public class Main {
sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double side) implements Shape {}
static double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.r() * c.r();
case Square q -> q.side() * q.side();
};
}
public static void main(String[] args) {
System.out.println(area(new Square(3)));
}
}This prints 9.0. If an enum later gains a constant, or a sealed hierarchy a subtype, and the switch is not recompiled, the unmatched value throws MatchException at run time. A null selector is a different case: it throws NullPointerException (next section).
Switch statements with only constant labels on int, String or enum selectors need not be exhaustive — values with no matching label simply run nothing. A switch statement that uses a pattern label or a case null label must be exhaustive, just like an expression.
The type of a switch expression
When the result is assigned to an explicit type, each arm must be assignable to it. When it is inferred (var, or used in a string concatenation), the type comes from the arms, using the same rules as the conditional operator ? ::
| Arms | Result type | Example result |
|---|---|---|
7 and 1.5f | float (numeric promotion) | the arm 7 prints 7.0 |
'a' and the constant 48 | char (an int constant that fits in char) | 48 prints as 0 |
'a' and the constant 70000 | int (the constant does not fit in char) | 'a' prints as 97 |
a Character variable and a short variable | int (unboxed, then promoted) | a Character holding 'A' prints 65 |
"n/a" and 4L | a common reference supertype; the number is boxed | the arm 4L is a Long at run time |
Every row was compiled and run on JDK 25. Only the arm that actually runs is evaluated, but its value is converted to the switch's type.
null selectors, case null and pattern labels
A switch whose selector is null throws NullPointerException unless it has a case null label — and default does not catch null. This holds for String, wrapper, enum and pattern switches, statements and expressions alike.
public class Main {
enum Fuel { DIESEL, PETROL }
static String tier(Integer pts) {
return switch (pts) {
case 0 -> "none";
case null, default -> "some or unknown";
};
}
public static void main(String[] args) {
System.out.println(tier(null) + " / " + tier(0));
Fuel f = null;
try {
switch (f) {
case DIESEL -> System.out.println("diesel");
default -> System.out.println("anything else");
}
} catch (NullPointerException e) {
System.out.println("NullPointerException");
}
}
}This prints some or unknown / none and then NullPointerException. Ways to write the null case:
case null -> …as its own label, alongside a separatedefault;case null, default -> …— the only combination that putsdefaultin a list, andnullmust come first.
A case null makes the switch an "enhanced" switch, so a switch statement on a String with one constant label and case null but no default fails: the switch statement does not cover all possible input values.
Type patterns and when guards
A type pattern label such as case Double d matches when the selector is an instance of that type and binds it to a new variable for that arm. A guard, when followed by a boolean expression, adds a condition. Labels are tried top to bottom, and the first one whose type and guard both match wins.
public class Main {
static String classify(Object o) {
return switch (o) {
case Double d when d < 0 -> "negative";
case Double d -> "double";
case Number n -> "number " + n.intValue();
case String s when s.isBlank() -> "blank";
case String s -> "text";
default -> "other";
};
}
public static void main(String[] args) {
System.out.println(classify(-1.5) + ", " + classify(2.0)
+ ", " + classify(7) + ", " + classify(" ")
+ ", " + classify('c'));
}
}This prints negative, double, number 7, blank, other. 7 is an Integer, which fails both Double labels and matches the broader Number pattern. 'c' is boxed to a Character, which is neither a Number nor a String, so it reaches default. A selector of type Object needs a default or an unguarded case Object x to be exhaustive. Pattern matching with instanceof and sealed hierarchies are covered further in the object-oriented lessons; here the focus is the switch.
Dominance and record patterns
A case label is dominated when an earlier label would always match first, so it could never run; that is a compile error, this case label is dominated by a preceding case label. The rules:
- An unguarded type pattern dominates a later pattern of the same type or a subtype:
case Number nbeforecase Double dfails; the reverse order compiles. - A guarded pattern dominates nothing that follows, so
case Double d when d > 0beforecase Double dcompiles — but an unguardedcase Double dbefore the guarded one does not. - An unguarded pattern also dominates later constant labels of its type: on a
Stringselector,case String tbeforecase "yes"fails. - A guarded pattern before a constant label is legal, and then order decides: a value that satisfies the earlier guard is taken by the pattern, even when a later constant label names that value exactly.
defaultdominates every pattern label after it:default ->followed bycase String sfails, so putdefaultafter the patterns. (Before plain constant labels its position is free.)
Record patterns
A record pattern deconstructs a record in the label itself: case Range(int lo, int hi) matches a Range and binds its components in declaration order. Each component pattern can be a type pattern, var x (type inferred from the component), the unnamed pattern _ (matches anything, binds nothing) or another record pattern, and the label can carry a when guard.
public class Main {
record Range(int lo, int hi) {}
record Tagged(String tag, Object value) {}
static String read(Object o) {
return switch (o) {
case Range(var lo, var hi) when hi < lo -> "empty";
case Range(int lo, _) when lo < 0 -> "signed";
case Range r -> "range";
case Tagged(var t, Range(var lo, var hi)) ->
t + ":" + (hi - lo);
case Tagged(String t, Integer n) -> t + "=" + n;
case Tagged(var t, _) -> t + "?";
default -> "other";
};
}
public static void main(String[] args) {
System.out.println(read(new Range(5, 1)) + " "
+ read(new Range(-2, 3)) + " "
+ read(new Range(0, 4)) + " "
+ read(new Tagged("w", new Range(2, 9))) + " "
+ read(new Tagged("n", 3)) + " "
+ read(new Tagged("x", 1.5)));
}
}This prints empty signed range w:7 n=3 x?: Range(5, 1) satisfies the first guard before the second is tried, and the nested Range inside the first Tagged is deconstructed in the same label. The rules for each label:
- It lists exactly as many component patterns as the record has components; one too few fails with incorrect number of nested patterns.
- Each component pattern must be applicable to that component's declared type. A reference type pattern on an
Objectcomponent, such asInteger n, is a run-time test: it matches only when the component's value is anInteger, otherwise matching moves on to the next label. - A primitive type pattern such as
int nis fine on anintcomponent, but on anObjectorIntegercomponent it is a primitive pattern, which is a preview feature in Java 25 — it does not compile (primitive patterns are a preview feature), and unboxing does not rescue it. WriteInteger ninstead. - Binding names cannot repeat within one label: reusing a name fails with illegal attempt to redefine an existing match binding.
while and do-while loops
A while loop tests its condition before each iteration, so its body may run zero times; a do-while loop tests after the body, so the body always runs at least once. A do-while ends with a semicolon after the condition.
public class Main {
public static void main(String[] args) {
int stock = 0;
do {
System.out.print("check ");
} while (stock > 0);
while (stock > 0) {
System.out.print("sell ");
}
int tries = 5;
while (--tries > 2)
System.out.print(tries);
System.out.print(" " + tries + " | ");
int left = 5;
while (left-- > 2)
System.out.print(left);
System.out.println(" " + left);
}
}This prints check 43 2 | 432 1. The do-while body runs once although stock > 0 is false, and the while body never runs. In --tries > 2 the decrement happens first and the comparison uses the new value, so the body sees 4 and 3, and the loop stops with tries at 2. In left-- > 2 the comparison uses the old value and the decrement happens anyway: the body sees 4, 3 and 2, and the final failing test (2 > 2) still decrements left to 1. Prefix and postfix forms in a condition shift both the printed values and the final value, so trace every test explicitly — including the last, failing one.
A variable declared inside a do block is out of scope in its while condition: a block that declares boolean again followed by while (again); fails with cannot find symbol. Declare the variable before the loop when the condition needs it.
for loops, enhanced for loops and loop-variable scope
A basic for loop has three parts: the initializer runs once, the condition is tested before every iteration (including the first), and the update runs after every completed body, followed by the next test. Any part may be empty, and for (;;) loops forever.
public class Main {
static int n = 3;
static boolean more() {
System.out.print("?");
return n > 1;
}
public static void main(String[] args) {
for (System.out.print("<"); more(); n--)
System.out.print(n);
System.out.println();
for (int lo = 1, hi = 9; lo <= hi; lo *= 2, hi--)
System.out.print(lo + "-" + hi + " ");
}
}The first loop prints <?3?2?: one initializer, then test-body-update twice, then a final failing test. The second prints 1-9 2-8 4-7 — after the third update lo is 8 and hi is 6, and 8 <= 6 is false. The initializer may declare several variables of one type (int lo = 1, hi = 9) or list expression statements; declaring variables of two different types in one initializer does not compile. The update may list several expression statements separated by commas. Changing the loop variable inside the body also changes what the update and the next test see, so trace the body's change and the update together on each pass.
Loop-variable scope
A variable declared in the initializer exists only inside the loop. After the loop ends, the same name can be declared again — in another loop or as an ordinary local. It cannot be declared while a local of the same name is still in scope: with int idx already declared in the method, for (int idx = 0; …) fails with variable idx is already defined, whereas an initializer that only assigns the existing variable (idx = 0, with no type) reuses it. A loop variable cannot be read after the loop either. A variable declared in the body is created fresh, with its initializer run again, on every iteration.
The enhanced for loop
The enhanced for loop, for (Type v : source), visits each element of an array or of any Iterable (a List, a Set …) in order. The loop variable holds a copy of each element. Reassigning the variable changes nothing in the array or collection. But when the element is a reference, the copy points at the same object, so changing that object — by a method call, or by assigning to one of its fields or, for an inner array, one of its elements — changes the object the array or collection holds.
public class Main {
static class Cell {
int v;
Cell(int v) { this.v = v; }
}
public static void main(String[] args) {
double[] prices = {1.5, 2.5};
for (double p : prices) {
p = p * 2;
}
Cell[] cells = {new Cell(1), new Cell(2)};
for (Cell c : cells) {
c.v += 5;
c = new Cell(0);
}
System.out.println(prices[0] + " " + prices[1] + " "
+ cells[0].v + " " + cells[1].v);
}
}This prints 1.5 2.5 6 7: doubling the copy p leaves the array alone, the assignment c.v += 5 writes through the copy into each shared Cell, and the later c = new Cell(0) only redirects the copy. The same applies to an array of arrays: assigning an element of the inner array changes the grid, assigning a new array to the loop variable does not.
The loop variable's declared type must accept each element: the element type, a supertype of it, or var (which infers the element type) all work, while an unrelated type does not compile. The source must be an array or an Iterable; an Iterator or a Stream is neither (for-each not applicable to expression type). The variable may be declared final, which forbids assigning to it in the body.
break, continue, labels and unreachable code
break ends the innermost enclosing loop or switch; continue skips the rest of the current iteration of the innermost loop and goes to its next test (in a for loop, the update runs first). A label (find:) names a statement so a nested break find leaves that statement, and continue find starts the labelled loop's next iteration.
public class Main {
public static void main(String[] args) {
int[][] grid = {{4, 1, 8}, {6, 2, 9}, {10}};
find:
for (int[] row : grid) {
for (int v : row) {
if (v % 2 == 1) continue find;
if (v < 3) break find;
System.out.print(v + " ");
}
}
System.out.println();
String out = "";
for (char ch : "go-on!".toCharArray()) {
switch (ch) {
case '-' -> {
continue;
}
case '!' -> {
break;
}
default -> out += Character.toUpperCase(ch);
}
out += "|";
}
System.out.println(out);
}
}Output: 4 6 and then G|O|O|N||. In the grid, the odd 1 sends control to the next row (so 8 is skipped), and the 2 in the second row ends both loops, so the last row is never visited. In the second loop, continue inside the switch skips straight to the next character, so '-' gets no bar, while break leaves only the switch, so '!' still gets its "|". The same is true in colon switches. Remember that continue (and break) can never jump out of a switch expression.
break labelworks with any labelled statement, including a plain block:guard: { … break guard; … }skips the rest of the block and carries on after it.continue labelneeds a labelled loop; using it with the label of a block, or of any other statement that is not a loop, fails with not a loop label.- A label is only visible inside the statement it labels. A
breakorcontinuenaming a label that does not enclose it fails with undefined label, even when that label exists elsewhere in the method.
Unreachable statements
javac rejects a statement it can prove can never run, with unreachable statement. For loops, the analysis treats only constant expressions specially; every other condition is assumed to be possibly true and possibly false.
| Code | Compiles? | Reason |
|---|---|---|
while (1 > 2) { x++; } | No | the condition is a constant expression that is false, so the body can never run |
for (; 3 < 2; ) { x++; } | No | same rule for a constant-false for condition |
final int LIMIT = 0; while (LIMIT > 0) { … } | No | LIMIT is a constant variable, so the condition is constant false |
boolean off = false; while (off) { … } | Yes | off is not final, so the condition is not constant |
if (1 > 2) { x++; } | Yes | if is deliberately exempt, to allow conditional compilation |
while (true) { if (x > 9) break; x++; } then a statement | Yes | the break makes the loop able to complete |
for (;;) { … }, while (true) { … } or do { … } while (true); with no break, then a statement | No | the loop can never complete normally, so the code after it is unreachable |
A break that only leaves a switch inside an infinite loop does not count — the loop still cannot complete, so a statement after it is unreachable. A return inside the body does not make the loop completable either. The same analysis decides whether a method with a return type needs a final return: a body that ends in an infinite loop with no break can never fall off the end, so it needs none, but a loop with a non-constant condition might finish, so the method then fails with missing return statement unless a return follows the loop.
Tip. Objective 2.1 is tested through short complete programs that ask what is printed, which lines compile or which statement can be inserted to make code compile, with 'Compilation fails' and runtime exceptions such as NullPointerException among the options. Items combine rules: fall-through from a default placed mid-switch, a dangling else hidden by indentation, a postfix increment inside a loop condition, break or continue inside a switch that sits in a loop, the result type of a switch expression whose arms mix char, int and double, or pattern labels whose order decides both the output and whether the switch compiles. Trace each construct exactly as the compiler and JVM do, check every label against the selector type, and look for exhaustiveness, dominance and reachability errors before computing any output.
- Without braces, an else binds to the nearest unmatched if, whatever the indentation, and only one statement belongs to each branch.
- Conditions must be boolean or Boolean; assigning an int in a condition does not compile, while assigning a boolean compiles and tests the assigned value.
- Colon labels fall through every following group, including default, until a break; default's position never changes which label matches.
- Arrow labels never fall through, take one expression statement, block or throw, and cannot be mixed with colon labels in one switch.
- A switch expression must yield or throw on every path and cannot be left with return, break or continue.
- A switch expression is exhaustive with a default, an unguarded pattern of the selector's own type, every enum constant, or every permitted subtype of a sealed type.
- Constant labels need compile-time constants that fit the selector type and never repeat; long, float, double and boolean selectors do not compile in Java 25.
- A null selector throws NullPointerException unless the switch has case null; default alone does not match null.
- Pattern labels are tried top to bottom; an unguarded pattern before a subtype or constant label of its type is a dominance error.
- Inside a switch in a loop, break leaves only the switch and continue goes to the loop's next iteration; continue label needs a loop label.
Frequently asked questions
What is the difference between a switch statement and a switch expression in Java?
A switch statement performs actions and produces no value, while a switch expression evaluates to a value that can be assigned, returned or passed as an argument. A switch expression must be exhaustive — through a default, an unguarded type pattern of the selector's own type, every constant of an enum, or every permitted subtype of a sealed type — and every path must yield a value or throw. Both forms can use colon labels or arrow labels, but a switch statement with only constant labels on an int, String or enum selector does not have to cover every value.
When do you use yield instead of an arrow expression in a switch?
An arrow arm such as case "FRI" -> 6 produces its expression's value directly. You need yield when the arm is a block of statements, as in default -> { int h = 4 * 2; yield h; }, and in every group of a colon-form switch expression, as in case "TUE": yield 'E';. yield ends the switch expression with that value; return, break and continue cannot be used to leave a switch expression.
Does default handle a null selector in a Java switch?
No. A switch whose selector is null throws a NullPointerException unless it has a case null label, even when it has a default. To handle null and every other unmatched value in one arm, write case null, default ->. A case null label also makes a switch statement enhanced, so it must then be exhaustive.
Which types can be used as a switch selector in Java 25?
With constant case labels, the selector can be char, byte, short, int, Character, Byte, Short, Integer, String or an enum. Any other reference type, such as Object or a sealed interface, can be switched on with pattern labels, or with qualified enum constants such as case Level.LOW when the enum type is assignable to the selector type. Selectors of type long, float, double or boolean do not compile in Java SE 25, because primitive patterns are still a preview feature.
Why does a case label give a 'constant expression required' error?
Constant case labels must be compile-time constants: literals, enum constants, or final variables of a primitive type or String that are initialised with a constant expression in their declaration. A final variable assigned on a later line, any non-final variable, a final wrapper such as a Character or Integer, and any value produced by a method call are not constants, so none of them can be used as a case label.
What does dominated by a preceding case label mean?
It means a label can never be selected because an earlier label always matches first. An unguarded pattern such as case Number n dominates a later case Double d, and on a String selector an unguarded case String t dominates a later constant such as case "yes". The fix is to put the more specific label first. A guarded pattern, such as case Double d when d > 0, does not dominate the labels after it, and default placed before a pattern label dominates that label.
Does break inside a switch exit the enclosing loop?
No. An unlabelled break inside a switch leaves only the switch, and the loop continues with the statement after the switch. To leave the loop from inside the switch, label the loop and use break label, or return. An unlabelled continue inside a switch statement does go to the enclosing loop's next iteration, because a switch is not a loop. Neither can be used to jump out of a switch expression.
Why does code after while (true) not compile?
javac treats a statement it can prove never runs as an unreachable-statement error. A while (true), for (;;) or do { } while (true) loop with no break that exits the loop can never complete, so the statement after it is unreachable. A break that only leaves a switch inside the loop does not count. Likewise, a while or for loop whose condition is a constant expression that is false makes its body unreachable, whereas if with a constant-false condition is allowed.
Source
This lesson covers the "Implementing Program Flow Control Using Decision and Looping Constructs" domain of the official 1Z0-831 exam guide. Vendors revise their guides — check the source for the current version.
- Java SE 25 Developer Professional (1Z0-831) exam page and topics — Oracle University
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.