Recursive Void Methods and the Leap of Faith

A method that invokes itself: the countdown that unwinds, the stack diagram that explains it, the base case that stops it, factorial derived straight from its mathematical definition, and the leap of faith that lets you write a recursive method without tracing it. Follows Think Java 2e, Chapter 8 (Recursive Methods), Sections 8.1-8.4, pp. 127-134, cross-referenced against Java SE 21 API — java.lang.StackOverflowError.

Subject: Java · 65 slides · code lesson

Open the interactive version of this deck

What this lesson covers

The lesson, slide by slide

1. Recursive Void Methods and the Leap of Faith

Title

Think Java 2e · Chapter 8 · Recursive Methods

Sections 8.1-8.4 · pp. 127-134

2. What you will be able to do

Objectives

This lesson follows Think Java 2e, Chapter 8 (Recursive Methods), Sections 8.1-8.4, pp. 127-134. Everything on these slides can be checked against those pages.

1. Write a recursive void method with a base case and a recursive call.

2. Draw the stack diagram for a recursive call and identify the base case frame.

3. Explain what causes a StackOverflowError and how a base case prevents it.

4. Translate a recursive mathematical definition directly into a Java method.

5. Trace a value-returning recursive method, following return values back up the stack.

6. Apply the leap of faith: assume the recursive call works, and check only the step you are writing.

3. Retrieve before you read

Warm-up

One picture from Chapter 4 is about to do much more work than it did there.

Discussion prompt

When one method calls another, what does Java create, and what happens to it when the method returns? Draw the stack for main calling threeLine calling newLine.

Hint: One box per method that has started and not finished.

Answer:

Java creates a frame containing the method's parameters and local variables, and the frame is discarded when the method returns. Three methods running means three frames — main at the top, then threeLine, then newLine.

Nothing in that mechanism says the frames have to be for different methods. This chapter is what happens when they are all for the same one.

4. A method that invokes itself

Concept

Up to now we have used loops whenever we needed to repeat something; methods that use iteration are called iterative. They are straightforward, but sometimes more elegant solutions exist. This chapter explores one of the most magical things a method can do: invoke itself to solve a smaller version of the same problem.

Figure (svg): A call tree showing factorial of 3 calling factorial of 2 calling factorial of 1 calling factorial of 0, with return values coming back

Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 8 (Recursive Methods), Sections 8.1-8.4, pp. 127-134 — Chapter 8 opens on printed page 127.

5. Recursive void methods

Section

Section 8.1

6. countdown calls countdown

Concept

A method that invokes itself is called recursive. This one takes a single integer: if it is 0, it displays Blastoff!; otherwise it displays the number and then invokes itself, passing n - 1.

public static void countdown(int n) {
    if (n == 0) {
        System.out.println("Blastoff!");
    } else {
        System.out.println(n);
        countdown(n - 1);
    }
}
callnwhich branchoutput
countdown(3)3else3, then calls countdown(2)
countdown(2)2else2, then calls countdown(1)
countdown(1)1else1, then calls countdown(0)
countdown(0)0ifBlastoff! — and returns

iterative — A method or algorithm that repeats steps by using one or more loops.

recursive — A method or algorithm that invokes itself one or more times with different arguments.

The total output is 3, 2, 1, Blastoff! — and then all four calls return in turn, back up to main. Nothing new is happening in the language: a method call is still the detour from Lesson 4a, and it happens to be a detour into the same method.

7. Four calls, one method

Picture it

The frames are the ones you drew in Chapter 4. What is new is that they all belong to the same method, each with a different value of n.

Figure (svg): A stack diagram with main at the top and four countdown frames below it holding n values 3, 2, 1 and 0

Every time a method gets called, Java creates a new frame containing its parameters and variables. Four calls, four frames, four separate copies of n — which is exactly Lesson 4a's rule that local variables live in their own frame.

8. Following the calls down and back

Worked example

The execution reads as a nested story: each call starts, produces output, hands over, and later gets control back.

countdown(3);
stepwhat happensoutput so far
1countdown(3) begins; n is not 0, so it prints 33
2it invokes countdown(2), which prints 23 2
3which invokes countdown(1), which prints 13 2 1
4which invokes countdown(0); n IS 0, so it prints Blastoff!3 2 1 Blastoff!
5countdown(0) returns, then (1), then (2), then (3)unchanged
6back in mainunchanged

Read the calls going down.

Why: Each one prints its own n and then hands over to a smaller version of itself.

Notice where the recursion stops.

Why: At n == 0 the if branch runs, which contains no recursive call — so nothing further is started.

Read the returns coming back up.

Why: The countdown that got n == 1 returns, then the one that got 2, then the one that got 3, and then you are back in main.

Notice that the returns produce no output here.

Why: Every print happened on the way down. That is a choice, and reversing it is what Lesson 8b opens with.

Verify: Run countdown(3) and expect exactly four lines: 3, 2, 1, Blastoff!

Why: Then run countdown(0) and expect one line. The method works for every non-negative n, which is worth checking at the boundary as well as in the middle.

9. What does countdown(2) print?

Prediction

Follow the calls down.

public static void countdown(int n) {
    if (n == 0) {
        System.out.println("Blastoff!");
    } else {
        System.out.println(n);
        countdown(n - 1);
    }
}
callprints
countdown(2)2
countdown(1)1
countdown(0)Blastoff!

Predict first

What is the output?

  • 2, 1, Blastoff!
  • 2, 1, 0, Blastoff!
  • Blastoff!, 1, 2
  • 2, Blastoff!

Correct: 2, 1, Blastoff!

