跳转到内容

Java 调试之道

来自维基教科书,自由的教科书,开放的世界

程序中可能会出现几种不同的错误,为了更快地跟踪它们,将它们区分开来非常有用。

itemize

编译时错误是由编译器产生的,通常表示程序的语法存在问题。例如:在语句末尾省略分号。

运行时错误是由运行时系统产生的,如果程序在运行时出现问题。大多数运行时错误都是异常。例如:无限递归最终会导致 StackOverflowException。

语义错误是程序编译并运行但没有执行正确操作的问题。例如:表达式可能不会按预期顺序计算,从而导致意外结果。

itemize

compile-time error
run-time error
semantic error
error!compile-time
error!run-time
error!semantic
exception

调试的第一步是确定您正在处理哪种错误。虽然以下部分按错误类型组织,但有一些技术适用于多种情况。


编译时错误

[编辑 | 编辑源代码]

编译器正在输出错误信息。

[编辑 | 编辑源代码]
error messages
compiler

如果编译器报告 100 个错误信息,并不意味着您的程序中有 100 个错误。当编译器遇到错误时,它会被暂时打乱。它尝试恢复并在第一个错误之后继续,但有时会失败,并报告虚假的错误。

一般来说,只有第一个错误信息是可靠的。我建议您一次只修复一个错误,然后重新编译程序。您可能会发现一个分号“修复”了 100 个错误。当然,如果您看到多个合法的错误信息,您不妨在每次编译尝试中修复多个错误。


我收到一个奇怪的编译器信息,它不会消失。

[编辑 | 编辑源代码]

首先,仔细阅读错误信息。它用简洁的术语写成,但通常会隐藏一些信息。

如果没有其他,该信息会告诉您程序中的问题出现在哪里。实际上,它告诉您编译器在注意到问题时在哪里,这并不一定是错误所在的位置。将编译器提供的信息作为指导,但如果您没有在编译器指向的地方看到错误,请扩大搜索范围。

通常错误会出现在错误信息之前,但有些情况下它会完全出现在其他地方。例如,如果您在方法调用处收到错误信息,实际错误可能是在方法定义中。

如果您正在逐步构建程序,您应该对错误所在的位置有一个很好的了解。它将是您添加的最后一行代码。

如果您正在从书中复制代码,请首先仔细比较您的代码和书中的代码。检查每一个字符。同时,请记住这本书可能是错误的,因此,如果您看到类似语法错误的东西,它可能是错误的。

如果您没有很快找到错误,请深呼吸,更广泛地查看整个程序。现在是仔细查看整个程序并确保它正确缩进的好时机。我不会说良好的缩进使查找语法错误变得容易,但糟糕的缩进确实会使它变得更难。

现在,开始检查代码中常见的语法错误。

syntax

enumerate

检查所有括号和方括号是否平衡并正确嵌套。所有方法定义都应嵌套在类定义中。所有程序语句都应在方法定义中。

请记住,大写字母与小写字母不同。

检查语句末尾是否有分号(并且在花括号之后没有分号)。

确保代码中的所有字符串都有匹配的引号(并且您使用双引号,而不是单引号)。

对于每个赋值语句,确保左侧的类型与右侧的类型相同。

对于每个方法调用,确保您提供的参数顺序正确,类型正确,并且您正在调用方法的对象类型正确。

如果您正在调用一个有返回值的方法,请确保您对结果做了一些操作。如果您正在调用一个无返回值的方法,请确保您没有尝试对结果做任何操作。

如果您正在调用一个对象方法,请确保您正在对具有正确类型的对象调用该方法。如果您正在从定义该方法的类外部调用一个类方法,请确保您指定了类名。

在对象方法内部,您可以引用实例变量,而无需指定对象。如果您尝试在类方法中这样做,您将收到一条令人困惑的信息,如“静态引用非静态变量”。

enumerate

如果什么都不起作用,请继续下一节...


无论我做什么,都无法让我的程序编译。

[编辑 | 编辑源代码]

如果编译器说有错误而您没有看到它,可能是因为您和编译器没有查看相同的代码。检查您的开发环境,确保您正在编辑的程序是编译器正在编译的程序。如果您不确定,请尝试在程序开头添加一个明显的故意语法错误。现在重新编译。如果编译器没有找到新错误,可能是您的项目设置方式有问题。

否则,如果您已经彻底检查了代码,是时候采取极端措施了。您应该从一个可以编译的程序重新开始,然后逐步添加您的代码。

