第18章 处理程序中的错误
本章介绍如下内容:
如何响应Java程序中的异常;
如何创建忽略异常,让其他类去处理的方法;
如何使用引起异常的方法;
如何创建异常。
错误、bug、严重错误、输入错误及其他导致程序无法正确运行的问题是软件开发过程的正常组成部分。“正常”可能是用于描述错误的最善意的用词。我在编程时,当不能找出导致程序不能正确运行的难懂错误时,我会用让强盗都脸红的词。
有些错误将被编译器捕获,进而阻止你创建类;导致程序无法成功运行的其他错误将被解释器发现。Java将错误划分为如下两类。
异常(Exception):表明程序运行时发生了异常情况的事件情况。
错误(Errors):表明解释器遇到问题,但可能与程序无关。
Java程序不能从错误中恢复过来继续运行,所以它不是本章关注的重点。在进行Java编程时,读者可能遇到过OutOfMemoryError的错误,程序对这种类型的错误无能为力,只能退出运行。
而异常可以采用某种方式来处理,而且程序也会继续运行。
18.1 异常
虽然刚开始学习它,但通过本书前17章,读者可能已经对异常非常熟悉了。编写的Java程序编译成功但运行时遇到问题时,出现的错误就是异常。
例如,一种常见的编程错误是引用不存在的数组元素,如下面的语句所示:
在这个例子中,String数组geek有3个元素。数组的第一个元素编号为0而不是1,因此第一个元素是 greek[0],第二个元素是 greek[1],第三个元素是 greek[2],所以试图显示greek[3]的语句是错误的,因为该元素不存在。上述语句可以成功编译,但运行程序时,Java解释器将显示下面的消息并停止运行:
Output ▼
这条消息表明应用程序引发了异常,解释器通过显示错误消息并停止运行程序来指出这一点。
这条错误消息引用了java.lang包中的ArrayIndexOutOfBoundsException类,这是一个异常,该对象用于指出Java程序中发生了异常情况。
Java类遇到异常后,它向类的用户指出错误类型。在这个例子中,类的用户是Java解释器。
注意:
有两个术语可用于描述该过程:引发和捕获。对象抛出异常,以指出发生了异常。这些异常可被其他对象或Java解释器捕获。
所有异常都是 Exception 的子类,Exception 位于 java.lang 包中。正如读者预期的, ArrayIndexOutOfBoundsException类指出使用了数组边界外的元素。
Java中有数百种异常,其中很多表明问题可以通过修改程序得到解决,如数组异常,这些异常类似于编译错误,纠正后就不用担心它会再出现。
其他异常必须使用 5 个新关键词在程序运行时进行处理:try、catch、finally、throw 和throws。
到目前为止,读者通过纠正导致异常的问题来处理异常。有时这样无法解决异常,因此必须在Java类中处理异常。
为说明为何这很有用,在新的Java空文件中输入程序清单18.1中的Java应用程序,将其命名为Calculator,然后保存。
程序清单 18.1 Calculator.java 的源代码
该应用程序通过命令行参数接受一个或多个数字,然后将它们相加并显示结果。
在Java应用程序中,所有命令行参数都用字符串表示,将它们相加前,程序必须将其转换为浮点数。第 5 行的 Float.parseFloat( )方法完成这项任务,并将转换得到的数加到变量sum中。
在运行该应用程序之前,先对命令行参数进行设置:在 NetBeans 中选择Run->Set Project Configuration->Customize 命令,然手输入 8 6 7 5 3 0 9。然后选择 Run->Run Main Project 来运行该程序,其输出如图18.1所示。
图18.1 Calculator应用程序的输出
使用不同的数字作为参数运行该应用程序多次,程序都将成功处理,这让读者产生疑惑,这与异常有什么关系?
要看到异常,将 Calculator 应用程序的命令行参数修改为 1 3 5x。
这里的第三个参数包含输入错误:数字5后不应是字符x。Calculator应用程序不知道这是错误,所以将5x同其他数字相加,导致下面的异常:
Output ▼
尽管该消息对程序员来说很有用,但他们不希望用户看到。Java程序可以使用try-catch块语句来处理异常,其格式如下:
对于希望类方法来处理的任何异常,都必须使用一个 try-catch 块。在 catch 语句中的Exception对象应是下面3个中的一个:
可能发生的异常类;
多个异常类,期间使用|字符隔开;
可能发生的多种异常的超类。
try-catch块的try部分应包含可能引发异常的语句。在应用程序Calculator中,第5行调用了 Float.parseFloat(String)方法,当该方法中的参数是不能转换为浮点数的字符串时,将抛出NumberFormatException。
为了改善应用程序Calculators,使其遇到这种错误时不停止运行,可使用一个trycatch块。
创建一个名为NewCalculator的Java空文件,然后输入程序清单18.2中的所有文本。
程序清单 18.2 NewCalculator.java 程序
保存该应用程序之后,使用命令行参数 1 3 5x 运行该程序,将会看到如图 18.2 所示的输出。
图18.2 NewCalculator应用程序的输出
第5~9行的try-catch块处理Float.parseFloat( )方法抛出的NumberFormatException错误。这些异常是在 NewCalculator 类中捕获的,如果某个参数不是数字,该类将显示一条错误消息。由于在这个类中处理了异常,因此Java解释器不会显示错误消息。可以使用try-catch块来处理与用户输入和其他意外数据相关的问题。
try-catch块可用来处理几种不同类型的异常,即使这些异常是由不同的语句抛出的。
处理多种异常类的一种方法是在每一个异常中使用一个 catcth 语句块,如下面的代码所示:
在 Java 7 中,可以在同一个 catch 块中处理多个异常,方法如下:多个异常之间使用管道字符(|)隔开,并在最后使用异常变量来结尾。其示例如下:
如果该代码捕获了NumberFormatException或ArithmeticException异常,则将其赋值给exc变量。
程序清单18.3包含了名为NumberDivider的应用程序,该应用程序从命令行接受两个整型参数,然后将它们用于除法表达式中。
该应用程序必须能够处理两个潜在的用户输入问题:
非数字型参数;
除数为0。
创建一个名为NumberDivider的新Java空文件,然后输入程序清单18.3中的所有文本。
程序清单 18.3 NumberDivider.java 的源代码
使用命令行参数来指定该应用程序的两个参数。在运行该程序时,你可以将其参数指定为整数、浮点数和非数字型参数。
第3行的if语句检查是否给应用程序传递了两个参数,如果不是,程序将退出且不显示任何信息。
应用程序 NumberDivider 执行整数除法运算,结果也是整数。在整数除法中,5 除以 2等于2,而不是2.5。
如果使用浮点数或非数字作为参数,第6~7行将抛出NumberFormatException异常,并被第10~11行捕获。
如果使用整数作为第一个参数,使用 0 作为第二个参数,第 6~7 行将抛出 Arithmetic Exception异常,并被第12~13行捕获。
使用try-catch块处理多种异常时,有时希望不管是否发生异常,程序都在块的后面执行某种操作。
为此,可以使用try-catch-finally块,其格式如下:
其中,finally部分的语句将在其他语句执行后执行,而不管是否发生异常。
其用途之一是在从磁盘文件中读取数据的程序中,这将在第 20 章介绍。访问数据时可能发生多种异常:文件不存在、磁盘错误等。如果读取磁盘的语句在try部分,并在catch部分处理错误,可以在finally部分关闭文件。这可以确保无论读取文件是否发生异常,都将关闭文件。
调用另一个类的方法时,那个类可以通过抛出异常来控制如何使用该方法。
使用Java类库中的类时,编译器经常会显示下面这样的消息:
Output ▼
NetReader.java:14: unreported exception
java.net.MalformedURLException;
must be caught or declared to be thrown
看到包含“must be caught or declared to be thrown”这样的错误消息时,表明你试图使用的方法抛出了异常。
调用这些方法的任何类,例如你编写的应用程序,必须做下面的工作之一:
使用try-catch块处理异常;
抛出异常;
先用try-catch块处理异常,然后再抛出异常。
至此,读者看到了如何处理异常。如果想先处理异常再抛出它,可使用关键字throw和要抛出的异常对象。
下面的语句在catch块中处理错误NumberFormatException,然后再抛出它:
下面这种改写的代码可以处理try块语句中生成的所有异常,并在处理完毕后再抛出它:
Exception是所有异常子类的父类。这个catch语句可以捕获Exception类,以及类层次结构中位于Exception类下的所有子类。
当使用throw抛出一个异常时,通常意味着没有完成处理异常需要完成的所有工作。
一个采用这种方式很有用的情形是:一个CreditCardChecker应用程序验证信用卡支付,它使用了一个名为CheckDatabase的类,该类完成如下工作:
连接到信用卡贷方的计算机;
询问该计算机,消费者的信用卡号是否有效;
询问该计算机,消费者是否有足够的信用。
当 CheckDatabase 类执行其工作时,如果信用卡贷方的计算机根本不应答电话请求将如何呢?这种错误正是try-catch块要解决的,在CheckDatabase类使用它来处理连接错误。
如果CheckDatabase类自己处理这种错误,CreditCardChecker应用程序将根本不知道发生了异常。这不是好主意——应用程序应知道不能建立连接,以便向使用应用程序的用户报告。
通知应用程序CreditCardChecker的一种方法是,在CheckDatabase类中使用catch块捕获异常,然后使用throw语句抛出它。异常将在CheckDatabase中抛出,CheckDatabase必须像处理其他异常一样处理它。
出现错误或其他不正常情况时,异常处理是不同类相互进行通信的一种方式。
当在捕获父类异常(比如Exception)的catch块中使用throw时,将会抛出该父类的异常。这样将无法获悉所发生错误的详情,因为子类异常(比如NumberFormatException)提供的信息要比Exception类更为详细。
Java 7 提供了新的方法来记录异常的详情:catch 语句中的 final 关键字。
catch语句中的final关键字会导致throw语句在执行时,表现出不同的行为,即抛出所捕获的特定异常类。
本章将介绍的最后一种技术是如何完全忽略异常。在类中,可以在方法的定义中使用关键字throw,让方法忽略异常。
下面的方法抛出MalformedURLException,这是在Java程序中使用网络地址时可能发生的一种错误:
其中的第二条语句 URL page = new URL(address);创建一个 URL 对象,该对象表示一个网络地址。URL类的构造函数抛出MalformedURLException,指出使用的地址无效,因此无法创建这样的对象。下面的语句将抛出这样的异常:
字符串http:www.java24hours.com不是有效的URL,因为冒号后少一些符号:两个反斜线(//)。
由于 loadURL( )方法被声明为抛出 MalformedURLException 错误,因此不用在方法中处理它们。这种异常将由调用 loadURL( )方法的方法负责处理。
18.2 抛出和捕获异常
接下来创建一个类,该类使用异常将发生的错误告诉另一个类。
该类为HomePage,代表网上的个人网站。PageCatalog是一个对个人网站进行分类的应用程序。
创建一个新的Java空文件,将其命名为HomePage,然后输入程序清单18.4中的完整文本。
程序清单 18.4 HomePage.java 的完整源代码
你可以在其他程序中使用这个编译后的HomePage类。这个类代表网上的个人网站,它包含3个实例变量:address(代表网站地址的URL对象)、owner(代表网站的主人)和category (描述网站主要内容的注释)。
与创建RUL对象的其他类一样,HonePage也必须在try-catch块中处理MalformedURL Exception错误或声明忽略这些错误。
这个类采用后一种方法,如第8~9行和第15~16行所示。通过在两个构造函数中使用throw语句,HomePage避免了处理MalformedURLException错误。
为了创建一个使用HomePage类的应用程序,回到NetBeans并创建一个名为PageCatalog的Java空文件,然后输入程序清单18.5中的文本。
程序清单 18.5 PageCatalog.java 的完整源代码
运行编译后的程序时,将显示如下输出:
Output ▼
应用程序PagaCatalog创建一个HomePage对象数组,然后显示该数组的内容。创建每个HomePage对象时最多使用3个参数:
网站主人的姓名;
网站地址(String而不是URL);
页面分类。
第3个参数是可选的,第15~16行没有使用。
HomePage 类的构造函数收到不能转换为有效 URL 对象的字符串时,将抛出MalformedURLException错误。这些异常在PagaCatalog应用程序中使用来try-catch块来处理。
要修复导致“no protocol”错误的问题,编辑第 16 行,使字符串以 http://打头,就像第 7~14行的网络地址一样。当再次运行该程序时,其输出如图18.3所示。
图18.3 PageCatalog应用程序的输出
18.3 总结
使用Java异常处理技术后,希望有关错误的主题要更受读者的欢迎。
可以使用这种技术做很多工作:
捕获异常并处理它;
忽略异常,将其留给另一个类或解释器去处理;
在同一个try-catch块中捕获多种异常;
抛出自己的异常。
通过在Java程序中管理异常,可使程序更可靠、更通用、更易于使用,因为别人在使用你的软件时,不会显示晦涩的错误消息。
18.4 问与答
问:能够创建自己的异常吗?
答:可以通过创建现有异常(如Exception,它是所有异常的超类)的子类来创建自己的异常。在 Exception 的子类中,可能需要覆盖两个方法:不接受任何参数的 Exception( )方法和接受一个 String 参数的Exception( )方法。在后一个方法中,字符串参数应是一条消息,它描述发生的错误。
问:除异常外,本章为什么没有描述如何抛出和捕获错误?
答:Java 将问题分为错误和异常两类,因为它们的严重程度不同。异常不那么严重,因此应在程序中使用try-catch或throw来处理它们;而错误更严重,仅在程序中处理它们是不够的。
有关错误的两个例子是栈溢出和内存耗尽,这些错误可能导致Java解释器崩溃,而且在解释器运行时,没有办法在程序中修复这种错误。
18.5 测验
本章充斥“错误”一词,看看能否不出任何错误地回答下列问题。
1.单条catch语句能够处理多少种异常?
a.一种。
b.多种异常。
c.我不想回答这个问题。
2.finally部分中的语句在什么情况下运行?
a.出现异常且try-catch块结束后。
b.不出现异常且try-catch块结束后。
c.两者都是。
1.b.catch语句中的Exception对象能够各种异常。
2.c.finally部分中的语句总是在try-catch块执行完毕后执行,而不管是否发生异常。
18.6 练习
要检测你是否是一名优秀的Java程序员,尽量少犯错误地完成下列练习。
修改应用程序NumberDivider,使其抛出它能够捕获的任何异常。然后运行程序,看看发生的情况。
第15章创建的LottoEvent类有一个try-catch块,根据这个块创建自己的Sleep类,它处理InterruptedException异常,这样其他类(如LottoEvent)就不需要处理这种异常了。
有关为完成这些练习而编写的Java程序,请访问本书的配套网站www.java24hours.com。