Why: Each call prints its own n before recursing, so 2 and 1 appear on the way down, and the base case at n == 0 prints Blastoff! instead of a number. Zero itself is never printed, because the branch that prints n is the else branch — which is exactly the branch that does not run when n is 0.

10. Recursion generalises what threeLine could not

Concept

Section 4.1's newLine and threeLine worked, but they would not help if you wanted two newlines, or a hundred. A recursive method takes the count as a parameter.

public static void nLines(int n) {
    if (n > 0) {
        System.out.println();
        nLines(n - 1);
    }
}
callprintsthen calls
nLines(3)one newlinenLines(2)
nLines(2)one newlinenLines(1)
nLines(1)one newlinenLines(0)
nLines(0)nothing — n is not > 0nothing

The structure is the same as countdown. As long as n is greater than 0 it displays a newline and then invokes itself to display n-1 more. The total is 1 + (n - 1), which is exactly n — and writing the count that way is a good habit for convincing yourself a recursive method is right.

11. A base case that is never reached

Trap

The trap

The recursive call does not make progress toward the base case.

public static void countdown(int n) {
    if (n == 0) {
        System.out.println("Blastoff!");
    } else {
        System.out.println(n);
        countdown(n);          // n, not n - 1
    }
}
callnnext callcloser to 0?
countdown(3)3countdown(3)no
countdown(3)3countdown(3)no
...3...never

The base case exists and is perfectly correct — it is simply never reached, because n never changes. This is Lesson 6a's infinite loop wearing different clothes.

The fix

Every recursive call must move toward the base case.

public static void countdown(int n) {
    if (n == 0) {
        System.out.println("Blastoff!");
    } else {
        System.out.println(n);
        countdown(n - 1);
    }
}
callnnext callcloser to 0?
countdown(3)3countdown(2)yes
countdown(2)2countdown(1)yes
countdown(1)1countdown(0)yes
countdown(0)0none — base casearrived

The two questions to ask about any recursive method are the same two from Lesson 6a's loops: is there a stopping condition, and does every step move toward it? Here they are the base case and the argument to the recursive call.

12. Base case or recursive case?

Definition probe

Every recursive method has both, and they do different jobs.

Sort into buckets

Sort each part of countdown.

the base case
if (n == 0); System.out.println("Blastoff!")
the recursive case
System.out.println(n); countdown(n - 1)
base
The condition that stops the recursion, and the work done when it is reached. Crucially it contains NO recursive call, which is what makes the stack stop growing.
rec
The work done on this call, and the call to a smaller version of the same problem. The argument must move toward the base case.

13. Complete a recursive method

Fill the middle

Print n newlines.

Fill in the blanks

public static void nLines(int n) >} 0) -} 1);
}
}

Why: n > 0 is the condition for doing any work at all, so when n reaches 0 the method does nothing and the recursion stops — the base case here is the absence of an else branch. Subtracting 1 is what moves each call toward that base case; passing n unchanged would recurse forever.

14. Why is nothing new happening in the language?

Socratic

Recursion feels like a new feature. Argue that it is not.

Discussion prompt

countdown calls countdown. Which rules from Chapter 4 does that rely on, and is Java doing anything it was not already doing when threeLine called newLine?

Hint: Think about frames and local variables.

Answer:

It relies on exactly two rules you already have: a method call is a detour that returns to the statement after it, and each call gets its own frame with its own local variables. Java is doing nothing new at all.

What is new is only that the frames happen to be for the same method, so several copies of n exist at once with different values. Chapter 4's insistence that the two hour variables in main and printTime were different variables was preparing for exactly this.

That is worth holding on to when recursion feels mysterious: the mechanism is ordinary, and only the shape is unfamiliar.

15. Recursive stack diagrams

Section

Section 8.2

16. One frame per call, all for the same method

Concept

In Section 4.5 we used a stack diagram to represent the state of a program during a method invocation. The same kind of diagram makes a recursive method much easier to interpret — because every time a method is called, Java creates a new frame containing its parameters and variables.

Figure (svg): A stack diagram for countdown called with 3, showing main at the top and four countdown frames with n values 3, 2, 1 and 0

base case — The case of a recursive method that does not make a recursive call, so the stack stops growing.

By convention the frame for main is at the top and the stack of other frames grows down — so you can draw the diagram on paper without guessing how far it will grow. main's frame is empty because it has no variables we are using.

17. Reading a recursive stack diagram

Notation

Four things are worth naming in this picture, and the last one is the whole reason the method terminates.

Annotate

  • There are four frames for countdown, each with a different value for the parameter n. They are separate variables that happen to share a name — Lesson 4a's rule, now with four frames instead of two.
  • The last frame is the base case. It does not make a recursive call, so there are no more frames below it. That is what makes the stack stop growing.
  • The stack grows down in this convention, which is the opposite of how it grew in Lesson 4a's two-frame diagrams — the book draws main at the top in both, and here the growth is downward from it.
  • The depth is the number of calls in progress. For countdown(n) that is n + 1 frames, which is worth noticing: a recursive method's stack depth is proportional to its input.

That last point is the practical consequence. A loop uses one frame however many times it repeats; a recursive method uses one per repetition, and frames are a finite resource.

18. What happens without a base case

Worked example

If there is no base case, or if it is never reached, the stack would grow forever — at least in theory. In practice the size of the stack is limited.

public static void forever(String s) {
    System.out.println(s);
    forever(s);
}
callframes so farprogress toward stopping
forever(s)1none
forever(s)2none
forever(s)...thousands...none
forever(s)the limitStackOverflowError

Notice there is no condition at all.

Why: No if statement, so no base case — every call makes another call unconditionally.

Notice the argument never changes.

Why: Even a base case would not help, because s is passed through untouched.

Each call adds a frame.

Why: And no call ever returns, so no frame is ever removed.

Eventually the stack runs out.

Why: Java throws a StackOverflowError, which is what the finite stack does when asked for one frame too many.

Verify: Try it. The method displays the string until the stack overflows, and you may be surprised by how long the error message is.

Why: The length is the point: the error prints a stack trace with one line per frame, and there are thousands. That is the clearest possible demonstration that each call really did get its own frame.

19. How many frames for countdown(4)?

Prediction

Count the calls, including the base case.

countdown(4);
calln
1st4
2nd3
3rd2
4th1
5th0 — base case

Predict first

How many countdown frames exist at the deepest point?

  • 5
  • 4
  • 3
  • 1

Correct: 5

Why: The calls take n from 4 down to 0, which is five calls and therefore five frames — plus main's frame above them. In general countdown(n) reaches a depth of n + 1 frames, because the base case at zero is a call too.

20. Recursion depth against loop repetitions

Concept

A loop and a recursion can repeat the same number of times and use very different amounts of memory. That difference is worth knowing before you choose between them.

a loop repeating n timesa recursion of depth n
frames usedonen plus one
local variables in existenceone setn sets
what happens when n is hugeit just takes longerStackOverflowError
how it stopsthe loop condition failsthe base case is reached

The third row is the practical limit. A loop counting to ten million is slow; a recursion ten million deep crashes. That is not a reason to avoid recursion — it is a reason to prefer it where the depth is small or grows slowly, which is exactly what Chapter 13's merge sort does.

21. Assuming a StackOverflowError means an infinite loop

Trap

The trap

The diagnosis stops too early. A StackOverflowError has two quite different causes.

// cause 1: no base case, or never reached
public static void forever(String s) {
    forever(s);
}

// cause 2: a correct method, called with too large an argument
public static int sum(int n) {
    if (n == 0) return 0;
    return n + sum(n - 1);      // correct, but sum(1000000) overflows
}
causeis the method wrong?the fix
no base caseyesadd one, and make the argument move toward it
depth too largenorewrite iteratively, or reduce the depth

Both throw the same error. Concluding 'my base case is broken' when the real problem is depth sends you looking for a bug that does not exist.

The fix

Check the base case first, then check the depth.

// the two questions:
//  1. Is there a base case, and does every call move toward it?
//  2. How deep will the recursion go for this input?
questionif the answer is bad
is there a base case?add one
does the argument move toward it?fix the recursive call
how deep for a realistic input?if thousands, consider a loop instead

The second question is the one people skip. A recursive method over a million-element array is correct and unusable, and the error message will not distinguish it from a genuine bug.

22. Watch the stack grow and shrink

Invariant

Step through countdown(2) and watch the frames.

Step through it

At the deepest point, how many different variables named n exist?

  1. One frame. Nothing recursive has started.
  2. countdown(2) begins, prints 2, and is about to call itself.
  3. countdown(1) begins, prints 1. Two countdown frames now hold different values of n.
  4. countdown(0) begins. n IS 0, so it prints Blastoff! and makes no further call — this is the base case.
  5. The base case returns first. Its frame disappears.
  6. Then countdown(1) returns, having nothing left to do after its recursive call.
  7. And countdown(2) returns. The stack is back to where it started.

Three — one per countdown frame, holding 2, 1 and 0. They share a name and nothing else, which is the same rule as Lesson 4a's two hour variables, applied three times over.

23. What error does forever throw?

Prediction

No base case at all.

public static void forever(String s) {
    System.out.println(s);
    forever(s);
}
what growswhat limits it
the number of framesthe size of the stack
frames removednone — no call ever returns

Predict first

What happens when you run this?

  • It prints the string many times and then throws StackOverflowError
  • It prints the string forever with no error
  • It fails to compile
  • It throws OutOfMemoryError immediately

Correct: It prints the string many times and then throws StackOverflowError

Why: Each call adds a frame and none ever returns, so the stack grows until it reaches its limit and Java throws StackOverflowError. It compiles perfectly well — a method calling itself is legal, and the compiler has no way to know that the base case is missing.

24. Can a loop overflow the stack?

Counterexample

Compare the two mechanisms directly.

Discussion prompt

while (true) { } runs forever and never throws StackOverflowError, while forever(s) does. What is the difference, and what does it tell you about what the stack actually holds?

Hint: How many frames does the loop use?

Answer:

The loop uses one frame, however many times it repeats — it is the same method call, going round again. The recursion uses one frame per repetition, because each repetition is a new call.

So the stack holds calls in progress, not work done. An infinite loop does unbounded work in bounded memory; infinite recursion does unbounded work in unbounded memory, and memory runs out first.

That is the real trade between the two forms. Recursion buys you a structure that mirrors the problem, and pays for it in stack frames — which is why the depth question matters as much as the base-case question.

25. Value-returning recursive methods

Section

Section 8.3

26. A recursive definition becomes a recursive method

Concept

A recursive definition refers to the thing being defined. Many mathematical functions are defined this way because it is the simplest way — and if you can formulate a recursive definition, you can easily write a Java method to evaluate it.

// the mathematical definition:
//     0! = 1
//     n! = n * (n - 1)!

// the Java method:
public static int factorial(int n) {
    if (n == 0) {
        return 1;
    }
    return n * factorial(n - 1);
}
the definition saysthe method says
0! = 1if (n == 0) return 1;
n! = n times (n-1)!return n * factorial(n - 1);
the base casethe if
the recursive casethe return after it

Do not confuse the mathematical symbol !, which means factorial, with the Java operator !, which means not. And note how closely the method matches the definition — line for line, which is the point of the whole section.

27. Building it up, one decision at a time

Notation

Lesson 4b's incremental development applies exactly here, and the order of the three steps is what makes recursion writable.

Annotate

  • Step 1: parameters and return type. Factorial is defined for integers and produces an integer, so the method takes an int and returns an int. This is Lesson 4b's stub, unchanged.
  • Step 2: the base case, first. If the argument is 0 we return 1. Writing the base case before the recursive case is the habit that makes recursion tractable — it is the part with no leap of faith in it.
  • Step 3: the recursive case. Make a recursive call to find the factorial of n-1, then multiply by n. This is the only step where you have to trust something not yet finished.
  • The temporary variables recurse and result are for illustration. In each call, recurse stores the factorial of n-1 and result stores the factorial of n — naming them makes the trace readable and makes debugging easier.

Notice that the hard-looking part is the shortest. Once the base case and the return type are settled, the recursive case is one line that says what the definition already said.

28. Tracing factorial(3)

Worked example

The flow of execution is like countdown, with one addition: values come back. Follow it down and then watch the multiplications happen on the way up.

public static int factorial(int n) {
    if (n == 0) {
        return 1;
    }
    int recurse = factorial(n - 1);
    int result = n * recurse;
    return result;
}
callnrecurseresultreturns
factorial(3)3266
factorial(2)2122
factorial(1)1111
factorial(0)0——1

Go down first.

Why: 3 is not 0, so calculate the factorial of 2; 2 is not 0, so calculate the factorial of 1; and so on to 0.

The base case returns immediately.

Why: Since 0 is 0, we take the first branch and return 1 without any further call.

Now the multiplications happen, on the way back up.

Why: The returned 1 is multiplied by n = 1 giving 1; that is multiplied by n = 2 giving 2; that is multiplied by n = 3 giving 6.

Notice the last frame has no recurse or result.

Why: When n == 0 the code that declares them never executes, so those variables do not exist in that frame.

Verify: Expect factorial(3) to return 6 — and check it against 3 * 2 * 1 * 1, which is the definition unrolled.

Why: The row worth staring at is the last one: the base case frame is genuinely different in shape from the others, which is what the book's Figure 8.2 shows and why it is worth drawing rather than imagining.

29. What does factorial(4) return?

Prediction

Multiply on the way back up.

public static int factorial(int n) {
    if (n == 0) {
        return 1;
    }
    return n * factorial(n - 1);
}
callcomputesreturns
factorial(0)base case1
factorial(1)1 * 11
factorial(2)2 * 12
factorial(3)3 * 26
factorial(4)4 * 624

Predict first

What is the result?

  • 24
  • 12
  • 10
  • 6

Correct: 24

Why: Each call multiplies its own n by the result of the call below it, so the answers build up 1, 1, 2, 6, 24 as the stack unwinds. That is 4 * 3 * 2 * 1, which is the definition unrolled — and the base case returning 1 is what makes the final multiplication come out right.

30. Where the work actually happens

Concept

In countdown all the output happened on the way down. In factorial all the multiplication happens on the way up. That distinction is the key to reading any recursive method.

Figure (svg): A call tree for factorial of 3, with each edge labelled by the value returned back up it

Many algorithms can be written concisely with recursive methods that perform computations on the way down, on the way up, or both. Asking which one a method does is the fastest way to understand it — and Lesson 8b's countup is the same method with the work moved from one to the other.

31. A base case that returns the wrong thing

Trap

The trap

The recursion is right and the base case is not. Every answer is zero.

public static int factorial(int n) {
    if (n == 0) {
        return 0;              // should be 1
    }
    return n * factorial(n - 1);
}
callreturns
factorial(0)0 — wrong
factorial(1)1 * 0 = 0
factorial(2)2 * 0 = 0
factorial(3)3 * 0 = 0

One wrong value at the bottom contaminates everything above it, because every result is built from the one below. The method terminates correctly and returns nonsense.

The fix

0! is 1, and the definition says so.

public static int factorial(int n) {
    if (n == 0) {
        return 1;
    }
    return n * factorial(n - 1);
}
callreturns
factorial(0)1
factorial(1)1 * 1 = 1
factorial(2)2 * 1 = 2
factorial(3)3 * 2 = 6

The base case is the one part of a recursive method you can check without any recursion at all, so test it first and on its own. factorial(0) should be 1 — if it is not, nothing above it can be right.

32. Order the events of factorial(2)

Ranking

Down, then up.

Put in order

  1. factorial(2) begins; n is not 0
  2. factorial(1) begins; n is not 0
  3. factorial(0) begins and returns 1
  4. factorial(1) computes 1 * 1 and returns 1
  5. factorial(2) computes 2 * 1 and returns 2

Why: All the calls happen before any multiplication does: the recursion descends to the base case first, and only then do the multiplications happen as each frame returns. Recognising that the work is on the way up is what makes this method readable.

33. Write factorial from its definition

Fill the middle

0! = 1, and n! = n times (n-1)!

Fill in the blanks

public static int factorial(int n) 0}) 1};
}
return n * factorial(n - 1);
}

Why: The three blanks are the three parts of the mathematical definition: the base case is at 0, its value is 1, and the recursive case reduces n by one. Returning 0 from the base case would make every answer zero, since each result is built by multiplying the one below.

34. What is missing from this recursive definition?

Missing information

A definition that refers to itself is not automatically useful.

Discussion prompt

recursive: an adjective used to describe a method that is recursive. Think Java offers this as a joke. What exactly is missing that stops it being a usable definition — and what does the factorial definition have that it lacks?

Hint: Compare it with 0! = 1.

Answer:

It has no base case. Every attempt to expand it produces another copy of itself, so it never resolves into anything you did not already need to know.

The factorial definition has two clauses: 0! = 1 is an outright answer that refers to nothing, and n! = n * (n-1)! reduces the problem. The first is what makes the second useful.

So a recursive definition needs the same two things a recursive method does: a case that resolves directly, and a case that gets closer to it. A circular definition is exactly a recursive one with the first part missing — which is why 'did you mean: recursion' is funny.

35. The leap of faith

Section

Section 8.4

36. Assume the recursive call works

Concept

Following the flow of execution is one way to read programs, but it quickly becomes overwhelming. Another way to understand recursion is the leap of faith: when you come to a method invocation, instead of following the flow, you assume that the method works correctly and returns the appropriate value.

public static int factorial(int n) {
    if (n == 0) {
        return 1;
    }
    return n * factorial(n - 1);
    //          ^^^^^^^^^^^^^^^^
    //          assume this gives (n-1)! and ask:
    //          can I now compute n!?   Yes - multiply by n.
}
questionanswer
Does the base case return the right value?0! is 1 — check it directly
Assuming factorial(n-1) is correct, is factorial(n)?yes, by multiplying by n
Does every call move toward the base case?yes, n decreases by 1
Therefore?the method is correct

You are already practising this leap of faith whenever you use a library method. When you invoke Math.cos or System.out.println, you do not think about the implementation — you assume it works.

37. Two ways to convince yourself

Picture it

Both are legitimate. One of them stops scaling almost immediately.

Figure (svg): Two panels comparing tracing every call against assuming the recursive call is correct

The left column is fine for n = 3 and impossible for n = 30. The right column is the same length for every n — which is why it is the technique you actually use once the method gets interesting.

38. Fibonacci, where tracing is hopeless

Worked example

Here the leap of faith is not merely convenient. If you try to follow the flow of execution, even for small values of n, your head will explode.

public static int fibonacci(int n) {
    if (n == 1 || n == 2) {
        return 1;
    }
    return fibonacci(n - 1) + fibonacci(n - 2);
}
the definitionthe method
fibonacci(1) = 1if (n == 1 || n == 2) return 1;
fibonacci(2) = 1the same branch
fibonacci(n) = fib(n-1) + fib(n-2)return fibonacci(n - 1) + fibonacci(n - 2);

Check the base cases directly.

Why: There are two of them here, and both return 1. No recursion is involved, so they can be checked by reading.

Take the leap for both recursive calls.

Why: Assume fibonacci(n-1) and fibonacci(n-2) are correct.

Ask whether the combination is right.

Why: Each Fibonacci number is the sum of the two preceding ones — so adding them is correct, by the definition.

Check that both calls move toward a base case.

Why: n-1 and n-2 are both smaller than n, and the base cases catch 1 and 2.

Verify: Expect fibonacci(6) to be 8: the sequence is 1, 1, 2, 3, 5, 8.

Why: Now notice what you did NOT do: you never traced the calls. fibonacci(6) makes fifteen calls and the tree has branches everywhere — and yet three short checks were enough to be confident it is right.

It is strange to assume the method works correctly when you have not finished writing it. But that is why it is called a leap of faith.

39. What is fibonacci(5)?

Prediction

Take the leap rather than tracing.

public static int fibonacci(int n) {
    if (n == 1 || n == 2) {
        return 1;
    }
    return fibonacci(n - 1) + fibonacci(n - 2);
}
nfibonacci(n)
11
21
31 + 1 = 2
42 + 1 = 3
53 + 2 = 5

Predict first

What does fibonacci(5) return?

  • 5
  • 3
  • 8
  • 4

Correct: 5

Why: Each value is the sum of the two before it: 1, 1, 2, 3, 5. Note that the table is far easier to build than the call tree — going forwards from the base cases is the practical way to check a recursive definition, even though the method itself works backwards.

40. Why Fibonacci's call tree branches

Concept

factorial makes one recursive call and fibonacci makes two. That single difference changes the shape of the computation completely.

Figure (svg): A branching call tree for fibonacci of 4 showing repeated subcalls to fibonacci of 2

One recursive call makes a chain; two make a tree. The tree does far more work — and repeats itself, since fib(2) is computed twice even here. That is a real inefficiency, and the standard fix (remembering results) is a topic for later. For now the point is that the leap of faith proves the method correct, and says nothing about whether it is fast.

41. Taking the leap before checking the base case

Trap

The trap

The leap of faith applied to a method with no base case. The reasoning looks fine and the method never terminates.

public static int countDigits(int n) {
    return 1 + countDigits(n / 10);
}
// "Assume countDigits(n/10) is correct, then adding 1 is right."
// Perfectly good reasoning - and it runs forever.
the leap of faith checksdoes it check this?
is the combination step right?yes
is there a base case?NO
does the argument move toward it?no

The leap of faith is only one of three checks. On its own it verifies the recursive case and says nothing about termination — which is exactly what is missing here.

The fix

Three checks, every time. The leap is the third.

public static int countDigits(int n) {
    if (n < 10) {              // 1. a base case that resolves directly
        return 1;
    }
    return 1 + countDigits(n / 10);   // 2. moves toward it  3. combines correctly
}
checkfor countDigits
1. is there a base case, and is it right?n < 10 means one digit — yes
2. does every call move toward it?n / 10 shrinks n — yes
3. assuming the call works, is the combination right?one more digit than the rest — yes

These are the same three questions as a loop's: a stopping condition, progress toward it, and correct work each step. The leap of faith is just a comfortable way of asking the third.

42. Which check does each question belong to?

Definition probe

Three separate things must be true for a recursive method to be correct.

Sort into buckets

Sort each question.

the base case is correct
Does factorial(0) return 1?; Is there an if statement that returns without recursing?
every call makes progress
Does n - 1 get closer to 0?; Do both fibonacci calls use smaller arguments?
the leap of faith — the combination
If factorial(n-1) is right, is n times it right?
base
Checks the case that resolves directly, with no recursion involved. This is the one you can verify just by reading, and it should be checked first.
prog
Checks that the recursion terminates — each call must be on a smaller problem, or the base case is never reached.
leap
Assumes the recursive call is correct and asks only whether this step combines the result properly. This is the only check that involves any faith.

43. State the leap of faith as a question

Explain it to yourself

One sentence you can apply to any recursive method.

Discussion prompt

Write the leap of faith as a question you would ask yourself while writing the recursive case of a method. Then apply it to a method that reverses a string.

Hint: It has the form 'assuming ... , can I ... ?'

Answer:

Assuming the recursive call solves the smaller problem correctly, can I build the answer to this one?

For reversing a string: assuming I can reverse the rest of the string, can I reverse the whole thing? Yes — reverse the rest, then put the first character on the end. That is the entire design, and it took one sentence.

Notice what the question does: it replaces an unbounded trace with a single step. That is why it scales to methods whose call trees you could never follow.

44. Does the leap of faith prove the method is fast?

Edge cases

It proved fibonacci correct. Ask what it did not prove.

Discussion prompt

The three checks establish that fibonacci returns the right answer. Try running fibonacci(45) and predict what happens. What does that tell you about the limits of this reasoning?

Hint: Look at the call tree again and count.

Answer:

It takes an extremely long time — the call tree has roughly a billion nodes, because each call branches into two and the same subproblems are recomputed over and over.

So the leap of faith establishes correctness and nothing else. A method can be provably right and completely unusable, and the three checks will not tell you which.

The fix is to stop recomputing: remember each answer the first time it is worked out. That idea has a name — memoisation — and it is one of the standard reasons to convert a recursive method into an iterative one. Correct first, then fast is the right order, but the second question does have to be asked.

45. Recursion against iteration

Section

Sections 8.1-8.4, in review

46. Two ways to repeat

Concept

Every recursive method in this lesson could be written with a loop, and every loop could be written recursively. Neither is more powerful; they differ in what they make easy to say.

// iterative
public static int factorial(int n) {
    int result = 1;
    for (int i = 1; i <= n; i++) {
        result *= i;
    }
    return result;
}

// recursive
public static int factorial(int n) {
    if (n == 0) {
        return 1;
    }
    return n * factorial(n - 1);
}
iterativerecursive
how it repeatsa loopcalls to itself
frames usedonen + 1
what carries the answeran accumulatorthe return values
resembles the definition?not obviouslyline for line

The last row is the argument for recursion, and the second is the argument against it. Which matters more depends entirely on the problem.

47. Where the accumulated answer lives

Picture it

Both versions build up the same product. The difference is where the partial results are kept.

Figure (svg): Two panels comparing an accumulator variable in a loop against partial results held in stack frames

The recursive version's stack is the accumulator — the partial results are held in the waiting frames rather than in one variable. That is why depth is proportional to n, and it is the clearest way to see what the stack is actually for.

48. Choosing between them

Worked example

Three problems, three different answers. The deciding question is whether the problem's own definition is recursive.

// 1. sum an array          -> iterative. There is nothing recursive
//                              about "add up these elements".

// 2. factorial              -> either. The definition is recursive,
//                              but the loop is just as clear.

// 3. walk a folder tree     -> recursive. A folder contains folders,
//                              so the problem is recursive by nature.
problemnatural formwhy
summing an arrayiterativea flat sequence, visited once
factorialeitherrecursively defined, but simple enough to loop
Fibonaccirecursive to read, iterative to runthe definition branches; the loop does not
searching a folder treerecursivethe structure itself contains smaller copies of itself
merge sort (Chapter 13)recursivesorting half a list is the same problem, smaller

Ask whether the problem contains smaller copies of itself.

Why: A folder contains folders; a list to be sorted contains halves to be sorted. Those are naturally recursive.

Ask how deep the recursion would go.

Why: Proportional to the data, or to its logarithm? A depth of 20 is nothing; a depth of a million is a crash.

Ask which version a reader would understand faster.

Why: For factorial, honestly, the loop. For merge sort, unquestionably the recursion.

Prefer the one that matches the problem's shape.

Why: Code that mirrors its problem is easier to check, which is worth more than either speed or brevity.

Verify: Write both versions of factorial and confirm they agree for 0 through 10.

Why: Then notice which one you would rather check against the definition 0! = 1, n! = n(n-1)!. That is the real criterion, and it is why the book presents recursion after loops rather than instead of them.

49. Loop or recursion

Trade off

Fill the blanks from what each form costs and buys.

Comparison matrix

looprecursion
stack frames for n repetitionsonen plus one
what stops itthe condition becoming falsereaching the base case
failure modeinfinite loopStackOverflowError
best suited toflat sequencesstructures containing smaller copies of themselves

Neither column is better. The bottom row is the one that should decide it, and the top row is the one that occasionally overrules everything else.

50. The three questions, one last time

Concept

Whether a repetition is written as a loop or a recursion, the same three things must be true. Only the vocabulary changes.

questionin a loopin a recursion
what stops it?the condition becomes falsethe base case is reached
what makes progress?the update statementthe argument to the recursive call
what does the work?the bodythe combination step
what goes wrong without itinfinite loopStackOverflowError

Reading the table across is the point: these are the same three questions. A student who can check a loop can check a recursion, once they know which part plays which role.

51. Converting a loop to recursion by reflex

Trap

The trap

Recursion for its own sake. Deeper, slower, and no clearer.

// summing an array recursively, for no reason
public static int sum(int[] a, int i) {
    if (i >= a.length) {
        return 0;
    }
    return a[i] + sum(a, i + 1);
}
// sum(millionElementArray, 0)  ->  StackOverflowError
array sizeframes usedoutcome
1011fine
10,00010,001probably fine
1,000,0001,000,001StackOverflowError

The method is correct by all three checks. It is also strictly worse than the loop: same clarity, linear stack depth, and a hard limit on the data it can handle.

The fix

Use a loop for flat repetition; use recursion for nested structure.

// flat sequence -> loop
int total = 0;
for (int x : a) {
    total += x;
}

// nested structure -> recursion
// (a folder containing folders; a list split into halves)
the data isuse
a flat sequence, visited oncea loop
a structure containing smaller copies of itselfrecursion
defined by a recursive formulaeither — pick the clearer one

Think Java's own framing is worth keeping: iterative methods are straightforward, and recursion is for when a more elegant solution exists. Elegance means the code matches the problem — not that it avoids loops.

52. Which form fits the problem?

Discrimination

Ask whether the problem contains smaller copies of itself.

Sort into buckets

Sort each task.

a loop fits better
print every element of an array; count the vowels in a string; sum a million numbers
recursion fits better
compute a Fibonacci number, matching the definition; search a folder and all its subfolders
loop
A flat sequence visited once. A loop uses one frame however long the sequence is, and nothing about the problem is self-similar.
rec
Either the definition is itself recursive, or the data structure contains smaller instances of itself — a folder holds folders. The recursive code mirrors the problem's shape.

53. How deep does the recursion go?

Prediction

Depth decides whether a correct method is usable.

public static int sum(int[] a, int i) {
    if (i >= a.length) return 0;
    return a[i] + sum(a, i + 1);
}
array lengthframes
1011
1,0001,001
1,000,0001,000,001

Predict first

For an array of one million elements, what happens?

  • StackOverflowError — the depth is proportional to the array length
  • It works, just slowly
  • It returns 0
  • A compile error

Correct: StackOverflowError — the depth is proportional to the array length

Why: Each element gets its own frame and none returns until the base case is reached, so a million elements means a million frames — far beyond the stack's limit. The method passes all three correctness checks and is still unusable, which is why depth is a question worth asking separately.

54. Explain recursion without saying 'it calls itself'

Explain it

The usual phrase describes the mechanism and not the idea.

Discussion prompt

Explain to someone what a recursive method is, without using the words 'calls itself'. Then explain what a base case is for, using an example that is not a program.

Hint: The idea is about problems, not about calls.

Answer:

A recursive method solves a problem by solving a smaller version of the same problem, and then building the answer from that. Factorial of 5 is 5 times factorial of 4 — the same question, one step easier.

The base case is the version that is easy enough to answer outright, with no smaller version needed. Without it you would keep asking easier and easier questions forever and never actually answer one.

A non-program example that works well: looking up a word in a dictionary whose definitions use other words. You keep looking things up until you reach a word you already know — and that word is the base case. Someone who knows no words at all can never finish.

55. countdown, factorial and fibonacci

Comparison

Three recursive methods, differing in what they return and how many calls they make. Fill the blanks.

Comparison matrix

countdownfactorialfibonacci
returnsnothing — voidan intan int
recursive calls per invocationoneonetwo
work happenson the way downon the way upon the way up
shape of the call structurea chaina chaina branching tree

The bottom row is what makes fibonacci expensive: one recursive call gives a chain of n frames, while two give a tree that can have exponentially many nodes.

56. The pattern to carry away

Pattern

Every recursive method has the same three parts, and checking them in this order is what makes recursion writable rather than mysterious.

public static ReturnType solve(Problem p) {

    if (isSmallEnough(p)) {          // 1. BASE CASE
        return directAnswer;         //    no recursive call here
    }

    ReturnType partial = solve(smaller(p));   // 2. PROGRESS
                                              //    strictly closer to the base

    return combine(p, partial);      // 3. COMBINE
                                     //    assume partial is correct
}
partthe question to askwhat goes wrong without it
base casedoes this resolve with no recursion?StackOverflowError
progressis the argument strictly smaller?StackOverflowError
combineassuming the call works, is this right?a wrong answer

57. Check: the base case

Check

Work it out before you click.

public static int f(int n) {
    if (n == 0) {
        return 2;
    }
    return n * f(n - 1);
}
callreturns
f(0)2
f(1)1 * 2 = 2
f(2)2 * 2 = 4
f(3)3 * 4 = 12

Check your understanding

What does f(3) return?

  • A. 12 (correct)
  • B. 6
  • C. 24
  • D. 2

Answer: A

Why: The base case returns 2 rather than 1, and every result above it is built by multiplying, so the whole answer is doubled: 3 * 2 * 1 * 2 is 12. A wrong value in the base case contaminates every result derived from it, which is why the base case should be tested first and on its own.

Why B tempts people
This is 3 factorial, which is what the method would return if the base case correctly returned 1.
Why C tempts people
This is 4 factorial; the recursion starts at n = 3 and multiplies three times, not four.
Why D tempts people
This is the base case's own value, which is only returned for f(0).

58. Check: stack depth

Check

Work it out before you click.

public static void countdown(int n) {
    if (n == 0) {
        System.out.println("Blastoff!");
    } else {
        System.out.println(n);
        countdown(n - 1);
    }
}
calln
1st5
......
6th0 — base case

Check your understanding

