Control Flow

Loop Control & Patterns

break leaves a loop early, continue skips to the next pass, and a labeled break escapes nested loops. Each one affects only the innermost loop unless you name another. Also covers the accumulation idioms most loops are built from (running sum, count, max) and the trap of seeding an accumulator wrong. Every behavior is shown with verified output.

Suggest an edit

Loop Control & Patterns — Steering the Repetition

A plain loop runs its body from start to finish on every pass. Real loops need to steer. They stop as soon as they've found what they're looking for, skip the passes that don't apply, or break out of a nested search entirely. Java gives you three controls:

  • break leaves the loop now;
  • continue skips to the next pass;
  • a labeled break or continue acts on an outer loop.

One quiet rule governs all of them: each affects only the innermost loop unless you name another. On top of these sits a handful of accumulation patterns, and almost every loop turns out to be one of them: sum, count, max, "found it?".

💡 The core idea.

  • Real loops need to steer: break leaves now, continue skips to the next pass, a labeled break leaves 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.

You'll be able to: predict what a loop prints when break or continue runs inside it, with nested loops and a switch included; leave or skip an outer loop with a labeled break or continue; place a while loop's update so that a continue cannot skip it; write sum, count, max, min and found-it loops, each seeded with the right starting value; compute an average with a fraction, and guard it against empty input.

📘 How to read the Intuition boxes. Each one is built in three moves:

  1. The mechanism — what the compiler and the JVM do.
  2. A concrete bite — a specific, runnable failure (often a real compiler error), shown so the trap is visible.
  3. The earned rule — the decision heuristic, now justified rather than asserted, plus its cost.

Table of contents

  1. break: leave the loop now
  2. continue: skip to the next pass
  3. Labeled break and continue: nested loops
  4. Accumulation patterns
  5. Mental-model summary
  6. Gotcha checklist
  7. Check yourself
  8. Sources

1. break: leave the loop now

break stops a loop immediately and jumps to the code after it [1]. It's how you search: scan until you find a match, then stop. There's no point carrying on.

Output:

21

Analysis. The loop checked 21 first. 21 % 7 == 0 is true, so it stored 21 in found. break ended the loop on the spot, so it never tested 22, 23, or any later number. break turns "look at everything" into "look until you find it".

found starts at -1, a value no search can produce. If the loop ends without a match, -1 tells you so. A value used this way, to mean "nothing", is a sentinel.

A statement right after break, in the same block, can never run, and javac rejects it with unreachable statement [5]. A break outside any loop or switch fails too: break outside switch or loop.

A loop inside another loop is a nested loop. The inner loop runs all of its passes for each pass of the outer one:

Output:

1,1  1,2  1,3  
2,1  2,2  2,3  

Two outer passes times three inner passes make six pairs. The println after the inner loop ends each row.

Intuition. Mechanism. break exits the innermost statement that encloses it and that it can leave: a loop, or a switch [1]. It doesn't exit the method, and in nested loops it doesn't 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,1

For each i, the inner loop printed j = 1, then hit break at j == 2. But that break ended only the inner loop. The outer loop dutifully moved on to the next i and started a fresh inner loop, three times over. break stopped the inner search, not the whole thing.

Non-example: break inside a switch. A colon-style switch from Conditionals uses break to end each case. Inside a loop, that break leaves the switch, not the loop:

Output:

checked 1
found 2
checked 3
checked 4

The search found 2 and kept going. The break belonged to the switch, so the loop never saw it. A labeled break, coming up in §3, reaches the loop.

💡 Earned rule. Use break to stop a scan the moment its job is done. Remember that it leaves only the innermost loop or switch.

That scoping has a cost. A break meant to abandon a nested search stops only the inner loop, and silently lets the outer one carry on. The labeled break in §3 fixes exactly that.


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 [2]. It's how you filter: skip the elements that don't qualify, and 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 story. 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 odd

checking i prints on every pass, because it's 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.

Non-example: continue in a while, above the update. Here the i++ sits below the continue:

