algoz
Chapters/01 Java basics/Loops

Loops

Repeat instructions safely with for, while, and do-while without running forever.

You'll gain
Write loops that end on timeand control nested execution safely
Practice
Sheet coming sooncoding problems for this note
Summary

Loops execute a block of statements repeatedly until a condition changes, letting you process collections and repeat calculations without duplicating code.

The idea in real life

Think of a runner completing laps around a track. Before each lap, the runner checks the lap counter. If laps remain, the runner completes another lap, taps the counter at the finish line, and checks again. Loops in Java repeat work in this exact sequence: inspect a condition, execute a block of instructions, update a counter, and decide whether to run another pass.

Why it exists

  1. Writing the same instruction dozens or thousands of times clutters code and makes programs difficult to maintain.
  2. Repeating statements dynamically lets programs process collections and streams of unknown or variable size at runtime.
  3. Controlling execution with explicit boundary conditions prevents programs from freezing or running indefinitely.
  4. Structured loop controls like break and continue allow you to stop early when an answer is found or skip irrelevant entries.

How it works

The for loop

A for header groups setup, condition check, and step update together.

The for loop is the standard loop in Java when you know how many times a block of code should run, or when you need to track an index.

The loop header contains three parts separated by semicolons: for (initialisation; termination; increment).

  • Initialisation: Runs exactly once before the loop begins. You declare and initialise your counter variable here, such as int i = 0.
  • Termination: A boolean condition evaluated before every pass. If it evaluates to true, the loop body runs. If it evaluates to false, the loop ends immediately.
  • Increment: An update statement executed after the loop body finishes, right before Java tests the termination condition again.

When iterating over n items, standard practice in Java starts the index at 0 and continues while i < n.

Main.java
public class Main {
    public static void main(String[] args) {
        int n = 4;
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum += i;
        }
        System.out.println(sum);
    }
}
Output
6

In this program:

  • n stores the upper boundary.
  • sum accumulates the running total of index values.
  • i is the loop index, initialised to 0.
  • i < n checks whether i has reached the upper boundary before each pass.
  • i++ increments i by 1 after each body pass, as taught in operators.

Watch the cycle move through its four parts in order: start runs once, then check, body, and update repeat until the check fails. Change condition to i <= n to see how an inclusive check runs one extra pass.

The while loop

Repeats while a condition holds when the number of steps is unknown.

Use a while loop when you do not know the exact number of iterations before starting, and the loop must continue until an external condition changes.

A while loop checks its boolean condition before executing the body. If the condition evaluates to false on the very first check, the body does not execute at all.

A common example is breaking down an integer digit by digit. Because different numbers have different digit lengths, you cannot hard-code a fixed iteration count. Dividing by 10 discards the last digit on each pass until the value reaches 0.

Main.java
public class Main {
    public static void main(String[] args) {
        int number = 3456;
        int digitCount = 0;

        while (number > 0) {
            number /= 10;
            digitCount++;
        }

        System.out.println("Digit count: " + digitCount);
    }
}
Output
Digit count: 4

In this program:

  • number begins at 3456 and is reduced by a factor of 10 on each pass (3456 → 345 → 34 → 3 → 0).
  • digitCount increments by 1 during each iteration.
  • When number becomes 0, the condition number > 0 evaluates to false, and the loop terminates with digitCount equal to 4.

The do-while loop

Executes the body once first before testing the condition.

The do-while loop is a post-test loop. Unlike for and while, which test their condition at the top, do-while executes its body first and checks the condition at the bottom.

Because the condition is checked at the end, the body of a do-while loop always runs at least once, even if the condition is false from the start. Notice the required semicolon ; after the closing parenthesis of while (condition);.

A practical case for do-while is digit counting that must handle the value 0. In decimal representation, the number 0 has 1 digit. A while (number > 0) loop would check 0 > 0, evaluate to false, and report 0 digits. A do-while loop executes the body once, registers the single digit, and then exits.

Main.java
public class Main {
    public static void main(String[] args) {
        int number = 0;
        int steps = 0;

        do {
            steps++;
            number /= 10;
        } while (number > 0);

        System.out.println("Steps for zero: " + steps);
    }
}
Output
Steps for zero: 1

In this program:

  • number is initialised to 0.
  • The do block executes immediately, incrementing steps to 1 and setting number to 0 / 10, which remains 0.
  • The condition number > 0 evaluates to false, so the loop terminates after exactly one iteration.

