if, switch expressions and loops
Control flow is the part of a language you think you already know. if, loops,
switch — every language has them and they mostly look the same.
Java's differ in two ways that matter. Its old switch has a default behaviour
that is almost never what you want, and its new one fixes that while also
becoming an expression. Knowing both is not optional, because you will write the
new form and read the old one.
if, and the brace habit
if (totalPaise >= 50_000) {
System.out.println("Free delivery");
} else if (distanceMetres <= 3_000) {
System.out.println("Delivery Rs 20");
} else {
System.out.println("Delivery Rs 20 plus Rs 8 per extra km");
}
The condition must be a boolean. Java will not accept a number, so C's
if (count) meaning "count is non-zero" does not compile — a small mercy, and
one reason Java has fewer of these bugs than C.
It does not save you from this, though:
boolean delivered = false;
if (delivered = true) {
System.out.println("this branch runs, and delivered is now " + delivered);
}
this branch runs, and delivered is now true
One = instead of two. Because the assignment's value is a boolean, it is a
legal condition, the branch always runs, and the variable is quietly overwritten.
IntelliJ warns about it. Take the warning seriously.
Always use braces, even for one statement. if (x) doThing(); is legal and
has caused famous security bugs, because the next person adds a second line,
indents it to match, and it is not inside the if at all.
The old switch, and why it bites
switch (plan) {
case "veg":
System.out.println(plan + " -> Rs 82.35");
case "jain":
System.out.println(plan + " -> Rs 91.00");
break;
default:
System.out.println(plan + " -> unknown");
}
Run it with "veg", "jain" and "eggs":
veg -> Rs 82.35
veg -> Rs 91.00
jain -> Rs 91.00
eggs -> unknown
The first customer was charged twice. There is no break after the veg case,
so execution falls through into the next one and keeps going until it meets a
break or the end.
This is the default. Every case needs a break that you have to remember, which
means a missing one is a silent bug rather than an error. Occasionally
fall-through is deliberate — stacking case "sat": case "sun": to share a body —
and that legitimate use is why the language cannot simply change the behaviour.
When you write an old-style switch, every case ends with break or
return. When you read one, check each case for a missing break before
assuming it is correct.
The modern switch
Java 14 made switch an expression with arrow labels, and it removes the whole
problem:
String kind = switch (day) {
case 1, 2, 3, 4, 5 -> "working day";
case 6, 7 -> "weekend";
default -> "not a day";
};
Four improvements in one line each:
- No fall-through. Arrow cases do not run on into the next.
- Several labels per case, comma separated, instead of stacked empty cases.
- It produces a value, so the variable can be assigned once and be
final. The old form forced you to declareString kind;and assign it in each branch, which is where a forgotten branch left it unassigned. - Exhaustiveness is checked when the compiler can know all the cases — on an
enum or a sealed type. If you add a value later and forget a case, the build
fails instead of the
defaultquietly swallowing it. This becomes properly useful in module 3.
When a branch needs more than one statement, use a block and yield to produce
its value:
int discountPercent = switch (tiffins / 10) {
case 0 -> 0;
case 1 -> 5;
default -> {
int extra = tiffins / 10;
yield Math.min(5 + extra, 15);
}
};
26 tiffins -> discount 7%
yield is to a switch expression what return is to a method: it supplies the
value of this branch. return inside a switch expression is not allowed, which
is a good thing — it would be ambiguous.
switch works on int, char, String, enums, and — from Java 21 — any type
at all through pattern matching, which module 3 covers.
One sharp edge: switching on a null String throws NullPointerException,
even when there is a default. Check for null first, or use pattern matching's
case null in Java 21.
Loops, and which to use
| Loop | Use it when |
|---|---|
for (int i = 0; i < n; i++) |
You need the index, or a count |
for (T item : collection) |
You need the items and not their positions |
while (condition) |
The number of iterations is unknown in advance |
do { } while (condition) |
The body must run at least once |
int[] counts = {4, 0, 12};
for (int i = 0; i < counts.length; i++) {
System.out.print(counts[i] + " ");
}
for (int c : counts) {
System.out.print(c + " ");
}
Both print 4 0 12. Prefer the enhanced for — no index means no
off-by-one — and reach for the indexed form only when you genuinely need i.
do/while is rare, and menu loops are its one natural home: show the menu,
read a choice, repeat until the user quits. The body must run once before there
is anything to test.
break, continue, and labels
break leaves the loop. continue skips to the next iteration.
A labelled break leaves an outer loop from inside an inner one:
outer:
for (int[] row : grid) {
for (int value : row) {
if (value == 4) {
break outer;
}
System.out.println("checked " + value);
}
}
checked 1
checked 2
checked 3
found 4, leaving both loops
A plain break would only have left the inner loop. Labels are the one piece of
goto-like machinery Java kept, and they are worth using for exactly this — two
nested loops with a single exit condition. Three levels deep, extract a method
and return instead.
Three loop bugs worth causing on purpose
Off by one. <= where < belongs:
int[] days = {1, 2, 3};
for (int i = 0; i <= days.length; i++) {
System.out.print(days[i] + " ");
}
1 2 3
java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
Three elements at indices 0, 1 and 2. length is 3, and index 3 does not exist.
A for loop over an array uses <, never <=.
continue skipping the increment. In a for loop, continue still runs the
increment. In a while loop it does not:
int i = 0;
while (i < 3) {
if (i == 1) {
continue; // i is never incremented again
}
System.out.println("i = " + i);
i++;
}
That prints i = 0 and then hangs forever. The increment is inside the body, and
continue jumps over it. If you must continue in a while, increment before
the check, or use a for.
Comparing doubles with ==.
for (double d = 0.0; d != 1.0; d += 0.1) {
...
}
After twenty iterations d is 2.0000000000000004 and still going. Ten
additions of 0.1 never land exactly on 1.0, for the reason the types lesson
gave. Loop counters are integers. If you need decimal steps, count in
integers and divide when you use the value.
Check your work
Why did the old-style switch print two prices for "veg"? No break after
the veg case, so execution fell through into jain and ran its body too.
Fall-through is the default, and a missing break is not an error.
Name three things the arrow form of switch fixes. No fall-through; several
labels per case; it produces a value so the result can be assigned once; and
exhaustiveness checking on enums and sealed types.
What is yield for? Producing the value of a switch expression branch that
needs more than a single expression. return is not allowed there.
What happens when you switch on a null String? NullPointerException,
even with a default branch present. Check for null first.
Why does if (delivered = true) compile? The assignment's value is a
boolean, so it is a valid condition. It always evaluates to true and
overwrites the variable as a side effect.
Why is i <= array.length wrong? Valid indices run from 0 to
length - 1. length itself is one past the end, giving
ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3.
Why can continue hang a while loop but not a for loop? A for loop's
increment is part of the loop's own machinery and runs on continue. A while
loop's increment is a statement in the body, and continue jumps over it.
Practice 3, FizzBuzz for a delivery sheet.
for (int day = 1; day <= 30; day++) {
String note = switch (day % 7) {
case 0 -> "rest day";
default -> (day % 10 == 0) ? "collect payment" : "deliver";
};
System.out.println(day + " : " + note);
}
Day 70 would be both, and this version silently picks "rest day". If both matter,
a switch is the wrong shape and an if/else if chain that you order
deliberately is the right one. That is the real lesson of the exercise.
Practice 5, the grading loop. It does not compile:
error: variable grade might not have been initialized
System.out.println(marks + " -> " + grade);
^
There is no else, so when marks is 40 no branch assigns grade, and the
compiler refuses to let you read it. That is the local-variable rule from the
types lesson doing real work.
Two things worth noticing. Had grade been declared outside the loop and
initialised once, it would have compiled and each unmatched student would have
silently inherited the previous student's grade — a far worse outcome than a
build failure. And the switch expression form makes the whole mistake
impossible, because every path must produce a value:
String grade = switch (marks / 10) {
case 10, 9 -> "A";
case 8 -> "B";
case 7 -> "C";
case 6 -> "D";
default -> "F";
};
Practice
-
Reproduce the fall-through. Write the old-style
switchfrom this lesson without thebreakin the first case, run it with"veg", and see the double charge. Then convert it to the arrow form and confirm the problem cannot be expressed. -
Convert an
if/else ifchain to aswitchexpression. Take the delivery charge from the operators lesson — free above Rs 500, Rs 20 within 3 km, Rs 20 plus Rs 8 per extra km — and write both. Note which one you can assign to afinalvariable. -
A delivery sheet for 30 days. Print each day numbered 1 to 30. Every seventh day is a rest day. Every tenth day is a payment collection day. Print the right note against each. Decide what day 70 should say, and make your code say it on purpose.
-
Cause all three loop bugs. The
<=off-by-one, thecontinuethat hangs awhile, and thedoubleloop counter that never terminates. Read each failure. For the two that hang, stop them withCtrl+Cand note that a hanging program gives you no error message at all — which is why they are harder to find than a crash. -
Find the bug in this, then fix it with a
switchexpression.for (int marks : new int[]{95, 82, 40, 71}) { String grade; if (marks >= 90) grade = "A"; else if (marks >= 80) grade = "B"; else if (marks >= 70) grade = "C"; System.out.println(marks + " -> " + grade); }It does not compile as written. Work out why, then write the version that does — and notice that the compiler caught for you exactly the mistake that the old
switchwould have let through. -
Harder — a menu loop. Use
do/whileto show a three-option menu (1 add delivery,2 show total,3 quit), read a number, and act on it. Handle an unknown option without exiting. You need the next lesson but one to read input properly, so for now drive it from an array of choices and loop over that instead — the shape is what matters.
Next: arrays, their fixed size, and why printing one gives you [I@1b6d3586.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship