Unit 05 · lesson
Off-by-One Errors Live at the Boundary
A loop can be almost correct and still process one too many or one too few values.
That is why boundaries deserve their own tests.
Compare:
for (int i = 0; i < 5; i++) {
IO.println(i);
}
with:
for (int i = 0; i <= 5; i++) {
IO.println(i);
}
The single = changes the set of visited values.
First loop:
0 1 2 3 4
Second:
0 1 2 3 4 5
Neither is inherently wrong. The requirement decides.
Half-open ranges are common for a reason
Java arrays and lists use zero-based indexes. If a collection has length 5, valid indexes are 0 through 4.
That pairs naturally with:
for (int i = 0; i < length; i++)
The lower boundary is inclusive and the upper boundary is exclusive.
[0, length)
That mental model becomes useful far beyond Java loops.
Boundary table first
Requirement:
Process exactly the first 10 sample numbers, numbered 1 through 10.
Possible loop:
for (int sample = 1; sample <= 10; sample++) {
IO.println(sample);
}
Boundary evidence should answer:
- first value processed?
- last value processed?
- number of iterations?
- value immediately after the loop?
Do not test only a middle iteration like 5. Off-by-one errors usually live at the ends.
Nested loops multiply mistakes
A grid:
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 4; col++) {
IO.println(row + "," + col);
}
}
Expected number of coordinate pairs: 3 * 4 = 12.
If you accidentally use col <= 4, each row emits five columns and the total becomes 15.
Counting total work is another useful invariant.
Break and continue are control-flow changes
break exits a loop early. continue skips the remaining body and moves toward the next iteration.
They are not forbidden, but they create additional paths that must be explained.
If you use them, ask whether a clearer condition could express the same rule with fewer hidden exits.
Fault injection
Write a loop that must emit exactly 8 numbered records.
Version 1 should pass.
Then create one fault:
- wrong starting value;
<versus<=error;- update by 2 instead of 1;
- incorrect stop boundary.
Before running the broken version, predict:
- first emitted value;
- last emitted value;
- number of emissions.
Then run it and compare.
Evidence
Keep the passing and failing versions side by side. Mark the boundary condition that changed and explain why a middle-value test would not have exposed the defect as clearly as checking the first, last, and count.