Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Java  Module 3 – Control flow, methods and recursion

Recursion and the call stack

Write recursive Java methods with clear base cases, see each call's stack frame, measure when StackOverflowError strikes, and turn deep recursion into loops.

  • Intermediate
  • 20 minutes
  • Examples run with Java temurin-25.0.4+101.0.LTS and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Write recursive methods with a base case that every call moves towards
  • Trace the frames of a recursion on the call stack and explain StackOverflowError
  • Estimate how many frames a stack holds, and why the count changes with the JIT
  • Apply memoisation and halving to cut the number of calls and the depth
  • Convert a tail recursion or a deep recursion into a loop

Before you start

What you’ll learn · Before you start

What you will learn

  • Write recursive methods with a base case that every call moves towards
  • Trace the frames of a recursion on the call stack and explain StackOverflowError
  • Estimate how many frames a stack holds, and why the count changes with the JIT
  • Apply memoisation and halving to cut the number of calls and the depth
  • Convert a tail recursion or a deep recursion into a loop

Before you start

On this page

factorial(4) is 4 × factorial(3), and so on down to factorial(0), which is 1: five calls, each waiting for the next. A recursion: A method that calls itself on a smaller version of its problem, until it reaches a case it answers directly, the base case. Each call waits in its own frame until the call it made returns. method calls itself on a smaller piece of its job, until the piece is small enough to answer directly, the base case: The input a recursive method answers directly, without calling itself, such as factorial(0) = 1. Every other call must move closer to it, or the calls never end and the stack overflows.. Every call gets its own frame on the thread’s stack, holding its parameters and local variables, and the frame stays there until that call returns. The stack has a fixed size, so a recursion that goes too deep, or never reaches its base case, fills it, and the JVM throws StackOverflowError.

You will step through the frames of a small recursion, measure how many frames fit on a thread’s stack, and use the three tools that make recursion practical: memoisation, halving the problem, and turning deep recursion into a loop. Java gives every call a frame, even a call in tail position, so the safe rule is: recursion when the depth stays small, a loop when it grows with the input.

A recursive call waits in its own frame for a smaller call

Every recursive method needs two things: a base case that returns without calling itself, and a recursive case that calls itself on a problem that is strictly closer to the base case. The program below prints each call on the way in and each result on the way out, indented by its depth:

Five frames on the way in and out Java · Factorial.java
// factorial(n) = n * factorial(n - 1), and factorial(0) = 1 ends the chain.
// The depth parameter only indents the trace, one step per frame on the stack.
long factorial(int n, int depth) {
    String indent = "  ".repeat(depth);
    IO.println(indent + "call factorial(" + n + ")");
    if (n == 0) {
        IO.println(indent + "base case: return 1");
        return 1;
    }
    long result = n * factorial(n - 1, depth + 1);
    IO.println(indent + "return " + n + " * " + (result / n) + " = " + result);
    return result;
}

void main() {
    IO.println("factorial(4) = " + factorial(4, 0));
}

Output

call factorial(4)
  call factorial(3)
    call factorial(2)
      call factorial(1)
        call factorial(0)
        base case: return 1
      return 1 * 1 = 1
    return 2 * 1 = 2
  return 3 * 2 = 6
return 4 * 6 = 24
factorial(4) = 24

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Factorial.java

Step through the same run frame by frame. Each call pushes a frame: The room a method call gets on its thread's stack: its parameters, its local variables and its own operand stack. The JVM creates it when the call starts and removes it when the call returns. onto the call stack: The frames of a thread's calls that have started and not yet returned, the most recent on top. Its size is fixed when the thread starts; a call that needs more room than is left throws StackOverflowError., and each return removes one:

Interactive · step by step The frames of factorial(4), pushed and popped Step through the run of Factorial.java and watch a frame appear for every call and go when the call returns.
The frames of factorial(4), pushed and popped, step 7 of 7. Showing main (prints factorial(4) = 24). In focus: main.mainprints factorial(4) = 24

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The first frame. main calls factorial(4, 0), and a frame for that call is pushed onto the stack with its own n, 4, and its own depth, 0. The call prints call factorial(4).
  2. Three more frames. factorial(4) cannot finish before factorial(3) returns, so it calls it and waits. factorial(3), factorial(2) and factorial(1) do the same, each with its own n, each printed one step further in.
  3. The base case. factorial(0) finds n == 0, prints base case: return 1 and returns 1 without calling itself. Five frames of factorial are on the stack now, the deepest point of the run.
  4. 1 * 1. factorial(0)'s frame is removed. factorial(1) gets 1, prints return 1 * 1 = 1 and returns 1, and its own frame goes too.
  5. 2 * 1. factorial(2) gets 1 and returns 2 * 1 = 2. The multiplications run on the way back out, from the newest frame to the oldest.
  6. 3 * 2. factorial(3) gets 2 and returns 3 * 2 = 6, and its frame is removed in turn.
  7. 4 * 6. factorial(4) returns 4 * 6 = 24 to main, which prints factorial(4) = 24. Only main's frame is left, as before the first call.

The trace is the call stack drawn sideways. factorial(4) cannot finish until factorial(3) returns, which waits for factorial(2), and so on: five frames are on the stack at the deepest point, each with its own n. The base case returns first, and the multiplications happen on the way back out, from the innermost frame to the outermost.

Checkpoint

Check: A frame for every call

  1. Question 1 of 2 Factorial.java calls factorial(6, 0) instead. How many frames of factorial are on the stack at the deepest point?

    Type a number.

    A hint

    Count the calls from factorial(6) down to the base case.

    Show the answer: Question 1 of 2

    Answer: 7 frames

    factorial(6), (5), (4), (3), (2), (1) and (0) are all waiting or running at the moment the base case returns: seven frames, one more than n, as factorial(4) had five.

  2. Question 2 of 2 In the output of Factorial.java, which line comes right after base case, return 1?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: return 1 * 1 = 1, printed by factorial(1)

    The calls are printed on the way in, oldest first, and the returns on the way out, newest first, because each frame waits for the one it called.

Without a base case, frames pile up until the stack overflows

Remove the base case and nothing stops the chain:

NoBase.java Java · NoBase.java
// The base case is missing, so every call makes another one until the stack is full.
long factorial(int n) {
    return n * factorial(n - 1);
}