How many countdown frames exist at the deepest point of countdown(5)?

  • A. 6 (correct)
  • B. 5
  • C. 1
  • D. It depends on the stack size

Answer: A

Why: The calls take n from 5 down to 0, which is six calls and therefore six frames — the base case at zero is a call like any other and gets its own frame. In general countdown(n) reaches a depth of n + 1.

Why B tempts people
This counts the calls that print a number but omits the base case call, which also has a frame.
Why C tempts people
That would be a loop, which uses one frame however many times it repeats. Each recursive call gets its own.
Why D tempts people
The stack size limits how deep the recursion CAN go before failing, but it does not change how many frames this particular call uses.

59. Check: the leap of faith

Check

Work it out before you click.

Check your understanding

You are writing a recursive method and have checked that the base case is correct and that every call uses a smaller argument. What does the leap of faith let you assume?

  • A. That the recursive call returns the correct answer for the smaller problem (correct)
  • B. That the method will not overflow the stack
  • C. That the method is efficient
  • D. That the base case will always be reached

Answer: A

Why: The leap of faith is exactly the assumption that the recursive call works, so you only have to verify that your combination step builds the right answer from it. It is one of three separate checks, and the other two — a correct base case and progress toward it — are what actually establish termination.

Why B tempts people
Stack depth is a separate question that none of the three correctness checks addresses, as fibonacci and the recursive array sum both demonstrate.
Why C tempts people
Correctness and efficiency are independent; fibonacci passes all three checks and is exponentially slow.
Why D tempts people
That is the progress check, which you have to verify yourself — it is not something the leap of faith grants you.

60. Where recursion is the only sensible shape

Real world

Factorial can be a loop. Some problems resist that, and they share a property worth being able to recognise.

Discussion prompt

Think about searching every file in a folder, including every subfolder, and every subfolder of those. Why is a single loop awkward here — and what does the problem have in common with the ones Chapter 13 will sort?

Hint: How many levels deep does the folder tree go?

Answer:

A loop would need to know how many levels deep to go, and you cannot know that in advance. The structure is self-similar: a folder contains folders, each of which is the same kind of thing as the one you started with.

That is exactly the property. Whenever a structure contains smaller instances of itself — folders, family trees, nested expressions, a list being split into halves — recursion mirrors it directly and iteration has to simulate it with an explicit stack of its own.

Chapter 13's merge sort is the same shape: sorting a list means sorting two half-lists, which is the same problem one step smaller. Recognising self-similarity is what tells you recursion is the right tool rather than a clever alternative to a loop.

61. How sure are you?

Commit first

Commit to an answer and to your confidence.

Predict first

What happens if a recursive method has a correct base case that is never reached?

  • StackOverflowError — the same as having no base case at all
  • The method returns the base case's value
  • The method returns 0
  • A compile error

Correct: StackOverflowError — the same as having no base case at all

Why: A base case only stops the recursion if the arguments actually arrive at it, so a correct base case that is never reached is worth exactly as much as no base case: each call adds a frame, none returns, and the stack runs out. This is why the two checks are separate — is there a base case and does every call move toward it — and why passing countdown(n) instead of countdown(n - 1) overflows despite the if (n == 0) being perfectly written.

62. Explain it to someone else

Explain it

Two minutes, drawing as you go.

Discussion prompt

Explain to a classmate how factorial(3) produces 6. Draw the stack diagram and mark clearly where the multiplications happen. Then explain why countdown produces all its output before any call returns, and factorial produces its answer only as the calls return.

Hint: The difference is where the work sits relative to the recursive call.

Answer:

Draw four frames. Going down, each one just makes another call — nothing is computed yet. The bottom one, n = 0, returns 1 without calling anything. Then the multiplications happen on the way back: 1 times 1, then times 2, then times 3, giving 6.

countdown prints BEFORE its recursive call, so all the output happens on the way down. factorial multiplies AFTER its recursive call, so all its work happens on the way up. Same shape of recursion, opposite timing — and it is decided by one line's position.

That last observation is worth making explicitly, because Lesson 8b opens by swapping those two lines in countdown and getting a method that counts up.

63. Exit ticket

Exit ticket

One question before you close the deck.

Predict first

What are the two things every recursive method needs in order to terminate?

  • A base case that makes no recursive call, and arguments that move toward it
  • A return type and a parameter
  • A loop and a base case
  • A stack diagram and a leap of faith

Correct: A base case that makes no recursive call, and arguments that move toward it

Why: Both are required and neither is sufficient alone: a base case that is never reached does not stop anything, and arguments that shrink forever without a case to catch them do not either. These are the same two requirements as a loop's stopping condition and update statement, and their absence produces the same kind of failure — an infinite loop in one case, a StackOverflowError in the other.

64. Draw the whole lesson

Connect it up

One page, from memory.

Draw it

Draw the stack diagram for factorial(3), with main at the top and one frame per call, and label each frame with its n, its recurse and its result — leaving the base case frame's last two blank, as they do not exist there. Draw an arrow up each boundary labelled with the value returned. Beside it, write the three checks for a recursive method and apply them to countdown. Finally, write one sentence saying why countdown prints on the way down while factorial computes on the way up.

65. Recap

Recap

Four sections that add a second way to repeat — and, more usefully, a way of thinking about problems that contain smaller versions of themselves.

if you remember one thingit is this
about writing onebase case first, then the leap of faith
about terminationa base case AND progress toward it
about choosingrecursion for self-similar structure, loops for flat sequences

Sources

  1. Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 8 (Recursive Methods), Sections 8.1-8.4, pp. 127-134
  2. Java SE 21 API — java.lang.StackOverflowError
  3. Think Java 2e — free online edition and source code

Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108