Module 3 · Control Flow & Program Structure ⏱ 19 min

Loop Patterns and Invariants

By the end of this lesson you will be able to:
  • Apply the accumulator pattern to compute sums, products, and counts
  • Use search patterns with break to find a matching element
  • Predict the final value of counters and accumulators after a loop completes

Loops are where programs do their real work: adding up scores, searching for a name, counting occurrences, finding the largest value. After you have written a few dozen loops, you notice they are not random — they fall into a small set of patterns. Recognising the pattern lets you predict exactly what the loop will produce without mentally stepping through every single iteration.

This lesson covers the three patterns you will use most often: the accumulator (build a result piece by piece), the search (stop when you find what you need), and the min / max (keep the best value seen so far). We will also meet the invariant: a simple statement that stays true every time the loop body runs, which is the secret to knowing a loop is correct before you run it.

The accumulator pattern

The accumulator pattern builds a result one element at a time. Start with an initial value before the loop — 0 for a sum, 1 for a product, an empty string for concatenation. Then, for every element, update the accumulator with that element. When the loop ends, the accumulator holds the final answer.

The pattern looks like this:

int total = 0;
for (int value : values) {
    total = total + value;
}

The key to reading an accumulator loop is to ask: what does the variable hold after processing the first N items? If the answer is obvious — total holds the sum of the first N items — the loop is correct. That simple question is the germ of a loop invariant.

flowchart LR
  INIT["total = 0"] --> L["for each value"]
  L --> U["total = total + value"]
  U --> L
  L -->|"done"| R["total = sum of all values"]
  style INIT fill:#c2410c,color:#fff
  style R fill:#166534,color:#fff
The accumulator pattern: initialise, then fold each element into the running total.
Main.java — sum and product accumulators. Real, compiling Java.
public class Main {
    public static void main(String[] args) {
        int[] scores = {4, 7, 2, 9};
        int sum = 0;
        int product = 1;
        for (int s : scores) {
            sum = sum + s;
            product = product * s;
        }
        System.out.println("Sum: " + sum);
        System.out.println("Product: " + product);
    }
}

Search patterns: find-first and find-all

A search loop looks for something that satisfies a condition. The simplest version checks every element and keeps going — a find-all that might count matches or collect them. The more common version stops the instant it finds a match, using break. That is a find-first search, and it saves work: if the answer is in the first ten elements of a million-element list, you do not process the other 999,990.

A search loop needs a way to report not found. The usual idiom is to initialise a result variable to a sentinel value — -1 for an index, false for a boolean flag — and overwrite it only when a match is found. After the loop, check whether the sentinel is still there.

flowchart TD
  subgraph FF["Find-first"]
    F1["start loop"] --> F2{"match?"}
    F2 -->|"yes"| F3["record result"]
    F3 --> F4["break"]
    F2 -->|"no"| F5["continue"]
    F5 --> F2
  end
  subgraph FA["Find-all"]
    A1["start loop"] --> A2{"match?"}
    A2 -->|"yes"| A3["record result"]
    A3 --> A2
    A2 -->|"no"| A4["continue"]
    A4 --> A2
  end
  style FF fill:#3776ab,color:#fff
  style FA fill:#166534,color:#fff
A find-first search stops at the first match. A find-all search processes every element.
Main.java — find-first search and min/max pattern. Real, compiling Java.
public class Main {
    public static void main(String[] args) {
        int[] temps = {18, 22, 15, 29, 12};

        int firstHot = -1;
        for (int i = 0; i < temps.length; i++) {
            if (temps[i] > 20) {
                firstHot = i;
                break;
            }
        }
        System.out.println("First hot index: " + firstHot);

        int max = temps[0];
        for (int t : temps) {
            if (t > max) {
                max = t;
            }
        }
        System.out.println("Max temp: " + max);
    }
}

The min / max pattern

Finding the smallest or largest value is a variation of the accumulator pattern. Instead of adding each element to a total, you compare each element to the best value seen so far and keep the winner. Initialise max to the first element (or to Integer.MIN_VALUE if the array might be empty), then walk through every element. If the current element is larger, it becomes the new max.

This pattern works for any type that can be compared: integers, doubles, even strings (using .compareTo). The same structure gives you min if you flip the comparison.