itemize

将您正在处理的文件复制一份。如果您正在处理 Fred.java,请复制一份名为 Fred.java.old 的副本。

从 Fred.java 中删除大约一半的代码。再次尝试编译。

itemize

如果程序现在可以编译,那么您就知道错误在另一半中。将您删除的大约一半代码放回,然后重复。

如果程序仍然无法编译,则错误一定在这部分代码中。删除大约一半的代码,然后重复。

itemize

找到并修复错误后,开始逐步恢复您删除的代码。

itemize

这个过程被称为“二分法调试”。作为替代方案,您可以注释掉代码块,而不是删除它们。但是,对于非常棘手的语法问题,我认为删除更可靠——您不必担心注释的语法,并且通过使程序更小,您会使它更易读。

bisection!debugging by
debugging by bisection

运行时错误

[编辑 | 编辑源代码]

我的程序挂起了。

[编辑 | 编辑源代码]
infinite loop
infinite recursion
hanging

如果一个程序停止并且似乎没有做任何事情,我们说它“挂起了”。通常这意味着它陷入无限循环或无限递归。

itemize

如果您怀疑某个特定循环是问题所在,请在循环之前添加一个打印语句,该语句说“进入循环”,并在循环之后添加另一个语句说“退出循环”。

运行程序。如果您收到第一条消息,但没有收到第二条消息,那么您遇到了无限循环。转到标题为“无限循环”的部分。

大多数情况下,无限递归会导致程序运行一段时间然后产生 StackOverflowException。如果发生这种情况,请转到标题为“无限递归”的部分。

如果您没有收到 StackOverflowException,但您怀疑递归方法存在问题,您仍然可以使用无限递归部分中的技术。

如果这些方法都没有奏效,请开始测试其他循环和其他递归方法。

如果这些方法都没有奏效,那么您可能不了解程序中执行流程。转到标题为“执行流程”的部分。

itemize


无限循环
[编辑 | 编辑源代码]

如果您认为您遇到了无限循环,并且认为您知道是哪个循环导致了问题,请在循环末尾添加一个打印语句,打印条件中变量的值和条件的值。

例如,

逐字

   while (x > 0 && y < 0) 
       // do something to x
       // do something to y
       System.out.println ("x: " + x);
       System.out.println ("y: " + y);
       System.out.println ("condition: " + (x > 0 && y < 0));
   

逐字

现在,当您运行程序时,您将看到循环每次执行时的三行输出。最后一次循环时,条件应该为假。如果循环继续进行,您将能够看到 x 和 y 的值,并且您可能会弄清楚为什么它们没有正确更新。


无限递归
[编辑 | 编辑源代码]

大多数情况下,无限递归会导致程序运行一段时间然后产生 StackOverflowException。

如果您怀疑该方法会导致无限递归,请首先检查以确保存在基本情况。换句话说,应该存在导致方法返回而不会进行递归调用的条件。如果没有,那么您需要重新考虑算法并确定基本情况。

如果存在基本情况,但程序似乎没有达到它,请在方法开头添加一个打印语句,打印参数。现在,当您运行程序时,您将看到每次调用方法时的几行输出,并且您将看到参数。如果参数没有朝着基本情况移动,您将获得一些关于原因的线索。


执行流程
[编辑 | 编辑源代码]
flow of execution

如果您不确定执行流程如何在程序中移动,请在每个方法的开头添加打印语句,并显示类似“进入方法 foo”的消息,其中foo 是方法的名称。

现在,当您运行程序时,它将打印每个方法调用时的跟踪信息。

在方法被调用时打印每个方法接收的参数通常很有用。当您运行程序时,请检查参数是否合理,并检查是否存在经典错误——参数顺序错误。


当我运行程序时,我遇到了一个异常。

[编辑 | 编辑源代码]
Exception

如果在运行时出现错误,Java 运行时系统会打印一条消息,其中包含异常的名称、发生问题的程序行以及堆栈跟踪。

堆栈跟踪包括当前正在运行的方法,然后是调用它的方法,然后是调用它的方法,依此类推。换句话说,它追踪了导致您到达当前位置的方法调用路径。

第一步是检查程序中发生错误的位置,看看您是否能弄清楚发生了什么。

描述

[NullPointerException:] 您尝试访问当前为 null 的对象的实例变量或调用对象的方法。您应该弄清楚哪个变量为 null,然后弄清楚它是怎么变成这样的。