The for-each loop

Iterates directly over array elements without managing an index.

The enhanced for loop, commonly called the for-each loop, provides clean syntax for stepping through every item in an array or collection from start to finish.

Arrays (taught in note 9) store fixed-length sequences of elements. When you only need to read each element and do not need to know the current index or modify the original array, the for-each loop removes the boilerplate of managing counter variables.

An important rule of the for-each loop is that the iteration variable holds a copy of each value. Assigning a new value to the iteration variable does not modify the underlying array.

Main.java
public class Main {
    public static void main(String[] args) {
        int[] values = {10, 20, 30};

        for (int item : values) {
            item += 5;
        }

        for (int item : values) {
            System.out.println("Element: " + item);
        }
    }
}
Output
Element: 10
Element: 20
Element: 30

In this program:

  • values is an array literal containing three integers: 10, 20, and 30.
  • In the first loop, item receives a copy of each element in turn. The expression item += 5 modifies only the local variable item.
  • The second loop prints the elements of values, showing that the stored numbers remain 10, 20, and 30. If you need to modify elements in place, use an indexed for loop instead.

Going deeper

break and continue

break terminates immediately, while continue skips to the next iteration.

Java provides two branching statements that alter loop execution:

  • break: Halts the enclosing loop immediately. Execution jumps to the first statement following the loop's closing brace.
  • continue: Skips the remainder of the current iteration. In a for loop, execution jumps directly to the increment step (i++). In a while or do-while loop, execution jumps directly to the condition check.

As introduced in conditions and switch, break is also used to prevent fall-through in switch blocks. Inside loops, it provides an early exit as soon as a search finishes or an abort condition occurs.

Main.java
public class Main {
    public static void main(String[] args) {
        for (int i = 1; i <= 6; i++) {
            if (i == 3) {
                continue;
            }
            if (i == 5) {
                break;
            }
            System.out.println("Value: " + i);
        }
    }
}
Output
Value: 1
Value: 2
Value: 4

In this program:

  • When i equals 3, the continue statement skips the System.out.println call and triggers the update step i++, so 3 does not appear in the output.
  • When i reaches 5, the break statement halts the loop immediately, preventing 5 and 6 from being printed.

Labelled break and continue

Labels allow breaking out of multiple nested loops in one step.

By default, break and continue apply only to the innermost loop containing them. When searching a two-dimensional grid, locating the target item inside the inner loop leaves the outer loop running unless you add extra flags or return from a method.

Java supports labelled statements. You place an identifier followed by a colon before any loop statement: labelName: for (...). You can then specify break labelName; or continue labelName; to target that outer loop directly.

Main.java
public class Main {
    public static void main(String[] args) {
        int[][] grid = {
            {1, 2, 3},
            {4, 5, 6},
            {7, 8, 9}
        };
        int target = 5;
        int foundRow = -1;
        int foundCol = -1;

        searchGrid:
        for (int row = 0; row < grid.length; row++) {
            for (int col = 0; col < grid[row].length; col++) {
                if (grid[row][col] == target) {
                    foundRow = row;
                    foundCol = col;
                    break searchGrid;
                }
            }
        }

        System.out.println("Found " + target + " at (" + foundRow + ", " + foundCol + ")");
    }
}
Output
Found 5 at (1, 1)

In this program:

  • grid is a 3-by-3 two-dimensional array written as a nested literal.
  • target is 5, located at row index 1 and column index 1.
  • When grid[row][col] == target is true, break searchGrid; exits both the inner and outer loops simultaneously, jumping directly to the print statement.
  • Similarly, continue searchGrid; would immediately abort the remaining columns of the current row and advance the outer loop to the next row.

Off-by-one errors

Half-open ranges run b minus a times and prevent boundary errors.

An off-by-one error occurs when a loop executes one time too many or one time too few. It is an easy mistake to make when turning a range into code.

Ranges are structured in two standard ways:

  • Half-open range [a, b): Includes a but excludes b. In code, you write for (int i = a; i < b; i++). The total number of iterations is exactly b - a.
  • Closed range [a, b]: Includes both a and b. In code, you write for (int i = a; i <= b; i++). The total number of iterations is b - a + 1.

Because Java array indexes start at 0 and end at length - 1, half-open ranges [0, n) match array indexing. A loop iterating over an array of size n with < executes n - 0 = n times, visiting indexes 0 through n - 1. If you write <= n, the loop runs n + 1 times and attempts to access index n, which throws an exception.