void main() {
    IO.println(factorial(4));
}

The session runs it twice: the first time head keeps the first four lines of what it printed, and the second time grep -c counts the lines of the trace that name the method. After each $ comes the command:

A stack that overflows Python · nobase_session.py
from session import run

# head keeps the first four lines of the error; grep -c counts the lines of the whole trace that name factorial.
run("""
java NoBase.java 2>&1 | head -n 4
java NoBase.java 2>&1 | grep -c "at NoBase.factorial"
""")

Output

# openjdk version "25.0.4.1" LTS
$ java NoBase.java 2>&1 | head -n 4
Exception in thread "main" java.lang.StackOverflowError
	at NoBase.factorial(NoBase.java:3)
	at NoBase.factorial(NoBase.java:3)
	at NoBase.factorial(NoBase.java:3)
$ java NoBase.java 2>&1 | grep -c "at NoBase.factorial"
1019

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 nobase_session.py

Stack frames for factorial(4): main, then factorial(4) down to factorial(0); the base case returns first and results flow back.main thread's stackoldest frame on topno base case: frames keep cominguntil the stack is full:StackOverflowErrormainwaits for factorial(4)factorial(4), n = 4waits, then returns 4 * 6 = 24factorial(3), n = 3waits, then returns 3 * 2 = 6factorial(2), n = 2waits, then returns 2 * 1 = 2factorial(1), n = 1waits, then returns 1 * 1 = 1factorial(0)base case: returns 1 at oncecallscallscallscallscallswithout the base case

Each call waits in its own frame until the call it made returns

Text description of the diagram

The figure shows the frames on the main thread's stack while factorial(4) runs, from the oldest frame at the top to the newest at the bottom.

  • main called factorial(4) and waits for its result.
  • factorial(4), with n = 4, called factorial(3) and waits; when that returns 6, it returns 4 * 6 = 24.
  • factorial(3), with n = 3, called factorial(2) and waits; when that returns 2, it returns 3 * 2 = 6.
  • factorial(2), with n = 2, called factorial(1) and waits; when that returns 1, it returns 2 * 1 = 2.
  • factorial(1), with n = 1, called factorial(0) and waits; when that returns 1, it returns 1 * 1 = 1.
  • factorial(0) is the base case and returns 1 at once, so it is the first frame to finish.

Each frame holds its own n. A dashed arrow leads from the last frame to a note: without the base case, calls would keep adding frames until the stack is full, and the JVM throws StackOverflowError.

Each call makes another one and none ever returns, so the frames keep coming, as the dashed path of the diagram shows, until the JVM throws StackOverflowError. Its stack trace: What an uncaught exception prints: its class and message, then the calls that were open when it was thrown, the most recent first, each with its file and line, and any Caused by sections. More in “IDEs, compiler errors and your first debug” shows the same frame, NoBase.factorial at line 3, over and over: 1,019 times, as the second command counted. The stack held many more frames than that, as the next section measures, but the Throwable documentation allows a JVM to leave frames out of a trace, and this one keeps only the newest. When you see one line repeated like that, look for a base case that is missing or never reached.

A base case the calls can step over

if (n == 0) return; with a recursive call on n - 2 works for even numbers only: countdown(5) goes 5, 3, 1, -1 and never meets 0, and a negative argument never meets it either. Test n <= 0, so that every way past the base case still ends there.

The result has its own limit. A long holds at most 9,223,372,036,854,775,807, so factorial(20), about 2.4 × 10¹⁸, is the largest factorial that fits; factorial(21) overflows silently, an integer overflow: What happens when the result of +, -, * or ++ does not fit its integer type: Java keeps the lowest 32 or 64 bits of the true result, so the value wraps round without an error, and Integer.MAX_VALUE + 1 is Integer.MIN_VALUE. More in “Integer arithmetic, division and overflow”. For bigger values, java.math.BigInteger computes exactly.

Big Number Calculator Work out 25! or 100! exactly and see how far past the largest long they are.

Checkpoint

Check: The base case

  1. Question 1 of 2 What ends the run of NoBase.java, the factorial without a base case?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A StackOverflowError, once the thread's stack is full of frames

    The session printed java.lang.StackOverflowError and then the same line, NoBase.factorial at line 3, over and over: the sign of a base case that is missing or never reached.

  2. Question 2 of 2 What happens when countdown(5) is called?

    Read the code, then choose one answer.

    void countdown(int n) {
        if (n == 0) {
            return;
        }
        countdown(n - 2);
    }
    Show the answer: Question 2 of 2

    Answer: It throws StackOverflowError, because n goes 5, 3, 1, -1 and never equals 0

    A base case must be one that every call is sure to reach. Testing n <= 0 instead of n == 0 catches every way past 0.

The stack’s size limits the depth, and the JIT changes it

The Java Virtual Machine Specification gives each thread its own stack, created with the thread, and requires a StackOverflowError when a computation needs a larger stack than is permitted. How many frames fit depends on the stack’s size and on the size of each frame. This program counts:

Counting frames on the main thread and on a thread with a 64 MiB stack

Java (StackDepth.java) · StackDepth.java

// How many nested calls fit on the main thread's stack before StackOverflowError?
int depth = 0;

void dive() {
    depth++;
    dive();
}

void main() {
    try {
        dive();
    } catch (StackOverflowError e) {
        // Caught here only to count the frames. Real code should never try to carry on after this error.
    }
    IO.println("main thread: " + "%,d".formatted(depth) + " frames");
}

Java (BigStack.java) · BigStack.java

// The same dive on a thread created with a 64 MiB stack instead of the default.
int depth = 0;

void dive() {
    depth++;
    dive();
}

void main() throws InterruptedException {
    Thread big = Thread.ofPlatform().stackSize(64L << 20).start(() -> {
        try {
            dive();
        } catch (StackOverflowError e) {
            // counting only
        }
    });
    big.join();
    IO.println("64 MiB thread: " + "%,d".formatted(depth) + " frames");
}

StackDepth.java catches the error only to count the frames, and BigStack.java starts a thread only to give it a bigger stack; exceptions and threads have modules of their own later in this track. The session runs them three ways:

Frames that fit, with the interpreter only Python · depth_session.py
from session import run

