Recursion mechanics
A recursive method solves a problem by calling itself on a smaller version of the same problem. Every recursive method needs two things: a base case that stops the recursion, and a recursive case that makes progress toward it.
Learning goals
- Identify the base case and the recursive case of a method
- Trace the call stack as calls are made and as they return
- Explain why a recursion terminates and state an invariant
- Recognise tail recursion and know what the JVM does with it
Prerequisites
Looking up a word you do not know in a dictionary, whose definition uses another unknown word, means looking that word up first. You keep going until you reach a word you know (the base case), then work back out with each answer. That is recursion.
Base case
The base case is the input for which the answer is known directly, with no further recursive call. Without it the method calls itself forever.
Choose the smallest input that is still meaningful: n == 0 for factorial, an empty or one-element range for array problems, null for linked structures. A recursion can have several base cases, such as fib(0) and fib(1).
1static long factorial(int n) {2if (n == 0) return 1; // base case: 0! is 1 by definition3return n * factorial(n - 1); // recursive case4}
Recursive case
The recursive case reduces the problem and trusts the recursive call to solve the smaller version. You do not need to follow every call in your head: assume factorial(n - 1) works, and then check that n * factorial(n - 1) is correct for n.
That trust is justified by induction: if the base case is correct and each call is correct whenever its smaller calls are, the whole method is correct.
Call stack
Each call gets its own frame on the call stack, holding its parameters, local variables and the place to return to. A new call pushes a frame, and a return pops it.
The animation shows factorial(4). Watch the stack grow as each call waits for the next, and shrink as the answers come back. The recursion tree on top is the same story drawn as a picture: each box is one call, and its value appears when it returns.
Worst
O(n) time
Space
O(n) stack
Factorial: calls, base case and return path
Interactive · edit the inputRecursion tree · 1 call
Call factorial(4). It is frame number 1 on the call stack.
1static long factorial(int n) {2if (n == 0) return 1; // base case3return n * factorial(n - 1); // recursive case4}
Live state
n
4
depth
1
Call stack (top first)
- factorial(4)
Execution trace (dry run table)Show 20 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | n=4 depth=1 | Call factorial(4). It is frame number 1 on the call stack. |
| 2 | L2 | n=4 | n is not 0, so this call cannot answer yet. It needs factorial(3). |
| 3 | L3 | n=4 | Pause factorial(4) and call factorial(3). The pending multiplication by 4 waits on the stack. |
| 4 | L1 | n=3 depth=2 | Call factorial(3). It is frame number 2 on the call stack. |
| 5 | L2 | n=3 | n is not 0, so this call cannot answer yet. It needs factorial(2). |
| 6 | L3 | n=3 | Pause factorial(3) and call factorial(2). The pending multiplication by 3 waits on the stack. |
| 7 | L1 | n=2 depth=3 | Call factorial(2). It is frame number 3 on the call stack. |
| 8 | L2 | n=2 | n is not 0, so this call cannot answer yet. It needs factorial(1). |
| 9 | L3 | n=2 | Pause factorial(2) and call factorial(1). The pending multiplication by 2 waits on the stack. |
| 10 | L1 | n=1 depth=4 | Call factorial(1). It is frame number 4 on the call stack. |
| 11 | L2 | n=1 | n is not 0, so this call cannot answer yet. It needs factorial(0). |
| 12 | L3 | n=1 | Pause factorial(1) and call factorial(0). The pending multiplication by 1 waits on the stack. |
| 13 | L1 | n=0 depth=5 | Call factorial(0). It is frame number 5 on the call stack. |
| 14 | L2 | n=0 | Base case reached: no more recursion. Everything below this point is the return path. |
| 15 | L2 | n=0 return=1 | return 1. |
| 16 | L3 | n=1 return=1 | factorial(1) resumes: 1 × 1 = 1. Return it to the caller. |
| 17 | L3 | n=2 return=2 | factorial(2) resumes: 2 × 1 = 2. Return it to the caller. |
| 18 | L3 | n=3 return=6 | factorial(3) resumes: 3 × 2 = 6. Return it to the caller. |
| 19 | L3 | n=4 return=24 | factorial(4) resumes: 4 × 6 = 24. Return it to the caller. |
| 20 | L3 | return=24 | factorial(4) = 24, using 5 calls and a maximum stack depth of 5. |
Return path
Work can happen on the way down (before the recursive call), on the way back up (after it returns), or both. In n * factorial(n - 1) the multiplication happens on the return path: no multiplication is done until the base case has returned 1.
Step through the animation after the base case. Each frame resumes, multiplies by its own n, and returns to its caller. That return path is where the answer is actually assembled.
| Call | Waiting for | Returns |
|---|---|---|
| factorial(3) | factorial(2) | 3 × 2 = 6 |
| factorial(2) | factorial(1) | 2 × 1 = 2 |
| factorial(1) | factorial(0) | 1 × 1 = 1 |
| factorial(0) | nothing (base case) | 1 |
Stack frames
A stack frame stores the method's parameters, local variables, and the return address. Because each frame has its own copy of the variables, recursive calls do not overwrite each other: the n in factorial(3) and the n in factorial(2) are different variables.
Frames take memory, typically tens to a few hundred bytes each. The JVM thread stack is limited (often 512 KB to 1 MB by default, settable with -Xss), which is why very deep recursion fails. Objects live on the heap, not in the frame; the frame holds only references to them.
Termination
A recursion terminates if every call moves strictly closer to a base case. Name the quantity that shrinks: n decreases by 1, the range hi - lo halves, the list gets shorter. If you cannot name one, the recursion may loop forever.
Check two things: the base case is reachable from every valid input, and the recursive call makes progress. factorial(n) with a negative n never reaches n == 0 because it keeps subtracting. Guard against it or use n <= 0 for the base case with an input check.
Recursive invariants
An invariant is a fact that holds at the start of every call. Stating it is the best way to prove a recursion correct. For recursive binary search the invariant is: "if the target is in the array, it is in arr[lo..hi]". Each call keeps it true while shrinking the range.
For reverse(a, lo, hi): "everything outside lo..hi is already in its final place". Each call fixes two more elements and the invariant still holds for the smaller range.
Tail recursion
A call is a tail call when it is the very last thing the method does and its result is returned unchanged. gcd(b, a % b) is a tail call. n * factorial(n - 1) is not, because the multiplication still has to happen afterwards.
Some languages reuse the current frame for a tail call, so the recursion uses no extra stack. The JVM does not do this. A tail-recursive Java method still uses one frame per call, so convert it to a loop when the depth could be large (see lesson 5.07).
1// not a tail call: the multiplication waits for the result2static long fact(int n) { return n == 0 ? 1 : n * fact(n - 1); }34// tail call with an accumulator: nothing is left to do after the call5static long factTail(int n, long acc) { return n == 0 ? acc : factTail(n - 1, acc * n); }
Practice ladder
Now try it yourself
Easy
- Print the numbers from n down to 1 recursively
- Compute the sum of 1..n recursively
- Count the digits of an integer recursively
Medium
- Check whether a string is a palindrome recursively
- Compute x^n with recursion
- Draw the call stack for factorial(5) on paper
Advanced
- Prove by induction that your recursive method is correct
- Find the deepest n your JVM handles for sum(n) and explain the limit
- Write factTail and rewrite it as a loop
Key takeaways
- A recursive method needs a reachable base case and a recursive case that makes progress.
- Every call has its own stack frame; answers are assembled on the return path.
- State an invariant and a shrinking quantity to argue correctness and termination; Java does not optimise tail calls.