C.2 运行时错误

导致运行时错误的原因并非总是那么明显,但在程序中添加打印语句通常能找出原因。

C.2.1 程序挂起

如果程序停止运行,看起来什么都不做,我们就说它“挂起”了。这通常意味着遭遇了无限循环或无限递归。

  • 如果你怀疑问题出在某个循环上,可在该循环前面添加一条显示“进入循环”的打印语句,并在它后面添加一条显示“退出循环”的打印语句。

运行程序。如果你看到了第一条消息,但没有看到第二条,就知道程序在什么地方卡壳了。为解决这种问题,请参阅“无限循环”一节。

  • 在大多数情况下,无限递归会导致程序运行一段时间后出现 StackOverflowError 异常。如果出现这种异常,请参阅“无限递归”一节。

如果没有出现 StackOverflowError 异常,但你怀疑问题出在某个递归方法上,可用“无限递归”一节介绍的技巧来解决问题。

  • 如果上述两个建议都不管用,可能是因为你没有搞明白程序的执行流程。在这种情况下,可参阅“执行流程”一节。

1. 无限循环

如果你认为程序包含无限循环并知道是哪一个,可在这个循环末尾添加打印语句,以显示条件变量的值以及条件的值。

例如:

  1. while (x > 0 && y < 0) {
  2. // 修改x
  3. // 修改y
  4. System.out.println("x: " + x);
  5. System.out.println("y: " + y);
  6. System.out.println("condition: " + (x > 0 && y < 0));
  7. }

这样的话,当运行程序时,该循环每执行一次都将显示 3 行输出。最后一次执行循环时,添加应为 false。如果循环不断地执行,你将看到 xy 的值,进而也许能够搞明白它们未能正确更新的原因。

2. 无限递归

在大多数情况下,无限递归将导致程序引发 StackOverflowError 异常。但如果程序的运行速度很慢,可能需要很长时间才能填满栈。

如果你知道无限递归是哪个方法导致的,可检查它是否包含基线条件。必须存在某种条件,让方法不再进行递归调用而是返回。如果没有,就需要重新审视算法并找出基线条件。

如果有基线条件,但程序好像满足不了这个条件,可在方法开头添加显示形参的打印语句。这样的话,在程序运行期间,每当这个方法被调用时,你都将看到几行输出并获悉形参的值。如果形参没有逐渐接近基线条件,你也许能够搞明白其中的原因。

3. 执行流程

如果不知道程序的执行流程,可在每个方法开头添加打印语句,以显示“进入方法 foo”这样的消息,其中 foo 为当前方法的名称。这样的话,程序运行时,每个被调用的方法都将留下痕迹。

还可显示每个方法收到的实参。这样的话,你可以在程序运行时检查实参的值是否合理,还能发现最常见的错误之一——实参的指定顺序不正确。

C.2.2 程序运行时出现异常

出现异常时,Java 会显示一条消息,其中包含异常的名称、出现异常的代码的行号以及“栈跟踪”。栈跟踪包含当时运行的方法和方法调用链。

你应该先检查错误发生的地方,并看看能不能找出其中的原因。

  • NullPointerException

试图通过值为 null 的对象变量访问实例变量或调用方法时,将引发这种异常。在这种情况下,你需要确定哪个变量为 null,再搞清楚它是怎么变成 null 的。

别忘了,声明数组变量时,其元素在被赋值前默认为 null。例如,下面的代码将引发 NullPointerException 异常:

  1. int[] array = new Point[5];
  2. System.out.println(array[0].x);
  • ArrayIndexOutOfBoundsException

访问数组时,如果使用的索引为负或大于 array.length - 1,将引发这种异常。如果你能够确定问题出在什么地方,可在它前面添加打印语句来显示索引的值和数组的长度。数组的长度对吗?索引的值对吗?

然后,在程序中往后回溯,确定数组和索引来自何方。找到最近的赋值语句,看看其所作所为是否正确。如果数组或索引为形参,那么就跳转到调用方法的地方,看看这些值来自何方。

  • StackOverflowError

参见前面的“无限递归”一节。

  • FileNotFoundException

这意味着 Java 没有找到要查找的文件。如果你使用的是基于项目的开发环境,如 Eclipse,可能必须将这个文件导入项目。否则,请确保这个文件存在且路径正确。这种问题与文件系统相关,可能难以追查。

  • ArithmeticException

算术运算的执行出现了问题,如除以零。

C.2.3 添加了很多打印语句,输出都泛滥成灾了

用打印语句帮助调试带来的一个问题是,最终的输出可能泛滥成灾。解决之道有两个:要么简化输出,要么简化程序。

要想简化输出,可将不再有帮助的打印语句删除或注释掉、合并打印语句或设置输出的格式使其易于理解。开发程序时,应编写代码来生成简洁而信息丰富的消息,对程序的所作所为进行跟踪。

要想简化程序,可缩小程序处理的问题的规模。例如,对数组进行排序时,可使用较小的数组。如果程序从用户那里获取输入,可向它提供导致错误的最简单输入。

另外,对代码进行清理:删除多余或实验性部分,并重新组织程序使其更易阅读。例如,如果你怀疑错误出在程序中的一个多层嵌套的部分,可用更简单的结构重新编写这部分;如果你怀疑错误出现在一个很大的方法中,可将这个方法分成多个小方法,再分别进行测试。

在确定最简单的测试用例的过程中,常常能够发现导致 bug 的线索。例如,如果你发现程序在数组包含偶数个元素时没问题,但包含奇数个元素时出现问题,这可能就获得了找出原因的线索。

重新组织程序可帮助你找出微妙的 bug。如果修改程序时发现,原本以为这样的修改不会有任何影响,但结果并非如此,这便透露出了蛛丝马迹。