Main.java
public class Main {
    public static void main(String[] args) {
        int countHalfOpen = 0;
        for (int i = 0; i < 5; i++) {
            countHalfOpen++;
        }

        int countClosed = 0;
        for (int i = 1; i <= 5; i++) {
            countClosed++;
        }

        System.out.println("Half-open [0, 5) iterations: " + countHalfOpen);
        System.out.println("Closed [1, 5] iterations: " + countClosed);
    }
}
Output
Half-open [0, 5) iterations: 5
Closed [1, 5] iterations: 5

In this program:

  • countHalfOpen counts iterations for i = 0 while i < 5. The calculation 5 - 0 gives 5 iterations (0, 1, 2, 3, 4).
  • countClosed counts iterations for i = 1 while i <= 5. The calculation 5 - 1 + 1 gives 5 iterations (1, 2, 3, 4, 5).

Infinite loops

Loops run forever when the condition never becomes false.

An infinite loop occurs when a loop's termination condition never evaluates to false. When this happens, the thread executing the loop runs continuously, consuming processor time and causing the program to hang.

Infinite loops usually stem from three causes:

  1. Forgotten update: Omitting the increment or state change inside a while loop (such as forgetting i++), meaning the tested variables never change.
  2. Wrong direction: Incrementing when you intended to decrement (for example, for (int i = 5; i > 0; i++)), moving the counter further away from the termination threshold.
  3. Primitive counter overflow: Using an integer type whose capacity wraps around before reaching the condition limit.

As taught in variables and primitive types, an 8-bit byte variable has a range of -128 to 127. If you write for (byte b = 0; b <= 127; b++), incrementing b when it equals 127 wraps to -128 due to two's complement arithmetic. Because -128 is less than or equal to 127, the condition b <= 127 remains true forever.

Main.java
public class Main {
    public static void main(String[] args) {
        byte counter = 125;
        for (int step = 0; step < 5; step++) {
            System.out.println("Counter: " + counter);
            counter++;
        }
    }
}
Output
Counter: 125
Counter: 126
Counter: 127
Counter: -128
Counter: -127

In this program:

  • counter starts at 125.
  • Incrementing past 127 wraps the value to -128.
  • If a loop condition checked counter <= 127, the counter would continuously cycle between -128 and 127, creating an infinite loop. Always use int for loop counters unless you have a specific low-level memory constraint.

Under the hood

Loop variable scope

Variables declared in a for header exist only inside that loop.

In the Java Language Specification (JLS §6.3), the scope of a local variable declared in the forInit part of a basic for statement includes the initialization expression itself, the termination check, the increment expression, and the entire loop body.

Once execution exits the loop's closing brace }, the loop variable is out of scope. Its slot in the stack frame cannot be referenced by subsequent statements in the method. If you attempt to access it, javac rejects the program with cannot find symbol.

If you need to know what index a loop terminated at (for instance, when recording where a search found its target), declare the counter variable before the loop header.

Main.java
public class Main {
    public static void main(String[] args) {
        int lastIndex = -1;
        for (int i = 0; i < 5; i++) {
            if (i == 3) {
                lastIndex = i;
                break;
            }
        }
        System.out.println("Found target at index: " + lastIndex);
    }
}
Output
Found target at index: 3

In this program:

  • i is declared in the for header and exists only during loop execution.
  • lastIndex is declared before the loop in the main method scope. When i == 3, lastIndex captures the current index before break exits the loop.
  • After the loop closes, lastIndex remains accessible and prints 3.

How a loop compiles

Bytecode uses conditional branches and backward jumps to repeat code.

At the bytecode level, the Java Virtual Machine has no dedicated instruction named loop. Instead, the Java compiler translates for, while, and do-while loops into a combination of conditional branch instructions and backward unconditional jumps (goto).

Consider compiling a standard loop that accumulates numbers:

LoopBytecode.java
public class LoopBytecode {
    public static void main(String[] args) {
        int sum = 0;
        for (int i = 0; i < 5; i++) {
            sum += i;
        }
        System.out.println(sum);
    }
}

Running javap -c LoopBytecode.class produces the compiled bytecode instructions:

