尾遞歸:函數最後一步調用自身,且直接返回調用結果,無後續計算。
尾調用優化:編譯器將尾遞歸轉換為循環,消除棧幀分配,實現 O(1) 空間複雜度。
JLS 不強制要求 JVM 實現尾調用優化。這意味著:
public class Factorial {
// 普通遞歸:O(n) 棧空間
public long factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // 非尾調用:返回後還要乘 n
}
// 尾遞歸:累加器模式
public long factorialTail(int n) {
return factHelper(n, 1);
}
private long factHelper(int n, long acc) {
if (n <= 1) return acc;
return factHelper(n - 1, n * acc); // 尾調用:直接返回
}
}
面向對象特性導致動態分派:
class Calculator {
long fact(int n, long acc) { ... }
}
class LoggingCalculator extends Calculator {
@Override
long fact(int n, long acc) {
log("call: " + n);
return super.fact(n, acc); // 子類覆寫,靜態編譯無法確定最終調用目標
}
}
編譯期無法確定具體調用版本,只能由 JIT 在運行時根據類層次分析決定。
JVM 必須保證異常棧跡蹤準確性。尾調用優化會合併棧幀,導致:
public class TCODectector {
public static void main(String[] args) {
try {
tailRecursive(100000); // 足夠深度觸發 StackOverflowError
System.out.println("JVM 支持尾調用優化!");
} catch (StackOverflowError e) {
System.out.println("JVM 不支持 TCO,棧深度: " + e.getStackTrace().length);
}
}
static void tailRecursive(int n) {
if (n <= 0) return;
tailRecursive(n - 1); // 簡單尾遞歸
}
}
| JVM 模式 | 測試結果 | 說明 |
|---|---|---|
| -client | StackOverflowError ~8000 深度 | 無優化 |
| -server | StackOverflowError ~12000 深度 | 極少數情況觸發優化,不可靠 |
| -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI | 視 GraalVM 版本而定 | Graal 編譯器有更好支持 |
public long factorialIterative(int n) {
long result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
@FunctionalInterface
interface TailCall<T> {
TailCall<T> next();
boolean isDone();
T getResult();
static <T> TailCall<T> done(T value) {
return new TailCall<T>() {
public TailCall<T> next() { throw new IllegalStateException(); }
public boolean isDone() { return true; }
public T getResult() { return value; }
};
}
static <T> TailCall<T> call(TailCall<T> next) {
return new TailCall<T>() {
public TailCall<T> next() { return next; }
public boolean isDone() { return false; }
public T getResult() { throw new IllegalStateException(); }
};
}
}
// 使用
TailCall<Long> fact(int n, long acc) {
if (n <= 1) return TailCall.done(acc);
return TailCall.call(() -> fact(n - 1, n * acc));
}
// 執行器
long factorial(int n) {
TailCall<Long> tc = fact(n, 1);
while (!tc.isDone()) tc = tc.next();
return tc.getResult();
}
long factorialStream(int n) {
return LongStream.rangeClosed(2, n).reduce(1, (a, b) -> a * b);
}
理解 TCO 限制,是寫出健壯 Java 代碼的關鍵——不要賭 JVM 會幫你優化棧。