# -Xint runs every method in the interpreter, whose frames always have the same size, so the counts repeat exactly.
run("""
java -Xint StackDepth.java
java -Xint -Xss8m StackDepth.java
java -Xint BigStack.java
""")

Output

# openjdk version "25.0.4.1" LTS
$ java -Xint StackDepth.java
main thread: 17,640 frames
$ java -Xint -Xss8m StackDepth.java
main thread: 73,814 frames
$ java -Xint BigStack.java
64 MiB thread: 598,108 frames

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 depth_session.py

These runs use -Xint, which makes the JVM interpret every method instead of compiling the busy ones, so each frame of dive() has the same size and the counts repeat exactly. The main thread’s stack is 2 MiB by default on macOS on Apple silicon, as the java command’s documentation lists, -Xss8m sets an 8 MiB stack, and a thread built with Thread.ofPlatform().stackSize(64L << 20) gets 64 MiB. Dividing each size by its count gives the room a frame takes:

Stack Frames that fit Bytes per frame
2 MiB, the default 17,640 about 119
8 MiB, with -Xss8m 73,814 about 114
64 MiB, a platform thread 598,108 about 112

The step from 2 MiB to 8 MiB added 56,174 frames for 6 MiB, 112 bytes each. The rest of each stack, about 120,000 bytes in all three runs (2 MiB less 17,640 frames of 112 bytes), holds something other than the frames of dive(), which is why the share per frame is highest in the smallest stack. A method with more parameters and local variables has bigger frames, and fits fewer. With the JIT compilation: Just-in-time compilation: while a program runs, the JVM compiles the methods that run often from bytecode to native machine code, so a Java program gets faster after a short warm-up. The option -Xint switches it off. More in “What Java is, how it runs and where it is used” on, which is how Java normally runs, the same program gives a different count each time:

The same count with the JIT compiler on, three times Python · jit_session.py
from session import run

# With the JIT compiler on (the default), the count changes from one run to the next.
run("""
java StackDepth.java
java StackDepth.java
java StackDepth.java
""")

Output

# openjdk version "25.0.4.1" LTS
$ java StackDepth.java
main thread: 36,140 frames
$ java StackDepth.java
main thread: 40,352 frames
$ java StackDepth.java
main thread: 36,072 frames

This output changes from run to run: the JIT compiler turns dive() into machine code partway through the dive, at a moment that varies from run to run, and compiled frames are smaller than interpreted ones

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 jit_session.py

The JIT compiles dive() while the dive is under way, and from then on each frame is smaller, so the count depends on when compilation finished. The documentation of stackSize warns that its effect is “highly platform dependent”, and the JVM may treat it as a suggestion. So never write code that needs a particular depth to work: if the input decides how deep a recursion goes, a big enough input will overflow some stack somewhere.

Key idea

The depth a recursion may reach depends on the stack’s size, each frame’s size and the JIT, so code that works only up to some depth fails on some input, on some machine.

Checkpoint

Check: How deep the stack goes

  1. Question 1 of 2 At about 112 bytes per frame, about how many frames of dive() would a 16 MiB stack hold with -Xint?

    Type a number.

    A hint

    16 MiB is 16 × 1,048,576 bytes.

    Show the answer: Question 1 of 2

    Answer: 150000 frames (anything from 144000 to 156000 counts)

    16 MiB is 16,777,216 bytes, and 16,777,216 ÷ 112 is about 149,800, so about 150,000 frames, a little under, because part of every stack goes to frames the program does not count.

  2. Question 2 of 2 With the JIT compiler on, three runs of StackDepth.java counted 36,140, 40,352 and 36,072 frames. Why do they differ?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The JIT compiles dive() partway through the dive, compiled frames are smaller, and the moment varies from run to run

    With -Xint every frame of dive() has the same size, so the count repeats exactly. With the JIT on, the count depends on when compilation finished, which is why no code should rely on a particular depth.

Tail calls still use the stack

A call is in tail position when it is the last thing a method does, as in return sumTo(n - 1, total + n);. Some languages reuse the caller’s frame for such a tail call: A call that is the last thing a method does, its result returned as it is, as in return sumTo(n - 1, total + n);. HotSpot still gives it a frame of its own, so tail recursion can overflow the stack.. HotSpot, the JVM in this JDK, does not:

A tail-recursive sum overflows; the loop does not Java · TailSum.java
// The recursive call is the last thing sumTo does (a tail call), yet each call still needs a frame of its own.
long sumTo(long n, long total) {
    if (n == 0) {
        return total;
    }
    return sumTo(n - 1, total + n);
}

// The same computation as a loop: one frame, whatever n is.
long sumToLoop(long n) {
    long total = 0;
    while (n > 0) {
        total += n;
        n--;
    }
    return total;
}

void main() {
    IO.println("sumTo(3, 0) = " + sumTo(3, 0));
    IO.println("sumToLoop(3) = " + sumToLoop(3));
    IO.println("sumTo(1000, 0) = " + sumTo(1000, 0));
    IO.println("sumToLoop(1000000) = " + sumToLoop(1_000_000));
    try {
        IO.println("sumTo(1000000, 0) = " + sumTo(1_000_000, 0));
    } catch (StackOverflowError e) {
        IO.println("sumTo(1000000, 0): StackOverflowError");
    }
}

Output

sumTo(3, 0) = 6
sumToLoop(3) = 6
sumTo(1000, 0) = 500500
sumToLoop(1000000) = 500000500000
sumTo(1000000, 0): StackOverflowError

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java TailSum.java

Step through sumTo(3, 0) beside the loop that computes the same total:

