Java

Control Flow in Java: if, switch, Loops, and Modern switch Expressions

Executive Summary

Control flow in Java gives you four shapes: sequence, branch (if and else), loop (while, do-while, for, enhanced for), and selection (switch). Use guard clauses to keep branches flat, and use the classic for when you need an index and the enhanced for when you do not. break exits a loop, continue skips a pass, and labeled break escapes nested loops only as a last resort. For all new code, prefer switch expressions with arrow labels: no fall-through, a value you can assign, and exhaustiveness the compiler enforces on enums. Java 21 adds pattern matching for switch, which branches on type and shape and pairs with records and sealed types. Choose the shape that states the intent, and the compiler will catch more of your mistakes before runtime.

The Shapes of Control Flow in Java

Every Java method reduces to four shapes: do this, do this if, do this until, do this per option. Sequence, branch, loop, and selection. Once you see programs as these shapes composed together, control flow stops being syntax and starts being design. If you met if statements and loops first in the Java Mini Series post on methods and control flow, this article goes further: switch expressions, labels, and the off-by-one bug.

SEQUENCE        BRANCH              LOOP              SELECTION
--------        ------              ----              ---------
statement       if (c) {            while (c) {       switch (v) {
                ...;                ...;                case a -> ...;
execute           } else {             }                }
next               ...;
                }

one path        two or more         zero or more      one of many
                paths               passes            labeled paths

Branching with if, else, and else if

The if statement runs a block when its boolean condition is true. Chain conditions with else if, and close with else when a default path exists. Java checks from the top and runs the first match only.

int score = 87;

if (score >= 90) {
    System.out.println("A");
} else if (score >= 80) {
    System.out.println("B");
} else {
    System.out.println("keep practicing");
}

Always write braces, even for one-line bodies. Moreover, prefer guard clauses over nesting: check the failure paths first and return early, and the happy path stays flat.

// Nested: hard to read
if (user != null) {
    if (user.isActive()) {
        process(user);
    }
}

// Guard clauses: flat
if (user == null) return;
if (!user.isActive()) return;
process(user);

The ternary operator is the expression form of if: String label = (score >= 60) ? “pass” : “fail”;. Use it for simple value selection, never for side effects.

while and do-while: Loop Until a Condition Changes

A while loop checks its condition before each pass, so a false condition on entry means the body never runs. A do-while checks after the body, so it always runs at least once, which fits menus and retries.

int fuel = 3;
while (fuel > 0) {
    System.out.println("burning, " + fuel + " left");
    fuel--;                       // the loop must change something
}
System.out.println("tank empty");
var scanner = new java.util.Scanner(System.in);
int choice;
do {
    System.out.print("Enter 1 to play, 0 to quit: ");
    choice = scanner.nextInt();
} while (choice != 0);
System.out.println("bye");

Every while loop must move toward termination: change a variable, consume input, or break. Otherwise you wrote a CPU warmer, and production will find it.

for and the Enhanced for

The classic for loop bundles three parts on one line: initialize, test, update. It is the natural fit when you know the pass count or need the index. The enhanced for iterates any array or collection and removes the bookkeeping.

int[] scores = {72, 88, 95, 61};

for (int i = 0; i < scores.length; i++) {      // note: <, not <=
    System.out.println("run " + i + ": " + scores[i]);
}

for (int score : scores) {                     // enhanced for: no index
    if (score >= 90) System.out.println("top score: " + score);
}

However, the enhanced for hides the index, so use the classic form whenever you need the position. Assigning to the loop variable changes nothing: score = 0 inside the loop leaves the array untouched, because score is a copy of the element.

break, continue, and Labels

break exits the nearest loop immediately. continue skips the rest of the current pass and jumps to the next check. Both change the flow, and both earn their keep in small doses.

for (int i = 1; i <= 100; i++) {
    if (i % 3 == 0 && i % 5 == 0) continue;   // skip multiples of 15
    if (i > 20) break;                          // stop early
    System.out.println(i);
}