请记住,当您用对象类型声明变量时,它最初为 null,直到您为它分配一个值。例如,以下代码会导致 NullPointerException

逐字 Point blank; System.out.println (blank.x); 逐字

[ArrayIndexOutOfBoundsException:] 您用来访问数组的索引为负数或大于 array.length-1。如果您能找到问题所在,请在它之前添加一个打印语句,打印索引的值和数组的长度。数组大小是否正确?索引是否正确?

现在,从程序中向后追踪,看看数组和索引是从哪里来的。找到最近的赋值语句,看看它是否按预期工作。

如果两者都是参数,请转到调用方法的位置,看看这些值是从哪里来的。

[StackOverFlowException:] 参见“无限递归”。

描述


我添加了太多打印语句,导致我被输出淹没。

[编辑 | 编辑源代码]
print statement
statement!print

使用打印语句进行调试的其中一个问题是,您最终可能会被输出淹没。有两种方法可以继续:简化输出或简化程序。

为了简化输出,您可以删除或注释掉没有帮助的打印语句,或者将它们合并,或者格式化输出以便更容易理解。

为了简化程序,您可以做几件事。首先,缩小程序正在处理的问题范围。例如,如果您正在对数组进行排序,请对一个小的数组进行排序。如果程序从用户那里获取输入,请提供最简单的输入,该输入会导致错误。

其次,清理程序。删除无用代码并重新组织程序,使其尽可能易读。例如,如果您怀疑错误位于程序的嵌套层级较深的区域,请尝试使用更简单的结构重写该部分代码。如果您怀疑一个大型方法,请尝试将其拆分为更小的方法并分别测试它们。

通常,找到最小测试用例的过程会引导您找到错误。例如,如果您发现程序在数组具有偶数个元素时可以正常工作,但在数组具有奇数个元素时无法正常工作,那么这会给您关于发生情况的线索。

同样,重写一段代码可以帮助您找到细微的错误。如果您进行了您认为不会影响程序的更改,但它确实影响了,那么这会提醒您。


语义错误

[编辑 | 编辑源代码]

我的程序无法正常工作。

[编辑 | 编辑源代码]

在某些方面,语义错误是最难的,因为编译器和运行时系统没有提供有关错误的信息。只有您知道程序应该做什么,也只有您知道它没有按照预期工作。

第一步是在程序文本和您看到的行为之间建立联系。您需要一个关于程序实际在做什么的假设。使这变得困难的一件事是计算机运行速度非常快。您经常希望能够将程序速度降低到人类速度,但没有直接的方法可以做到这一点,即使有,这也不是一个很好的调试方法。

以下是一些需要自问的问题

itemize

程序是否应该做某事,但似乎没有发生?找到执行该功能的代码部分,并确保它在您认为它应该执行的时候执行。在可疑方法的开头添加一个打印语句。

是否发生了不应该发生的事情?找到程序中执行该功能的代码,看看它是否在不应该执行的时候执行。

代码部分是否产生了与您预期的效果不同的效果?确保您理解所讨论的代码,尤其是当它涉及调用内置 Java 方法时。阅读您调用的方法的文档。尝试通过直接调用方法并使用简单的测试用例来调用方法,并检查结果。

itemize

为了编程,你需要对程序的工作原理有一个心理模型。如果你的程序没有按预期工作,很多时候问题不在程序本身,而在于你的心理模型。

model!mental
mental model

修正心理模型的最佳方法是将程序分解成各个组件(通常是类和方法),并独立测试每个组件。一旦你发现模型与现实之间的差异,你就可以解决问题。

当然,你应该在开发程序的同时构建和测试组件。如果你遇到问题,应该只有一小部分新代码是未知的。

以下是一些你可能想检查的常见语义错误

itemize

如果你在 if、while 或 for 语句的条件中使用赋值运算符 =,而不是相等运算符 ==,你可能会得到一个语法上合法的表达式,但它没有按预期执行。

当你在对象上应用相等运算符 == 时,它检查的是浅层相等性。如果你想检查深层相等性,你应该使用 equals 方法(或者为用户定义的对象定义一个)。

一些 Java 库要求用户定义的对象定义像 equals 这样的方法。如果你没有自己定义它们,你将从父类继承默认行为,这可能不是你想要的。

总的来说,继承会导致微妙的语义错误,因为你可能会在没有意识到的情况下执行继承的代码。同样,确保你理解程序中的执行流程。

itemize


我有一个很大的复杂表达式,它没有按预期执行。