Interactive · step by step sumTo(3, 0) beside the loop that computes the same total Step through the recursive sumTo on the left, one frame per call, and sumToLoop on the right, in one frame.
sumTo(3, 0) beside the loop that computes the same total, step 5 of 5. Showing sumTo(3, 0) (returns 6), sumTo(2, 3) (passes 6 back), sumTo(1, 5) (passes 6 back), sumTo(0, 6) (n = 1,000,000: overflow), sumToLoop(3) (prints 500000500000). In focus: sumTo(0, 6), sumToLoop(3).tail calltail calltail callsumTo(3, 0)returns 6sumTo(2, 3)passes 6 backsumTo(1, 5)passes 6 backsumTo(0, 6)n = 1,000,000: overflowsumToLoop(3)prints 500000500000

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. A tail call. sumTo(3, 0) returns sumTo(2, 3) as the very last thing it does, with no work left after it. The JVM still gives the new call a frame of its own, below the first one.
  2. Four frames. sumTo(1, 5) and sumTo(0, 6) follow the same way, so four frames of sumTo are on the stack for n = 3, each holding an n and a total.
  3. Passing 6 back. sumTo(0, 6) returns 6, and each frame hands the same 6 to the one above it unchanged, so the run printed sumTo(3, 0) = 6.
  4. The loop. sumToLoop(3) keeps n and total as two variables of one frame and changes them: 3 and 0, then 2 and 3, 1 and 5, 0 and 6, the same values the four recursive calls received. It also returns 6.
  5. A million. For n = 1,000,000 the recursion needs a million frames, and the run ended in StackOverflowError. The loop stayed in its one frame and printed 500000500000.

Three nested calls, or a thousand, are fine, but a million need a million frames, and the stack overflows although no frame has any work left to do after its call. The loop computes the same total in one frame. A tail-recursive method turns into a loop almost mechanically: the parameters become variables, and the recursive call becomes an update of those variables followed by another pass.

Checkpoint

Check: Tail calls

  1. Question 1 of 2 How many frames of sumTo are on the stack at the deepest point of sumTo(3, 0)?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 4 frames

    sumTo(3, 0), sumTo(2, 3), sumTo(1, 5) and sumTo(0, 6) each get a frame, although each call is the last thing its caller does: four frames for n = 3, a million for n = 1,000,000.

  2. Question 2 of 2 long count(long n, long acc) { return n == 0 ? acc : count(n - 1, acc + 2); } must become a loop. Which body is right for every n of 0 or more?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: while (n > 0) { acc += 2; n--; } return acc;

    A tail call turns into a loop mechanically: test the base case in the loop condition, and replace the call with the same updates of the parameters, as sumToLoop does for sumTo.

Memoisation computes each smaller problem once

Some recursions solve the same small problem many times. The Fibonacci numbers are the classic case: fib(n) calls fib(n - 1) and fib(n - 2), and fib(n - 1) calls fib(n - 2) again.

Calls made by plain and memoised recursion Java · Fibonacci.java
long calls = 0;

// Plain recursion: fib(n - 1) and fib(n - 2) solve the same smaller problems again and again.
long fib(int n) {
    calls++;
    if (n < 2) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}

// Memoised: each answer is worked out once and kept in memo[n]; 0 means "not known yet".
long[] memo = new long[93];

long fibMemo(int n) {
    calls++;
    if (n < 2) {
        return n;
    }
    if (memo[n] != 0) {
        return memo[n];
    }
    memo[n] = fibMemo(n - 1) + fibMemo(n - 2);
    return memo[n];
}

void main() {
    for (int n : new int[] {5, 10, 20, 30, 40}) {
        calls = 0;
        long value = fib(n);
        IO.println("fib(" + n + ") = " + value + " after " + "%,d".formatted(calls) + " calls");
    }
    for (int n : new int[] {5, 40, 90}) {
        calls = 0;
        memo = new long[93];
        long value = fibMemo(n);
        IO.println("fibMemo(" + n + ") = " + value + " after " + calls + " calls");
    }
}

Output

fib(5) = 5 after 15 calls
fib(10) = 55 after 177 calls
fib(20) = 6765 after 21,891 calls
fib(30) = 832040 after 2,692,537 calls
fib(40) = 102334155 after 331,160,281 calls
fibMemo(5) = 5 after 9 calls
fibMemo(40) = 102334155 after 79 calls
fibMemo(90) = 2880067194370816120 after 179 calls

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Fibonacci.java

The plain version made 15 calls for fib(5) and 331,160,281 for fib(40), and the count grows by a factor of about 1.6 with each step of n, the same rate as the Fibonacci numbers themselves. Its depth, though, is only 40: the problem is the number of calls, not the stack. memoisation: Keeping each answer of a function once it is worked out, in an array or a map, so that a later call with the same argument returns it at once instead of working it out again. keeps each answer once it is known, here in an array, so every value from 2 to n is worked out only once. Step through fibMemo(5) and watch the array fill:

Interactive · step by step fibMemo(5) filling its memo array Step through fibMemo(5), its calls on the left and the array memo on the right, and count the calls.
fibMemo(5) filling its memo array, step 6 of 6. Showing fibMemo(5) (returns 5 after 9 calls), fibMemo(4), fibMemo(3), fibMemo(2) (call 4), fibMemo(1), fibMemo(0) (calls 5 and 6), memo[5] (5), memo[4] (3), memo[3] (2), memo[2] (1). In focus: fibMemo(5).storesstoresstoresstoresfibMemo(5)returns 5 after 9 callsfibMemo(4)fibMemo(3)fibMemo(2)call 4fibMemo(1), fibMemo(0)calls 5 and 6memo[5]5memo[4]3memo[3]2memo[2]1

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Down one level at a time. fibMemo(5) finds memo[5] empty and calls fibMemo(4), which calls fibMemo(3), which calls fibMemo(2). Each first call goes one level down: 4 calls so far.
  2. The base cases. fibMemo(2) calls fibMemo(1) and fibMemo(0), which return 1 and 0 at once. fibMemo(2) keeps 1 + 0 = 1 in memo[2]: 6 calls so far.
  3. memo[3]. Back in fibMemo(3), the second call, fibMemo(1), returns 1, so memo[3] = 1 + 1 = 2: 7 calls.
  4. A known answer. fibMemo(4) needs fibMemo(2) again. memo[2] is already 1, so this call returns at once without calling anything, and memo[4] = 2 + 1 = 3: 8 calls.
  5. memo[5]. fibMemo(5) needs fibMemo(3), and memo[3] is known, so memo[5] = 3 + 2 = 5, after 9 calls in all, where the plain fib(5) made 15.
  6. 2n - 1 calls. The first calls go down from fibMemo(n) to fibMemo(0), n + 1 calls, and each level from 3 up makes one more call that returns at once, n - 2 calls: 2n - 1 in all, 79 for fib(40) and 179 for fib(90), as the run printed.

fibMemo(5) made 9 calls where fib(5) made 15, and for any n of 1 or more the memoised version makes 2n − 1: 79 calls for fib(40) and 179 for fib(90). It cuts the calls, not the depth: the first calls still go down one level at a time, so fibMemo(40) is 40 frames deep at its deepest. A HashMap does the same job when the arguments are not small numbers. The array has 93 places because fib(92) is the largest Fibonacci number that fits in a long.

Checkpoint

Check: Memoisation

  1. Question 1 of 2 How many calls does fibMemo(10) make, with an empty memo array and the first call included?

    Type a number.

    A hint

    The run printed 9 calls for fibMemo(5) and 79 for fibMemo(40).

    Show the answer: Question 1 of 2

    Answer: 19 calls

    fibMemo(n) makes 2n − 1 calls for n of 1 or more: 11 first calls down to fibMemo(0), and one more call at each level from 3 to 10, which returns at once. 2 × 10 − 1 = 19.

  2. Question 2 of 2 How deep does fibMemo(40) go on the stack?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: About 40 frames, because the first call of each level goes one step down

    Memoisation cuts the number of calls, not the depth: the chain of first calls still goes n levels down, so a memoised recursion whose n grows with the input can still overflow the stack.

Halving the problem keeps the depth tiny

A recursion is safe when each call makes the problem much smaller, not just one step smaller. Raising a number to a power shows the difference:

40 calls against 7 for the same power Java · FastPower.java
int calls = 0;

// base^exp the slow way: one call, and one multiplication, for each unit of exp.
long powerSlow(long base, int exp) {
    calls++;
    if (exp == 0) {
        return 1;
    }
    return base * powerSlow(base, exp - 1);
}

// Halving: base^exp = (base^(exp / 2))^2, times one more base when exp is odd.
long power(long base, int exp) {
    calls++;
    if (exp == 0) {
        return 1;
    }
    long half = power(base, exp / 2);
    long square = half * half;
    return exp % 2 == 0 ? square : square * base;
}

void main() {
    calls = 0;
    IO.println("powerSlow(3, 39)     = " + powerSlow(3, 39) + " after " + calls + " calls");
    calls = 0;
    IO.println("power(3, 39)         = " + power(3, 39) + " after " + calls + " calls");
    calls = 0;
    IO.println("power(1, 1000000000) = " + power(1, 1_000_000_000) + " after " + calls + " calls");
    calls = 0;
    IO.println("power(2, 62)         = " + power(2, 62) + " after " + calls + " calls");
    calls = 0;
    IO.println("power(2, 64)         = " + power(2, 64) + " after " + calls + " calls");
}

Output

powerSlow(3, 39)     = 4052555153018976267 after 40 calls
power(3, 39)         = 4052555153018976267 after 7 calls
power(1, 1000000000) = 1 after 31 calls
power(2, 62)         = 4611686018427387904 after 7 calls
power(2, 64)         = 0 after 8 calls

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java FastPower.java

Step through power(3, 39): down through the halved exponents, then back up through the squares:

Interactive · stage by stage power(3, 39) in seven calls Step down through the halved exponents of power(3, 39), then back up through the squares.
power(3, 39) in seven calls, step 6 of 6. Showing power(3, 39) (returns 3 to the 39th), power(3, 19) (returns 1,162,261,467), power(3, 9) (returns 19,683), power(3, 4) (returns 81), power(3, 2) (returns 9), power(3, 1) (returns 3), power(3, 0) (returns 1). In focus: power(3, 19), power(3, 39).power(3, 39)returns 3 to the 39thpower(3, 19)returns 1,162,261,467power(3, 9)returns 19,683power(3, 4)returns 81power(3, 2)returns 9power(3, 1)returns 3power(3, 0)returns 1

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Halving on the way in. power(3, 39) calls power(3, 19): 39 / 2 is 19, because integer division drops the remainder. The odd 39 will need one more factor of 3 on the way out.
  2. Down to 0. 19 halves to 9, then 4, 2, 1 and 0: seven calls in all, one for each binary digit of 39 and one for the base case, where powerSlow needs 40.
  3. The base case. power(3, 0) returns 1 without calling itself, and the squaring starts on the way back up.
  4. 3, 9 and 81. exp 1 is odd: 1 squared, times 3, is 3. exp 2 is even: 3 squared is 9. exp 4: 9 squared is 81.
  5. 19,683. exp 9 is odd: 81 squared is 6,561, and times 3 that is 19,683, which is 3 to the 9th.
  6. 3 to the 39th. exp 19: 19,683 squared, times 3, is 1,162,261,467. exp 39: that squared, times 3, is 4,052,555,153,018,976,267, the number both methods printed.

powerSlow makes one call per unit of the exponent, 40 calls for 3³⁹, and an exponent of a billion would overflow the stack. power uses base^exp = (base^(exp / 2))², times one more base when the exponent is odd, so each call halves the exponent: 7 calls for 3³⁹, and only 31 for an exponent of a billion, as the third line shows. The depth grows with the number of binary digits of the exponent, which keeps it tiny. The last line is a reminder that recursion does not protect you from overflow: 2⁶⁴ does not fit in a long, and the result wraps to 0. Computing powers modulo a number, as the exercise below does, keeps every intermediate value small.

Exponent Calculator Check a power such as 3 to the 39th exactly, then compare it with what power(3, 39) printed.

Checkpoint

Check: Halving the problem

  1. Question 1 of 2 How many calls does the halving power(5, 100) of FastPower.java make, the call with exp = 0 included?

    Type a number.

    A hint

    Halve 100 until you reach 0, dropping remainders.

    Show the answer: Question 1 of 2

    Answer: 8 calls

    The exponents are 100, 50, 25, 12, 6, 3, 1 and 0: eight calls, one for each of the 7 binary digits of 100 and one for the base case. powerSlow would need 101.

  2. Question 2 of 2 Why does power multiply by base once more when exp is odd?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Because exp / 2 drops the remainder, and squaring 3 to the 19th gives only 3 to the 38th

    For an even exponent the square of the half is exactly right; for an odd one it is one factor short, which the last multiplication supplies.

When the depth grows with the input, use a loop