Nested loops need labels when break must escape more than one level. In practice, reach for a label only after considering a method extraction; labels read like an apology for the design.

outer:
for (int row = 0; row < 3; row++) {
    for (int col = 0; col < 3; col++) {
        if (row * col == 4) break outer;   // exits both loops
    }
}

The Off-by-One Bug, Up Close

Wrong code first. This is the most common loop bug in beginner and production Java alike:

// WRONG: the index runs one step too far
int[] scores = {72, 88, 95};
for (int i = 0; i <= scores.length; i++) {
    System.out.println(scores[i]);      // crashes on the last pass
}
// Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException:
// Index 3 out of bounds for length 3
// RIGHT: stop before the length
for (int i = 0; i < scores.length; i++) {
    System.out.println(scores[i]);
}

The delta is one character: <= becomes <. Indexes start at 0, so the last valid index is length – 1. Read the exception message slowly: it names the bad index and the real length, which is the fastest diagnosis you will ever get.

Modern switch Expressions

The colon-form switch statement is a legacy shape with two traps: silent fall-through between cases, and a missing default that quietly does nothing. Java 14 replaced it for new code with switch expressions: arrow labels, no fall-through, and a value you can assign. The syntax comes from JEP 361.

int month = 2;
int days = switch (month) {
    case 1, 3, 5, 7, 8, 10, 12 -> 31;
    case 4, 6, 9, 11           -> 30;
    case 2                     -> 28;   // ignore leap years for now
    default                    -> throw new IllegalArgumentException();
};
System.out.println(days);   // 28

When a case needs multiple statements, use a block and yield the result:

int days = switch (month) {
    case 2 -> {
        boolean leap = false;   // stand-in for real leap-year logic
        yield leap ? 29 : 28;
    }
    default -> 30;
};

On enums, the compiler enforces exhaustiveness. Cover every constant and you may omit default; add a constant later and every switch that misses it fails to compile. That is the compiler catching a missing case in every file at once.

enum Direction { UP, DOWN, LEFT, RIGHT }

String arrow = switch (Direction.UP) {
    case UP    -> "^";
    case DOWN  -> "v";
    case LEFT  -> "<";
    case RIGHT -> ">";
};   // exhaustive: no default needed
Feature switch statement (colon form) switch expression (arrow form)
Fall-through Yes, between labels Never
Produces a value No Yes, assign it directly
Exhaustiveness default optional Required unless all enum constants are covered
Multiple labels case A: case B: case A, B ->
Best for Reading legacy code All new code

Pattern Matching for switch: A First Look

Java 21 finalized pattern matching for switch through JEP 441, so a switch can now branch on the type and shape of data, guard cases with when, and match null explicitly. It pairs naturally with records and sealed types, which is why the course builds toward it in the sealed classes and pattern matching article and the modern Java features article.

Object input = "hello";

String described = switch (input) {
    case Integer i when i > 0 -> "positive int: " + i;
    case String s          -> "string of length " + s.length();
    case null              -> "it was null";
    default                -> "something else";
};
System.out.println(described);   // string of length 5

How Real Systems Do This

Order and payment systems are full of state machines, and modern Java teams implement them as an enum plus a switch expression. The exhaustive switch means the compiler enforces the state matrix: adding a REFUNDED state without handling it everywhere fails the build, not the customer. By contrast, the legacy colon-form switch hid two such gaps in one codebase I inherited, and one fell-through case sent a duplicate confirmation email to every customer in the batch.

That duplicate-email bug is a good scar. The fix was one word, however the lesson was structural: the arrow form makes the wrong thing hard to write. In my experience converting a 400-line if-else pyramid in a pricing service to enum-based switch expressions removed 200 lines and turned a missing discount case into a compile error instead of a silent zero.

