Control Flow: if/switch/loops (with the modern switch)
Every program does two things: it makes decisions, and it repeats work. Control flow is the part of Java that handles both. This post covers the complete toolkit — if, the ternary operator, the classic and modern forms of switch, and every loop Java offers — with one rule guiding everything: learn the modern form first, and learn the old form only well enough to read the legacy code you will inevitably inherit.
We assume you know types, operators, and var from posts 01–02. After this post, post 04 will put these tools inside objects, where they really live.
if, else if, else: decisions in order
An if runs a block when a condition is true. Chain else if after it for alternatives, and finish with else for "everything else." The branches are checked top to bottom, and the first true condition wins — so order matters.
public class GradeCheck {
public static void main(String[] args) {
int score = 87;
if (score >= 90) {
System.out.println("A");
} else if (score >= 80) {
System.out.println("B");
} else if (score >= 70) {
System.out.println("C");
} else {
System.out.println("Needs work");
}
}
}
Output:
B
Notice the conditions go from most specific to least specific. If you reversed the order and checked score >= 70 first, a score of 95 would print "C" — the first true branch wins, so a sloppy order silently gives the wrong answer. Three decision rules to carry with you:
- Order conditions from narrowest to widest. Think "which branch would steal another branch's values?"
- Always use braces, even for one-line bodies. The most infamous bug in this post exists because someone skipped them (see the traps section).
- Keep nesting to two levels. If your
ifis inside anifinside anif, the logic is asking to be moved into its own method. Post 04 shows you how.
The ternary operator: one line, one decision
When an if/else exists only to pick one of two values, the ternary operator compresses it:
int score = 87;
String status = (score >= 60) ? "Pass" : "Fail";
System.out.println(status); // Pass
Read it as: "condition ? value-if-true : value-if-false." The decision rule is about what the ternary is for: it picks a value. It is not a shorthand for doing work — don't bury method calls with side effects inside it, and never nest it:
// A smell — don't write this:
String gradeNested = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F";
// Readable — write this:
String grade;
if (score >= 90) grade = "A";
else if (score >= 80) grade = "B";
else if (score >= 70) grade = "C";
else grade = "F";
The nested version "works," but it forces every reader to mentally parse a chain of punctuation to find the boundaries. One ternary, one decision — that's the limit. Anything more belongs in if/else or a switch.
The classic switch: read it, don't write it
You will meet this form in older codebases, so learn to read it. A classic switch matches a value against case labels written with colons:
int day = 3;
String dayName;
switch (day) {
case 1:
dayName = "Monday";
break;
case 2:
dayName = "Tuesday";
break;
case 3:
dayName = "Wednesday";
break;
default:
dayName = "Unknown";
}
System.out.println(dayName); // Wednesday
The critical detail: after a matching case, execution falls through into the next case unless it hits a break. Delete the break after case 3 and dayName becomes "Unknown" — the default block runs too, even though day is 3. This fall-through behavior is the single biggest source of switch bugs in legacy Java, and it's exactly why the modern form exists.
One legitimate use survives in the old form: deliberately grouping cases that share one outcome. If you ever see it, it should carry a comment:
switch (day) {
case 6:
case 7:
// fall through: both days are weekends
weekend = true;
break;
default:
weekend = false;
}
The modern switch: the default you should write
Since Java 14, switch has a modern form with arrow labels (->). Two upgrades come with it: no fall-through is possible, and switch can be used as an expression that produces a value. This is the form to reach for in all new code:
int day = 3;
String dayName = switch (day) {
case 1 -> "Monday";
case 2 -> "Tuesday";
case 3 -> "Wednesday";
default -> "Unknown";
};
System.out.println(dayName); // Wednesday
Notice what changed: the value is assigned directly from the switch, each case maps to exactly one outcome, and there is nothing to forget — no break needed because arrows never fall through. Cases can also share an arm with commas instead of stacked labels:
String label = switch (day) {
case 1, 2, 3, 4, 5 -> "Weekday";
case 6, 7 -> "Weekend";
default -> "Unknown";
};
When one arm needs several statements, wrap it in braces and hand the value back with yield:
int day = 3;
String label = switch (day) {
case 1, 2, 3, 4, 5 -> "Weekday";
case 6, 7 -> {
String kind = (day == 6) ? "Saturday" : "Sunday";
yield "Weekend (" + kind + ")";
}
default -> throw new IllegalArgumentException("Not a day: " + day);
};
System.out.println(label); // Weekday
Two things to notice there: a block arm uses yield (not return) to produce the arm's value, and default can throw an exception directly — handy when "unknown" should be loud instead of silent.
The decision rule: use a switch expression when one input maps to one of a fixed set of outcomes — day numbers to names, status codes to messages, enum constants to handlers. If the branches do unrelated work with different shapes, that's an if/else chain.
Loops: for, while, do-while
A for loop is for when you know how many times to repeat, or you need the index as you go:
for (int i = 0; i < 5; i++) {
System.out.println("Attempt " + (i + 1));
}
Output:
Attempt 1
Attempt 2
Attempt 3
Attempt 4
Attempt 5
The three parts of the header — initialize; condition; update — are the whole contract: start at 0, keep going while i < 5, add 1 each round. A while loop drops the counter and keeps only the condition, which fits situations where you don't know the count up front:
int n = 5;
while (n > 0) {
System.out.print(n + " ");
n--;
}
System.out.println("liftoff!");
Output:
5 4 3 2 1 liftoff!
A do-while checks the condition after the body, so the body always runs at least once. That's the right shape for menus and input validation, where you must ask before you can check the answer:
int choice;
do {
System.out.println("1. Retry 2. Quit");
choice = readChoice(); // returns what the user typed
} while (choice != 2);
Choose by answering two questions:
- Do I know the count, or need the index? →
for. - Must the body run at least once? →
do-while. Otherwise →while.
And a warning that belongs to every loop: if the condition can never become false, the loop never ends. Always make sure something inside a while moves the condition toward false — the n-- above is doing that job.
The enhanced for loop: iterate without the index
Most of the time you don't need the index at all — you just want every element. The enhanced for (for-each) is the default choice for that:
String[] cities = {"Pune", "Mumbai", "Delhi"};
for (String city : cities) {
System.out.println(city);
}
Output:
Pune
Mumbai
Delhi
Read it as "for each city in cities." Two limits to know: you don't get the position (use a classic for when the index matters), and you can't structurally change the collection while the loop runs — removing an element mid-loop throws ConcurrentModificationException.
break and continue
break exits the loop entirely; continue skips the rest of the current round and starts the next one:
for (int i = 1; i <= 10; i++) {
if (i % 2 == 0) continue; // skip evens
if (i > 7) break; // stop after 7
System.out.print(i + " ");
}
// Output: 1 3 5 7
The decision rule: use them to keep loop bodies flat — a continue that skips uninteresting cases beats another level of nesting. One line on labels: Java also has labeled break/continue (break outer;) for escaping nested loops — it exists, and you'll almost never need it.
Four traps that catch everyone
- Off-by-one errors. Arrays are 0-based, so the last valid index is
length - 1. The idiomi < items.lengthis correct;i <= items.lengthcrashes on the final trip with anArrayIndexOutOfBoundsException. When a loop misbehaves at the boundary, check the comparison operator first. - The accidental empty statement. A semicolon right after the condition ends the
if— and the compiler accepts it silently:
The block below always executes. This is why the "always use braces" rule exists: braces don't prevent the stray semicolon, but they make it visually obvious.boolean ready = false; if (ready); // the ; ends the if — this compiles fine { launch(); // ...and runs no matter what ready is } - Fall-through in legacy code. When you read an old colon-style
switchand acasehas nobreak, don't "fix" it on reflex — check whether the fall-through is intentional (the grouped weekend/weekday pattern). Missingbreakis a bug when it's accidental and a feature when it's commented; the comment is what tells you which. - Modifying a collection while iterating it. This throws
ConcurrentModificationExceptionat runtime:
The rule is simple: never structurally modify a collection inside its own for-each loop. Post 11 (collections) shows the safe alternatives —List<String> names = new ArrayList<>(List.of("Asha", "Ravi", "Meera")); for (String n : names) { if (n.startsWith("R")) names.remove(n); // boom at runtime }Iterator.remove()andremoveIf.
What to take into post 04
- Default to the modern switch expression (arrows,
yield) whenever one input maps to one of a fixed set of outcomes. Read the colon form; don't write it. - Braces on every
if, ternaries for single value picks only, nesting kept shallow. - Pick loops by the question: know the count or need the index →
for; just need every element → for-each; count unknown →while; must run once →do-while. breakexits,continueskips a round — both keep loop bodies flat.
Post 04 puts all of this inside objects: classes, fields, and methods are where control flow earns its keep.
Continue: Java Learning Roadmap 2026
Comments
Post a Comment