When the depth of a recursion grows with the size of the input (one call per character, per list element or per unit of a count), rewrite it as a loop before a large input reaches it:

  • Tail recursion: replace the call with a loop that updates the parameters, as sumToLoop does.
  • A running count or total: keep the state in local variables, and visit the input once with for or while.
  • Branching recursion over trees or grids: keep your own stack of work still to do (an ArrayDeque, from the collections module) and loop until it is empty, so the size of the input lives on the heap instead of the thread’s stack.

Raising the stack size with -Xss or a thread with a larger stack only moves the limit, and catching StackOverflowError is not a fix: the error can strike in the middle of any method on the stack and leave its work half done. Answer the questions below for a recursion of your own:

Interactive · answer and compare Recursion or a loop for your problem? Answer three questions about a recursion of your own. The table marks the shapes that fit and says why the others do not.
How deep can the calls go for the largest input you expect?
Does the recursion work out the same smaller problem more than once?
Does one call make several recursive calls, as a walk over a tree or a grid does?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Five ways to write a repeated computation, against what this lesson's programs printed on JDK 25
Shape Depth on the stack Repeated work Reads like the problem
plain recursion Suits: a small, bounded depth Poor one frame per step: sumTo(1000000, 0) overflowed Poor fib(40) made 331,160,281 calls Good factorial(n) = n * factorial(n - 1), as the definition reads
recursion that halves Suits: powers, balanced splits Good one frame per binary digit: 31 calls for an exponent of a billion Good one call per halving, nothing worked out twice Good base^exp is (base^(exp / 2))², times base when exp is odd
memoised recursion Suits: the same subproblems again Mixed fibMemo(40) still goes 40 frames deep Good fib(40) in 79 calls instead of 331,160,281 Good the same recursion, plus one array or map of answers
a loop with variables Suits: one step at a time, no branching Good one frame: sumToLoop(1000000) printed 500000500000 Mixed each step runs once, but only for a problem that does not branch Mixed the parameters become variables and the call an update
a loop with your own stack Suits: deep trees and grids Good the work still to do sits on the heap, in an ArrayDeque Mixed as much work as the recursion it replaces Poor more code: push the work, then pop it until the stack is empty
How the answers decide
  • “How deep can the calls go for the largest input you expect?” One level per item, character or unit of a count: rules out plain recursion and memoised recursion and adds a caveat to recursion that halves. When the depth grows with the input, a big enough input overflows the stack, as sumTo(1000000, 0) did. Memoisation saves calls, not frames, and halving helps only where the problem splits in halves.
  • “How deep can the calls go for the largest input you expect?” A few dozen levels, such as the binary digits of a number: adds a caveat to a loop with your own stack. A depth bounded by something small never comes near the 17,640 frames the default stack held, so the recursion can stay as it is, and a stack of your own is only more code.
  • “Does the recursion work out the same smaller problem more than once?” Yes, as fib does: rules out plain recursion. When the same smaller problems come back, the calls multiply: fib(40) made 331,160,281 of them. Keep each answer once it is known, as fibMemo does.
  • “Does one call make several recursive calls, as a walk over a tree or a grid does?” Yes, it branches and “How deep can the calls go for the largest input you expect?” One level per item, character or unit of a count: rules out a loop with variables. A branching recursion leaves work to come back to, which plain variables cannot hold. Keep it on a stack of your own, such as an ArrayDeque, and loop until that stack is empty.
  • “Does one call make several recursive calls, as a walk over a tree or a grid does?” No, one call at most and “How deep can the calls go for the largest input you expect?” One level per item, character or unit of a count: adds a caveat to a loop with your own stack. With one call at most per step there is nothing to come back to, so the variables of a loop are enough, as in sumToLoop.

In short: recursion stays the clearer choice when the depth is bounded by something small, such as the height of a balanced tree or the number of binary digits of an exponent; memoise when the same smaller problems come back; and write a loop, with a stack of your own where the problem branches, when the depth grows with the input.

Checkpoint

Check: Recursion or a loop

  1. Question 1 of 2 A method follows a chain of 1,000,000 parent records with one recursive call per record. What should change?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Rewrite it as a loop that follows the chain in one frame

    One call per item means one frame per item. When the input decides the depth, write the walk as a loop, as sumToLoop does for sumTo.

  2. Question 2 of 2 A recursion over a deep tree becomes a loop with an ArrayDeque of the nodes still to visit. Where does the work still to do live now?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: On the heap, in the ArrayDeque, which can grow far beyond the size of the thread's stack

    A branching recursion keeps the work to come back to in its frames; the rewrite keeps it in a deque instead, where the heap's size, not the stack's, sets the limit.

Practise: modular power and bracket depth

RecursionCheck.java has two methods that work on small inputs and overflow the stack on large ones, so three of its eleven checks fail and the program ends with exit status 1:

The checks against the first versions Java · RecursionCheck.java
// Try it yourself: rewrite both methods so that every check passes, then run java RecursionCheck.java.
//
// power(base, exp, mod) is base to the power exp, modulo mod, for base >= 0, exp >= 0 and 1 <= mod <= 2,000,000,000
// (so that the product of two remainders still fits in a long). maxDepth(parens) is the deepest nesting of a balanced
// string of '(' and ')': 0 for "", 1 for "()()", 3 for "((()))".
long power(long base, int exp, long mod) {
    if (exp == 0) {
        return 1 % mod;
    }
    return base % mod * power(base, exp - 1, mod) % mod; // one call for each unit of exp
}

int maxDepth(String parens) {
    return deepest(parens, 0, 0);
}

// The deepest level from position i on, when the level before position i is depth: one call per character.
int deepest(String s, int i, int depth) {
    if (i == s.length()) {
        return 0;
    }
    int here = s.charAt(i) == '(' ? depth + 1 : depth - 1;
    return Math.max(here, deepest(s, i + 1, here));
}

int passed = 0;
int total = 0;

void report(String call, String got, String expected) {
    total++;
    if (got.equals(expected)) {
        passed++;
        IO.println("PASS  " + call + " is " + got);
    } else {
        IO.println("FAIL  " + call + " is " + got + ", expected " + expected);
    }
}

