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

أساسيات الاستدعاء الذاتي (recursion)

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

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

Lesson Overview

Most of the methods so far called other methods. A method may also call itself. This is recursion: solving a problem by reducing it to a smaller version of the same problem, until the problem is so small that the answer is obvious.

Recipe-card picture: a card for "peel a pile of potatoes" says: "if the pile is empty, you are done; otherwise peel one potato and follow this same card for the rest of the pile." The card refers to itself, but each time with a smaller pile, and one condition tells you when to stop. Those two things, a smaller problem and a stopping condition, are everything a safe recursive method needs.

This lesson builds on Lesson 5 (the call stack, since recursion is just the stack in action) and Lesson 2 (return values). You will write factorial and sumOfDigits, see what an endless recursion does, and learn when a loop is the better choice.

Learning Objectives

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

  • Explain recursion in terms of a smaller subproblem and a base case.
  • Identify the base case and the recursive case of a recursive method.
  • Write recursive factorial and sumOfDigits methods.
  • Trace a recursive call on paper, including the way results flow back up.
  • Explain why a missing or unreachable base case ends in StackOverflowError.
  • Say when a loop is a simpler or safer choice than recursion.

Written Explanation

Two parts: base case and recursive case

Every correct recursive method has:

  1. A base case: an input small enough to answer directly, without calling itself.
  2. A recursive case: a call to the same method with an input that is closer to the base case, combined with a little work of its own.

Factorial is the classic example. The factorial of n, written n!, is n * (n-1) * ... * 1, and by convention 0! and 1! are both 1. Notice that 4! = 4 * 3!. The factorial of a number is that number times the factorial of the next smaller number. As Java:

java
static int factorial(int n) {
    if (n <= 1) {
        return 1;                    // base case
    }
    return n * factorial(n - 1);     // recursive case: smaller problem
}

The base case is n <= 1. We use <= instead of == 1 so that 0 and any negative value also stop instead of recursing forever. (Negative factorials are not defined; Lesson 7 shows how the calculator rejects them before they get here.)

How it runs: frames pile up, then answers flow back

Each call to factorial gets its own frame with its own n (Lessons 4 and 5). Tracing factorial(4):

text
factorial(4) = 4 * factorial(3)
                   factorial(3) = 3 * factorial(2)
                                      factorial(2) = 2 * factorial(1)
                                                         factorial(1) = 1   <- base case

Phase 1, going down: no multiplication can finish yet because each frame is waiting for the call above it, so four factorial frames pile up on main: n = 4, 3, 2, 1. Phase 2, coming back: the base case returns 1; the n = 2 frame computes 2 * 1 = 2; the n = 3 frame computes 3 * 2 = 6; the n = 4 frame computes 4 * 6 = 24. Each frame is popped as it returns, handing its result to the frame below it.

The two rules that keep it safe

  1. Every recursive call must move toward the base case. factorial(n - 1) shrinks n. A call such as factorial(n) or factorial(n + 1) would never reach n <= 1 from above.
  2. The base case must be reachable and must come first. Check it before making the recursive call.

Break either rule and the calls never stop. The stack fills until the JVM throws a StackOverflowError (see below).

A second pattern: sum of digits

The digit sum of 123 is 1 + 2 + 3 = 6. Break a number into its last digit and "the rest": n % 10 is the last digit and n / 10 (integer division, Module 10) drops it.

java
static int sumOfDigits(int n) {
    if (n < 10) {
        return n;                          // one digit: that is the sum
    }
    return n % 10 + sumOfDigits(n / 10);   // last digit + sum of the rest
}

123 becomes 3 + sumOfDigits(12), then 3 + 2 + sumOfDigits(1), then the base case returns 1, giving 6. Each call has a number with one digit fewer, which guarantees it reaches a single digit. This method assumes n is not negative.

A third pattern: recursion that does not return a value

A recursive method can also be void; it acts, then calls itself with a smaller input.

java
static void countdown(int n) {
    if (n == 0) {
        System.out.println("Liftoff!");
        return;
    }
    System.out.println(n);
    countdown(n - 1);
}

What goes wrong: no base case

Remove the base case from factorial:

java
static int factorial(int n) {
    return n * factorial(n - 1);
}

Every call makes another call. No frame ever returns, frames keep stacking, and eventually the JVM exceeds the stack size and throws the error. The Java API describes StackOverflowError as thrown when an application recurses too deeply. A real run of factorial(4) with this broken version printed:

text
Exception in thread "main" java.lang.StackOverflowError
	at Broken.factorial(Broken.java:3)
	at Broken.factorial(Broken.java:3)
	at Broken.factorial(Broken.java:3)
	...

The signature of the bug is the same line repeated hundreds of times. (In my run the trace listed 1,024 identical lines; the JVM caps how many it prints, so the count says nothing about how deep the stack really went.) The fix is never "catch the error". It is to add a correct base case and make sure the recursive call gets closer to it.

Recursion versus loops

