使用 Eclipse 的调试器了解 Java 递归

Understanding Java recursion using Eclipse's debugger

我正在 Java 中进行一些简单的递归练习,以理解这个概念(我很难理解)。对于到目前为止的所有研究,我都严重依赖 Eclipse 的调试器来准确理解我的代码在做什么。然而,当谈到递归时,我发现情况并非如此,因为很难准确跟踪正在发生的事情。

考虑以下代码,returns第n个斐波那契数的方法:

public int fibonacci(int n) {
    if (n == 0 || n == 1) {
        return n;
    } else {
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
}

使用此代码的调试器时,很难准确跟踪正在发生的事情 where/when。只有一个变量,它每一步都在变化,并且对于较小的 n 值,例如 7,它已经变得难以跟踪,因为在 13 之前执行的步骤太多了终于到了。

我想知道:

  1. 如何以更好的方式调试我的递归代码(通常),以便更好地理解递归?
  2. 考虑到 return fibonacci(n - 1) + fibonacci(n - 2) 这个概念很容易理解,我是不是太专注于调试这类事情了?

您可以在打印 n 值及其 fibonnacci 值的每条指令中使用简单的 System.out.prinln() 对其进行调试。

这是一个示例代码:

public int fibonacci(int n) {
  if (n == 0 || n == 1) {
     System.out.println("your value is: " +n+ " and its Fibonacci value is: "+n);
     return n;
  } else {
    System.out.println("your value is: " +n+ " and its Fibonacci value is: "+fibonacci(n - 1) + fibonacci(n - 2));
    return fibonacci(n - 1) + fibonacci(n - 2);
  }
}

你可以测试DEMO here.

我花了一段时间才掌握递归,出于某种原因,我从未发现调试器有用。我将尝试向您解释我是如何做的,它不涉及调试器(免责声明:这是个人方法,可能不正确或不通用)。

  1. 在递归代码中,你总是至少有一个终止块和一个 递归块。在精神上隔离这两个部分。

    return n; -> 终止块

    return fibonacci(n - 1) + fibonacci(n - 2); -> 递归块

  2. 递归块表示递归的抽象规则。您可以使用相同的函数获取这些值,而不是将这些值保存在变量 Fn1Fn2 中。想想一堵砖墙:你的递归函数创建了一面墙,在现有的墙上增加了一块砖。在递归块内部,在某个步骤中,您不介意现有墙由谁创建以及如何创建,您只需向其中添加一块新砖。碰巧墙是由同一个函数创建的,当时是一块砖。

  3. 在终止块处调用带有一些值的代码。在这个值的过程结束时应该发生什么?谈到斐波那契,在过程结束时(n = 1n = 0)我必须再次将这些数字添加到总数中。这是由递归块完成的。换句话说,终止块将具体值(而不是如何获取它们的过程)提供给递归块。

当我必须进行故障排除时,我会打印每一步的值,这是我为我找到的最佳解决方案。然后我检查它们是否符合预期。对于您的斐波那契数列,我希望看到类似

的输出

代码:

public static int fibonacci( int n ) {
    System.out.println( "\nInput value: " + n );
    if( n == 0 || n == 1 ) {
        System.out.println( "Terminating block value: " + n );
        return n;
    }
    else {
        System.out.println( "Recursion block value: fibonacci(" + (n - 1) + ") + fibonacci(" + (n - 2) + ")" );
        int result = fibonacci( n - 1 ) + fibonacci( n - 2 );
        System.out.println( "Recursion block return value: " + result );
        return result;
    }
}

输出:

Input value: 4
Recursion block value: fibonacci(3) + fibonacci(2)

Input value: 3
Recursion block value: fibonacci(2) + fibonacci(1)

Input value: 2
Recursion block value: fibonacci(1) + fibonacci(0)

Input value: 1
Terminating block value: 1

Input value: 0
Terminating block value: 0
Recursion block return value: 1

Input value: 1
Terminating block value: 1
Recursion block return value: 2

Input value: 2
Recursion block value: fibonacci(1) + fibonacci(0)

Input value: 1
Terminating block value: 1

Input value: 0
Terminating block value: 0
Recursion block return value: 1
Recursion block return value: 3

您还可以阅读有关 Induction 的有用信息,它与递归密切相关。

如何调试我的递归代码?

首先,确保您已切换到“调试”透视图,并且您看到的是正确的 windows(VariablesExpressionsDebug 和您的源代码) 例如像这样:

接下来,请注意在 Debug 中您可以看到该方法当前被调用的频率。此列表会根据调用和尚未返回的方法的数量而增长和缩小。

您可以单击其中一种方法来更改范围。查看更改范围时 Variables 的内容如何变化。

最后,要检查任意事物,请在 Expressions window 中输入表达式。这几乎就像实时编码。您几乎可以检查任何东西。

我是不是太专注于调试了?

没有。学会正确地做事,它会在以后为你节省很多时间。

添加一个System.out.println()需要重新编译,你需要重现情况,这并不总是那么简单。

"Inline" 代码使使用 Eclipse 调试器变得更加困难,因为它非常注重显示不存在的局部变量。您可以通过使事情更详细并保存到变量来使其更容易逐步完成。通过这种方式,您可以更轻松地查看正在发生的事情以及结果。例如,按如下方式修改您的代码将使调试器在以下位置更容易使用:

public int fibonacci(int n) {
    if (n == 0 || n == 1) {
        return n;
    } else {
        int nMinus1 = fibonacci(n - 1);
        int nMinus2 = fibonacci(n - 2);
        int retValue = nMinus1 + nMinus2;
        return retValue;
    }
}

免责声明:我没有尝试编译此代码。