void checkPower(long base, int exp, long mod, long expected) {
    String call = "power(" + base + ", " + exp + ", " + mod + ")";
    try {
        report(call, String.valueOf(power(base, exp, mod)), String.valueOf(expected));
    } catch (StackOverflowError e) {
        report(call, "a StackOverflowError", String.valueOf(expected));
    }
}

void checkDepth(String name, String parens, int expected) {
    String call = "maxDepth(" + name + ")";
    try {
        report(call, String.valueOf(maxDepth(parens)), String.valueOf(expected));
    } catch (StackOverflowError e) {
        report(call, "a StackOverflowError", String.valueOf(expected));
    }
}

void main() {
    checkPower(2, 10, 1000, 24);
    checkPower(3, 0, 7, 1);
    checkPower(5, 3, 1, 0);
    checkPower(10, 18, 1_000_000_007, 49);
    checkPower(7, 1_000_000_000, 1_000_000_007, 312_556_845);
    checkPower(123_456_789, 987_654_321, 1_000_000_007, 652_541_198);
    checkDepth("\"\"", "", 0);
    checkDepth("\"()()\"", "()()", 1);
    checkDepth("\"(())()\"", "(())()", 2);
    checkDepth("\"((()))\"", "((()))", 3);
    checkDepth("a million nested pairs", "(".repeat(1_000_000) + ")".repeat(1_000_000), 1_000_000);
    IO.println(passed + " of " + total + " checks pass");
    if (passed < total) {
        System.exit(1);
    }
}

Output (exit status 1)

PASS  power(2, 10, 1000) is 24
PASS  power(3, 0, 7) is 1
PASS  power(5, 3, 1) is 0
PASS  power(10, 18, 1000000007) is 49
FAIL  power(7, 1000000000, 1000000007) is a StackOverflowError, expected 312556845
FAIL  power(123456789, 987654321, 1000000007) is a StackOverflowError, expected 652541198
PASS  maxDepth("") is 0
PASS  maxDepth("()()") is 1
PASS  maxDepth("(())()") is 2
PASS  maxDepth("((()))") is 3
FAIL  maxDepth(a million nested pairs) is a StackOverflowError, expected 1000000
8 of 11 checks pass

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java RecursionCheck.java

  1. Copy RecursionCheck.java and rewrite power and maxDepth.
  2. Run it with java RecursionCheck.java.
  3. You are done when the last line reads “11 of 11 checks pass”.

A first hint: power can stay recursive if each call halves the exponent, as FastPower.java does; take the remainder after every multiplication. maxDepth needs one counter that goes up at ( and down at ), and a loop that remembers the highest value it reached.

Then work through the practice below. It moves from frames to loops, begins with the recursion idea you answer worst, and explains every option as soon as you choose:

Practice that adapts to you

Practise this lesson

14 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 14 What does each call of a recursive method get on the thread's stack?

    Choose one answer.

    Show the answer: Question 1 of 14

    Answer: A frame of its own, with its own parameters and local variables

    A frame is created when a call starts and removed when it returns. That is why factorial(4) can wait for factorial(3) with its own n still 4.

  2. Question 2 of 14 What does countdown(3) print?

    Read the code, then choose one answer.

    void countdown(int n) {
        if (n == 0) {
            return;
        }
        IO.print(n + " ");
        countdown(n - 1);
        IO.print(n + " ");
    }
    Show the answer: Question 2 of 14

    Answer: 3 2 1 1 2 3

    Each call prints its n on the way in, then waits for the smaller call, then prints n again on the way out. The deepest call, n = 1, finishes its second print first, so the second half comes out in reverse: 1 2 3. The call with n = 0 is the base case and prints nothing.

  3. Question 3 of 14 A recursive method has no base case. What happens when it is called?

    Choose one answer.

    Show the answer: Question 3 of 14

    Answer: Each call adds a frame until the thread's stack is full, and a StackOverflowError is thrown

    The compiler accepts the method, and every call pushes a new frame on the thread's stack. After some tens of thousands of frames on the default stack, the JVM throws StackOverflowError, as NoBase.java did in this lesson.

  4. Question 4 of 14 What happens when this is called as factorial(-1)?

    Read the code, then choose one answer.

    long factorial(int n) {
        if (n == 0) {
            return 1;
        }
        return n * factorial(n - 1);
    }
    Show the answer: Question 4 of 14

    Answer: It throws StackOverflowError, because n goes -1, -2, -3 … and never reaches 0

    A base case must be reachable from every input. Refuse a negative n at the start, or test n <= 0, so that no call can step past the base case.

  5. Question 5 of 14 What decides how many frames fit on a thread's stack?

    Choose one answer.

    Show the answer: Question 5 of 14

    Answer: The size of the stack and the size of each frame

    The default stack, -Xss and a thread's stackSize set the room; each frame's parameters and local variables set how much a call takes. The JIT changes the second, which is why the counts vary when it is on.

  6. Question 6 of 14 Which of these recursions is safe for every argument its type allows?

    Choose one answer.

    Show the answer: Question 6 of 14

    Answer: The halving power(base, exp) of FastPower.java, for any int exp of 0 or more

    A recursion is safe when its depth has a small bound whatever the input, such as the number of binary digits of an exponent; one call per item, character or unit never is.

  7. Question 7 of 14 long sum(long n, long acc) { return n == 0 ? acc : sum(n - 1, acc + n); } is called with n = 1,000,000. What happens on JDK 25?

    Choose one answer.

    Show the answer: Question 7 of 14

    Answer: It throws StackOverflowError, because each call still gets its own frame

    The recursive call is the last thing the method does, but the JVM still creates a new frame for it, so a million nested calls overflow the default stack. TailSum.java showed exactly this. Write such code as a loop.

  8. Question 8 of 14 When is a call in tail position?

    Choose one answer.

    Show the answer: Question 8 of 14

    Answer: When it is the last thing the method does, and its result is returned as it is

    A tail call leaves its caller nothing to do afterwards. Some languages reuse the caller's frame for it; HotSpot does not, so tail recursion in Java needs a frame per call like any other recursion.

  9. Question 9 of 14 What does memoisation keep?

    Choose one answer.

    Show the answer: Question 9 of 14

    Answer: Each answer once it is worked out, so a later call with the same argument returns it at once

    An array or a map from argument to answer turns a recursion that repeats its subproblems into one that computes each of them once.

  10. Question 10 of 14 How many calls does the plain recursive fib(5) of this lesson make in all, the first call included?

    Type a number.

    Show the answer: Question 10 of 14

    Answer: 15

    fib(5) calls fib(4) and fib(3); fib(4) calls fib(3) and fib(2); and so on down to fib(1) and fib(0). The count obeys calls(n) = 1 + calls(n − 1) + calls(n − 2) with calls(0) = calls(1) = 1, which gives 1, 1, 3, 5, 9 and 15 for fib(5), the count the run printed. The same rule gives 331,160,281 calls for fib(40).

  11. Question 11 of 14 Why does power(3, 39) need only 7 calls where powerSlow(3, 39) needs 40?

    Choose one answer.

    Show the answer: Question 11 of 14

    Answer: Each call of power halves the exponent, while powerSlow takes 1 away

    A problem that halves at every call is done after about log₂ n calls: 7 for 39, 31 for a billion.

  12. Question 12 of 14 How many calls does the halving power(base, exp) of this lesson make for exp = 1,000,000, the call with exp = 0 included?

    Type a number.

    A hint

    2 to the 19th is 524,288 and 2 to the 20th is 1,048,576.

    Show the answer: Question 12 of 14

    Answer: 21 calls

    Each call halves exp (dropping the remainder) until it reaches 0. 1,000,000 lies between 2 to the 19th and 2 to the 20th, so it has 20 binary digits: 20 calls that halve and one more for exp = 0, 21 in all. The run printed 31 for a billion, which has 30 binary digits. The one-step-at-a-time version would need a million and one calls and overflow the default stack long before that.

  13. Question 13 of 14 How do you turn a tail-recursive method into a loop?

    Choose one answer.

    Show the answer: Question 13 of 14

    Answer: The parameters become variables, and the recursive call becomes an update of them followed by another pass

    The base case becomes the loop condition and the call becomes the update, so the whole computation runs in one frame, as sumToLoop does.

  14. Question 14 of 14 A program overflows its stack on large inputs, and someone suggests running it with -Xss64m. What is the problem with that fix?

    Choose one answer.

    Show the answer: Question 14 of 14

    Answer: It only moves the limit, so a large enough input overflows a 64 MiB stack too

    A bigger stack buys time, not safety. When the input decides the depth, rewrite the recursion as a loop, with a stack of your own where it branches.