Parsing code still leans on while: read lines with hasNextLine(), consume bytes while a buffer has room, retry while attempts remain. The loop shape you choose is the first thing a reviewer reads, so make it honest. The full statement rules live in the JLS chapter on blocks and statements.

Decision Framework

  1. Is the branch driven by boolean conditions or ranges? Use if with guard clauses.
  2. Is it driven by one value with three or more cases? Use a switch expression with arrow labels.
  3. Is it driven by the type or shape of the data? Use pattern matching for switch in Java 21.
  4. Do you know the iteration count, or are you walking a collection? Use the classic for or the enhanced for.
  5. Is the end defined by a condition rather than a count? Use while, or do-while when the body must run at least once.
  6. Do you need to escape nested loops? Extract a method first; use labeled break only as a last resort.

When NOT to Use This

  • Do not branch on type codes when polymorphism fits. A switch over kind == 1, 2, 3 with per-kind behavior belongs in Part 2’s inheritance and interfaces, where the compiler carries the dispatch.
  • Do not build state machines out of if-else pyramids. Model states as an enum and transitions as a switch expression; the shape stays visible and exhaustive.
  • Do not hide exit conditions behind break and continue. If a reader cannot tell when a loop stops, restructure it. A boolean flag plus three breaks is a design smell, not a style choice.

Common Mistakes

  • Using <= where < belongs. Array indexes end at length – 1, and the exception message names the exact bad index.
  • Fall-through in colon-form switch: a missing break processes two cases in a row, silently, in production.
  • while (true) with no exit change: the loop spins forever and eats a core while logs pile up.
  • Looping on double equality: i != 1.0 never becomes false when 0.1 steps cannot land exactly on 1.0.
  • Switching on a null selector: the classic switch throws NullPointerException before the first case; the pattern form can match null explicitly.
  • Assigning to the enhanced-for variable and expecting the array to change: the loop variable is a copy, not an alias.

Key Takeaways

  • Four shapes cover everything: sequence, branch, loop, and selection. Recognize the shape before you write the syntax.
  • if with guard clauses keeps branches flat; the ternary operator selects values, never effects.
  • Use the classic for when you need the index, the enhanced for when you only need each element.
  • while checks before the pass; do-while checks after, so its body always runs once.
  • break exits, continue skips, and labeled break is a last resort for nested escapes.
  • Prefer switch expressions with arrow labels in all new code: no fall-through, assignable values, enforced exhaustiveness.
  • Java 21 pattern matching for switch branches on type and shape, with when guards and explicit null cases.

FAQ

Should I use if-else or switch in Java?

Use if for boolean conditions and ranges. Use a switch expression when one value selects among three or more cases: the arrow form is shorter, cannot fall through, and must be exhaustive.

What is the difference between while and do-while in Java?

while tests the condition before the body, so the body can run zero times. do-while tests after the body, so it always runs at least once, which suits menus and retry prompts.

Can you use String in a switch statement in Java?

Yes, since Java 7, alongside int, char, byte, short, their wrapper types, and enum constants. The classic form does not accept long, float, or double selectors; pattern matching for switch extends selection to reference types by branching on the value’s shape.

What is yield in Java switch expressions?

yield returns the value from a case that needs a block of statements: case 2 -> { … yield 29; }. A case with a single expression needs no yield at all.

How do I break out of nested loops in Java?

Name the outer loop with a label and write break outer. Better, extract the nested loop into a method and return, because labels and control-flow jumps read worse over time.

Conclusion

Control flow is the shape of your program: branches pick paths, loops repeat them, and the syntax you choose decides how honestly that shape reads. Modern switch expressions and guard clauses do most of the work nesting used to do, and the compiler now catches the gaps.

Flat beats nested, exhaustive beats defaulted, and the compiler beats the debugger. Write loops that say when they stop, and branches that say why they fire.

Last updated on 2 September 2026.

Share this article

Leave a Reply

Your email address will not be published. Required fields are marked *