Home
Java from First Principles / Chapter 13 — Control Flow and Operators

Control Flow and Operators

if, while, for, switch — plus the modern switch expression and pattern matching. Operators and the precedence rules nobody memorises.


The basics nobody googles

If you've programmed in any C-family language, Java's control flow looks familiar. Curly braces. Semicolons. if, while, for, switch. Most developers learn this in their first hour with Java and never look back.

This chapter covers the basics quickly, then spends most of its time on the parts that have actually changed: the modern switch expression, pattern matching, and a few operator gotchas that catch even experienced developers.

If you've never written a line of Java, read it all. If you've written Java for years, skim the first few sections and pay attention to the modern switch parts — they're newer than you might realise.


if / else

The classic two-branch conditional:

Java
if (age >= 18) {
    grantAccess();
} else {
    deny();
}

Chained for multiple conditions:

Java
if (score >= 90) {
    return "A";
} else if (score >= 80) {
    return "B";
} else if (score >= 70) {
    return "C";
} else {
    return "F";
}

The braces are optional for single-statement bodies but **always include them**. The "no braces" form is responsible for entire categories of bugs:

Java
// Don't do this
if (admin)
    grantAccess();
    audit();        // runs unconditionally — indentation lied to you

Modern style guides (Google's, the JDK's own) require braces always. Your IDE will format them in. Just leave them.

**Ternary operator** for simple inline branches:

Java
String label = age >= 18 ? "adult" : "minor";

Useful for short value-producing expressions. Don't nest them — chained ternaries are unreadable. Use if/else if/else for anything beyond two branches.


Loops: while, do-while, for, for-each

**while** — runs as long as a condition holds:

Java
while (queue.hasNext()) {
    process(queue.poll());
}

**do-while** — runs the body once unconditionally, then checks:

Java
String input;
do {
    input = prompt();
} while (!isValid(input));

Rare in modern code. Useful when you must execute at least once.

**for (classic three-part)** — initialise, condition, step:

Java
for (int i = 0; i < items.length; i++) {
    process(items[i]);
}

Still useful when you need the index. Otherwise prefer the for-each form below.

**for-each (enhanced for)** — iterates over any Iterable or array:

Java
for (String name : names) {
    System.out.println(name);
}

Cleaner, no off-by-one risk, can't accidentally modify the index. Use this by default.

**break and continue.** break exits the loop; continue skips to the next iteration. Use sparingly — heavy reliance on break/continue makes loops harder to reason about. Often a sign that the loop should be refactored or replaced with a stream.

**Labelled break** for breaking out of nested loops:

Java
outer:
for (int i = 0; i < rows; i++) {
    for (int j = 0; j < cols; j++) {
        if (grid[i][j] == TARGET) {
            found = true;
            break outer;
        }
    }
}

Used rarely. When you find yourself reaching for it, consider whether extracting the loop into a method (with a normal return) is cleaner.


switch statement — the classic and the modern

The classic switch is a multi-branch comparison against a single value:

Java
switch (day) {
    case "MONDAY":
        startWeek();
        break;
    case "FRIDAY":
        startWeekend();
        break;
    default:
        normalDay();
        break;
}

Two famous bugs hide in the classic form:

**1. Fall-through.** If you forget break, control "falls through" to the next case:

Java
switch (level) {
    case "WARN":
        log.warn(msg);
        // no break — falls through to ERROR
    case "ERROR":
        alertOps();
        break;
}

A WARN logs the message AND alerts ops, which probably isn't what you wanted. Fall-through is occasionally useful (combining cases) but most of the time it's a bug.

**2. Variable scope.** Classic switch shares one scope across all cases. You can't declare String result = ... in one case and use the same name in another. Awkward and error-prone.

Both bugs are gone in the modern switch expression (Java 14+), which we cover next.


Switch expression (modern Java)

Java 14 (released 2020) added the **switch expression** — switch as a value-producing expression with cleaner syntax.

Java
String label = switch (day) {
    case "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY" -> "weekday";
    case "SATURDAY", "SUNDAY" -> "weekend";
    default -> throw new IllegalArgumentException("unknown day: " + day);
};

Differences from the classic form:

For multi-statement branches, use a yield:

Java
String description = switch (level) {
    case LOW -> "okay";
    case MEDIUM -> "monitor closely";
    case HIGH -> {
        alert();
        notifyOnCall();
        yield "urgent attention required";
    }
};

The classic statement form still works (you'll see it constantly in older codebases). For new code, the expression form is almost always better — safer and more concise.

**Switching on type** (Java 21+): pattern matching for switch lets you switch on an object's type:

Java
Object obj = ...;
String description = switch (obj) {
    case Integer i -> "integer: " + i;
    case String s  -> "string of length " + s.length();
    case List<?> l -> "list with " + l.size() + " elements";
    case null      -> "it's null";
    default        -> "something else";
};

This is much cleaner than a chain of instanceof checks. Combined with sealed types (also Java 17+), it enables Java to handle ADT-style code that used to require Scala or Kotlin.


Operators that bite

Three operator situations that catch even experienced Java developers.

**1. Integer overflow.**

Java
int a = 2_000_000_000;
int b = 2_000_000_000;
int sum = a + b;          // -294967296 — silent overflow
long correct = (long) a + b;   // 4_000_000_000 — cast widens first

int is 32-bit. Adding two large positives can wrap to negative. The JVM doesn't throw — it silently wraps. Use long for anything that might exceed ~2 billion. For absolute safety: Math.addExact(a, b) throws on overflow.

**2. Integer division.**

Java
int half = 5 / 2;         // 2, not 2.5
double rate = 1 / 3;      // 0.0 — integer division produces 0, then widens to double
double correct = 1.0 / 3;  // 0.333...

Division of two ints produces an int. Cast one operand to floating-point if you want a fractional result.

**3. Short-circuit evaluation.**

Java
if (user != null && user.isActive()) { ... }   // short-circuits — user.isActive() only runs if user != null
if (user != null & user.isActive()) { ... }    // BOTH always evaluate — NPE risk if user is null

&& and || short-circuit — they stop evaluating as soon as the result is determined. & and | (single character) are bitwise operators that always evaluate both sides. Mixing them up causes NPEs.

**Operator precedence.** Java mostly follows C-family rules. The ones worth remembering:

When in doubt, add parentheses. They cost nothing and prevent bugs.


var, instanceof patterns, and other modern conveniences

Briefly, the modern Java additions that change how control flow looks.

**var for local variable type inference** (Java 10+):

Java
var users = userRepo.findActive();        // type inferred from return
for (var user : users) {
    var account = user.getAccount();       // inferred
    process(account);
}

Don't use var for things the reader needs the type to understand. Use it when the type is obvious from the right-hand side.

**Pattern matching for instanceof** (Java 16+):

Java
if (obj instanceof String s) {
    // s is in scope here, already cast
    System.out.println(s.length());
}

Replaces the older clunky form:

Java
if (obj instanceof String) {
    String s = (String) obj;     // separate declaration and cast
    System.out.println(s.length());
}

**Text blocks** for multi-line strings:

Java
String html = """
    <div>
        <p>Hello</p>
    </div>
    """;

These aren't strictly control-flow features, but they show up in any sufficiently modern code. The combined effect: Java written for Java 21 looks dramatically less verbose than Java written for Java 8.


⁂ Back to all modules