[edit | edit source]
expression!big and hairy

编写复杂的表达式是可以的,只要它们可读,但它们可能难以调试。将复杂表达式分解成一系列对临时变量的赋值通常是一个好主意。

例如

verbatim rect.setLocation (rect.getLocation().translate

                    (-rect.getWidth(), -rect.getHeight()));

逐字

可以改写为

verbatim int dx = -rect.getWidth(); int dy = -rect.getHeight(); Point location = rect.getLocation(); Point newLocation = location.translate (dx, dy); rect.setLocation (newLocation); verbatim

显式版本更容易阅读,因为变量名提供了额外的文档,并且更容易调试,因为我们可以检查中间变量的类型并显示它们的值。

temporary variable
variable!temporary
order of evaluation
precedence

大型表达式可能出现的另一个问题是,求值的顺序可能不是你预期的。例如,如果你正在翻译表达式

into Java, you might write

verbatim double y = x / 2 * Math.PI; verbatim

这是不正确的,因为乘法和除法具有相同的优先级,并且从左到右求值。所以这个表达式计算的是。

调试表达式的有效方法是添加括号来使求值的顺序明确。

verbatim double y = x / (2 * Math.PI); verbatim

任何时候你不确定求值的顺序,都使用括号。程序不仅会是正确的(在做你想做的事情的意义上),而且对于没有记住优先级规则的其他人来说也更容易阅读。


我有一个方法没有返回我期望的值。

[edit | edit source]
return statement
statement!return

如果你有一个带有复杂表达式的 return 语句,你没有机会在返回之前打印返回值。同样,你可以使用一个临时变量。例如,而不是

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

逐字

你可以写

verbatim 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;

逐字

现在你可以在返回之前显示任何中间变量。


我的打印语句没有起作用

[edit | edit source]
print statement
statement!print

如果你使用 println 方法,输出会立即显示,但如果你使用 print(至少在某些环境中),输出会存储起来,直到下一个换行符输出才会显示。如果程序在没有产生换行符的情况下终止,你可能永远看不到存储的输出。

如果你怀疑这种情况正在发生,尝试将所有 print 语句更改为 println。


我真的,真的卡住了,我需要帮助

[edit | edit source]

首先,尝试离开电脑几分钟。电脑会发出影响大脑的波,引起以下症状

itemize

沮丧和/或愤怒。

迷信(“电脑讨厌我”)和魔法思维(“只有当我戴着帽子倒着的时候程序才会运行”)。

随机漫步编程(尝试通过编写所有可能的程序并选择做正确事情的程序来进行编程)。

itemize

如果你发现自己患有这些症状中的任何一种,站起来去散散步。当你冷静下来后,思考一下程序。它在做什么?这种行为的可能原因是什么?你上次拥有一个可运行的程序是什么时候,接下来你做了什么?

有时找到一个错误只需要时间。我经常在远离电脑的时候,让我的思绪漫无目的地游走的时候找到错误。找到错误的一些最佳地点是火车、淋浴和床上,就在你快要睡着的时候。


不,我真的需要帮助。

[edit | edit source]

这种情况很常见。即使是最优秀的程序员也会偶尔陷入困境。有时你会长时间地在一个程序上工作,以至于你看不到错误。一双新的眼睛就是解决问题的方法。

在你请别人帮忙之前,确保你已经用尽了这里描述的技术。你的程序应该尽可能简单,你应该在导致错误的最小输入上工作。你应该在适当的地方添加打印语句(并且它们产生的输出应该是可以理解的)。你应该对这个问题有足够的了解,能够简洁地描述它。

当你请别人帮忙时,一定要给他们他们需要的信息。

itemize

是什么类型的错误?编译时、运行时还是语义错误?

如果错误发生在编译时或运行时,错误消息是什么,它指示程序的哪一部分?

在你遇到这个错误之前你最后做了什么?你最后写的几行代码是什么,或者哪个新的测试用例失败了?

你尝试了什么,你学到了什么?

itemize

当你找到错误时,花点时间想想你本可以做什么来更快地找到它。下次你看到类似的东西时,你将能够更快地找到错误。

记住,在这个课程中,目标不是让程序运行。目标是学习如何让程序运行。程序开发计划

如果你花费大量时间进行调试,可能是因为你没有一个有效的程序开发计划。

一个典型的、糟糕的程序开发计划是这样的

enumerate

编写一个完整的函数。

编写几个更多的函数。

尝试编译程序。

花费一个小时查找语法错误。

