انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-018-L01

منهجية لتصحيح الأخطاء

  • الدرس 1 من 6
  • 12 دقيقة قراءة
  • مبتدئ
في هذا الدرس 11 موضوعات

هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.

Lesson Overview

You have written loops, methods, and arrays, and sooner or later a program has printed something you did not expect. This module is about what to do next. The most important answer is not a tool. It is a method: a calm, repeatable way to find out why a program does what it does.

We will use a detective as our picture. A detective does not arrest the first person in the room. They gather clues, form a theory, test it, and only then close the case. Debugging works the same way: reproduce, isolate, hypothesize, test, fix, verify.

Why this matters: in a backend job, most of your time goes into understanding code that already exists and does not behave as expected. Developers who guess change code at random and create new bugs. Developers who follow a method find the cause faster and can explain what they did.

This lesson builds on methods and arrays from BEG-014 and BEG-015, and on hand-tracing from BEG-016. It needs no debugger yet; the next lesson introduces one.

Learning Objectives

By the end of this lesson you will be able to:

  • Define a bug as the gap between expected and actual behavior.
  • Name the six steps of the debugging process in order.
  • Reproduce a bug with known input and write down expected and actual results.
  • Isolate a bug by shrinking the code and input that still fail.
  • Write a hypothesis that can be proven wrong, and design a test for it.
  • Change one thing at a time, and tell a fix for the cause from a fix for the symptom.
  • Verify a fix with the original failing case and at least one more case.

Written Explanation

What a bug is

A bug is a difference between what you expected and what the program did. That definition has two parts, and both matter. If you cannot say what you expected, you cannot say there is a bug. If you have not looked at what really happened, you are guessing.

There are broadly two kinds of problem:

  • The program crashes or the compiler refuses it. These give you a message, and later lessons teach you to read it.
  • The program runs without complaint but gives a wrong answer. This is called a logic bug. Nothing tells you where the mistake is, so you need a method.

Compiler errors were covered in BEG-008. This module deals with logic bugs and runtime crashes.

The six steps

StepQuestion it answersDetective version
1. ReproduceCan I make it fail every time?Visit the scene again
2. IsolateWhat is the smallest code and input that still fails?Rule out suspects
3. HypothesizeWhat do I think is wrong, exactly?Form a theory
4. TestDoes evidence support or reject the theory?Check the alibi
5. FixWhat change removes the cause?Close the case
6. VerifyDoes it now work, and did I break anything?Confirm the arrest holds

The steps are a loop. A test often rejects your theory, and then you go back to step 3 with the new clue.

Step 1: Reproduce

If a bug appears "sometimes", you cannot study it. First find an input that makes it fail every time, and write three lines:

  • Input: exactly what you gave the program.
  • Expected: what should have happened.
  • Actual: what did happen.

Beginners skip expected. Work it out by hand, as in BEG-016, before looking at the output, so the output cannot talk you into believing it.

Step 2: Isolate

Shrink the problem:

  • Use the smallest input that still fails. If a list of 500 items fails, try 3.
  • Call the suspect method on its own from a tiny main, so the rest of the program cannot interfere.
  • If the bug disappears when you remove something, you have learned something about where it lives.

A useful clue is the contrast between a working case and a failing case. If [2, 4, 6] works and [1, 2] fails, what differs? That difference is where to look.

Step 3: Hypothesize

A hypothesis is a specific, testable claim about the cause. Compare:

  • Weak: "The average method is broken."
  • Good: "sum / values.length divides two int values, so the fraction is cut off before it is converted to double."

A good hypothesis predicts something you can check. Here it predicts that sum = 3 and count = 2 give 1.0, and that any case with a whole-number average works. If your theory predicts nothing, it cannot be tested.

Step 4: Test

Collect evidence that could prove the theory wrong. Print the values, or (from the next lesson on) pause the program with a debugger and look at them. Compare the evidence with the prediction. If it matches, the theory survives. If not, it is dead: form a new one from the new evidence.

Rule: change one thing at a time. If you change three things and the output improves, you do not know which one helped, and one of them may be a new bug.

Step 5: Fix

Fix the cause, not the symptom. A symptom fix makes the visible wrong answer go away without removing the reason it was wrong. Example: if an average prints 1.0 instead of 1.5, adding + 0.5 in a special case hides the bug for that input and leaves it for others. Dividing as double removes the reason for every input.

Step 6: Verify

Run the original failing case to see it now passes. Then run at least one other case, ideally one that worked before and one at a boundary (for example, one element or zero). This catches fixes that cure one case and break another. Finally, write down what you found; the note helps your future self and your teammates.

Examples

Example 1: the average that is sometimes wrong (simple)

Report: "average is right for [2, 4, 6] but wrong for [1, 2]."

java
static double average(int[] values) {
    int sum = 0;
    for (int v : values) {
        sum += v;
    }
    return sum / values.length;
}
  1. Reproduce. Input [1, 2]. Expected 1.5. Actual 1.0. The program printed:
