Lesson 8 of 25

Loops

The for Loop, Taken Apart

A loop repeats a block of code. The for loop is the right choice when you know, or can compute, how many times you want to repeat — printing a table of twelve rows, walking every element of an array, or counting from 1 to n.

Its header packs three separate jobs into one line, separated by semicolons, and understanding the exact order they run in explains every for loop you will ever read. for (int i = 0; i < 5; i++) means: run int i = 0 once, before anything else. Then test i < 5; if it is false, stop immediately. If it is true, run the body. Then run i++. Then go back and test again.

Two consequences fall out of that order. First, the condition is checked before the first pass, so a for loop whose condition is false at the start runs zero times — that is correct behaviour, not a bug, and it is exactly what you want when an array turns out to be empty. Second, the update runs after the body, so inside the body i still holds the value it was tested with.

The variable declared in the header exists only inside the loop. Try to use i after the closing brace and you get cannot find symbol. That is deliberate: it keeps counters from leaking into the rest of your method. If you genuinely need the value afterwards, declare the variable before the loop instead.

Example
//     1. runs once   2. tested before each pass   3. runs after each pass
for (int i = 1;        i <= 5;                     i++) {
    System.out.println("12 x " + i + " = " + (12 * i));
}
// 12 x 1 = 12 ... 12 x 5 = 60

// System.out.println(i);   // compile error: i does not exist out here

// Counting down
for (int i = 5; i >= 1; i--) {
    System.out.print(i + " ");     // 5 4 3 2 1
}
System.out.println();

// Any step size you like
for (int i = 0; i <= 100; i += 25) {
    System.out.print(i + " ");     // 0 25 50 75 100
}
System.out.println();

// Zero iterations, and that is fine
int[] empty = {};
for (int i = 0; i < empty.length; i++) {
    System.out.println("never printed");
}

// Two counters at once — commas, not semicolons
for (int i = 0, j = 10; i < j; i++, j--) {
    System.out.println(i + " " + j);
}
Notes
  • All three parts of the header are optional. for (;;) { } is a legal infinite loop, equivalent to while (true). You will see it occasionally in code that always exits via a break or a return.

while and do-while: Looping Without a Count

Use while when you cannot say in advance how many repetitions you need — keep reading lines until the file ends, keep asking for input until it is valid, keep halving a number until it reaches 1. The loop simply tests a condition before every pass and stops when it becomes false.

Everything a for loop does, a while loop can do, and vice versa. The difference is where the counter lives. for keeps initialisation, test and update on one line where you can see all three together, which is why counted loops read better as for. In a while loop those three pieces are scattered, and the update in particular is easy to forget — which produces an infinite loop.

do-while puts the test at the bottom, so the body always runs at least once before the condition is examined. That is exactly right for a menu: you must show the menu before you can know whether the user wants to quit. Note the semicolon after the closing while (...) — it is required, and leaving it out is a compile error people stare at for a while.

In real code do-while is uncommon. Reach for it only when "do this, then decide whether to repeat" is a genuinely accurate description; otherwise a plain while is easier to reason about because its exit condition is visible at the top.

Example
// while — the count is not known in advance
int n = 1000;
int steps = 0;
while (n > 1) {
    n = n / 2;
    steps++;
}
System.out.println("Halved " + steps + " times");   // 9

// do-while — the body runs before the first test
import java.util.Scanner;

Scanner sc = new Scanner(System.in);
int choice;
do {
    System.out.println("1. Deposit");
    System.out.println("2. Withdraw");
    System.out.println("0. Exit");
    System.out.print("Choice: ");
    choice = sc.nextInt();

    switch (choice) {
        case 1 -> System.out.println("Depositing...");
        case 2 -> System.out.println("Withdrawing...");
        case 0 -> System.out.println("Bye");
        default -> System.out.println("Invalid choice");
    }
} while (choice != 0);          // <- the semicolon is required

// The difference in one line: this while prints nothing...
int i = 10;
while (i < 5) { System.out.println(i); }

// ...this do-while prints once
int j = 10;
do { System.out.println(j); } while (j < 5);   // prints 10
Notes
  • Never use a double as a loop counter. for (double d = 0; d != 1.0; d += 0.1) never stops, because repeatedly adding one-tenth in binary never lands exactly on 1.0. Count with an int and divide when you need the fraction.

Infinite Loops and Off-by-One Errors

Two failures account for nearly every loop bug, and both are worth being able to recognise instantly.

An infinite loop happens when the condition never becomes false. The usual cause is a forgotten update — the classic while (i < 5) with no i++ inside. A subtler cause is updating the wrong variable, which happens easily in nested loops where i and j look alike. If your program hangs and prints nothing, or prints the same line forever, stop it with Ctrl+C and look at what changes the condition variable.

An off-by-one error means the loop runs one time too many or too few. In Java, indices run from 0 to length - 1, so the correct test for walking an array is i < arr.length. Writing i <= arr.length compiles cleanly and throws ArrayIndexOutOfBoundsException on the final pass. Conversely, if you are counting from 1 to n rather than indexing, i <= n is correct and i < n loses the last value.

The reliable habit is to say out loud what the first and last values of the counter should be, then check the header produces exactly those. For an array of 5 elements: first index 0, last index 4, so i = 0 and i < 5. For a multiplication table: first 1, last 10, so i = 1 and i <= 10.

Example
int[] marks = {90, 85, 78, 92, 65};    // valid indices: 0,1,2,3,4