Output (prints two lines, then runs until it is stopped; the sandbox stops it at its time limit):

0
1

At i == 2, continue jumped back to the test and skipped i++. i stayed 2, so the same continue ran again, and again, forever. Move the update above the continue:

Output:

1 2 4 5 

The counter now advances on every pass, so the loop skips 3 and still ends.

💡 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 catch is the for-vs-while asymmetry. A for always runs its update. But a while whose increment sits below a continue skips the increment and spins forever. So in a while, advance the counter before any continue.


3. Labeled break and continue: nested loops

When a break needs to leave more than the innermost loop, give the outer loop a label: a name followed by : [3]. Then write break label, which exits the named loop and everything inside it.

Output:

1,1
1,2
1,3
2,1
stop at 2,2
done

Analysis. The loops ran until i * j == 4 (at i = 2, j = 2). Then break outer left both loops at once, and execution jumped past the outer loop to done. A plain break there would have ended only the inner loop and let the outer one carry on to i = 3. The label is what made it abandon the whole nested search.

The same label fixes the §1 switch non-example. Name the loop, and break search leaves the loop from inside the switch:

Output:

checked 1
found 2

continue takes a label too. continue rows ends the current pass of the loop named rows and starts its next pass [2]. Here each row prints only the pairs where j is not bigger than i:

Output:

1,1  2,1  2,2  3,1  3,2  3,3  

For i = 1, the pair 1,2 fails the test, so continue rows abandoned the inner loop and moved i on to 2. A plain continue would have skipped only 1,2, then tried 1,3 for no reason.

Intuition. Mechanism. A label names a statement. break label exits that statement, and continue label starts that loop's next pass. The label must sit on an enclosing loop. You can't break to a label that isn't above you in the nesting.

Concrete bite. Refer to 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 error

No enclosing loop carries the label inner (the only label is outer), so the compiler rejects it. The compiler checks labels, so it catches a mistyped one right away.

💡 Earned rule. Reach for a labeled break or continue only to leave or skip genuinely nested loops in one move. Name the label for what it leaves (search:, rows:).

The catch is that labels are rare enough to surprise the next reader. Overuse them, and you recreate the tangled "goto" control flow that structured loops were meant to replace. Java has no goto statement at all [3]. So prefer extracting the nested search into its own method, and returning from it once that tool is available.


4. Accumulation patterns

Most loops take one of a few shapes: build up a sum, keep a count, track a running max or min, or set a flag when something turns up. The skeleton is always the same. Initialize an accumulator (the variable that collects the result) before the loop, and update it on each pass.

Output:

sum=108 count=6 max=42

Analysis. Three accumulators, one pass:

  • sum adds each value, ending at 108;
  • count ticks up once per element, ending at 6;
  • max keeps the largest seen so far, ending at 42.

Each one starts at a value that's correct for "nothing seen yet". sum and count start at 0. max starts at Integer.MIN_VALUE, the smallest int [4], so the very first element is guaranteed to beat it.

An average is sum / count, and both are ints. So the division truncates, just as Numbers & Arithmetic showed. Cast one side to keep the fraction:

Output:

7
7.5

A flag answers "is there any …?". It starts out false, and the first match sets it and stops the scan:

Output:

true

15 is the first odd value, so the loop never looked at 16, 23 or 42.

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:

0

The 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.

Non-example: no data at all. An empty list defeats the accumulators in a different way, because the loop body never runs:

Output (prints a line, then a thrown exception):

count=0 max=-2147483648
Exception in thread "main" java.lang.ArithmeticException: / by zero

max kept its seed, a number that isn't in the data. sum / count divided by 0 and threw. Check count > 0 before you report a max or divide by the count.

💡 Earned rule. Seed an accumulator with the right answer for "nothing seen yet":

  • 0 for a sum or a count;
  • Integer.MIN_VALUE (or the first element) for a max, and Integer.MAX_VALUE for a min;
  • false for a found-it flag.

Never seed a max or min with a "convenient" 0 that happens to be a valid value. A wrong seed costs you a silent wrong answer. It survives every test with positive data, and breaks only on the negative or empty case you forgot.