Loop invariants: the sanity check

A loop invariant is a statement that is true before the loop starts and remains true after every iteration. It is not magic — it is just a way to describe what the loop has accomplished so far.

For the sum accumulator, the invariant is: sum equals the total of all elements processed so far. Before the loop, zero elements have been processed and sum is 0, so the invariant holds. After processing the third element, sum holds the total of the first three, so it still holds. When the loop ends, every element has been processed, so sum is the total of all of them.

For the max search, the invariant is: max is the largest value seen so far. Before the loop, max is the first element, so it is trivially the largest of the one-element prefix. After each iteration, either max keeps its title or the challenger takes it — either way, the invariant holds.

Invariants do not change how the code runs; they change how confidently you can read it. If you can state a clear invariant, the loop is probably correct. If you cannot, it probably has a bug.

flowchart TD
  subgraph LT["i < 5"]
    L1["i = 0"] --> L2["i = 1"]
    L2 --> L3["i = 2"]
    L3 --> L4["i = 3"]
    L4 --> L5["i = 4"]
    L5 --> L6["stop: 5 passes"]
  end
  subgraph LE["i <= 5"]
    E1["i = 0"] --> E2["i = 1"]
    E2 --> E3["i = 2"]
    E3 --> E4["i = 3"]
    E4 --> E5["i = 4"]
    E5 --> E6["i = 5"]
    E6 --> E7["stop: 6 passes"]
  end
  style LT fill:#3776ab,color:#fff
  style LE fill:#b45309,color:#fff
The off-by-one trap: i < 5 runs 5 times (0..4); i <= 5 runs 6 times (0..5).
Exercise

Accumulator check. Predict the exact output (two lines) — track the sum and the product through every iteration.

public class Main {
    public static void main(String[] args) {
        int[] values = {2, 3, 4};
        int sum = 0;
        int product = 1;
        for (int v : values) {
            sum = sum + v;
            product = product * v;
        }
        System.out.println(sum);
        System.out.println(product);
    }
}
Exercise

Search with break. Predict the exact output (two lines) — the loop stops early when it finds a match.

public class Main {
    public static void main(String[] args) {
        int[] nums = {5, 8, 3, 12, 4};
        int target = 12;
        int index = -1;
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] == target) {
                index = i;
                break;
            }
        }
        System.out.println(index);
        System.out.println(nums[0]);
    }
}
Exercise

For a sum accumulator int total = 0; followed by a loop that adds each array element to total, what is the loop invariant?

Exercise

Off-by-one trap. This loop has the classic bug, but a catch reports it instead of crashing. Predict the exact output (two lines) — how many elements were added before it went wrong?

public class Main {
    public static void main(String[] args) {
        int[] arr = {10, 20, 30};
        int sum = 0;
        try {
            for (int i = 0; i <= arr.length; i++) {
                sum = sum + arr[i];
            }
            System.out.println("finished: " + sum);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("crashed reading index " + arr.length);
            System.out.println("sum before the crash: " + sum);
        }
    }
}
Exercise

Min/max transfer. Predict the exact output (two lines) — track the largest and smallest values seen.

public class Main {
    public static void main(String[] args) {
        int[] nums = {7, 2, 9, 3, 9};
        int max = nums[0];
        int min = nums[0];
        for (int n : nums) {
            if (n > max) {
                max = n;
            }
            if (n < min) {
                min = n;
            }
        }
        System.out.println(max);
        System.out.println(min);
    }
}

Recap

  • The accumulator pattern builds a result one element at a time: initialise, loop, update.
  • A search loop stops early with break when it finds a match; otherwise it processes every element.
  • The min / max pattern keeps the best value seen so far, updating whenever a better candidate appears.
  • A loop invariant is a statement true before and after every iteration. It is the best tool for spotting off-by-one errors before you run the code.
  • The off-by-one trap is the most common loop bug: use < for zero-based indices, and double-check that the condition stops exactly where it should.

Next you will learn how break and continue give you finer control over loop flow, and how labelled loops let you escape nested structures cleanly.

Checkpoint quiz

In a find-first search loop, why is break used?

What is wrong with for (int i = 0; i <= arr.length; i++) when accessing arr[i]?

Go deeper — technical resources