Anything you can do recursively you can do with a loop (Module 13), and the reverse. A loop version of the digit sum:

java
static int sumOfDigitsLoop(int n) {
    int sum = 0;
    while (n > 0) {
        sum += n % 10;
        n /= 10;
    }
    return sum;
}

Choose by clarity. A loop is usually shorter in memory (one frame, not many) and cannot overflow the stack. Recursion shines when the problem is naturally defined in terms of smaller copies of itself, such as folders inside folders or trees, which come much later. For simple counting tasks like factorial, use whichever you find clearer. Both are acceptable, and in the lab's calculator you will use a loop for power and recursion for factorial so that you practise both.

Examples

Example 1: factorial and its limit (simple)

java
public class Fact {
    static int factorial(int n) {
        if (n <= 1) {
            return 1;
        }
        return n * factorial(n - 1);
    }

    public static void main(String[] args) {
        System.out.println(factorial(0));
        System.out.println(factorial(1));
        System.out.println(factorial(4));
        System.out.println(factorial(12));
        System.out.println(factorial(13));
    }
}

Output (JDK 21):

text
1
1
24
479001600
1932053504

The first three match the hand trace. factorial(12) is correct, but factorial(13) prints 1932053504, which is wrong: 13! is 6,227,020,800, larger than the biggest int (2,147,483,647). The result overflowed silently, as in Module 9. The recursion is fine; the type is too small. In the lab, think about whether long is a better return type and what its limit is (a long also overflows eventually: a quick loop on JDK 21 showed 20! still fits, while 21! wraps to a negative number).

Example 2: sum of digits, and the loop twin (practical)

java
public class Digits {
    static int sumOfDigits(int n) {
        if (n < 10) {
            return n;
        }
        return n % 10 + sumOfDigits(n / 10);
    }

    public static void main(String[] args) {
        System.out.println(sumOfDigits(123));
        System.out.println(sumOfDigits(7));
        System.out.println(sumOfDigits(9005));
    }
}

Output:

text
6
7
14

The 7 case is the base case immediately: no recursion. 9005 gives 5 + 0 + 0 + 9 = 14; the zeros show that a digit of 0 is handled correctly because it is just added. Running the loop version sumOfDigitsLoop(123) from the explanation also gives 6, so both styles agree.

Example 3: a countdown that acts on the way down (realistic)

java
public class Countdown {
    static void countdown(int n) {
        if (n == 0) {
            System.out.println("Liftoff!");
            return;
        }
        System.out.println(n);
        countdown(n - 1);
    }

    public static void main(String[] args) {
        countdown(3);
    }
}

Output:

text
3
2
1
Liftoff!

Here the work (printing) happens before the recursive call, so the output appears in call order. If you moved System.out.println(n); to after the call, the numbers would print in the order the frames return: Liftoff!, then 1, 2, 3. Try it: it makes the "going down, coming back" picture concrete. (A real countdown would more likely use a loop; the point here is to see the order.)

Backend Insight

Real backend code uses recursion mostly when data is nested: a category with sub-categories, a folder tree, a JSON document containing objects inside objects. Libraries do the recursion for you, but when it is your code, the same two questions apply: what is the smallest case? and does every call get closer to it? An unexpected cycle in the data (a category that is its own ancestor) is the data version of a missing base case, and it too ends in StackOverflowError.

Because the stack is limited, production code that must handle very large or untrusted input often prefers a loop. Interviewers like recursion for exactly this reason: it tests whether you can name the base case and the shrinking step. The same reasoning appears whenever you design a method that "calls itself".

Common Mistakes

Mistake 1: No base case

The classic. The method never stops and ends in StackOverflowError. Write the base case first, before the recursive line.

Mistake 2: A base case that cannot be reached

if (n == 0) return 1; looks fine, but factorial(-3) never hits 0 going down, so it overflows the stack. Prefer n <= 1 or validate the input.

Mistake 3: Not moving toward the base case

Calling factorial(n) (same value) or factorial(n + 1) (away from the base) runs forever. Each call must make the problem smaller.

Mistake 4: Forgetting to use the result

Writing factorial(n - 1); without multiplying by n or returning it computes the smaller answer and throws it away. In a value-returning method, the recursive call is part of the return expression.

Mistake 5: Overflow mistaken for a recursion bug

factorial(13) returning a strange number is an int overflow, not a broken base case. Check the size of the answer against the type.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. What are the two parts of every recursive method?

أظهر الحل A1

A1. A base case, which stops without calling itself, and a recursive case, which calls the method with a smaller problem.

A2. What does factorial(5) return? Write the expansion 5 * 4 * ... to check.

أظهر الحل A2

A2. 120, since 5 * 4 * 3 * 2 * 1 = 120.

A3. In sumOfDigits, what is the base case, and what does it return for n = 8?

أظهر الحل A3

A3. The base case is n < 10, a single digit. For n = 8 it returns 8.

Level B: Application

