Layers of Logic

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.

Core17 min read4 exercises
01

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.

02

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.

03

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
5

Three parts in the header, separated by semicolons:

What the three parts do

  1. int i = 1 (setup)Runs once before the first condition check.
  2. i <= 5 (condition)Checked before every pass, including the first. While it is true, the body runs.
  3. 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.

LoopReach for it when
forcounter, condition and step all in the headeryou are counting, or walking a fixed range
whilecondition onlyyou do not know how many passes
do-whilecondition at the bottomthe body must run before you can test
04

Under the hood

Core

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

StepState
setupint i = 0i is 0
condition, body, update0 < 3, print 0, i++i becomes 1
condition, body, update1 < 3, print 1, i++i becomes 2
condition, body, update2 < 3, print 2, i++i becomes 3
condition3 < 3 is falsestop 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);   // 6

At 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 symbol

i 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);        // 5

Note that i is 5, not 4. The loop only stopped because the condition failed, which means i had already reached 5.

break and continue

  • break leaves the loop completely.
  • continue skips 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

  1. A flagSet boolean found = true; and test it in the outer condition too. This keeps control local but adds state.
  2. A labelled breakPut a label before the outer loop and break to it: search: for (...) { for (...) { break search; } }. Jumps out of both at once.
  3. Extract a method and returnUsually the best answer. return leaves 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 stops

Look 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 passes

Prefer 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 passes

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

05

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.

06

Check yourself

Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.

  1. `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.0 stays true, d sails past 1.0, and the loop runs forever.

    An incremented floating-point value is a poor equality sentinel. Count with an int and calculate the decimal inside the loop: for (int i = 0; i < 10; i++) { double d = i * 0.1; }

  2. 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 while you 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 while instead means duplicating code, and duplicated code drifts apart.

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

    break only 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. return leaves everything, no flags and no labels, and you get a method with a name that says what the search was for.

07

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

The Layers of Logic VS Code extension runs the checks for exercises marked checked. For a manual exercise, run the program and compare its behaviour with the stated requirements and sample output.
A

Same Job, Three Loops

Warm up·20 min·15 points

checkedex-3-3-a

Write 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
Three.java
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
A for and a while check before the first pass, so a false condition means the body never runs.
Hint 2
A 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
That is the whole difference. A do-while suits anything that must happen at least once and then repeat: read input, then keep reading while it is invalid.
What this is really testing

Whether you can write all three loop shapes from memory and say which one fits a job. The shapes are interchangeable here on purpose, so the only thing left to think about is which one reads best.

B

Four Loops That Never Stop

Real work·25 min·20 points

checkedex-3-3-b

Four 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
Forever.java
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
The first and fourth never move the counter. The fourth is the sneakier one, because it does have an increment and the body cancels it.
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
Adding 0.1 ten times gives 0.9999999999999999, so 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.
What this is really testing

Whether you can spot an infinite loop by reading. Each of these has a different cause, and one of them comes straight back from Section 2.2.

C

Escaping Two Loops at Once

Real work·25 min·25 points

checkedex-3-3-c

Search 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
Escape.java
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
A plain 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
A label is a name before the loop: search: then break search;. It is one of the few places a label is genuinely the clearest option.
Hint 3almost the answer
The method return version is usually the best answer, because it removes the question entirely. If a loop wants to escape two levels, that is often a sign it should be its own method.
What this is really testing

Whether you know that break only leaves one loop, and whether you can pick the cleanest of the three escapes. All three work. Only one of them reads well.

D

Registry Roll Call

Hard·35 min·30 points·The Registry

checkedex-3-3-d

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

You type
75
45
150
88
-1
It prints
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
Guard the empty case before dividing. An average of no numbers is not zero in mathematics, and returning 0.0 is a decision worth making on purpose rather than by accident.
What this is really testing

Whether you can drive a loop from user input rather than a fixed count, and whether you pick the loop shape that fits. This is the first Registry program that keeps running until the user says stop.

08

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

Loops will return in 4.1 - Arrays