C.3 逻辑错误
C.3.1 程序不管用
逻辑错误难以发现,因为编译器和解释器不会提供有关这种错误的任何信息。只有知道程序该如何做时,你才能知道程序没有这样做。
首先,你需要在代码和程序的实际行为之间建立联系。你需要就程序的实际行为作出假设。下面是你需要回答的一些问题。
存在程序该做却没有做的事情吗?找出执行这项功能的代码片段,确定它在你认为该执行的时候执行了。参见前面的“执行流程”一节。
出现了原本不该发生的事情吗?在程序中找到执行这项功能的代码,看看它是不是在不该执行的时候执行了。
是否存在带来意外影响的代码片段?确保你搞明白了这些代码,尤其是调用了 Java 库中的方法时。阅读这些方法的相关文档,并用简单的测试用例来尝试使用它们。它们的功能可能不是你想的那样。
要编写程序,你需要建立有关代码行为的心理模型。如果代码的行为不符合预期,有问题的可能不是程序,而是你的心理模型。
要想校正你的心理模型,最佳的方式是将程序分成多个部分(通常是类和方法),再分别测试它们。一旦找出心理模型与实际情况的偏差,你就能把问题给解决了。
下面是需要检查的一些常见逻辑错误。
别忘了,整数除法的结果总是向下圆整的。如果你要获得小数结果,应用
double执行除法运算。推而广之,用整数表示可数的东西;用浮点数表示不可数的东西。浮点数只是近似值,不要指望它们绝对精确。根本就不能用运算符
==来比较浮点数。换句话说,不要编写if(d == 1.23)这样的代码,而应编写if(Math.abs(d - 1.23)< .000001)这样的代码。用于对象时,相等运算符(
==)检查两个对象是否相同。如果你要比较两个对象是否相等,应使用方法equals。对于用户定义的类型,默认的
equals方法检查两个对象是否相同。如果你要赋予相等不同的含义,必须重写这个方法。继承可能带来微妙的逻辑错误,因为你可能在不知不觉间运行了继承而来的代码。请参阅前面的“执行流程”一节。
C.3.2 冗长表达式的结果出乎意料
你完全可以编写复杂的表达式,只要它们易于理解,但调试起来可能很麻烦。通常而言,最好将复杂表达式分解成一系列给临时变量赋值的赋值语句。
rect.setLocation(rect.getLocation().translate(-rect.getWidth(), -rect.getHeight()));
前面的示例可重写为下面这样:
int dx = -rect.getWidth();int dy = -rect.getHeight();Point location = rect.getLocation();Point newLocation = location.translate(dx, dy);rect.setLocation(newLocation);
第二个版本更容易理解,这要部分归功于变量名提供了额外的说明。这个版本调试起来也更容易,因为你可以检查临时变量的类型并显示它们的值。
冗长表达式可能存在的另一个问题是,运算的执行顺序可能并非你以为的那样。例如,为计算 x/(2π),你可能编写下面的代码:
double y = x / 2 * Math.PI;
这不对,因为乘法和除法运算的优先级相同,因此按从左到右的顺序执行。上述代码计算的是 x 除以 2 再乘 π。
如果你对运算顺序没有把握,可查看相关文档,也可用括号来明确地指定。
double y = x / (2 * Math.PI);
这个版本是正确的,对不记得运算顺序的人来说也更容易理解。
C.3.3 方法的返回值出乎意料
如果你在返回语句中包含复杂的表达式,就根本没有机会在返回前显示这个表达式的值。
public Rectangle intersection(Rectangle a, Rectangle b) {return new Rectangle(Math.min(a.x, b.x), Math.min(a.y, b.y),Math.max(a.x + a.width, b.x + b.width)- Math.min(a.x, b.x)Math.max(a.y + a.height, b.y + b.height)- Math.min(a.y, b.y));}
不应将整个表达式放在一条语句中,而应使用一系列临时变量:
public Rectangle intersection(Rectangle a, Rectangle b) {int x1 = Math.min(a.x, b.x);int y2 = Math.min(a.y, b.y);int x2 = Math.max(a.x + a.width, b.x + b.width);int y2 = Math.max(a.y + a.height, b.y + b.height);Rectangle rect = new Rectangle(x1, y1, x2 - x1, y2 - y1);return rect;}
这样就可以在返回前显示任何中间变量的值了。另外,通过重用 x1 和 y1,代码也更短了。
C.3.4 打印语句什么都不显示
如果你使用的是方法 println,输出将立即显示出来,但如果你使用的是 print,输出将被存储起来,直到出现换行符才显示出来(至少在有些环境中如此)。如果程序直到终止都没有显示换行符,你可能根本看不到存储的输出。如果你怀疑这就是罪魁祸首,可将部分或全部 print 语句改为 println 语句。
C.3.5 陷入了绝境,无法自拔
首先,离开计算机一会儿。计算机发射的电波会影响人的大脑,让人出现如下症状:
气馁和愤怒;
怪异的想法(“计算机讨厌我。”)和迷信(“这个程序只在我将帽子反戴时才能正确地运行。”);
酸葡萄心理(“这个程序真不怎样。”)。
如果你出现了上述任何症状,赶快起来走一走。冷静下来后再来研究程序。程序当前的行为是什么样的?导致这种行为的原因可能是什么?程序最后一次正确地运行之后,你都做了些什么?
找出有些 bug 就是需要时间。人在放松时常常容易找出 bug,如坐公交车、洗澡和躺在床上时。
C.3.6 必须得有人帮我
每个人都会遇到这样的情况,即便是最优秀的程序员,也有陷入困境的时候。有时候,你需要别人的帮助。
找人帮忙前,务必尝试本附录介绍的所有方法。
你的程序应尽可能简单,并使用尽可能简单的输入来引发错误;你应在合适的地方添加打印语句,且这些语句的输出应易于理解;你对问题有足够的了解,能够简练地进行描述。
帮忙的人来了后,向他们提供所需的信息。
bug 是什么类型的?编译时错误、运行时错误还是逻辑错误?
这种错误发生前,你做了什么?你最后编写的是哪些代码行?或者哪个测试用例未通过?
如果错误发生在编译时或运行时,显示的是什么错误消息?它指出程序的什么地方有问题?
你采取了哪些措施?得出了什么样的结论?
等你向人说明完问题时,你可能已经找到了答案。鉴于这种现象非常普遍,有人推荐使用“橡皮鸭调试法”。这种调试法的步骤如下。
(1) 买个标准款橡皮鸭。
(2) 当你面对问题无计可施时,将橡皮鸭放在前面的桌子上,并对它说,“橡皮鸭,我深陷困境,情况是这样的……”。
(3) 向橡皮鸭描述面临的问题。
(4) 发现解决方案。
(5) 向橡皮鸭致谢。
没跟你开玩笑,这真的管用!详情见 https://en.wikipedia.org/wiki/Rubber-duck-debugging。
C.3.7 终于找到bug了!
bug 找到后,如何修复通常来说是显而易见的,但并非总是如此。有些看起来是 bug 的东西其实表明你没有理解程序或你使用的算法有问题。在这种情况下,你可能需要重新审视算法或调整心理模型。可暂时离开计算机,理清思路、手工执行测试用例或绘制计算图。
修复 bug 后,不要立即投入到再造新 bug 的编程过程中。花点时间想想这是什么样的 bug、你为何会犯这样的错误、这种错误有何特征以及如何更快地找出它。这样的话,再遇到类似的情况时,你就能更快找到 bug,乃至再也不让这样的 bug 出现。