text
4.0
1.0
  1. Isolate. The method is already small. The contrast: [2, 4, 6] has sum 12 and count 3 (exactly 4), while [1, 2] has sum 3 and count 2 (not a whole number). The bug seems to appear only when the true answer has a fraction.
  2. Hypothesize. Both sum and values.length are int, so / is integer division and drops the fraction before the result becomes a double.
  3. Test. Print sum and the count. With a temporary line System.out.println("sum=" + sum + " count=" + values.length); the output shows sum=3 count=2. Prediction confirmed: 3 / 2 in int arithmetic is 1.
  4. Fix. return (double) sum / values.length;
  5. Verify. [1, 2] now prints 1.5, [2, 4, 6] still prints 4.0, and the one-element case [5] prints 5.0.

Example 2: the discount that gives nothing (practical)

Report: "A 25 percent discount on a price of 200 still charges 200."

java
static double discounted(double price, int percent) {
    return price - price * (percent / 100);
}

Reproduce: discounted(200.0, 25). Expected 150.0, actual 200.0. Running the program printed 200.0 for 25 percent, and also 0.0 for 100 percent.

Look at the clue: 25 percent does nothing, but 100 percent works. What is special about 100? percent / 100 is int divided by int. For 25, that is 0, so the discount amount is price * 0. For 100, it is exactly 1.

Hypothesis: any percent from 0 to 99 becomes 0. Test: evaluating 25 / 100 on its own gives 0. Fix: divide by 100.0, which makes the division floating point. Verify: 25 gives 150.0, 100 gives 0.0, and 10 percent of 80 gives 72.0.

This is the same family of bug as Example 1: once you have seen it, you recognize it faster.

Example 3: isolating a bug in a bigger program (realistic)

Report: "The 10-item order did not get the bulk discount." The program calculates line totals in cents:

java
static int bulkDiscountPercent(int quantity) {
    if (quantity > 10) {
        return 10;
    }
    return 0;
}

static int lineTotal(int unitPriceCents, int quantity) {
    int gross = unitPriceCents * quantity;
    return gross - gross * bulkDiscountPercent(quantity) / 100;
}

The full program printed order total: 5500 for a 10-item line at 500 cents plus a 2-item line at 250 cents. With a discount the first line would be 4500, so the total would be 5000. That is the reproduction. Expected: 5000. Actual: 5500.

Isolate by calling the suspect on its own, with a loop around the boundary:

java
for (int q = 9; q <= 11; q++) {
    System.out.println("quantity " + q + " -> " + bulkDiscountPercent(q));
}
text
quantity 9 -> 0
quantity 10 -> 0
quantity 11 -> 10

Nine and ten behave the same; eleven differs. The hypothesis writes itself: the condition should be quantity >= 10 if "10 or more" is the rule. First confirm the rule: the report says ten items qualify, so >= is the fix. Verify 9 (no discount), 10, and 11 (discount) after the change. We did not need to touch lineTotal at all: isolation saved us from changing the wrong method.

Backend Insight

Backend bugs are often harder than the ones in this module, because the problem happened yesterday on a server you cannot sit in front of. The method is the same, but "reproduce" becomes the most valuable step. A good bug report contains the input, the expected result, and the actual result. When you can reproduce a failure with a small request, you can usually fix it. When you cannot, you are guessing.

Once a bug is understood, developers often write an automated test that fails before the fix and passes after, so the bug cannot return unnoticed. You will meet such tests later; for now, keep the failing input.

Common Mistakes

Mistake 1: Changing code at random

  • Incorrect: "Let me try + 1 here and <= there and see what happens."
  • Correct: Write a hypothesis first, then make the one change that tests it.

Random changes can make a program appear to work by accident while the cause stays hidden.

Mistake 2: Fixing the symptom

A special case such as if (values.length == 2) return 1.5; makes the report go away and leaves the real defect. Ask "why was it wrong?" until your answer names a cause, not just a number.

Mistake 3: Skipping the expected value

If you only look at the actual output, a wrong answer can look believable. Compute the expected answer by hand first.

Mistake 4: Changing several things at once

When the output changes, you cannot tell which edit did it. Change one thing, run, observe.

Mistake 5: Verifying with only the failing case

A fix that passes the failing case may break an earlier-working one. Always re-run a case that used to work.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. Write the six steps of debugging in order.

أظهر الحل A1

A1. Reproduce, isolate, hypothesize, test, fix, verify.

A2. A method should return the larger of two numbers but returns the smaller one. State the expected and actual result for inputs 3 and 8.

أظهر الحل A2

A2. Expected 8 (the larger). Actual 3 (the smaller).

A3. Which is a testable hypothesis? (a) "The loop is wrong." (b) "The loop starts at 1, so the first array element is skipped."

أظهر الحل A3

