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();

注意: 这只是一般方法的一个例子。通常,你可以想出一种更好的方式来表示帧和/或存储帧数据。