1.6 程序错误与调试
绝大多数的初学者在编写第一个程序时,往往都会出现这样或那样的错误,这是完全正常的。事实上,即使是有着丰富开发经验的编程者,也几乎不可能编写出不经任何修改就运行无误的程序(除非功能特别简单)。程序中的错误可以分为3类—语法错误、运行时错误和逻辑错误。
1.6.1 语法错误
语法错误是指源文件的某些代码不符合编程语言的语法规范(如缺少一个分号),具有语法错误的程序是不能运行的—因为无法通过编译。
在编译源文件时,编译器会分析源文件的语法,若有错误则会给出错误所在的行号与描述信息,因此语法错误的定位与修改较为简单。以1.5节中的HelloWorld.java为例,现对其做几处修改以故意制造语法错误:将第3行中的public改成Public,删除第8行中字符串的结束双引号。保存并编译,结果如图1-14所示。
图1-14 编译有语法错误的源文件
图1-14中,编译器提示了错误的个数、每个错误所在的行号(源文件名后的数字)、具体位置(“^”符号指示的地方)及描述信息(行号后的文字)等,需要注意以下几点。
(1)编译器提示的错误个数可能不准确。如刚才制造了2个语法错误,但编译器提示有4个。实际上,后3个错误都是由同一个错误引起的(缺少双引号)。因此,不要总是试图在程序中找出与提示相一致的错误个数,比较好的做法是,每改正一个错误(或所有能够确定的错误)后立刻保存并重新编译。
(2)编译器提示的错误描述信息也不一定准确。如第3个错误提示第8行“需要分号”,但该行并不缺少分号,而是缺少结束的双引号,因为Java中的字符串字面值(即常量)都是用一对双引号括起来的—该错误实际上已被第2个错误提示描述了。
(3)一般来说,提示的错误所在行号总是准确或相对准确的。因此,要根据错误所在行号并结合描述信息及位置,综合判断出真正的错误所在。
语法错误还包括一类不安全的、无效的或在特定情况下可能引发逻辑错误的“轻微错误”,例如,声明了从未被用到的变量、存在永远都不可能被执行的代码、使用了原始类型的容器等,这类“错误”被称为警告。
严格来说,警告并不属于语法错误的范畴,有警告的源文件依然能够被成功编译并运行,并且在通常情况下对程序的运行逻辑没有影响。因此,警告的去除并不是必需的,但出于可靠性和代码优化角度的考虑,去除警告有助于降低运行程序时出现逻辑错误的可能性。
此外,大多数编程语言的编译器及IDE都提供了多种编译选项,允许以不同的宽松级别对待程序中的警告。对于Java语言,还可以通过注解机制有选择性地“抑制”程序中的某些警告(参见17.2.3节)。
1.6.2 运行时错误
运行时错误是指程序在运行阶段出现的错误,这种错误通常由程序中的某些数据(如表示数组下标的变量超出范围)、来自用户的输入(如输入的除数是零)或程序所处的运行环境(如程序试图访问本机不存在的盘符)引起,因而无法在编译阶段检查出来。
运行时错误通常会中断程序的执行,严重的运行时错误甚至会引起程序的崩溃。在Java中,运行时错误通常以异常的形式出现(具体见第10章),编程者根据程序输出的异常信息,一般能够快速判断出运行时错误出现的原因及出错代码所在的位置。
因运行时错误与程序要处理的数据(特别是来自于用户输入的数据)及运行环境有关,故很多时候需要对程序进行大量的测试才能重现这种错误。为降低运行时错误出现的可能性,应尽量避免数据硬编码,并充分考虑各种有代表性的、将来用户可能会输入的数据。
1.6.3 逻辑错误
逻辑错误是指程序通过了编译,且运行时没有出现任何异常,但运行结果与预期不一致,例如,要求计算A乘B的值,但实际计算的是A加B。
程序出现逻辑错误是不可避免的,即使对于有着丰富经验的程序员也是如此。另一方面,逻辑错误发生时,程序不会出现任何异常或提示,因而这种错误也是最难察觉的。寻找具有逻辑错误的代码所耗费的时间往往比改正这个错误要多得多。
通常,应先根据程序的输出信息判断出错误所在的大致位置(范围),然后通过人工检查的方式逐行检查范围内的每行代码是否正确—注意不是检查语法上是否正确,而是检查代码是否完成了预期的逻辑,如“应该是乘而不是加”。
人工检查的方式只适合程序的代码行数较少或判断出的错误所在范围较小的情况,在实际开发中,这些情况很少满足。另一方面,由于粗心或思维定势等原因,这种方式经常不能检查出错误所在。因此,更为可靠的定位并改正逻辑错误的方法是调试程序。
1.6.4 程序调试
无论使用何种编程语言和编程工具,调试(Debug)的基本原理总是一样的。
(1)通过人工检查的方式粗略判断出错误所在的范围,若无法判断,则认为被执行的第一行代码就可能有错误。
(2)在范围的起始处设置断点,并以调试方式执行程序,程序执行到断点处会暂时停止执行。
(3)查看相关的、感兴趣的变量或者表达式在这一时刻的实际值与预期值(即人脑计算出来的值,假设该值总是正确的)是否一致,若一致,则让程序从该断点处继续执行下一行代码。重复这一过程直至发现不一致,此时,被执行完的最后一行代码就是错误所在的位置。在对比实际值与预期值的过程中,同时需要注意对比程序的实际执行流程是否与预期流程一致,这对于判断分支、循环等结构的代码错误所在尤为有用。
编程工具一般会提供诸如设置断点、查看变量或表达式,以及让程序从断点处执行下一行(或下若干行)代码等功能。JDK也提供了用于调试程序的工具—jdb.exe,但该工具是基于命令行的,在实际使用中非常不方便,对于较为复杂的程序,通常借助IDE来调试(参见附录A)。
人工检查与调试这两种判断逻辑错误所在位置的方式,读者都应当熟练掌握,当程序逻辑较为复杂时,应优先考虑使用调试方式。当然,调试方式也有自身的局限性,对于某些特定的程序,有时很难用调试的方式来定位逻辑错误,如多线程程序。对于这样的程序,可以采用一些辅助的技巧来帮助寻找逻辑错误,如在程序的合适位置添加输出语句将变量或表达式的值输出到命令行窗口以观察程序的运行细节、注释或取消注释某些代码行并结合排除法判断错误所在行等。
总而言之,在定位程序的逻辑错误时,没有一种方法能适用于所有场合。编程时,应学会并尽量遵守某些已被证明是行之有效的编码规范和最佳实践(参见附录C),以最大限度减少逻辑错误的出现机会。当错误不可避免地发生时,应当根据错误所表现的具体特征及实际情况,灵活运用多种方式来分析和定位逻辑错误。