The previous lesson ended with a specific frustration: when BiblioTechApp detects invalid data, it gives up and ends instead of asking again; and when it finishes processing one return, the program is over, even if Marta Ruiz has four books in her backpack. Conditionals taught your program to decide; loops teach it to repeat. Together with conditionals they are half of control flow, and probably the construct you will write most often in your professional life. In this lesson you will see the three classic loop forms in Java, the internal anatomy of each one, the fundamental patterns that solve 90 % of real cases (counting, accumulating, searching, validating, offering a menu) and the traps that turn a loop into a hang or a runaway memory consumer.
Contents
- What a loop is and what parts it has
- The
whileloop - The
do-whileloop - The
forloop - Comparison table: when to use each one
- The
for-eachloop: it exists, but it comes later - Nested loops: the BiblioTech fine table
- Fundamental patterns: counter, accumulator and flag search
- Input validation loop
- Menu loop (a preview of the project)
- Infinite loops: intentional and accidental
- The cost of iterations and the
Stringconcatenation trap - Common Mistakes and Tips
- Exercises
- What a loop is and what parts it has
A loop is a structure that repeatedly runs a block of code while a condition holds. Each complete pass through the block is called an iteration.
Every loop, whatever its shape, has four elements. Always identifying them —even when the language does not force you to write them together— is the best defence against infinite loops:
| Element | Purpose | If it is missing or wrong… |
|---|---|---|
| Initialisation | Prepares the control variables before starting | The loop starts with garbage or does not compile |
| Condition | Evaluated before (or after) each iteration; decides whether to continue | Infinite loop, or a loop that never runs |
| Body | The work that is repeated | The loop does nothing useful |
| Update | Modifies the control variable to move towards the end | Guaranteed infinite loop |
The mental picture:
flowchart TD
I["Initialisation"] --> C{"Condition"}
C -- "true" --> B["Body"]
B --> U["Update"]
U --> C
C -- "false" --> F["Continue after the loop"]
- The
while loop
while loopwhile is the most elementary loop: it repeats while the condition is true. It checks the condition before each iteration, so it can run zero times.
Anatomy dissected with a counter of processed returns:
int returnsProcessed = 0; // 1. INITIALISATION (outside the loop)
while (returnsProcessed < 3) { // 2. CONDITION (checked BEFORE entering)
System.out.println("Processing return number " + (returnsProcessed + 1));
// 3. BODY
returnsProcessed++; // 4. UPDATE (inside the body)
}
System.out.println("Total processed: " + returnsProcessed);Output:
A step-by-step trace, which is how you should always read a loop the first time:
| Iteration | returnsProcessed on entry |
< 3 |
Prints | On exit |
|---|---|---|---|---|
| 1 | 0 | true |
"…number 1" | 1 |
| 2 | 1 | true |
"…number 2" | 2 |
| 3 | 2 | true |
"…number 3" | 3 |
| — | 3 | false |
— | end |
In this loop the update lives inside the body and it is your responsibility not to forget it. That is while's big risk: if you delete the returnsProcessed++, the condition never changes and the program hangs, printing the same line forever.
while shines when you do not know how many iterations there will be, because the number depends on something external (what the user types, whether something has been found, whether there is work left):
final double MAX_FINE = 20.0;
final double DAILY_RATE = 0.25;
double accruedFine = 0.0;
int days = 0;
// How many days late does it take to reach the cap?
while (accruedFine < MAX_FINE) {
days++;
accruedFine += DAILY_RATE;
}
System.out.printf("After %d days late the cap of %.2f EUR is reached%n",
days, accruedFine);
// After 80 days late the cap of 20.00 EUR is reached
- The
do-while loop
do-while loopdo-while is identical to while except in one decisive point: it checks the condition at the end, so the body runs at least once, whatever happens.
The semicolon after the while is mandatory; it is the only place in Java where a while carries one.
flowchart TD
I["Initialisation"] --> B["Body (always at least once)"]
B --> U["Update"]
U --> C{"Condition"}
C -- "true" --> B
C -- "false" --> F["Continue after the loop"]
The practical difference shows when you compare both with a condition that is false from the start:
int daysLate = 0;
while (daysLate > 0) {
System.out.println("WHILE: it is late"); // NEVER printed
}
do {
System.out.println("DO-WHILE: it is late"); // printed ONCE
} while (daysLate > 0);do-while is the natural tool for anything that has to be done at least once and perhaps repeated: asking the user for a value, showing a menu, retrying an operation. It is the "ask first, decide later" loop.
Scanner scanner = new Scanner(System.in);
String answer;
do {
System.out.print("Register another return? (yes/no): ");
answer = scanner.nextLine().trim();
} while (!answer.equalsIgnoreCase("yes") && !answer.equalsIgnoreCase("no"));
System.out.println("You answered: " + answer);Notice that answer is declared outside the do. If you declared it inside the block, it would not exist in the while condition nor after the loop. It is a very common compilation error for beginners.
- The
for loop
for loopfor groups the four loop elements into a single line, which makes it impossible to forget the update. It is the go-to loop when you know how many times you have to repeat.
Dissected term by term:
int i = 1— initialisation. It runs only once, before everything else. The variableionly exists inside the loop (outside it you will getcannot find symbol), which is an advantage: it does not pollute the rest of the method.i <= 5— condition. Evaluated before each iteration, just like inwhile. If it is false from the start, the body never runs.i++— update. It runs after the body, before checking the condition again.- The body between braces.
The actual order of execution, worth committing to memory:
flowchart TD
A["int i = 1 (only once)"] --> B{"i <= 5"}
B -- "true" --> C["Body"]
C --> D["i++"]
D --> B
B -- "false" --> E["Leave the loop"]
Applied to BiblioTech, to print the fine scale for the first week:
final double DAILY_RATE = 0.25;
System.out.println("DAYS FINE");
for (int day = 1; day <= 7; day++) {
double fine = day * DAILY_RATE;
System.out.printf("%4d %6.2f EUR%n", day, fine);
}Variants you will see in real code:
// Countdown
for (int days = 15; days > 0; days--) { ... }
// Steps of five
for (int day = 0; day <= 30; day += 5) { ... }
// Two control variables (separated by a comma)
for (int i = 0, j = 10; i < j; i++, j--) { ... }
// Every section is optional: this is a valid infinite loop
for (;;) { ... }A naming tip: i, j, k are acceptable for pure numeric indexes (it is a universal convention inherited from mathematics), but as soon as the variable means something, give it a name: day, attempt, bookNumber. Code is read many more times than it is written.
- Comparison table: when to use each one
The three loops are theoretically interchangeable: anything you do with one you can do with another. The choice is a matter of communicating intent.
| Criterion | while |
do-while |
for |
|---|---|---|---|
| Condition evaluated | Before | After | Before |
| Minimum iterations | 0 | 1 | 0 |
| Do you know how many times? | No | No | Yes |
| Loop parts | Scattered | Scattered | Together in the header |
| Risk of forgetting the update | High | High | Low |
| Typical use | "While there is work left" | Menus, input validation | Traversals and counted repetitions |
| BiblioTech example | Accumulating the fine up to the cap | Asking for an option until it is valid | Fine table from day 1 to 30 |
A practical rule you can apply almost without thinking:
- Repeating a known number of times, or traversing indexes? →
for. - Repeating while something holds, perhaps zero times? →
while. - Doing it at least once and then deciding? →
do-while.
- The
for-each loop: it exists, but it comes later
for-each loop: it exists, but it comes laterJava has a fourth loop, the enhanced for or for-each, with this syntax:
It reads as "for each title in catalog". It is shorter and removes all the index arithmetic, so it is the preferred loop where it applies. Its limitation is that it only works over arrays and collections, and neither of those exists in your toolbox yet: arrays arrive in lesson 05-01 and collections in the rest of module 5.
That is why we merely note it here. When you reach module 5 and have a real list of books, for-each will be your default loop. Until then, while, do-while and for cover everything you need.
- Nested loops: the BiblioTech fine table
A nested loop is a loop inside the body of another. The inner loop completes all its iterations for each iteration of the outer one. If the outer one makes 4 passes and the inner one 5, the inner body runs 4 × 5 = 20 times.
The classic case is generating a two-dimensional table. Nexus Software wants a reference matrix with the fine by days late (rows) and number of books returned at once (columns):
package com.nexussoftware.bibliotech;
public class BiblioTechApp {
public static void main(String[] args) {
final double DAILY_RATE = 0.25;
final double MAX_FINE = 20.0;
System.out.println("BIBLIOTECH FINE TABLE (EUR)");
System.out.println("Rows: days late | Columns: books returned");
System.out.println();
// Column header
System.out.printf("%8s", "DAYS\\BKS");
for (int books = 1; books <= 4; books++) {
System.out.printf("%9d", books);
}
System.out.println();
// OUTER loop: goes through the days late in steps of five
for (int days = 5; days <= 25; days += 5) {
System.out.printf("%8d", days);
// INNER loop: for each row, goes through the 4 columns
for (int books = 1; books <= 4; books++) {
double fine = days * DAILY_RATE * books;
if (fine > MAX_FINE) {
fine = MAX_FINE; // cap per return
}
System.out.printf("%9.2f", fine);
}
System.out.println(); // line break when the row is finished
}
}
}Output:
BIBLIOTECH FINE TABLE (EUR)
Rows: days late | Columns: books returned
DAYS\BKS 1 2 3 4
5 1.25 2.50 3.75 5.00
10 2.50 5.00 7.50 10.00
15 3.75 7.50 11.25 15.00
20 5.00 10.00 15.00 20.00
25 6.25 12.50 18.75 20.00Three important details:
- Each loop has its own control variable (
daysandbooks). Reusing the same one in both is a guaranteed bug. - The
System.out.println()at the end of the outer loop is outside the inner one: it runs once per row, not once per cell. If you put it inside, you would get a vertical column instead of a table. - The
MAX_FINEcap is applied inside the inner loop, cell by cell, and it shows in the bottom-right corner, where 25 days × 4 books would give €25.00 but ends up at €20.00.
Beware of the combinatorial explosion: two nested loops of 1,000 iterations each are a million executions of the body. Three levels, a thousand million. Nesting is legitimate, but each level multiplies the cost.
- Fundamental patterns: counter, accumulator and flag search
Almost everything you will do with loops is a combination of three patterns. It is worth learning them by name, because you will recognise them in all the professional code you read.
8.1 Counter
An integer variable that is incremented when something happens. It answers "how many?".
int lateReturns = 0;
for (int days = 10; days <= 30; days += 5) {
if (days > 15) { // LOAN_DAYS
lateReturns++; // the counter only goes up if the condition holds
}
}
System.out.println("Returns after the deadline: " + lateReturns); // 38.2 Accumulator
A variable that adds up values. It answers "how much in total?". It is always initialised outside the loop and to zero (or to "" for text, although that has nuances we will see in section 12).
final double DAILY_RATE = 0.25;
double totalFines = 0.0;
for (int days = 5; days <= 25; days += 5) {
double fine = days * DAILY_RATE;
totalFines += fine; // accumulator
System.out.printf(" %2d days -> %.2f EUR%n", days, fine);
}
System.out.printf("TOTAL: %.2f EUR%n", totalFines); // 18.75 EURThe classic accumulator mistake is initialising it inside the loop. If you write double totalFines = 0.0; as the first line of the body, it resets on every pass and at the end it holds the same as the last element. It is such a frequent bug that lesson 02-05 uses it as a practical debugging case.
8.3 Search with a boolean flag
A boolean variable that starts at false and is set to true when what you are looking for is found. It answers "is there any that…?".
final int MINOR_THRESHOLD = 7;
boolean hasSevereDelay = false; // flag, initialised to false
for (int days = 1; days <= 6; days++) {
if (days > MINOR_THRESHOLD) {
hasSevereDelay = true; // the flag is raised
}
}
if (hasSevereDelay) {
System.out.println("There is at least one return with a severe delay.");
} else {
System.out.println("No return exceeds the minor threshold.");
}Once raised, the flag is never lowered again: that is what makes it reliable. And as soon as it is raised, carrying on iterating is usually pointless; the way to cut the loop short at that point is break, studied in detail in lesson 02-04.
8.4 The three patterns together: a batch of returns
This is the example that sums up the module. Marta Ruiz arrives at the desk with several books and the librarian processes them one by one:
package com.nexussoftware.bibliotech;
import java.util.Scanner;
public class BiblioTechApp {
public static void main(String[] args) {
final int LOAN_DAYS = 15;
final double DAILY_RATE = 0.25;
final double MAX_FINE = 20.0;
final int MINOR_THRESHOLD = 7;
Scanner scanner = new Scanner(System.in);
System.out.print("Employee: ");
String employee = scanner.nextLine().trim();
System.out.print("How many books are being returned? ");
int bookCount = Integer.parseInt(scanner.nextLine().trim());
// --- Batch accumulation variables ---
int lateBooks = 0; // counter
double totalFines = 0.0; // accumulator
int highestDelay = 0; // maximum
String mostOverdueBook = "-"; // data associated with the maximum
boolean capReached = false; // flag
System.out.println();
for (int book = 1; book <= bookCount; book++) {
System.out.printf("--- Book %d of %d ---%n", book, bookCount);
System.out.print(" Title: ");
String title = scanner.nextLine().trim();
System.out.print(" Elapsed days: ");
int elapsedDays = Integer.parseInt(scanner.nextLine().trim());
int daysLate = elapsedDays - LOAN_DAYS;
if (daysLate < 0) {
daysLate = 0;
}
double fine = daysLate * DAILY_RATE;
if (fine > MAX_FINE) {
fine = MAX_FINE;
capReached = true; // flag
}
if (daysLate > 0) {
lateBooks++; // counter
}
totalFines += fine; // accumulator
if (daysLate > highestDelay) { // maximum + associated data
highestDelay = daysLate;
mostOverdueBook = title;
}
String severity;
if (daysLate == 0) {
severity = "ON TIME";
} else if (daysLate <= MINOR_THRESHOLD) {
severity = "MINOR";
} else {
severity = "SEVERE";
}
System.out.printf(" -> %s | %d days late | fine %.2f EUR%n%n",
severity, daysLate, fine);
}
System.out.println("=== BATCH SUMMARY ===");
System.out.printf("%-24s %s%n", "Employee:", employee);
System.out.printf("%-24s %d%n", "Books returned:", bookCount);
System.out.printf("%-24s %d%n", "Overdue:", lateBooks);
System.out.printf("%-24s %.2f EUR%n", "Total to pay:", totalFines);
System.out.printf("%-24s %s (%d days)%n", "Highest delay:",
mostOverdueBook, highestDelay);
if (capReached) {
System.out.println("WARNING: some book reached the maximum fine.");
}
scanner.close();
}
}A sample session with Marta Ruiz and three books:
Employee: Marta Ruiz How many books are being returned? 3 --- Book 1 of 3 --- Title: Effective Java Elapsed days: 27 -> SEVERE | 12 days late | fine 3.00 EUR --- Book 2 of 3 --- Title: Design Patterns Elapsed days: 12 -> ON TIME | 0 days late | fine 0.00 EUR --- Book 3 of 3 --- Title: Refactoring Elapsed days: 120 -> SEVERE | 105 days late | fine 20.00 EUR === BATCH SUMMARY === Employee: Marta Ruiz Books returned: 3 Overdue: 2 Total to pay: 23.00 EUR Highest delay: Refactoring (105 days) WARNING: some book reached the maximum fine.
Compare this with the end of module 1, where processing two books required duplicating thirty lines and thirty numbered variables. Now it processes three, ten or a thousand books with the same code.
And here is the limitation you should consciously notice. When the loop finishes, what do you know about the books processed? Only what you accumulated into scalar variables: how many, how much they add up to, which was the worst. The individual data has been lost: book 1's title was overwritten by book 2's. You cannot print the detail at the end, nor sort the books by fine, nor search for a specific one. For that you would need variables that hold many values at once, and those are arrays (lesson 05-01) and collections (the rest of module 5). Hold on to that feeling: it is exactly what justifies module 5.
- Input validation loop
This is the pattern that solves the frustration this lesson opened with. Instead of ending the program when the value is invalid, it asks again.
The canonical structure is a do-while, because you always have to ask at least once:
Scanner scanner = new Scanner(System.in);
int elapsedDays;
boolean validInput;
do {
System.out.print("Elapsed days (0-3650): ");
String input = scanner.nextLine().trim();
elapsedDays = Integer.parseInt(input);
validInput = elapsedDays >= 0 && elapsedDays <= 3650;
if (!validInput) {
System.out.println(" Value out of range. Try again.");
}
} while (!validInput);
System.out.println("Accepted: " + elapsedDays + " days.");A sample session:
Elapsed days (0-3650): -5 Value out of range. Try again. Elapsed days (0-3650): 99999 Value out of range. Try again. Elapsed days (0-3650): 27 Accepted: 27 days.
The same pattern works for text:
String employee;
do {
System.out.print("Employee: ");
employee = scanner.nextLine().trim();
if (employee.isBlank()) {
System.out.println(" The name cannot be empty.");
}
} while (employee.isBlank());And a prudent variant for production: limit the number of attempts, so that the program is not left at the mercy of a user who keeps getting it wrong (or a badly written automated script):
final int MAX_ATTEMPTS = 3;
int attempts = 0;
boolean valid = false;
int days = 0;
while (!valid && attempts < MAX_ATTEMPTS) {
attempts++;
System.out.printf("Elapsed days (attempt %d of %d): ", attempts, MAX_ATTEMPTS);
days = Integer.parseInt(scanner.nextLine().trim());
valid = days >= 0;
if (!valid) {
System.out.println(" The days cannot be negative.");
}
}
if (!valid) {
System.err.println("Too many failed attempts. Operation cancelled.");
} else {
System.out.println("Accepted: " + days);
}The warning from the previous lesson still stands: if the user types twenty, Integer.parseInt stops the program and no loop can save it. That gap is closed in module 6.
- Menu loop (a preview of the project)
The pattern that will shape the module's final project (lesson 02-06) is a loop that shows options, reads the choice and acts, until the user decides to leave.
Scanner scanner = new Scanner(System.in);
boolean exit = false;
while (!exit) {
System.out.println();
System.out.println("=== BIBLIOTECH - Nexus Software ===");
System.out.println(" 1. Register a return");
System.out.println(" 2. Simulate a loan");
System.out.println(" 0. Exit");
System.out.print("Option: ");
String option = scanner.nextLine().trim();
if (option.equals("1")) {
System.out.println("[Registering return...]");
} else if (option.equals("2")) {
System.out.println("[Simulating loan...]");
} else if (option.equals("0")) {
System.out.println("See you soon.");
exit = true; // the flag cuts the loop
} else {
System.out.println("Option not recognised.");
}
}
scanner.close();Here the loop does not count iterations: it repeats while the user does not say they are leaving. The exit flag controls the condition. It is the direct application of the flag pattern from section 8.3.
In lesson 02-03 you will replace that if-else ladder with a switch, far more readable for dispatching options, and in 02-06 you will assemble the complete program.
- Infinite loops: intentional and accidental
An infinite loop is one whose condition never becomes false. There are two radically different categories.
Intentional. They are written on purpose and abandoned from within with break or return. They are idiomatic in menus, servers and event loops:
while (true) {
String option = scanner.nextLine().trim();
if (option.equals("0")) {
break; // the only exit, explicit
}
System.out.println("You chose: " + option);
}while (true) and for (;;) are equivalent; the first reads better. The condition for an infinite loop to be legitimate is that the exit must be obvious when reading the body.
Accidental. They are bugs. The three causes, by frequency:
// 1. The update is missing
int i = 0;
while (i < 10) {
System.out.println(i);
// i++ is missing! -> prints 0 forever
}
// 2. The update moves away from the condition
for (int days = 15; days > 0; days++) { // should be days--
System.out.println(days);
}
// 3. The condition depends on something the body never changes
boolean keepGoing = true;
while (keepGoing) {
System.out.println("Processing...");
// nobody sets 'keepGoing' to false
}How to escape an infinite loop hanging in the terminal: press Ctrl + C. That sends an interrupt signal to the Java process and terminates it. If you launched it from the IDE, there is a red square "Stop" button in the console. And if the loop is printing millions of lines and the terminal is unresponsive, close the window; you will not have lost anything, because the program does not save any state yet.
How to avoid them, in order of usefulness:
- When writing a
while, write the update first and then the rest of the body. - Before running it, ask yourself: which specific line makes this condition eventually false? If you cannot point at it, you have an infinite loop.
- During development, add a safety limit:
if (iterations++ > 1_000_000) break;.
- The cost of iterations and the
String concatenation trap
String concatenation trapEvery iteration costs time. A body that takes 1 millisecond, repeated a million times, is 16 minutes. Two practical consequences:
Take everything that does not change out of the loop. This calculation is repeated pointlessly:
// BAD: the multiplication of constants is recomputed on every pass
for (int day = 1; day <= 10_000; day++) {
double limit = DAILY_RATE * 30 * 12; // always the same value
...
}
// GOOD: computed once
double limit = DAILY_RATE * 30 * 12;
for (int day = 1; day <= 10_000; day++) {
...
}Do not concatenate Strings with + inside long loops. Remember from module 1 that String is immutable: each += does not modify the text, it creates a new object copying all the previous content. In a loop of n iterations that means n objects and a cost that grows with the square of n.
// BAD: 10,000 intermediate String objects, each longer than the last
String report = "";
for (int day = 1; day <= 10_000; day++) {
report += "Day " + day + "\n";
}
// GOOD: StringBuilder modifies a single internal buffer
StringBuilder report = new StringBuilder();
for (int day = 1; day <= 10_000; day++) {
report.append("Day ").append(day).append("\n");
}
System.out.println(report.toString());StringBuilder is a standard-library class designed exactly for this: append adds at the end without copying everything, and toString() produces the final String just once. For loops of three or four passes the difference is irrelevant and normal concatenation is more readable; from a few hundred iterations onwards, StringBuilder is mandatory. In a real measured case, building 100,000 lines drops from tens of seconds with += to a few milliseconds with StringBuilder.
One alternative when all you want is to display the text: print inside the loop instead of accumulating it. System.out.println inside a loop does not have that problem (although it is not free either: console output is one of the slowest operations there is).
Common Mistakes and Tips
1. Forgetting the update. The number one cause of infinite loops. In a for it is almost impossible; in a while it is the permanent risk.
2. A semicolon after the header.
for (int i = 0; i < 5; i++); // <-- empty loop
{
System.out.println(i); // does not even compile: 'i' no longer exists
}3. Off-by-one errors. The most frequent bug in programming. < and <= are not interchangeable:
for (int i = 1; i < 5; i++) // 1, 2, 3, 4 -> FOUR passes
for (int i = 1; i <= 5; i++) // 1, 2, 3, 4, 5 -> FIVE passes
for (int i = 0; i < 5; i++) // 0, 1, 2, 3, 4 -> FIVE passesWhen in doubt, write out the trace of the first three and the last iteration on paper.
4. Initialising the accumulator inside the loop. The final result ends up being that of the last element instead of the sum. Always initialise before the for/while.
5. Declaring inside the do a variable you need in the condition. It does not compile: the block's scope ends at the closing brace.
6. Modifying the control variable inside the body of a for. It compiles, but it turns the loop into a puzzle:
for (int i = 0; i < 10; i++) {
if (something) { i += 3; } // legal, but nobody expects it when reading the header
}If you need to skip iterations, use continue (lesson 02-04) or rethink the loop.
7. Comparing doubles in the loop condition. for (double d = 0.0; d != 1.0; d += 0.1) never ends, because 0.1 is not exactly representable in binary and the sum is never exactly 1.0. Use < instead of !=, or count with integers and divide.
8. Tip: name the control variables after what they mean. for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) is understood without comments; for (int i = 1; i <= n; i++) forces you to look at the body.
9. Tip: keep the bodies short. If your loop's body runs to fifty lines, it is hard to see where the update is and what is being accumulated. When you learn methods (lesson 03-03), extracting the body will be the natural solution.
Exercises
Exercise 1: fine scale with for
Write a program that prints BiblioTech's official fine scale from 1 to 30 days late, in steps of five, with three columns: days, uncapped fine and applied fine (with MAX_FINE). Add a final column with the severity label (MINOR if the days are <= MINOR_THRESHOLD, SEVERE otherwise). When it finishes, print how many of the rows shown exceeded the cap, using a counter.
Use printf to align the table and the constants DAILY_RATE = 0.25, MAX_FINE = 20.0 and MINOR_THRESHOLD = 7.
Exercise 2: robust validation with do-while
Write a program that asks the user for an employee's name and their elapsed days, repeating each question until a valid value is obtained:
- The name cannot be blank nor shorter than 3 characters.
- The elapsed days must be between 0 and 3650.
When both values are valid, calculate the delay clamped to zero and the capped fine, and print the receipt. Show in the output how many failed attempts there were in total (a counter incremented on each rejection).
Exercise 3: batch accumulation with nested loops
Nexus Software wants a report of a librarian's session. The program must:
- Ask how many employees were served.
- For each employee, ask for their name and how many books they are returning.
- For each book, ask for the elapsed days and calculate its capped fine.
- Show, at the end of each employee, their fine subtotal and how many books they brought back late.
- Show, at the very end, the grand total of fines, the total number of books processed and the name of the employee with the highest accumulated fine.
Use an outer loop for the employees and an inner one for their books. After solving it, answer in writing: what would you have needed in order to print, at the end, the book-by-book detail of the whole session?
Solutions
Solution 1
package com.nexussoftware.bibliotech;
public class BiblioTechApp {
public static void main(String[] args) {
final double DAILY_RATE = 0.25;
final double MAX_FINE = 20.0;
final int MINOR_THRESHOLD = 7;
int cappedRows = 0; // counter
System.out.println("OFFICIAL FINE SCALE - BiblioTech");
System.out.printf("%6s %12s %12s %s%n", "DAYS", "UNCAPPED", "APPLIED", "SEVERITY");
System.out.println("-".repeat(48));
// Initialisation: day = 1. Condition: <= 30. Update: +5.
// It goes through 1, 6, 11, 16, 21, 26 -> six rows.
for (int days = 1; days <= 30; days += 5) {
double uncappedFine = days * DAILY_RATE;
double appliedFine = uncappedFine;
if (appliedFine > MAX_FINE) {
appliedFine = MAX_FINE;
cappedRows++; // the counter only goes up when capped
}
String severity;
if (days <= MINOR_THRESHOLD) {
severity = "MINOR";
} else {
severity = "SEVERE";
}
System.out.printf("%6d %12.2f %12.2f %s%n",
days, uncappedFine, appliedFine, severity);
}
System.out.println("-".repeat(48));
System.out.println("Rows that reached the cap: " + cappedRows);
}
}Output:
OFFICIAL FINE SCALE - BiblioTech
DAYS UNCAPPED APPLIED SEVERITY
------------------------------------------------
1 0.25 0.25 MINOR
6 1.50 1.50 MINOR
11 2.75 2.75 SEVERE
16 4.00 4.00 SEVERE
21 5.25 5.25 SEVERE
26 6.50 6.50 SEVERE
------------------------------------------------
Rows that reached the cap: 0An interesting observation: with the current rate, no row up to 30 days reaches the €20 cap, because it would take 80 days late. The counter is 0 and that is correct, not a bug. Check that the program behaves as you expect by changing the loop limit to days <= 120.
Solution 2
package com.nexussoftware.bibliotech;
import java.util.Scanner;
public class BiblioTechApp {
public static void main(String[] args) {
final int LOAN_DAYS = 15;
final double DAILY_RATE = 0.25;
final double MAX_FINE = 20.0;
Scanner scanner = new Scanner(System.in);
int failedAttempts = 0;
// --- Name validation loop ---
String employee;
do {
System.out.print("Employee: ");
employee = scanner.nextLine().trim();
if (employee.length() < 3) {
System.out.println(" The name must have at least 3 characters.");
failedAttempts++;
}
} while (employee.length() < 3);
// --- Elapsed days validation loop ---
int elapsedDays;
boolean validDays;
do {
System.out.print("Elapsed days (0-3650): ");
elapsedDays = Integer.parseInt(scanner.nextLine().trim());
validDays = elapsedDays >= 0 && elapsedDays <= 3650;
if (!validDays) {
System.out.println(" Value outside the allowed range.");
failedAttempts++;
}
} while (!validDays);
// --- Calculation, now with data guaranteed valid ---
int daysLate = elapsedDays - LOAN_DAYS;
if (daysLate < 0) {
daysLate = 0;
}
double fine = daysLate * DAILY_RATE;
if (fine > MAX_FINE) {
fine = MAX_FINE;
}
System.out.println();
System.out.println("=== RETURN RECEIPT ===");
System.out.printf("%-20s %s%n", "Employee:", employee);
System.out.printf("%-20s %d%n", "Elapsed days:", elapsedDays);
System.out.printf("%-20s %d%n", "Days late:", daysLate);
System.out.printf("%-20s %.2f EUR%n", "Fine:", fine);
System.out.printf("%-20s %d%n", "Failed attempts:", failedAttempts);
scanner.close();
}
}A sample session:
Employee: The name must have at least 3 characters. Employee: JD The name must have at least 3 characters. Employee: Diego Alonso Elapsed days (0-3650): -3 Value outside the allowed range. Elapsed days (0-3650): 27 === RETURN RECEIPT === Employee: Diego Alonso Elapsed days: 27 Days late: 12 Fine: 3.00 EUR Failed attempts: 3
Two design decisions worth reasoning about:
employeeandelapsedDaysare declared outside thedo, because they are used in thewhilecondition and after the loop.- In the second loop the
validDaysflag is used instead of repeating the complex condition in thewhile. Repeating it would work, but duplicating a condition duplicates a place where you can get it wrong when changing it.
Solution 3
package com.nexussoftware.bibliotech;
import java.util.Scanner;
public class BiblioTechApp {
public static void main(String[] args) {
final int LOAN_DAYS = 15;
final double DAILY_RATE = 0.25;
final double MAX_FINE = 20.0;
Scanner scanner = new Scanner(System.in);
System.out.print("How many employees have you served? ");
int employeeCount = Integer.parseInt(scanner.nextLine().trim());
// Global session accumulators
double grandTotal = 0.0;
int totalBooks = 0;
double highestEmployeeFine = 0.0;
String employeeWithHighestFine = "-";
// ===== OUTER LOOP: employees =====
for (int e = 1; e <= employeeCount; e++) {
System.out.printf("%n===== Employee %d of %d =====%n", e, employeeCount);
System.out.print("Name: ");
String employee = scanner.nextLine().trim();
System.out.print("Books being returned: ");
int bookCount = Integer.parseInt(scanner.nextLine().trim());
// LOCAL accumulators for this employee: they reset on every pass
// of the outer loop, which is exactly what we want here.
double subtotal = 0.0;
int lateBooks = 0;
// ===== INNER LOOP: THIS employee's books =====
for (int b = 1; b <= bookCount; b++) {
System.out.printf(" Book %d - elapsed days: ", b);
int elapsedDays = Integer.parseInt(scanner.nextLine().trim());
int daysLate = elapsedDays - LOAN_DAYS;
if (daysLate < 0) {
daysLate = 0;
}
double fine = daysLate * DAILY_RATE;
if (fine > MAX_FINE) {
fine = MAX_FINE;
}
if (daysLate > 0) {
lateBooks++;
}
subtotal += fine;
totalBooks++; // global accumulator
System.out.printf(" %d days late -> %.2f EUR%n", daysLate, fine);
}
grandTotal += subtotal;
if (subtotal > highestEmployeeFine) {
highestEmployeeFine = subtotal;
employeeWithHighestFine = employee;
}
System.out.printf(" SUBTOTAL %s: %.2f EUR (%d overdue of %d books)%n",
employee, subtotal, lateBooks, bookCount);
}
System.out.println();
System.out.println("========== SESSION REPORT ==========");
System.out.printf("%-26s %d%n", "Employees served:", employeeCount);
System.out.printf("%-26s %d%n", "Books processed:", totalBooks);
System.out.printf("%-26s %.2f EUR%n", "Total collected:", grandTotal);
System.out.printf("%-26s %s (%.2f EUR)%n", "Highest fine:",
employeeWithHighestFine, highestEmployeeFine);
System.out.println("====================================");
scanner.close();
}
}An abbreviated session with two employees:
How many employees have you served? 2
===== Employee 1 of 2 =====
Name: Marta Ruiz
Books being returned: 2
Book 1 - elapsed days: 27
12 days late -> 3.00 EUR
Book 2 - elapsed days: 10
0 days late -> 0.00 EUR
SUBTOTAL Marta Ruiz: 3.00 EUR (1 overdue of 2 books)
===== Employee 2 of 2 =====
Name: Nuria Vidal
Books being returned: 1
Book 1 - elapsed days: 120
105 days late -> 20.00 EUR
SUBTOTAL Nuria Vidal: 20.00 EUR (1 overdue of 1 books)
========== SESSION REPORT ==========
Employees served: 2
Books processed: 3
Total collected: 23.00 EUR
Highest fine: Nuria Vidal (20.00 EUR)
====================================The key point of the exercise is the distinction between global and local accumulators. grandTotal and totalBooks are declared before the outer loop and survive the whole session. subtotal and lateBooks are declared inside the outer loop, so they are created and set to zero for each employee. Misplacing either of those declarations produces a silently incorrect report: if subtotal were outside, it would drag along the previous employee's fines.
Answer to the final question: to print the book-by-book detail at the end you would need to store all the data read, not just its aggregates. With scalar variables it is impossible: every title and every fine is overwritten on the next iteration. You would need structures capable of holding many values under a single name —an array (lesson 05-01) or an ArrayList (lesson 05-03)— and, to group title, ISBN and fine as a unit, a Book class of your own (module 3). That is exactly the path the course follows.
Conclusion
Your program now repeats. You have dissected the three classic loops: while, which checks first and may never run; do-while, which runs at least once and is the natural tool for validation and menus; and for, which groups initialisation, condition and update into a header and is the loop of counted repetitions. You know that for-each exists and why you will wait until module 5 to use it. You have nested loops to generate BiblioTech's fine table, and above all you have learnt the four patterns that solve almost everything: counter, accumulator, flag search and validation loop, plus the menu loop that will structure the module's project. And you know the two permanent dangers: the accidental infinite loop and String concatenation inside long loops.
BiblioTechApp now processes entire batches of returns, insists until the user enters valid data and produces an aggregate summary of the session. But you have also seen its ceiling: when the loop ends only the aggregates remain; the individual data has been lost because you have nowhere to store it. That shortcoming will be solved in module 5.
In the next lesson, Switch Statements, you will replace the menu dispatcher's if-else ladder with a construct designed specifically to choose between many values of a single variable. You will see the classic switch with its dreaded fall-through, the arrow switch from Java 14 onwards and the switch as an expression that returns a value, and with them you will write the first version of BiblioTech's option dispatcher.
Java Programming Course
Module 1: Introduction to Java
- Introduction to Java
- Setting Up the Development Environment
- Basic Syntax and Structure
- Variables and Data Types
- Operators
- Console Input and Output
- Your First Complete Program: BiblioTech
Module 2: Control Flow
- Conditional Statements
- Loops
- Switch Statements
- Break and Continue
- Debugging and Execution Traces
- Project: The BiblioTech Interactive Menu
Module 3: Object-Oriented Programming
- Introduction to OOP
- Classes and Objects
- Methods
- Constructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- The Object Class: equals, hashCode and toString
Module 4: Advanced Object-Oriented Programming
- Interfaces
- Abstract Classes
- Inner Classes
- Anonymous Classes
- Lambda Expressions
- Functional Interfaces and Method References
- Enums and Records
Module 5: Data Structures and Collections
- Arrays
- The Collections Framework
- ArrayList
- LinkedList
- HashMap
- HashSet
- Queue and Deque
- Stack
- Sorting and Searching Collections
Module 6: Exception Handling
- Introduction to Exceptions
- The Try-Catch Block
- Throw and Throws
- Custom Exceptions
- The Finally Block
- Try-with-resources and AutoCloseable
- Error Handling Strategies and Logging
Module 7: File Input/Output
- Reading Files
- Writing Files
- File Streams
- BufferedReader and BufferedWriter
- Serialization
- The NIO.2 API: Path and Files
- Interchange Formats: CSV and Properties
Module 8: Multithreading and Concurrency
- Introduction to Multithreading
- Creating Threads
- Thread Lifecycle
- Synchronization
- Concurrency Utilities
- Concurrent Collections and Atomic Variables
- Asynchronous Tasks with CompletableFuture
Module 9: Networking
- Introduction to Networking
- Sockets
- ServerSocket
- DatagramSocket and DatagramPacket
- URL and HttpURLConnection
- The Modern HTTP Client
Module 10: Advanced Topics
- Generics
- Annotations
- Reflection
- Java 8 Features: Streams and Optional
- Dates and Times with java.time
- Java 9 and Beyond
- Memory, Garbage Collection and Performance
Module 11: Java Frameworks and Libraries
- Introduction to Java Frameworks
- Spring Framework
- Hibernate
- JUnit
- Maven
- Advanced Testing with Mockito
- Essential Ecosystem Libraries
