Running a program once with the numbers you had in mind while writing it proves almost nothing — of course it works on those. Testing is the deliberate search for inputs that break the program, and it is a planned stage with written cases and expected results decided in advance. Almost every logic bug lives at a boundary, because a boundary is exactly where < and <= disagree, and they disagree on precisely one value. Test with 73 and both versions pass; test with 100 and only the correct one does.
| Kind | What it checks | For a mark from 0 to 100 |
|---|---|---|
| Normal | The ordinary case works | 50, 73, 88 |
| Boundary | The exact edges of every valid range | 0, 1, 99, 100 |
| Invalid | Bad input is rejected instead of accepted | −1, 101, 3.5, "abc", nothing at all |
| Type | When it appears | Java example | How you find it |
|---|---|---|---|
| Syntax | At compile time — the program never runs | Missing semicolon, unmatched brace | The compiler names the file and line |
| Runtime | While running — the program stops | Divide by zero, ArrayIndexOutOfBoundsException, NullPointerException | A stack trace naming the exception and the line |
| Logic | Never announced — the program runs and is wrong | > written where >= was meant | Only testing against expected results reveals it |
Treat the three types as a diagnostic routine rather than a list to memorize. Did it refuse to compile? Syntax — read the first error message only, because later ones are often knock-on effects of the first. Did it start and then stop? Runtime — read the stack trace, which names the fault and the exact line. Did it run cleanly and produce the wrong answer? Logic, and now you need a trace table: one column per variable, one row per pass, filled in by hand, executing the code exactly as written rather than as intended. The moment a row disagrees with the value you expected, the bug is in the statement you just executed.
int[] data = {3, 7, 2};
int total = 0;
for (int i = 0; i < data.length; i++) {
total += data[i];
}
System.out.println(total);| Pass | i | i < 3? | data[i] | total after |
|---|---|---|---|---|
| before | — | — | — | 0 |
| 1 | 0 | true | 3 | 3 |
| 2 | 1 | true | 7 | 10 |
| 3 | 2 | true | 2 | 12 |
| exit | 3 | false | — | 12 |
When a Java program throws, it prints a stack trace. It looks intimidating and it is actually a gift: it tells you the exception type, a message, and the exact line of the exact file where the program stopped. Read it top down — the first line is what went wrong, the second is where, and each line after that is the call that led there, so the last line is usually main. The message itself carries both numbers you need: "Index 5 out of bounds for length 5" says what you asked for and what was actually there, and when those two numbers are equal the cause is nearly always a loop condition using less-than-or-equal instead of less-than.
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
at MarkReport.average(MarkReport.java:18)
at MarkReport.main(MarkReport.java:7)- Add System.out.println to show a variable's value at a point
- Always available, needs no setup, works anywhere
- You must guess in advance which values matter
- It changes the very code you are diagnosing, and forgotten ones become bugs of their own
- Set a breakpoint and the program pauses on that line
- Inspect every variable at once without changing the code
- Step line by line, or step into a method call, and watch values change
- Wins on any bug that takes more than two guesses
Maintenance is the longest stage of the life cycle, and the person doing it is usually reading the code cold. Four conventions do most of the work. Name variables for what they hold — passCount, not x or temp2. Indent four spaces per level every time, because inconsistent indentation hides the structure the logic depends on. Use comments to explain why a decision was made, never to restate what the code obviously does. And put a short description above every method saying what it takes, what it returns, and what happens when it fails. Keeping methods short enough to fit on one screen makes all four easier.
// Hard to maintain: nothing here says what any of it is for
int x=0;
for(int i=0;i<a.length;i++){if(a[i]>=50){x++;}}
// Maintainable: the names carry the meaning and the comment explains WHY
int passCount = 0;
for (int mark : marks) {
// 50 is the reporting cut-off set by the specification, not an arbitrary number
if (mark >= 50) {
passCount++;
}
}
/**
* Returns the index of the first occurrence of target in arr,
* or -1 if target does not appear in the array.
*/
public static int indexOf(int[] arr, int target) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
return i;
}
}
return -1;
}i++; // add one to i tells the reader nothing they could not see, and it becomes a lie the moment the line changes. Comment the reasoning instead: why this cut-off, why this order, why this special case exists. If a comment is needed to explain what a line does, the better fix is usually a clearer variable name.