Some problems are awkward to solve in a straight line but obvious once you let them shrink: 'count down from 3' is just 'say 3, then count down from 2'. A method that solves a smaller version of its own problem by calling itself is recursive. Recursion is not a trick for its own sake — it is the natural shape for anything nested or self-similar, from walking a folder tree to evaluating an expression with parentheses inside parentheses.
The trade for that elegance is discipline. A recursive method must know when to stop, or it will call itself forever and bring the program down. This lesson teaches you to read a recursive method, trace its calls, and predict exactly what it prints — by following the same call stack the JVM uses.
flowchart TD
R["recursiveMethod(input)"] --> Q{"is this the base case?"}
Q -->|"yes"| BASE["return the trivial answer<br/>no further call"]
Q -->|"no"| REC["recursive case:<br/>call on smaller input<br/>then combine the result"]
style BASE fill:#15803d,color:#fff
style REC fill:#3776ab,color:#fff
style Q fill:#c2410c,color:#fff
Two parts: base case and recursive case
Every working recursion has two halves. The base case is the condition that stops the recursion — the smallest, trivial input the method can answer without calling itself. The recursive case is everything else: it does a little work, then calls itself with an input that is strictly closer to the base case.
That last phrase is the whole game. If the recursive call does not move toward the base case, the method never stops. countdown(n) must call countdown(n - 1), never countdown(n + 1) or countdown(n) — only shrinking the input guarantees the base case is eventually reached.
public class Main {
static void countdown(int n) {
if (n == 0) { // base case: stop here
System.out.println("go");
return;
}
System.out.println(n); // recursive case: print, then shrink
countdown(n - 1);
}
public static void main(String[] args) {
countdown(3);
}
}
Shrinking is not optional
It is tempting to think any self-call counts as recursion — it does not, unless the problem actually got smaller. factorial(n) calling factorial(n) would loop forever, because n never approaches the base case; calling factorial(n - 1) works because each step removes one from n until n <= 1 halts it.
The base case and the shrinking call are a single contract: the recursive case exists to feed the base case, and the base case exists to end what the recursive case started. Break either half and the method is broken.
The call stack: each call gets its own frame
When a method calls itself, the JVM does not overwrite the first call — it stacks a new frame on top of it. Each frame holds its own private copy of the parameters and local variables, so countdown(3) and countdown(2) each have their own n and never confuse them. The first call pauses, mid-execution, while the calls above it run.
Frames pile up as the recursion deepens, then unwind in reverse order as each call finishes and hands control back to the one below it. Reading recursion is mostly a matter of picturing that stack: who is paused where, and who is waiting on whose answer.
flowchart TD F3["countdown(3)"] --> F2["countdown(2)"] F2 --> F1["countdown(1)"] F1 --> F0["countdown(0) base case"] F0 -.->|"returns"| F1 F1 -.->|"returns"| F2 F2 -.->|"returns"| F3 style F0 fill:#15803d,color:#fff style F3 fill:#3776ab,color:#fff
public class Main {
static int factorial(int n) {
if (n <= 1) { // base case: 1! = 1
return 1;
}
return n * factorial(n - 1); // recursive case
}
public static void main(String[] args) {
System.out.println(factorial(4));
}
}
Returns unwind back down the stack
For a method that returns a value — like factorial — the answer is assembled in reverse, as the stack unwinds. factorial(1) returns 1 first; factorial(2) then computes 2 * 1; factorial(3) computes 3 * 2; factorial(4) computes 4 * 6 and yields 24. Each frame multiplies its own n by the result handed back from above.
Where you put work relative to the recursive call changes everything. Print before the recursive call and output appears as the stack grows (descending). Print after it and output appears as the stack shrinks (ascending). Same method, opposite order — a favourite interview question, and a habit worth building now.
public class Main {
static void countUp(int n) {
if (n == 0) {
return;
}
countUp(n - 1); // recurse first
System.out.println(n); // print while unwinding
}
public static void main(String[] args) {
countUp(3);
}
}
flowchart TD A["call 1"] --> B["call 2"] B --> C["call 3"] C --> DOTS["...keeps going..."] DOTS --> ERR["call stack full<br/>StackOverflowError"] style A fill:#b45309,color:#fff style ERR fill:#b91c1c,color:#fff
Descending. The print sits BEFORE the recursive call. Predict all four lines from countdown(3).
public class Main {
static void countdown(int n) {
if (n == 0) {
System.out.println("go");
return;
}
System.out.println(n);
countdown(n - 1);
}
public static void main(String[] args) {
countdown(3);
}
}
Each call prints its own
nbefore it recurses, so 3 prints first.Then countdown(2) prints 2, countdown(1) prints 1, and countdown(0) prints go.
Because the print comes first, the order is descending.
Return value. factorial returns n * factorial(n - 1). Predict the single line from factorial(4) — the answer is built on the way back down.
public class Main {
static int factorial(int n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
public static void main(String[] args) {
System.out.println(factorial(4));
}
}
Unwind from the base: factorial(1) = 1.
factorial(2) = 2 * 1 = 2, factorial(3) = 3 * 2 = 6.
factorial(4) = 4 * 6 = 24.
Ascending. Now the print sits AFTER the recursive call. Predict all three lines from countUp(3) — this is the mirror image of countdown.
public class Main {
static void countUp(int n) {
if (n == 0) {
return;
}
countUp(n - 1);
System.out.println(n);
}
public static void main(String[] args) {
countUp(3);
}
}
The recursion goes all the way down to countUp(0) before anything prints.
As the stack unwinds, countUp(1) prints 1, then countUp(2) prints 2, then countUp(3) prints 3.
Printing after the recursive call reverses the order — ascending, not descending.
A recursive method has no base case — it always calls itself with a smaller argument. What happens when it runs?
Every recursive call pushes a new frame onto the finite call stack. With no base case to stop it, frames accumulate until the stack is exhausted, and the JVM throws StackOverflowError. The compiler does not catch this — it is a runtime failure.
A method is defined as static int sum(int n) { if (n == 0) return 0; return n + sum(n - 1); }. What does sum(4) return?
Each frame adds its own n to the result from below: sum(0)=0, sum(1)=1+0=1, sum(2)=2+1=3, sum(3)=3+3=6, sum(4)=4+6=10. This is the same shape as factorial but with + instead of *, so it sums rather than multiplies.
Recap
- A recursive method calls itself; it needs a base case (to stop) and a recursive case that moves the input toward that base case.
- Each call gets its own frame on the call stack, with its own parameters and locals; frames stack up as the recursion deepens and unwind in reverse as calls finish.
- A returning recursion assembles its answer on the way back down (factorial builds
1, then2, then6, then24). - Work done before the recursive call runs as the stack grows (descending); work done after runs as it shrinks (ascending).
- A missing or unreachable base case overflows the stack with a
StackOverflowError.
Next you will learn whether a method can change the data your caller passed in — Java's pass-by-value, for primitives and references alike.