text
Compiled from "LoopBytecode.java"
public class LoopBytecode {
  public static void main(java.lang.String[]);
    Code:
       0: iconst_0
       1: istore_1
       2: iconst_0
       3: istore_2
       4: iload_2
       5: iconst_5
       6: if_icmpge     19
       9: iload_1
      10: iload_2
      11: iadd
      12: istore_1
      13: iinc          2, 1
      16: goto          4
      19: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      22: iload_1
      23: invokevirtual #13                 // Method java/io/PrintStream.println:(I)V
      26: return
}

Notice how the control flow works:

  • istore_1 sets sum (local variable 1) to 0.
  • istore_2 sets i (local variable 2) to 0.
  • At instruction 4, iload_2 pushes i onto the operand stack, and iconst_5 pushes the constant 5.
  • if_icmpge 19 compares the two values. If i >= 5, execution jumps forward to instruction 19, leaving the loop.
  • If i < 5, execution continues to lines 9–12, computing sum + i and storing the result in sum.
  • iinc 2, 1 increments variable 2 (i) by 1 directly in local variable storage without pushing to the stack.
  • goto 4 jumps backward to instruction 4 to re-evaluate the termination condition.

A while loop compiles to the same pattern: a conditional jump out of the loop and a goto back to the test.

How much work a loop does

Total operations grow with the number of times the body executes.

The amount of computational work a program performs depends directly on how many times its loops run.

A single loop running from 0 to n - 1 performs approximately n iterations. If each iteration executes a small, fixed number of arithmetic operations, the total work grows in direct proportion to n. Doubling n doubles the total steps.

When you nest one loop inside another, the operations multiply:

  • If an outer loop runs n times and an inner loop runs m times on each outer pass, the inner body executes n * m times.
  • If both loops run n times, the inner body executes n * n (or n²) times. For n = 1,000, a single loop executes 1,000 steps, whereas a nested loop executes 1,000,000 steps.

A common variation in algorithm problems is a triangular loop, where the inner loop begins at j = i + 1:

  • On iteration i = 0, the inner loop runs n - 1 times.
  • On iteration i = 1, the inner loop runs n - 2 times.
  • Continuing down to 1 step, the total iterations equal (n - 1) + (n - 2) + ... + 1 = n * (n - 1) / 2.
  • While this does roughly half the operations of a full n * n grid, it still scales quadratically with n.

Chapter 06 introduces Big-O notation to formalise these growth curves. At this stage, you need to count how many times each loop body executes to understand how workload grows.

Main.java
public class Main {
    public static void main(String[] args) {
        int size = 4;
        int singleLoopSteps = 0;
        for (int i = 0; i < size; i++) {
            singleLoopSteps++;
        }

        int nestedLoopSteps = 0;
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                nestedLoopSteps++;
            }
        }

        int triangularSteps = 0;
        for (int i = 0; i < size; i++) {
            for (int j = i + 1; j < size; j++) {
                triangularSteps++;
            }
        }

        System.out.println("Single loop steps: " + singleLoopSteps);
        System.out.println("Nested loop steps: " + nestedLoopSteps);
        System.out.println("Triangular steps: " + triangularSteps);
    }
}
Output
Single loop steps: 4
Nested loop steps: 16
Triangular steps: 6

In this program:

  • size is set to 4.
  • singleLoopSteps executes 4 times.
  • nestedLoopSteps executes 4 * 4 = 16 times.
  • triangularSteps executes 4 * 3 / 2 = 6 times, representing all unique pairs where i < j.

Changing the counter inside the body

Altering the counter inside the body causes erratic and broken flow.

In a standard for loop, the header is solely responsible for advancing the counter (i++). If you also modify that same counter variable inside the loop body, you create dual points of control.

Modifying the counter in the body leads to two primary problems:

  • Skipped iterations: Incrementing i inside the body advances the counter twice in a single pass (once in the body and once in the header update), skipping elements silently.
  • Accidental infinite loops: Decrementing i inside the body can cancel out the header's increment, preventing i from ever reaching the termination boundary.

If your iteration logic requires non-uniform or condition-dependent steps, use a while loop instead. A while loop makes it clear that the body is in full control of when and how the state advances.

Main.java
public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            System.out.println("Index: " + i);
            if (i == 1) {
                i++;
            }
        }
    }
}
Output
Index: 0
Index: 1
Index: 3
Index: 4

In this program:

  • When i is 1, the body executes i++, changing i to 2.
  • The loop header then executes its regular i++ update, advancing i to 3.
  • Index 2 is never processed. To change step sizes cleanly, write the step directly in the header, such as for (int i = 0; i < 5; i += 2).

In Java