B1. Write a recursive static int sumTo(int n) returning 1 + 2 + ... + n (for n >= 0, with sumTo(0) equal to 0). Give its base case and trace sumTo(3).

أظهر الحل B1

B1.

java
static int sumTo(int n) {
    if (n == 0) {
        return 0;
    }
    return n + sumTo(n - 1);
}

Trace: sumTo(3) = 3 + sumTo(2) = 3 + (2 + sumTo(1)) = 3 + (2 + (1 + sumTo(0))); the base case returns 0, so the result is 3 + 2 + 1 + 0 = 6.

B2. Write static void printStars(int n) that prints a line of n asterisks using recursion and System.out.print("*"), and ends the line after the last star. (Hint: the base case is n == 0.)

أظهر الحل B2

B2.

java
static void printStars(int n) {
    if (n == 0) {
        System.out.println();
        return;
    }
    System.out.print("*");
    printStars(n - 1);
}

printStars(4) prints ****.

B3. The method below has a bug. Find it and say what happens when countUp(1) is called.

java
static void countUp(int n) {
    System.out.println(n);
    countUp(n + 1);
}
أظهر الحل B3

B3. There is no base case, and n moves away from any stopping point. countUp(1) prints 1, 2, 3, ... and keeps going until the stack is exhausted and StackOverflowError is thrown. Add a limit, for example if (n > 10) return; first.

Level C: Challenge

C1. Write a recursive static int countDigits(int n) for n >= 0 (count 0 as one digit). Give the base case and the output for 0, 7, 1234.

أظهر الحل C1

C1.

java
static int countDigits(int n) {
    if (n < 10) {
        return 1;
    }
    return 1 + countDigits(n / 10);
}

Base case: a single digit (n < 10) counts as 1. Results: countDigits(0) is 1, countDigits(7) is 1, countDigits(1234) is 4.

C2. Write static long power(int base, int exponent) recursively for exponent >= 0, using base^exponent = base * base^(exponent - 1). State the base case and compute power(2, 10) by hand. Then explain why the lab asks for an iterative power and a recursive factorial, even though either could be written both ways.

أظهر الحل C2

C2.

java
static long power(int base, int exponent) {
    if (exponent == 0) {
        return 1;
    }
    return base * power(base, exponent - 1);
}

Base case: exponent 0 returns 1. power(2, 10) unwinds to 2 * 2 * ... * 2 (ten times) which is 1024. The lab asks for both styles so you practise a loop and a recursion and can compare them. Neither is "right" for every case: a loop uses one frame, while recursion mirrors the maths of factorial closely, as long as the input stays small.

Mini Challenge

Write a recursive static int digitProduct(int n) that multiplies the digits of a non-negative number (digitProduct(234) is 24; a single digit returns itself). Then write the loop version and compare both on 234, 9 and 105. Finally, deliberately delete the base case, run it once to see the repeated lines in the stack trace, and restore it.

Check your work: both versions agree on all three inputs (24, 9 and 0), you can point to the base case and to the line that shrinks the problem, and the trace without the base case shows the same at line repeated.

Summary & Next Lesson

Key takeaways

  • Recursion solves a problem by calling the same method on a smaller version of it.
  • A correct recursive method has a base case that returns directly, and a recursive case that moves closer to it.
  • Each recursive call gets its own frame; frames pile up going down and results flow back up as they return.
  • A missing base case, an unreachable one, or a recursive call that does not get closer to it causes endless calls and StackOverflowError.
  • Many tasks can also be solved with a loop; choose the clearer, and prefer a loop when depth could be large.

Next lesson

The next lesson is a demo, "Building the calculator one method at a time". It uses everything from this module so far (returning methods, separating computing from printing, overloading, and a recursive factorial) to grow the command-line calculator that your lab will ask you to finish.

Self-check

  1. What is a base case?
  2. Which line of factorial is the recursive case?
  3. What error appears when there is no base case?
  4. In factorial(3), which frame returns first?
  5. Why did factorial(13) give a wrong number with an int return type?
أظهر الإجابات
  1. An input that the method answers directly, without calling itself.
  2. return n * factorial(n - 1);
  3. StackOverflowError.
  4. The deepest one, factorial(1), which hits the base case; the others return afterwards in reverse order.
  5. The true value (6,227,020,800) is larger than the largest int, so it overflowed.

References

  • Oracle, StackOverflowError (Java SE 21 API). Verified: it is thrown when an application recurses too deeply.
  • Oracle, The Java Virtual Machine Specification, SE 21, Chapter 2. Verified: each call has its own frame on a thread's stack, and exceeding the allowed stack size causes StackOverflowError.
  • All outputs, including the repeated stack trace and the wrong value of factorial(13), were produced by compiling and running the code with JDK 21 (javac 21.0.10). The factorial of 13 and the maximum int value (2,147,483,647) are standard values; the long overflow at 21! was checked with a short loop on JDK 21. That the JVM caps the printed trace length is my interpretation of the 1,024 lines seen; I did not find a page to cite for it.

التطبيق

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

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

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

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