// Correct
for (int i = 0; i < marks.length; i++) {
    System.out.println(marks[i]);
}

// Off by one — throws on the last pass
// for (int i = 0; i <= marks.length; i++) {
//     System.out.println(marks[i]);
// }
// ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5

// Counting 1..n — here <= IS correct
int n = 10;
for (int i = 1; i <= n; i++) {
    System.out.print(i + " ");
}
System.out.println();

// Infinite: the counter is never updated
// int k = 0;
// while (k < 5) {
//     System.out.println(k);   // prints 0 forever
// }

// Infinite: the wrong variable is updated
// for (int a = 0; a < 3; a++) {
//     int b = 0;
//     while (b < 3) {
//         System.out.println(a + "," + b);
//         a++;      // should be b++
//     }
// }
Notes
  • A deliberate infinite loop is fine and common — a server waiting for requests, or a menu that exits with break. Write it as while (true) so the intent is obvious, and make sure the exit path is easy to find.

The Enhanced for Loop, and Its Three Limits

The enhanced for loop — often called for-each — reads as "for each element in this collection". It works on arrays and on anything that implements Iterable, which covers ArrayList, HashSet and most collections you will meet.

It exists because the indexed form has three moving parts you can get wrong, and a for-each loop has none: no counter to initialise, no bound to compare, no chance of running off the end. When you simply want to look at every element, it is both shorter and safer, and you should default to it.

It has three limitations, and each one tells you when to go back to the indexed loop. First, you do not get the index, so if you need to print serial numbers or compare an element with its neighbour, use a counted loop. Second, you cannot modify the array through the loop variable. The variable is a fresh copy of each element; assigning to it changes the copy and leaves the array untouched, silently. Third, you cannot add or remove elements from a collection while iterating over it — doing so throws ConcurrentModificationException, which a later lesson covers along with its fix.

For objects the second limitation is narrower than it first sounds. The loop variable holds a copy of the reference, so calling a method that changes the object's own fields works fine; only reassigning the variable itself has no effect.

Example
String[] subjects = {"Maths", "Physics", "Chemistry"};

// Reads as: for each String called subject, in subjects
for (String subject : subjects) {
    System.out.println(subject);
}

import java.util.List;
List<Integer> marks = List.of(90, 85, 78);
int total = 0;
for (int m : marks) {
    total += m;
}
System.out.println("Total: " + total);   // 253

// Limit 1: no index available — use a counted loop for serial numbers
for (int i = 0; i < subjects.length; i++) {
    System.out.println((i + 1) + ". " + subjects[i]);
}

// Limit 2: assigning to the loop variable does NOT change the array
int[] nums = {1, 2, 3};
for (int x : nums) {
    x = x * 2;                    // changes the copy only
}
System.out.println(nums[0]);      // 1  — unchanged

for (int i = 0; i < nums.length; i++) {
    nums[i] = nums[i] * 2;        // this really doubles them
}
System.out.println(nums[0]);      // 2
Notes
  • Use the enhanced form by default. Switch to the indexed form only when you actually need the position, need to modify elements in place, or need to iterate backwards.

break, continue and Nested Loops

break leaves the loop immediately. Its natural use is a search: as soon as you have found what you were looking for, there is no reason to keep checking. continue skips the rest of the current pass and jumps to the next one, which is how you filter — "if this record is not what I want, move on".

There is a real trap in continue. In a for loop, continue still runs the update expression in the header, so the counter advances and everything is fine. In a while loop, the update is a statement inside the body, so a continue placed before it skips the increment and the loop runs forever. This is one of the few bugs that behaves differently between two loops that otherwise look equivalent.

In nested loops, both break and continue affect only the innermost loop containing them. Breaking out of an inner loop when you meant to abandon the whole search is a common mistake — the outer loop simply carries on. Java's answer is a label: put a name and a colon before the outer loop and write break outer; to leave both at once.

Labels are the one place where Java allows something close to a goto, and they should be rare. If you find yourself reaching for a label often, the loops probably want to be a method with a return instead, which is easier to read and easier to test.

Example
int[] marks = {45, 67, 89, 34, 92};

// break — stop as soon as you find one
for (int m : marks) {
    if (m > 85) {
        System.out.println("Found a distinction: " + m);
        break;
    }
}

// continue — skip the ones you do not want
for (int m : marks) {
    if (m < 40) continue;            // skip failures
    System.out.println("Passed with " + m);
}

// The continue trap in a while loop
int i = 0;
while (i < 5) {
    if (i == 2) {
        // continue;    <-- would skip i++ below and loop forever
    }
    System.out.println(i);
    i++;
}

// Nested loops: break leaves only the inner one
int[][] grid = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int target = 5;

outer:
for (int r = 0; r < grid.length; r++) {
    for (int c = 0; c < grid[r].length; c++) {
        if (grid[r][c] == target) {
            System.out.println("Found at row " + r + ", col " + c);
            break outer;             // leaves BOTH loops
        }
    }
}

// A nested loop building a pattern
for (int row = 1; row <= 4; row++) {
    for (int col = 1; col <= row; col++) {
        System.out.print("* ");
    }
    System.out.println();
}
// *
// * *
// * * *
// * * * *
Notes
  • Nested loops multiply work. Two loops of 1,000 iterations each run a million times; three run a billion. When a program is unexpectedly slow, count the nesting depth first — that is almost always where the time is going.
Ask AI