Control Flow
Loop Control & Patterns
break exits a loop early, continue skips to the next iteration, and a labeled break escapes nested loops — each affecting only the innermost loop unless labeled. Plus the accumulation idioms (running sum, count, max) that most loops are built from, and the accumulator-initialization trap. Every behavior shown with verified output.
Suggest an editLoop Control & Patterns — Steering the Repetition
A plain loop runs its body start to finish, every pass. Real loops need to steer: stop as soon as you've found what you're looking for, skip the passes that don't apply, or break out of a nested search entirely. Java gives you three controls — break (leave the loop now), continue (skip to the next pass), and the labeled break (leave an outer loop) — and one quiet rule governs all of them: each affects only the innermost loop unless you name another. On top of these sit the handful of accumulation patterns — sum, count, max, "found it?" — that almost every loop turns out to be.
💡 The core idea.
- Real loops need to steer:
breakleaves now,continueskips to the next pass, a labeledbreakleaves an outer loop. - Each control affects only the innermost loop unless you name another.
- Most loops are built from a few accumulation patterns — sum, count, max, "found it?".
This builds on loops and the boolean conditions that gate them. Every output below was produced by compiling and running the code.
📘 How to read the Intuition boxes. Each one is built in three moves:
- The mechanism — what the compiler and the JVM are actually doing.
- A concrete bite — a specific, runnable failure (often a real compiler error), shown so the trap is visible.
- The earned rule — the decision heuristic, now justified rather than asserted, plus its cost.
Table of contents
break: leave the loop nowcontinue: skip to the next pass- Labeled
break: escape nested loops - Accumulation patterns
- Mental-model summary
- Gotcha checklist
1. break: leave the loop now
break stops a loop immediately and jumps to the code after it. It is how you search: scan until you find a match, then stop — there is no point continuing.
Output:
21Analysis. The loop checked 21 first; 21 % 7 == 0 is true, so it stored 21 in found and break ended the loop on the spot — it never tested 22, 23, or any later number. break turns "look at everything" into "look until you find it."
Intuition.
Mechanism. break exits the single loop that encloses it — the innermost one — and only that loop. It does not exit the method, and in nested loops it does not exit the outer loop.
Concrete bite. Put a break in an inner loop and the outer loop keeps right on going:
Output:
1,1
2,1
3,1For each i, the inner loop printed j = 1, then hit break at j == 2 — but that break only ended the inner loop. The outer loop dutifully moved to the next i and started a fresh inner loop, three times over. break stopped the inner search, not the whole thing.
💡 Earned rule. Use break to stop a scan the moment its job is done; just remember it leaves only the innermost loop. The cost of that scoping is that a break meant to abandon a nested search stops only the inner loop and silently lets the outer one continue — which is exactly what the labeled break in §3 fixes.
2. continue: skip to the next pass
continue abandons the rest of the current pass and jumps straight to the loop's next step — the update and test. It is how you filter: skip the elements that don't qualify, keep looping for the ones that do.
Output:
0 1 3 4 Analysis. When i was 2, continue skipped the print and went to the next pass — so 2 is missing, but the loop did not stop: it printed 3 and 4 afterward. The diagram shows why: continue routes back to the update/test, while break routes out of the loop entirely.
Intuition.
Mechanism. continue jumps over everything below it in the body and proceeds to the loop's update-and-test. In a for, that update (i++) still runs — so the loop always advances. In a while, the increment is part of the body, so a continue placed before it skips it.
Concrete bite. The "rest of the body is skipped" is the whole behavior — the lines after continue run only on passes that don't continue:
Output:
checking 0
checking 1
1 is odd
checking 2
checking 3
3 is oddchecking i prints every pass (it is above the continue); i is odd prints only for odd i, because for even i the continue skipped it. The continue cut the body short without ending the loop.
💡 Earned rule. Use continue to skip the passes that don't apply and keep the body flat instead of wrapping it all in an if. The cost is the for-vs-while asymmetry: a for always runs its update, but a while whose increment sits below a continue will skip the increment and spin forever — so in a while, advance the counter before any continue.
3. Labeled break: escape nested loops
When a break needs to leave more than the innermost loop, give the outer loop a label — a name followed by : — and write break label. That exits the named loop and everything inside it.
Output:
1,1
1,2
1,3
2,1
stop at 2,2
doneAnalysis. The loops ran until i * j == 4 (at i = 2, j = 2), then break outer left both loops at once — execution jumped past the outer loop to done. A plain break there would have ended only the inner loop and let the outer one continue to i = 3; the label is what made it abandon the whole nested search.
Intuition.
Mechanism. A label names a statement; break label exits that statement. The label must sit on an enclosing loop — you cannot break to a label that isn't above you in the nesting.
Concrete bite. Reference a label that doesn't enclose the break and it won't compile:
Compiler error:
Main.java:6: error: undefined label: inner
break inner;
^
1 errorThere is no label inner on any enclosing loop (the only label is outer), so the compiler rejects it. Labels are checked at compile time, so a mistyped one is caught immediately.
💡 Earned rule. Reach for a labeled break only to escape genuinely nested loops in one move; name the label for what it leaves (search:, outer:). The cost is that labels are rare enough to surprise the next reader, and overusing them recreates the tangled "goto" control flow structured loops were meant to replace — so prefer extracting the nested search into its own method (Tutorial 11) and returning from it once that tool is available.
4. Accumulation patterns
Most loops are one of a few shapes: build up a sum, keep a count, track a running max or min, or set a flag when something is found. The skeleton is always the same — initialize an accumulator before the loop, update it each pass.
Output:
sum=108 count=6 max=42Analysis. Three accumulators, one pass: sum adds each value (ending at 108), count ticks up once per element (6), and max keeps the largest seen so far (42). Each starts at a value that is correct for "nothing seen yet": sum and count at 0, and max at Integer.MIN_VALUE so the very first element is guaranteed to beat it.
Intuition.
Mechanism. An accumulator's initial value must be the right answer for an empty sequence: 0 adds nothing to a sum, and the smallest possible int loses to any real value in a max. The loop then folds each element into that running result.
Concrete bite. Initialize a max to 0 and any all-negative data defeats it:
Output:
0The real maximum is -1, but 0 was never in the data — it was a bad starting value that no negative element could beat, so the loop reported 0, a value that isn't even in temps. The logic is right; the seed was wrong.
💡 Earned rule. Seed an accumulator with the identity for its operation — 0 for a sum, Integer.MIN_VALUE (or the first element) for a max, Integer.MAX_VALUE for a min — never a "convenient" 0 that happens to be a valid value. The cost of a wrong seed is a silent wrong answer that survives every test using positive data and only breaks on the negative (or empty) case you forgot.
5. Mental-model summary
| Principle | Consequence |
|---|---|
break leaves the innermost loop and jumps past it |
A break in a nested loop stops only the inner one; the outer keeps going |
continue skips the rest of the body, then updates and tests |
In a for the update still runs; in a while, advance before any continue |
A labeled break leaves the named enclosing loop |
The one move that escapes nested loops; an undefined label won't compile |
| Loops are accumulation: init before, update each pass | Sum/count/max/flag are the recurring shapes |
| The accumulator's seed must be correct for "nothing seen" | max = 0 breaks on all-negative data; seed with Integer.MIN_VALUE |
6. Gotcha checklist
- A nested search keeps running after a match → a plain
breakleft only the inner loop; use a labeledbreak(or extract a method andreturn). - A
whilewithcontinuehangs → the increment sits below thecontinueand gets skipped; advance the counter before anycontinue. undefined label→ the label isn't on an enclosing loop (typo or wrong scope); label the actual outer loop.- A max/min comes back as
0(or a value not in the data) → the accumulator was seeded with0; useInteger.MIN_VALUE/MAX_VALUEor the first element. continueskipped a line you expected to run → code belowcontinueruns only on passes that don't continue; move it above, or invert the condition.
🧪 Predict, then check. Predict the output of this nested loop with a plain break, then with break outer instead: for i in 1..2 { for j in 1..2 { if (i+j == 3) break; print(i,j); } }. Next, predict what for (int i = 0; i < 6; i++) { if (i % 3 != 0) continue; System.out.print(i + " "); } prints. Finally, write a loop that finds the minimum of {7, 3, 9, 2, 8} — and decide what to seed the accumulator with so it would still be correct for all-negative data.
Your Turn
Before you move on, check your understanding with the coach — explain the idea, apply it, weigh the trade-offs, then defend your reasoning.