花费一个小时查找运行时错误。

花费三个小时查找语义错误。

enumerate

当然,问题出在第一步和第二步。如果你在开始调试过程之前编写了多个函数,甚至一个完整的函数,你可能会编写比你能够调试的更多的代码。

如果你发现自己身处这种情况,唯一的解决办法是删除代码,直到你再次获得一个可运行的程序,然后逐渐重建程序。初级程序员往往不愿意这样做,因为他们精心编写的代码对他们来说是宝贵的。为了有效地调试,你必须毫不留情!


以下是一个更好的程序开发计划

enumerate

从一个可运行的程序开始,它可以做一些可见的事情。

  like printing something.

一次添加少量代码。

  and test the program after every change.

重复,直到程序执行它应该执行的操作。

enumerate

在每次更改后,程序应该产生一些可见的效果,以演示新的代码。这种编程方法可以节省大量时间。因为你一次只添加几行代码,所以很容易找到语法错误。此外,因为每个程序版本都产生一个可见的结果,所以你不断地测试你对程序工作原理的心理模型。如果你的心理模型有误,你将在编写大量错误代码之前遇到冲突(并有机会纠正它)。

这种方法的一个问题是,通常很难找到从起点到完整且正确程序的路径。

我将通过开发一个名为 isIn 的函数来演示,它接受一个字符串和一个向量,并返回一个布尔值:如果字符串出现在列表中则为真,否则为假。

enumerate

第一步是编写最短的函数,它可以编译、运行并执行一些可见的操作

verbatim public static boolean isIn (String word, Vector v)

   System.out.println ("isIn");
   return false;

逐字

当然,为了测试这个方法,我们必须调用它。在 main 函数中,或是在工作程序中的其他地方,我们需要创建一个简单的测试用例。

我们将从字符串出现在向量中的情况开始(因此我们预计结果为真)。

verbatim public static void main (String[] args)

   Vector v = new Vector ();
   v.add ("banana");
   boolean test = isIn ("banana", v);
   System.out.println (test);

逐字

如果一切按计划进行,这段代码将编译、运行并打印单词 isIn 和值 false。当然,答案是不正确的,但在这一点上我们知道方法正在被调用并返回了一个值。

在我的编程生涯中,我浪费了太多时间调试一个方法,最后才发现它从未被调用。如果我使用了这种开发计划,这种情况就不会发生。

下一步是检查方法接收的参数。

verbatim public static boolean isIn (String word, Vector v)

   System.out.println ("isIn looking for " + word);
   System.out.println ("in the vector " + v);
   return false;

逐字

第一个打印语句让我们可以确认 isIn 正在查找正确的单词。第二个语句打印了向量中元素的列表。

为了使事情更有趣,我们可以向向量中添加更多元素。

verbatim public static void main (String[] args)

   Vector v = new Vector ();
   v.add ("apple");
   v.add ("banana");
   v.add ("grapefruit");
   boolean test = isIn ("banana", v);
   System.out.println (test);

逐字

现在输出看起来像这样

verbatim isIn looking for banana in the vector [apple, banana, grapefruit] verbatim

打印参数可能看起来很愚蠢,因为我们知道它们应该是什么。重点是确认它们是我们认为的。


为了遍历向量,我们可以利用 Section vector 中的代码。一般来说,重用代码片段而不是从头开始编写代码是一个好主意。

verbatim public static boolean isIn (String word, Vector v)

   System.out.println ("isIn looking for " + word);
   System.out.println ("in the vector " + v);
   for (int i=0; i<v.size(); i++) 
       System.out.println (v.get(i));
   
   return false;

逐字

现在当我们运行程序时,它会一次打印一个向量的元素。如果一切顺利,我们可以确认循环检查了向量的所有元素。


到目前为止,我们还没有过多考虑这个方法将要做什么。在这一点上,我们可能需要想出一个算法。最简单的算法是线性搜索,它遍历向量并将每个元素与目标单词进行比较。

幸运的是,我们已经编写了遍历向量的代码。像往常一样,我们将通过一次添加几行代码来继续。

verbatim public static boolean isIn (String word, Vector v)

   System.out.println ("isIn looking for " + word);
   System.out.println ("in the vector " + v);
   for (int i=0; i<v.size(); i++) 
       System.out.println (v.get(i));
       String s = (String) v.get(i);
       if (word.equals (s)) 
           System.out.println ("found it");
       
   
   return false;

逐字