5. Mental-model summary

Principle Consequence
break leaves the innermost loop or switch and jumps past it A break in a nested loop stops only the inner one; in a switch inside a loop, it leaves only the switch
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; a labeled continue starts its next pass The one move that escapes or skips nested loops; an undefined label won't compile
Loops are accumulation: init before, update each pass Sum, count, max, min, average and 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
Empty input leaves every accumulator at its seed Check count > 0 before dividing or reporting a max

6. Gotcha checklist

Symptom Likely cause Fix
A nested search keeps running after a match a plain break left only the inner loop a labeled break, or a method that returns
A loop keeps going after a switch case's break that break left the switch, not the loop label the loop and break the label
A while with continue hangs the increment sits below the continue and gets skipped advance the counter before any continue
undefined label: inner the label isn't on an enclosing loop (a typo, or the wrong loop) label the loop that encloses the break
break outside switch or loop / continue outside of loop the statement is not inside any loop (or switch, for break) move it into the loop, or use return in a method
unreachable statement a statement sits straight after break or continue in the same block move it above, or delete it
A max or min comes back as 0, a value not in the data the accumulator was seeded with 0 Integer.MIN_VALUE / Integer.MAX_VALUE, or the first element
A max is -2147483648 the input was empty, so the seed was never replaced check count > 0 first
An average loses its fraction (7 for 7 and 8) int / int truncates (double) sum / count
ArithmeticException: / by zero computing an average count is 0: the input was empty check count > 0 before dividing
continue skipped a line you expected to run code below continue runs only on passes that don't continue move it above, or invert the condition

✅ Check yourself

One check per objective. Answer before you open anything.

The 🧪 box below: the nested loop with a plain break and with break outer; the continue loop; and the minimum.

With a plain break, the program prints 1,1 3,1 3,2 3,3. For i = 1 the break came at j = 2. For i = 2 it came at j = 1, before any print. For i = 3, no pair sums to 3.

With break outer, it prints only 1,1, because the first break ends both loops.

The continue loop prints 0 3: every i not divisible by 3 is skipped.

Seed the minimum with Integer.MAX_VALUE, so any real value beats it:

int min = Integer.MAX_VALUE; for (int x : v) { if (x < min) min = x; }

It gives 2 for {7, 3, 9, 2, 8}, and -11 for {-4, -11, -6}. A seed of 0 would report 0 for any all-positive data.


📚 Sources

  1. The Java Language Specification, Java SE 25, §14.15 "The break Statement" ("the innermost enclosing switch, while, do, or for statement") — https://docs.oracle.com/javase/specs/jls/se25/html/jls-14.html#jls-14.15
  2. The Java Language Specification, Java SE 25, §14.16 "The continue Statement" — https://docs.oracle.com/javase/specs/jls/se25/html/jls-14.html#jls-14.16
  3. The Java Language Specification, Java SE 25, §14.7 "Labeled Statements" ("the Java programming language has no goto statement") — https://docs.oracle.com/javase/specs/jls/se25/html/jls-14.html#jls-14.7
  4. Java SE 25 API, java.lang.Integer (MIN_VALUE, MAX_VALUE) — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Integer.html#MIN_VALUE
  5. The Java Language Specification, Java SE 25, §14.22 "Unreachable Statements" — https://docs.oracle.com/javase/specs/jls/se25/html/jls-14.html#jls-14.22

🧪 Predict, then check.

  1. Predict the output of this nested loop with a plain break, then with break outer instead (label the outer loop outer:): for (int i = 1; i <= 3; i++) { for (int j = 1; j <= 3; j++) { if (i + j == 3) break; System.out.print(i + "," + j + " "); } }.
  2. Predict what for (int i = 0; i < 6; i++) { if (i % 3 != 0) continue; System.out.print(i + " "); } prints.
  3. Write a loop that finds the minimum of {7, 3, 9, 2, 8}. 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.

Mark as readBook versionfd26460