Java Control Flow: if, for, while, switch
Every branching and looping construct in modern Java, including switch expressions and pattern matching, with examples that mirror real code.
What you'll learn
- ✓Write idiomatic if, else if, and ternary expressions
- ✓Choose between for, while, do while, and for each loops
- ✓Use modern switch expressions with arrow labels and yield
- ✓Apply pattern matching in switch (Java 21)
- ✓Break out of nested loops cleanly with labels
Prerequisites
Control flow in Java has quietly modernized. Switch is no longer the verbose fall through trap it used to be, and patterns now let you replace chains of instanceof with a single expression. If you learned Java a decade ago, half of this article will be new.
if and else
Boolean conditions only. There is no truthiness; if (someString) will not compile.
int score = 87;
if (score >= 90) {
System.out.println("A");
} else if (score >= 80) {
System.out.println("B");
} else {
System.out.println("C or lower");
}
For a single value selection, the ternary is cleaner:
String grade = score >= 90 ? "A" : score >= 80 ? "B" : "C";
Chain ternaries sparingly. If you need more than two, prefer a switch expression.
Classic for loop
Use when you need the index.
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
You can declare multiple loop variables and use commas:
for (int i = 0, j = 10; i < j; i++, j--) {
System.out.println(i + " " + j);
}
Enhanced for (for each)
The right default for iterating arrays and collections.
String[] langs = {"Java", "Kotlin", "Scala"};
for (String lang : langs) {
System.out.println(lang);
}
It works on anything that implements Iterable. You lose the index; if you need it, fall back to a classic for or use IntStream.range.
while and do while
while checks the condition first. do while runs the body at least once.
int n = 10;
while (n > 0) {
n /= 2;
}
int input;
do {
input = readInput();
} while (input != 0);
break and continue
break exits the innermost loop. continue skips to the next iteration. For nested loops, labels let you target an outer loop without flag variables.
outer:
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
if (i * j > 6) {
break outer;
}
System.out.println(i + "," + j);
}
}
Use labels sparingly. If you reach for them often, the loop probably wants to be extracted into a method that returns early.
switch statements (the old way)
The legacy form uses colons and falls through unless you write break:
int day = 3;
switch (day) {
case 1:
System.out.println("Mon");
break;
case 2:
System.out.println("Tue");
break;
default:
System.out.println("Other");
}
Forgetting break is a classic bug. Modern Java has a better form.
switch expressions
Since Java 14, switch can be an expression that returns a value. Use the arrow form and there is no fall through.
String name = switch (day) {
case 1 -> "Mon";
case 2 -> "Tue";
case 3, 4, 5 -> "midweek";
default -> "weekend";
};
Multiple labels on one arm, a single value per arm, exhaustive by default for enums. For multi statement arms, use a block with yield:
int code = switch (day) {
case 1, 2, 3, 4, 5 -> 1;
case 6, 7 -> {
System.out.println("weekend!");
yield 2;
}
default -> 0;
};
Pattern matching in switch
Java 21 lets switch match on type and shape. This replaces instanceof chains.
sealed interface Shape permits Circle, Square, Rect {}
record Circle(double r) implements Shape {}
record Square(double s) implements Shape {}
record Rect(double w, double h) implements Shape {}
static double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.r() * c.r();
case Square sq -> sq.s() * sq.s();
case Rect r -> r.w() * r.h();
};
}
With sealed types, the compiler verifies exhaustiveness. Add a new permitted subtype and any non exhaustive switch fails to compile. That is a refactoring superpower.
You can also guard a pattern with when:
String classify(Object o) {
return switch (o) {
case Integer i when i < 0 -> "negative int";
case Integer i -> "non-negative int";
case String s -> "string: " + s;
case null -> "null";
default -> "other";
};
}
instanceof with pattern binding
Outside of switch, the pattern form of instanceof binds the variable in one step:
Object o = "hello";
if (o instanceof String s && !s.isEmpty()) {
System.out.println(s.toUpperCase());
}
No more cast, no more redundant variable.
Early return over deep nesting
Java has no problem with multiple return statements. Use them to flatten code.
boolean isValid(User u) {
if (u == null) return false;
if (u.name() == null) return false;
if (u.age() < 0) return false;
return true;
}
A wall of guard clauses is easier to read than a triangle of nested if blocks.
Wrap up
Default to for each, reach for the arrow switch whenever you select on a value, and let pattern matching plus sealed types make illegal states unrepresentable. Next, learn how to organize behavior in Java Classes and Objects.