Main.java
public class Main {
    // Returns the sum of all elements in the values array.
    static int calculateTotal(int[] values) {
        int total = 0;
        for (int item : values) {
            total += item;
        }
        return total;
    }

    // Returns the number of times target appears in the values array.
    static int countOccurrences(int[] values, int target) {
        int count = 0;
        for (int i = 0; i < values.length; i++) {
            if (values[i] == target) {
                count++;
            }
        }
        return count;
    }

    // Returns the row index of target in grid, or -1 if target is absent.
    static int findTargetRow(int[][] grid, int target) {
        int targetRow = -1;
        searchGrid:
        for (int row = 0; row < grid.length; row++) {
            for (int col = 0; col < grid[row].length; col++) {
                if (grid[row][col] == target) {
                    targetRow = row;
                    break searchGrid;
                }
            }
        }
        return targetRow;
    }

    // Returns the number of decimal digits in positive number.
    static int countDigits(int number) {
        int digits = 0;
        do {
            digits++;
            number /= 10;
        } while (number > 0);
        return digits;
    }

    public static void main(String[] args) {
        int[] scores = {12, 18, 25, 18, 30};
        int[][] matrix = {
            {3, 6, 9},
            {12, 15, 18},
            {21, 24, 27}
        };

        int totalScore = calculateTotal(scores);
        int matches = countOccurrences(scores, 18);
        int rowLocation = findTargetRow(matrix, 15);
        int zeroDigits = countDigits(0);
        int numberDigits = countDigits(4825);

        System.out.println("Total score: " + totalScore);
        System.out.println("Occurrences of 18: " + matches);
        System.out.println("Row of target 15: " + rowLocation);
        System.out.println("Digits in 0: " + zeroDigits);
        System.out.println("Digits in 4825: " + numberDigits);
    }
}
Output
Total score: 103
Occurrences of 18: 2
Row of target 15: 1
Digits in 0: 1
Digits in 4825: 4

Traps

  1. Using <= with zero-based length running one extra time

    Zero-based indexing has valid elements from 0 to length - 1. Using <= attempts to access index length, running length + 1 times and throwing an index out of bounds exception.

    ✕ WRONGfor (int i = 0; i <= length; i++)
    ✓ RIGHTfor (int i = 0; i < length; i++)
  2. Accidental semicolon immediately after a loop header

    A semicolon immediately following the loop header terminates the loop with an empty statement. The loop runs 5 times doing nothing, and any subsequent block inside { } executes only once afterwards.

    ✕ WRONGfor (int i = 0; i < 5; i++);
    ✓ RIGHTfor (int i = 0; i < 5; i++) {
  3. Forgetting to update the loop condition variable in a while loop

    Unlike a for loop, a while loop does not provide an automatic update step in its header. If you omit the increment inside the body, index never reaches limit and the loop runs forever.

    ✕ WRONGwhile (index < limit) { process(); }
    ✓ RIGHTwhile (index < limit) { process(); index++; }
  4. Using a byte counter that wraps around at 127

    A byte has a maximum value of 127. When incremented from 127, it wraps to -128 as taught in variables and primitive types. Because -128 <= 127 is always true, the loop never terminates.

    ✕ WRONGfor (byte b = 0; b <= 127; b++)
    ✓ RIGHTfor (int b = 0; b <= 127; b++)
  5. Expecting a for-each loop to modify array elements

    The enhanced for loop assigns each array element into a local copy value. Modifying value updates only the local variable on the stack, leaving the array unchanged. Use an indexed loop when you need to overwrite array slots.

    ✕ WRONGfor (int value : nums) { value = 0; }
    ✓ RIGHTfor (int i = 0; i < nums.length; i++) { nums[i] = 0; }
  6. Modifying the loop index inside a for loop body

    Altering the loop counter inside the body causes the index to advance by an unexpected amount, skipping elements and risking infinite loops if the counter moves backward. Put step increments directly in the for header.

    ✕ WRONGfor (int i = 0; i < limit; i++) { i++; }
    ✓ RIGHTfor (int i = 0; i < limit; i += 2)
  7. Assuming break exits all nested loops

    An unlabelled break exits only the innermost enclosing loop. The outer loop continues to run its remaining iterations. Use a labelled break to exit both loops simultaneously.

    ✕ WRONGif (found) { break; }
    ✓ RIGHTif (found) { break searchLabel; }
AlgozSet in Geist & Geist MonoJava track