A3. (b). It makes a claim you can check (does the first element get skipped?). (a) predicts nothing.

Level B: Application

B1. This method should return the amount left after a percent discount, but a 10 percent discount on 80.0 returns 80.0:

java
static double discounted(double price, int percent) {
    return price - price * (percent / 100);
}

Write the reproduction (input, expected, actual), the hypothesis, one test, and the fix.

أظهر الحل B1

B1. Reproduction: input discounted(80.0, 10); expected 72.0; actual 80.0. Hypothesis: percent / 100 is int division, so 10 / 100 is 0 and nothing is subtracted. Test: print percent / 100 and see 0. Fix: percent / 100.0. After the fix, a run printed 72.0 for the original case, 150.0 for 25 percent of 200.0, and 0.0 for 100 percent.

B2. A teammate reports "the report total is wrong." List three questions you would ask before touching code, so that you can reproduce the bug.

أظهر الحل B2

B2. Examples of good questions: What exact input did you use? What total did you expect, and what did you see? Does it happen every time, or only for certain data? Which version of the program were you running? Any of these that narrow the input counts.

B3. Match these actions to the steps of the process: (a) rerun with [1, 2] and [2, 4, 6]; (b) change / to a double division; (c) print the sum and count; (d) run average on [1, 2] and see 1.0; (e) decide that integer division is the cause.

أظهر الحل B3

B3. Reproduce: (d). Test: (c). Hypothesize: (e). Fix: (b). Verify: (a). (In practice you may loop between hypothesis and test; the key point is that the fix comes after the theory.)

Level C: Challenge

C1. Report: "max returns 0 for a list of negative numbers." The code is:

java
static int max(int[] v) {
    int max = 0;
    for (int x : v) {
        if (x > max) {
            max = x;
        }
    }
    return max;
}

Write a short debugging trail with all six steps, including at least two verification cases.

أظهر الحل C1

C1. Reproduce: input [-3, -1, -2], expected -1, actual 0. Isolate: [3, 9, 4] works (9) but all-negative input fails; the difference is the sign. Hypothesize: max starts at 0, so no negative number is ever greater than it, and 0 is returned even though 0 is not in the list. Test: print max before the loop (0) and after (still 0). Fix: start from the first element, int max = v[0];. Verify: [3, 9, 4] gives 9, [-3, -1, -2] gives -1, and [7] gives 7. (A real run printed 9 -1 7 for these three.) The fix assumes the array is not empty; an empty array would fail on v[0], which would be a new case to decide on.

C2. A friend "fixed" the average bug by adding if (values.length == 2) return 1.5;. Explain why this is a symptom fix, and give an input that still fails.

أظهر الحل C2

C2. The change only handles inputs of length two. The real cause is integer division, which still affects every other case. For example [1, 2, 2] has sum 5 and count 3, so the true average is about 1.67, but the integer division gives 1.0.

Mini Challenge

Take a small program you wrote earlier and break it on purpose, for example by starting a loop at the wrong index. Put it away for an hour, then debug it as a stranger would.

Write a six-line trail, one line per step: input, expected and actual; what you simplified; your hypothesis; the evidence; the fix; the cases you ran afterwards.

Check your work: your hypothesis names a specific cause, and you changed only one thing between runs.

Summary & Next Lesson

Key takeaways

  • A bug is a gap between expected and actual behavior; you need both numbers.
  • The method: reproduce, isolate, hypothesize, test, fix, verify.
  • A good hypothesis is specific and can be proven wrong.
  • Change one thing at a time. Fix the cause, not the symptom.
  • Verify with the original case and at least one other.

Next lesson

Printing values works, but it is slow and clutters the code. The next lesson, "Breakpoints and stepping", shows how to pause a running program in IntelliJ and move through it line by line.

Self-check

  1. What two things do you need to call something a bug?
  2. Name the six steps in order.
  3. What makes a hypothesis good?
  4. Why change only one thing at a time?
  5. What is the difference between a symptom fix and a cause fix?
أظهر الإجابات
  1. The expected behavior and the actual behavior, which differ.
  2. Reproduce, isolate, hypothesize, test, fix, verify.
  3. It is specific and predicts something you can check, so evidence can prove it wrong.
  4. Otherwise you cannot tell which change caused the effect, and an edit may add a new bug.
  5. A symptom fix hides the wrong result for some input; a cause fix removes the reason it was wrong for every input.

References

  • Oracle, The Java Language Specification, Java SE 21, section 15.17.2 Division Operator. This section is the rule behind the average and discount bugs: dividing two integers gives an integer result with the fraction discarded. I read the page only in part; the behavior was also confirmed by compiling and running the examples with JDK 21.
  • The program outputs in this lesson (4.0, 1.0, 1.5, 200.0, 5500, 9 -1 7, and the others) come from compiling and running the code with javac 21 and java. The six-step process and the detective picture are this course's own teaching framework, not a quotation from a source.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.