3.3
Loops and Jump Statements
Choose a loop from its condition, trace its execution order, and prove that each pass makes progress toward stopping.
Previously on
Section 3.2 gave your program the ability to choose. It can now take one path or another.
It still cannot repeat work according to data available only while the program runs.
The problem
Print the numbers 1 to 5. You can already do this.
System.out.println(1);
System.out.println(2);
System.out.println(3);
System.out.println(4);
System.out.println(5);Now print 1 to 1000. Now print 1 to however many records are in a file you have not seen yet.
You cannot type your way out of this. The work has to be described once and repeated, and the number of repeats has to be decided while the program runs, not while you are writing it.
A loop is how you do it.
The idea
Java gives you three loop shapes. All three do the same job. They differ in where the bookkeeping lives.
for, when you are counting
public class CountDemo {
public static void main(String[] args) {
for (int i = 1; i <= 5; i++) {
System.out.println(i);
}
}
}Output:
1
2
3
4
5Three parts in the header, separated by semicolons:
What the three parts do
- int i = 1 (setup)Runs once before the first condition check.
- i <= 5 (condition)Checked before every pass, including the first. While it is true, the body runs.
- i++ (step)Runs after every pass, before the condition is checked again.
Use for when you know how many times, or you are walking through something with a count.
while, when you are waiting for a condition
while (hasMoreWork()) {
doSomeWork();
}No counter, no step. The loop runs until the condition goes false, and whatever makes that happen is your responsibility inside the body.
Use while when the number of passes is not known in advance.
do-while, when it must happen at least once
do {
System.out.print("Enter a number between 1 and 10: ");
input = scanner.nextInt();
} while (input < 1 || input > 10);The condition is checked at the bottom, so the body always runs at least once.
Look at why that matters here. You cannot know whether the input is valid until you have asked for it. With a plain while you would have to write the prompt twice, once before the loop and once inside.
| Loop | Reach for it when | |
|---|---|---|
| for | counter, condition and step all in the header | you are counting, or walking a fixed range |
| while | condition only | you do not know how many passes |
| do-while | condition at the bottom | the body must run before you can test |
Under the hood
CoreExact execution order
For this loop:
for (int i = 0; i < 3; i++) {
System.out.println(i);
}Java runs setup once. It then repeats condition, body and update. A failed condition ends the loop before the body.
| Step | State | |
|---|---|---|
| setup | int i = 0 | i is 0 |
| condition, body, update | 0 < 3, print 0, i++ | i becomes 1 |
| condition, body, update | 1 < 3, print 1, i++ | i becomes 2 |
| condition, body, update | 2 < 3, print 2, i++ | i becomes 3 |
| condition | 3 < 3 is false | stop without running the body |
continue in a for loop jumps to the update and then the next condition. break skips both and leaves the loop.
State an invariant
A loop invariant is a fact that is true before and after every completed pass. It makes a loop easier to verify.
int sum = 0;
for (int i = 0; i < 4; i++) {
sum += i;
}
System.out.println(sum); // 6At the condition check, sum equals the sum of integers from 0 up to, but not including, i. It is true at i = 0. The body adds i, and the update advances i, so the fact remains true. When the loop stops at i = 4, the invariant tells us sum is 0 + 1 + 2 + 3.
Where the counter lives
for (int i = 0; i < 5; i++) { }
System.out.println(i); // ERROR: cannot find symboli was declared inside the for header, so it stops existing when the loop ends. That is usually what you want, because it stops one loop’s counter leaking into the next.
If you need the value afterwards, declare it outside:
int i;
for (i = 0; i < 5; i++) { }
System.out.println(i); // 5Note that i is 5, not 4. The loop only stopped because the condition failed, which means i had already reached 5.
break and continue
breakleaves the loop completely.continueskips the rest of this pass and goes to the next one.
for (int i = 1; i <= 10; i++) {
if (i % 2 == 0) continue; // skip evens
if (i > 7) break; // stop entirely
System.out.println(i); // 1 3 5 7
}break only leaves one loop
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 3; col++) {
if (grid[row][col] == target) {
break; // leaves the col loop only. The row loop keeps going.
}
}
}Three ways to get out of both:
Escaping nested loops
- A flagSet
boolean found = true;and test it in the outer condition too. This keeps control local but adds state. - A labelled breakPut a label before the outer loop and break to it:
search: for (...) { for (...) { break search; } }. Jumps out of both at once. - Extract a method and returnUsually the best answer.
returnleaves everything, and you end up with a method whose name says what you were searching for.
Labelled break is real Java and worth recognising when you read it. In your own code, the method usually reads better.
Common non-terminating loops
Three ways to make a loop that never ends. All three are common.
One: you forget to change the thing the condition tests.
int i = 0;
while (i < 10) {
System.out.println(i); // no i++ anywhere
}Two: you used != on decimals.
for (double d = 0.0; d != 1.0; d += 0.1) { } // never stopsLook familiar? That is Section 2.2 coming back for you. 0.1 has no exact binary form, so you are not really adding 0.1. After ten additions you land on 0.9999999999999999, which is not equal to 1.0, so the loop sails past it and runs forever.
An ordered bound stops eventually, but may perform a different number of passes from the intended decimal steps:
for (double d = 0.0; d < 1.0; d += 0.1) { } // terminates, but may run 11 times
for (int i = 0; i < 10; i++) { double d = i * 0.1; } // exactly ten passesPrefer the second form when a fixed count controls the work. Count with whole numbers and work out the decimal inside the loop. The displayed decimal can still be approximate, but it does not control termination.
Three: you changed the counter inside the body as well as in the header.
for (int i = 0; i < 10; i++) {
if (something) i--; // now the loop may never advance
}Off by one
The most common loop bug there is, and it comes down to one character.
for (int i = 0; i < 5; i++) // 0 1 2 3 4 -> five passes
for (int i = 0; i <= 5; i++) // 0 1 2 3 4 5 -> six passes
for (int i = 1; i <= 5; i++) // 1 2 3 4 5 -> five passesThe convention that is worth adopting is start at 0 and use <. It matches how arrays are numbered, which you will meet in the very next section, and it makes the count obvious: i < n runs exactly n times.
What it costs
A loop hides how much work is happening. Three lines can do one operation or ten billion, and nothing on screen tells you which. Nested loops multiply, so a loop of 1,000 inside a loop of 1,000 is a million passes written in six lines.
One or two break statements in a body are clear. Five of them scattered through a long loop and nobody can work out which paths reach the bottom.
There is a subtler cost. for (int i = 0; i < list.size(); i++) is you giving step by step instructions. You are describing how, when what you meant was what. Java does have a way to say “give me the ones that match” directly, but not until Phase XI.
Loops express repetition whose count may be known only while the program runs. Later algorithms and data structures rely on the same condition-and-progress model.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
`for (double d = 0.0; d != 1.0; d += 0.1)` never stops. You are adding 0.1 ten times to reach 1.0. Why does it miss?
Show the answer
Because 0.1 has no exact form in binary, so you are never actually adding 0.1. You are adding the closest double to 0.1, and the error builds up with every step.
After ten additions you land on 0.9999999999999999, which is very close to 1.0 and is not equal to it. So
d != 1.0stays true,dsails past 1.0, and the loop runs forever.An incremented floating-point value is a poor equality sentinel. Count with an
intand calculate the decimal inside the loop:for (int i = 0; i < 10; i++) { double d = i * 0.1; }When does `do-while` state the requirement more directly than `while`?
Show the answer
When the body must run at least once, because the thing that decides whether to continue is produced by the body itself.
The clearest case is asking a user for input. You have to show the prompt and read the answer before you can know whether the answer was valid. With
whileyou end up writing the prompt twice, once before the loop and once inside it.Same for a retry loop: you must attempt the operation before you can know whether it failed. That is a
do-while.It is rare on purpose. But when the shape fits, using
whileinstead means duplicating code, and duplicated code drifts apart.You have two nested loops searching a grid. You find what you want in the inner loop and write `break`. The program keeps searching. Why, and what are your options?
Show the answer
breakonly leaves the loop it is directly inside. The outer loop knows nothing about it and carries on to the next row.Three ways out, worst to best:
A flag. Set
boolean found = true;and check it in the outer condition too. It works and it clutters both loops.A labelled break. Put a label before the outer loop and write
break search;. It jumps out of both at once. Legal, clear enough, and rare in real code.Extract a method and return. Usually the best answer.
returnleaves everything, no flags and no labels, and you get a method with a name that says what the search was for.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises90 pointsabout 105 minutes
Same Job, Three Loops
ex-3-3-aWrite the same small task three ways.
The task is deliberately boring, because the exercise is not the task. It is noticing that the three shapes put the same three pieces of information in different places, and that where they sit changes how easy the loop is to check.
After each method, write one line saying what that shape is genuinely for. Then say which one you would actually use for counting to ten, and why.
If your answer is “they are all the same”, read your while version again and count how many separate lines you would have to look at to work out how many times it runs.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Three {
static List<Integer> withFor()
static List<Integer> withWhile()
static List<Integer> withDoWhile()
static int forRunsWhenConditionIsFalse()
static int whileRunsWhenConditionIsFalse()
static int doWhileRunsWhenConditionIsFalse()
}The three loop methods each return 1 to 10 as a list, so the checks can compare them. The three counting methods run a loop whose condition is false from the start and report how many times the body ran.
What your program must do
- Write the same job three ways and confirm the results match
- Predict how many times each body runs when the condition starts false
- Say which of the three is genuinely different and why
- Say when you would reach for a do-while on purpose
import java.util.*;
public class Three {
// The same job three ways. Return 1 to 10 as a list.
static List<Integer> withFor() { return List.of(); } // TODO
static List<Integer> withWhile() { return List.of(); } // TODO
static List<Integer> withDoWhile() { return List.of(); } // TODO
// Now the difference. Each of these has a condition that is false from the
// start. How many times does the body run? Predict all three.
static int forRunsWhenConditionIsFalse() { return -1; } // TODO
static int whileRunsWhenConditionIsFalse() { return -1; } // TODO
static int doWhileRunsWhenConditionIsFalse() { return -1; } // TODO
public static void main(String[] args) {
// TODO: print all three lists and all three counts
}
}
Hint 1
for and a while check before the first pass, so a false condition means the body never runs.Hint 2
do-while checks at the bottom, so the body has already happened once by the time the condition is looked at.Hint 3almost the answer
Four Loops That Never Stop
ex-3-3-bFour loops. All four run forever. Four different reasons.
Work out every cause by reading, before you run anything. Then add a safety counter so you can actually watch them misbehave without having to kill your terminal.
One of these four is not really a loop bug at all. It is a floating point bug wearing a loop costume, and you already have everything you need to explain it from Phase II.
Fix all four so each does the thing it was clearly written to do. For the floating point one, give the better fix, not just the one that stops the bleeding.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Forever {
static List<Integer> oneFixed()
static List<Integer> twoFixed()
static List<String> threeFixed()
static List<Integer> fourFixed()
static String reason(int which)
static boolean tenthsReachOneExactly()
}Each fixed method returns what the broken loop should have printed. reason takes 1 to 4 and returns a short sentence naming the cause. twoFixed must still print only the odd numbers.
What your program must do
- Say why each of the four never stops, before fixing any of them
- Fix all four, keeping what each one was trying to do
- Show that the third one is a floating point problem rather than a loop problem
- Say why continue is the dangerous keyword in the second
import java.util.*;
public class Forever {
// Four loops that never stop. Work out why each one hangs, then write the
// fixed version, returning what it should have produced.
//
// one: int i = 0; while (i < 10) { print(i); }
// two: int i = 0; while (i < 10) { if (i % 2 == 0) continue; print(i); i++; }
// three: for (double d = 0.0; d != 1.0; d += 0.1) print(d);
// four: for (int i = 0; i < 10; i++) { print(i); i--; }
static List<Integer> oneFixed() { return List.of(); } // TODO: 0 to 9
static List<Integer> twoFixed() { return List.of(); } // TODO: the ODD numbers only
static List<String> threeFixed() { return List.of(); } // TODO: ten values
static List<Integer> fourFixed() { return List.of(); } // TODO: 0 to 9
// A short sentence naming why loop `which` never stopped.
static String reason(int which) { return ""; } // TODO
// Proof for number three.
static boolean tenthsReachOneExactly() { return true; } // TODO
public static void main(String[] args) {
// TODO: print all four fixed results and all four reasons
}
}
Hint 1
Hint 2
continue jumps to the next pass, which in a while means it skips everything below it, including the increment. In a for loop the update still runs, which is why the same bug does not appear there.Hint 3almost the answer
d != 1.0 is never false. Loop over ints and divide, or use < instead of !=. Never compare doubles for exact equality in a loop condition.Escaping Two Loops at Once
ex-3-3-cSearch a small grid for a value. The moment you find it, stop searching.
Start by writing the version that does not work: a plain break in the inner loop. Put a print statement in the outer loop so you can watch it keep going after the value was already found. Seeing that is the whole point.
Then write all three fixes. They should give identical answers.
Finish by saying which one you would put in real code. There is a right answer here, and the reason is not about the loops at all. It is about the fact that one of the three versions ends up with a name.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Escape {
static String withFlag(int target)
static String withLabelledBreak(int target)
static String withMethodReturn(int target)
}All three search the same grid and return exactly "found at row R col C" or "not found". They must agree with each other for every input.
What your program must do
- Write all three and confirm they agree for every value
- Handle a value that is not in the grid
- Say what the flag version costs in readability
- Say which of the three you would ship and why
public class Escape {
static int[][] grid = {
{ 3, 7, 1 },
{ 9, 4, 8 },
{ 2, 6, 5 },
};
// Three ways out of two loops. All three return exactly
// "found at row R col C" or "not found".
static String withFlag(int target) { return ""; } // TODO: a boolean the outer loop checks
static String withLabelledBreak(int target) { return ""; } // TODO: a label and break label
static String withMethodReturn(int target) { return ""; } // TODO: just return from inside
public static void main(String[] args) {
// TODO: try 4, 3, 5 and 99 through all three and confirm they agree
}
}
Hint 1
break only leaves the loop it is in. The flag version needs the outer loop to check it too, which is why the condition grows an extra clause.Hint 2
search: then break search;. It is one of the few places a label is genuinely the clearest option.Hint 3almost the answer
Registry Roll Call
ex-3-3-dThe Registry gets its first interactive program. It keeps running until the user decides to stop.
Read readiness values one at a time. Reject anything outside 0 to 100 and ask again, without losing the values already collected. Stop when the user enters -1. Then print a summary.
Two decisions matter here.
First, which loop shape for the outer loop? You cannot know whether a value is the stop signal until you have read it. That points at one particular shape.
Second, what happens if the user types -1 immediately? Dividing by zero with double does not throw an exception. It gives you NaN and carries on, and NaN in a report is worse than a crash, because nobody notices it.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class RollCall {
static List<Unit> readUntilDone(List<String> lines)
static double averageReadiness(List<Unit> units)
static Unit leastReady(List<Unit> units)
static int countBelow(List<Unit> units, double threshold)
}Unit is a record of a name and a readiness. Lines look like "Atlas,88". Reading stops at a line saying done, whatever its case. A malformed line is skipped, not fatal. An empty roll call averages 0.0 and has no least ready unit.
What your program must do
- Stop reading at the sentinel and ignore everything after it
- Match the sentinel whatever case it is typed in
- Skip a line you cannot parse without ending the roll call
- Handle an empty roll call without dividing by zero
import java.util.*;
public class RollCall {
record Unit(String name, double readiness) { }
// Lines look like "Atlas,88". Stop at a line saying done, whatever its case.
// Skip a line you cannot read rather than giving up on the whole roll call.
static List<Unit> readUntilDone(List<String> lines) {
return List.of(); // TODO
}
static double averageReadiness(List<Unit> units) { return 0; } // TODO: 0.0 for none
static Unit leastReady(List<Unit> units) { return null; } // TODO: null for none
static int countBelow(List<Unit> units, double threshold) { return 0; } // TODO
public static void main(String[] args) {
// TODO: run a roll call and print the count, average, least ready and
// how many are below 60
}
}
Sample run
75
45
150
88
-1
Rejected: 150 is not between 0 and 100
-- roll call --
units seen : 3
ready units : 2
average : 69.33
Hint 1
equalsIgnoreCase handles the sentinel, and it also protects you from the == trap in Section 3.2 at the same time.Hint 2
continue is the right tool for a bad line: skip this one and carry on. break would throw away every unit after the first typo.Hint 3almost the answer
After the credits
The next section applies for (int i = 0; i < arr.length; i++) to arrays. The half-open bound visits every valid array index once and handles an empty array without a special case.
In Section 4.1 you will also meet a fourth loop, the for-each:
for (String name : names) { }It is an enhanced for loop. For arrays it visits elements in index order. For an Iterable, Java uses an Iterator. A Map is not itself Iterable, so code loops over keySet(), values() or entrySet().
The same syntax later works with collection types that implement Iterable.
Phase XI introduces streams for transformations and reductions that can be expressed without an explicit loop. The loop model remains necessary for understanding their execution and cost.
Threads you opened in this section
- LoopsThe for-each loop is not a loop. It is an Iterator in disguise.Phase X. The Collections Framework
- LoopsStreams let you say what you want instead of writing the loop.11.3 - Streams, and When the Work Happens
Loops will return in 4.1 - Arrays