Interview questions

Fresher · What is a base case in recursion, and what happens if a recursive method has none?

The base case is the input a recursive method answers directly, without calling itself: factorial(0) returns 1, and a search of an empty range returns “not found”. Every other call must move closer to a base case, by making the problem smaller. Without a base case, or when the calls never reach it, each call starts another, every call adds a frame to the thread’s stack, and none of them returns. The compiler does not detect this; at run time the stack fills up after some tens of thousands of frames on the default stack, and the JVM throws StackOverflowError, whose trace repeats the same method line over and over. Check the smallest inputs, such as 0, 1 and an empty string, first.

The next question is usually about a deeper input or a repeated subproblem. Here is one of each; choose, then see why each option is right or wrong:

Checkpoint

Interview round: two follow-up questions

  1. Question 1 of 2 long sum(int[] a, int i) { return i == a.length ? 0 : a[i] + sum(a, i + 1); } passes its tests on small arrays. On JDK 25 with the default stack, what happens for an array of 100,000 ints, and what do you change?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It throws StackOverflowError, so it becomes a loop that adds the elements in one frame

    The depth grows with the input, so the safe shape is a loop: for (int x : a) total += x;. Raising the stack size would only move the limit to a bigger array.

  2. Question 2 of 2 long paths(int r, int c) counts the routes to a grid cell and returns paths(r - 1, c) + paths(r, c - 1), so the same cells come back again and again. How do you memoise it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Keep each answer in a long[][] indexed by r and c, or in a map keyed by both numbers

    Memoisation keeps one answer per different argument list, so with two arguments the store has two indexes, exactly as fibMemo keeps one answer per n.

Two longer questions of this module build on this lesson: a folder walk that meets an archive nested 50,000 levels deep, and how long a plain recursive fib(50) takes compared with the faster ways to compute it.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A recursive method walks the nested folders of an uploaded archive. It passes every test, but one customer's upload is nested 50,000 levels deep and the method throws StackOverflowError. What are your options, what does each one cost, and which would you ship?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A plain recursive fib(40) takes about a fifth of a second on a laptop. Estimate how long fib(50) takes, explain why, and compare memoisation, a bottom-up table and a loop that keeps only two values. What changes once n passes 92?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A recursive method needs a base case that returns directly and that every call is sure to reach, and a recursive case that makes the problem smaller.
  • Each call has its own frame on the thread’s stack until it returns, so a missing base case, or a depth that grows with the input, ends in StackOverflowError.
  • On this JDK the default 2 MiB stack held 17,640 frames of a trivial method in the interpreter, about 112 bytes each; with the JIT on the count changes from run to run, so never rely on an exact depth.
  • Java does not remove tail calls, so tail recursion overflows like any other; rewrite it as a loop whose variables take the parameters’ place.
  • Memoisation cut fib(40) from 331,160,281 calls to 79 but not its depth, and halving the problem kept the depth at 31 calls for an exponent of a billion.
  • When the depth grows with the input, write a loop, with a stack of your own on the heap where the recursion branches.

Terms in this lesson

base case
The input a recursive method answers directly, without calling itself, such as factorial(0) = 1. Every other call must move closer to it, or the calls never end and the stack overflows.
call stack
The frames of a thread's calls that have started and not yet returned, the most recent on top. Its size is fixed when the thread starts; a call that needs more room than is left throws StackOverflowError.
frame
The room a method call gets on its thread's stack: its parameters, its local variables and its own operand stack. The JVM creates it when the call starts and removes it when the call returns.
memoisation
Keeping each answer of a function once it is worked out, in an array or a map, so that a later call with the same argument returns it at once instead of working it out again.
recursion
A method that calls itself on a smaller version of its problem, until it reaches a case it answers directly, the base case. Each call waits in its own frame until the call it made returns.
tail call
A call that is the last thing a method does, its result returned as it is, as in return sumTo(n - 1, total + n);. HotSpot still gives it a frame of its own, so tail recursion can overflow the stack.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.