StackOverflowError 遞迴迴圈

如果遞迴呼叫太深,則會產生一個 StackOverflowError。Java 為其執行緒堆疊上的每個方法呼叫分配一個新幀。但是,每個執行緒堆疊的空間是有限的。堆疊上的幀太多會導致堆疊溢位(SO)。

public static void recursion(int depth) {
    if (depth > 0) {
        recursion(depth-1);
    }
}

使用大引數呼叫此方法(例如,recursion(50000) 可能會導致堆疊溢位。確切的值取決於執行緒堆疊大小,而執行緒堆疊大小又取決於執行緒構造,命令列引數(如 -Xss)或預設大小 JVM。

解決方法

通過將每個遞迴呼叫的資料儲存在資料結構中,可以將遞迴轉換為迴圈。此資料結構可以儲存在堆上而不是儲存線上程堆疊中。

通常,恢復方法呼叫狀態所需的資料可以儲存在堆疊中,而 while 迴圈可以用於模擬遞迴呼叫。可能需要的資料包括:

  • 呼叫該方法的物件(僅限例項方法)
  • 方法引數
  • 區域性變數
  • 執行或方法中的當前位置

以下類允許遞迴列印到指定深度的樹結構。

public class Node {

    public int data;
    public Node left;
    public Node right;

    public Node(int data) {
        this(data, null, null);
    }

    public Node(int data, Node left, Node right) {
        this.data = data;
        this.left = left;
        this.right = right;
    }

    public void print(final int maxDepth) {
        if (maxDepth <= 0) {
            System.out.print("(...)");
        } else {
            System.out.print("(");
            if (left != null) {
                left.print(maxDepth-1);
            }
            System.out.print(data);
            if (right != null) {
                right.print(maxDepth-1);
            }
            System.out.print(")");
        }
    }

}

例如

Node n = new Node(10, new Node(20, new Node(50), new Node(1)), new Node(30, new Node(42), null));
n.print(2);
System.out.println();

列印

(((...)20(...))10((...)30))

這可以轉換為以下迴圈:

public class Frame {

    public final Node node;

    // 0: before printing anything
    // 1: before printing data
    // 2: before printing ")"
    public int state = 0;
    public final int maxDepth;

    public Frame(Node node, int maxDepth) {
        this.node = node;
        this.maxDepth = maxDepth;
    }

}
List<Frame> stack = new ArrayList<>();
stack.add(new Frame(n, 2)); // first frame = initial call

while (!stack.isEmpty()) {
    // get topmost stack element
    int index = stack.size() - 1;
    Frame frame = stack.get(index); // get topmost frame
    if (frame.maxDepth <= 0) {
        // termial case (too deep)
        System.out.print("(...)");
        stack.remove(index); // drop frame
    } else {
        switch (frame.state) {
            case 0:
                frame.state++;

                // do everything done before the first recursive call
                System.out.print("(");
                if (frame.node.left != null) {
                    // add new frame (recursive call to left and stop)
                    stack.add(new Frame(frame.node.left, frame.maxDepth - 1));
                    break;
                }
            case 1:
                frame.state++;

                // do everything done before the second recursive call
                System.out.print(frame.node.data);
                if (frame.node.right != null) {
                    // add new frame (recursive call to right and stop)
                    stack.add(new Frame(frame.node.right, frame.maxDepth - 1));
                    break;
                }
            case 2:
                // do everything after the second recursive call & drop frame
                System.out.print(")");
                stack.remove(index);
        }
    }
}
System.out.println();

注意: 這只是一般方法的一個例子。通常,你可以想出一種更好的方式來表示幀和/或儲存幀資料。