和往常一样,我们使用 equals 方法来比较字符串,而不是 == 运算符!

同样,我添加了一个打印语句,以便在新的代码执行时产生可见的效果。

在这一点上,我们已经非常接近可工作的代码了。下一个更改是从方法中返回,如果我们找到了我们正在寻找的东西。

verbatim public static boolean isIn (String word, Vector v)

   System.out.println ("isIn looking for " + word);
   System.out.println ("in the vector " + v);
   for (int i=0; i<v.size(); i++) 
       System.out.println (v.get(i));
       String s = (String) v.get(i);
       if (word.equals (s)) 
           System.out.println ("found it");
           return true;
       
   
   return false;

逐字

如果我们找到了目标单词,我们返回 true。如果我们在整个循环中都没有找到它,那么正确的返回值是 false。

如果我们在这一点上运行程序,我们应该得到

verbatim isIn looking for banana in the vector [apple, banana, grapefruit] apple banana found it true verbatim


下一步是确保其他测试用例正常工作。首先,我们应该确认如果向量中没有这个词,该方法返回 false。

然后我们应该检查一些典型的麻烦制造者,比如一个空向量(大小为 0 的向量)和一个只有一个元素的向量。此外,我们还可以尝试给方法一个空字符串。

和往常一样,这种测试可以帮助发现错误(如果有的话),但它不能告诉你方法是否正确。

倒数第二步是删除或注释掉打印语句。

verbatim public static boolean isIn (String word, Vector v)

   for (int i=0; i<v.size(); i++) 
       System.out.println (v.get(i));
       String s = (String) v.get(i);
       if (word.equals (s)) 
           return true;
       
   
   return false;

逐字

如果您认为您可能需要稍后重新访问此方法,注释掉打印语句是一个好主意。但如果这是该方法的最终版本,并且您确信它是正确的,您应该删除它们。

删除注释可以让您最清晰地看到代码,这可以帮助您发现任何剩余的问题。

如果代码中有任何不清楚的地方,您应该添加注释来解释它。抵制逐行翻译代码的诱惑。例如,没有人需要这个

逐字

       // if word equals s, return true
       if (word.equals (s)) 
           return true;
       

逐字

您应该使用注释来解释非显而易见的代码,警告可能导致错误的条件,以及记录任何内置到代码中的假设。此外,在每个方法之前,最好写一个抽象描述该方法的功能。


最后一步是检查代码,看看您是否能说服自己它是正确的。

在这一点上,我们知道该方法在语法上是正确的,因为它可以编译。

为了检查运行时错误,您应该找到每个可能导致错误的语句,并找出导致错误的条件。

这个方法中可能产生运行时错误的语句是

tabularl l v.size() & if v is null. word.equals (s) & if word is null. (String) v.get(i) & if v is null or i is out of

                         bounds, 
                       & or the th element of v is not
                         a String.

tabular

由于我们从参数中获得 v 和 word,因此无法避免前两个条件。我们能做的最好的就是检查它们。

verbatim public static boolean isIn (String word, Vector v)

   if (v == null  word == null) return false;
   for (int i=0; i<v.size(); i++) 
       System.out.println (v.get(i));
       String s = (String) v.get(i);
       if (word.equals (s)) 
           return true;
       
   
   return false;

逐字

一般来说,方法最好确保其参数是合法的。

instanceof operator
operator!instanceof

for 循环的结构确保 i 始终介于 0 和 v.size()-1 之间。但是,没有办法确保 v 的元素是字符串。另一方面,我们可以一边走一边检查它们。instanceof 运算符检查对象是否属于某个类。

逐字

   Object obj = v.get(i);
   if (obj instanceof String) 
       String s = (String) v.get(i);
   

逐字

这段代码从向量中获取一个对象,并检查它是否是一个字符串。如果是,它会执行类型转换并将字符串赋给 s。

作为练习,修改 isIn,以便如果它在向量中找到一个不是字符串的元素,它会跳到下一个元素。

如果我们处理了所有问题条件,我们就可以证明这个方法不会导致运行时错误。

我们还没有证明该方法在语义上是正确的,但通过增量方式进行,我们已经避免了许多可能的错误。例如,我们已经知道该方法正在正确地接收参数,并且循环遍历了整个向量。我们也知道它正在成功地比较字符串,并在找到目标单词时返回 true。最后,我们知道如果循环退出,目标单词不可能在向量中。

除了正式证明之外,这可能是我们能做的最好的了。

华夏公益教科书