1.内容涉及
- 处理错误
- 捕获异常
- 使用异常的技巧
2.内容详解
(一)处理错误
在理想世界里,用户输入输入数据的格式永远是正确的,选择打开的文件也一定存在,代码永远不会出现bug。但现实是,程序员经常会转角遇到爱…
假设在一个Java程序运行期间出现了一个错误。这个错误可能是由于文件包含错误信息,或者网络连接出现问题造成的,也有可能是因为使用了无效的数组下标,或者试图使用一个没有被赋值的对象引用而造成的。用户期望在出现错误时,程序能够采取合理的行为。如果由于出现错误而使得某些操作没有完成,程序应该:
返回到一种安全状态,并能够让用户执行其他的命令;或者
允许用户保存所有工作的结果,并以妥善的方式终止程序。
异常处理的任务就是将控制权从产生错误的地方转移到能够处理这种情况的错误处理器。为了能够处理程序中的异常情况,必须考虑到程序中可能会出现的错误和问题。那么需要考虑哪些问题呢?
用户输入错误。除了哪些不可避免的键盘输入错误外,有些用户喜欢自行其是,而不遵守程序的要求(咱也没办法 )。例如,假设有一个用户请求连接一个URL,而这个URL语法却不正确。你的代码应该对此进行检查,如果没有检查,网络层就会报错。
设备错误。硬件并不总是让它做什么,它就做什么。打印机可能被关掉了。网页坑你临时性地不能浏览。在任务的处理过程中,硬件经常会出现问题。例如,打印机在打印过程中可能没有纸了。
物理限制。磁盘已满,你可能已经用尽了所有可用的存储空间。
代码错误。程序方法有可能没有正确的完成工作。例如,方法可能返回一个作物的答案,或者错误的调用了其他的方法。计算一个无效的数组索引,试图在散列表中找一个不存在的记录,或者试图让一个空栈执行弹出操作,这些都属于代码错误。
2.异常分类
在Java程序设计语言中,异常对象都是派生于Throwable类的一个实例。稍后还可以看到,如果Java中内置的异常类不能满足需求,用户还可以创建自己的异常类。图1是Java异常层次结构的一个简化示意图。
派生于RuntimeException的异常包括以下问题:
- 错误的强制类型转换
- 数组访问越界
- 访问null指针
不是派生于RuntimeException的异常包括: - 试图超越文件末尾继续读取数据
- 试图打开一个不存在的文件
- 试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在。
如果出现RuntimeException异常,那么就一定是你的问题。“是否存在”取决于环境,而不只是取决于你的代码。Java语言规范将派生于Error类或RuntimeException类的所有异常称为非检查型异常,所有其他的异常称为检查型异常。
3.声明检查型异常
在自己编写方法时,不必声明这个方法可能抛出的所有异常。至于什么时候需要在方法中用throws子句声明异常,以及要用throws子句声明哪些异常,需要记住在遇到下面4种情况时会抛出异常:
- 调用了一个抛出检查型异常的方法,例如,FileInputStream构造器。
- 检测到一个错误,并且利用throw语句抛出一个检查型异常。
- 程序出现错误,例如,a[-1]=0会抛出一个非检查型异常
- Java虚拟机或运行时库出现内部错误。
如果出现前两种情况,则必须告诉调用这个方法的程序员有可能抛出异常。因为任何一个抛出异常的方法都有可能是一个死亡陷阱。如果没有处理器捕获这个异常,当前执行的线程就会终止。
如果一个方法有可能抛出多个检查型异常类型,那么就必须在方法的首部列出所有的异常类。每个异常类之间用逗号隔开。如下面这个例子所示:
class MyAnimation
{
public Image loadImage(String s) throws FileNotFoundException,FOFException
{
...
}
}
但是,不需要声明Java的内部错误,即从Error继承的异常。任何程序代码都有可能抛出那些异常,而我们对此完全无法控制。如果特别担心数组小标错误,就应该多花时间修正这些错误,而不只是声明这些错误有可能发生。
当然,不只是声明异常,你还可以捕获异常。这样就不会从这个方法抛出这个异常,所以也没有必要使用throws。下文将会讨论如何决定究竟是捕获一个异常,还是将其抛出由其他人捕获。
4.如何抛出异常
如果一个已有的异常类能够满足你的要求,抛出这个异常就非常容易。在这种情况:
- 找到一个合适的异常类。
- 创建这个类的一个对象。
- 将对象抛出。
举个栗子:
String readData(Scanner in)thorws EOFException
{
while(...)
{
if(!in.hasNext())
{
if(n<len)
throw new EOFException();
}
...
}
return s;
}
一旦方法抛出 异常,这个方法就不会返回调用者。也就是说,不必操心建立一个默认的返回值或错误码。
5.创建异常类
你的代码可能会遇到任何标准异常类都无法描述清楚的问题。在这种情况下,创建自己的异常类就是一件顺利成章的事情了。我们需要做的只是定义一个派生于Exception的类,或者派生于Exception的某个子类,如IOEception。习惯做法是,自定义的这个类应该包含两个构造器,一个是默认的构造器,另一个是包含详细描述信息的构造器(超类Throwable的toString方法会返回一个字符串,其中包含这个详细信息,这在调试中非常有用)。
class FileFormatException extends IOException
{
public FileFormatException(){}
public FileFormatException(String gripe)
{
super(gripe);
}
}
(二)捕获异常
抛出一个异常,这个过程十分容易,只要将其抛出就不用理睬了。当然,有些代码必须捕获异常。捕获异常需要做更多规划。这正是接下来要介绍的内容。
如果发生了某个异常,但没有在任何地方捕获这个异常,程序就会终止,并在控制台上打印一个消息,其中包括这个异常的类型和一个堆栈轨迹。要想捕获一个异常,需要设置try/catch语句块。最简单的try语句块如下所示:
try
{
code
more code
}
catch(ExceptionType e)
{
handler for this type
}
如果try语句块中的任何代码抛出了catch子句中指定的一个异常类,那么
- 程序将跳过try语句块的其余代码。
- 程序将执行catch子句中的处理器代码。
- 如果try语句块中的代码没有抛出任何异常,那么程序将跳过catch子句。
- 如果方法中的任何代码抛出了catch子句中没有声明的一个异常类型,那么这个方法就会立即退出(希望它的调用者为这种类型的异常提供了catch子句)。
请记住,编译器严格地执行throws说明符。如果调用了一个抛出检查型异常的方法,就必须处理这个异常,或者继续传递这个异常。
那种方法更好呢?一般经验是,要捕获那些你知道如何处理的异常,而继续传播那些你不知道怎样处理的异常。如果想传播一个异常,就必须在方法的首部添加一个throws说明符,提醒调用者这个方法可能会抛出异常。
2.捕获多个异常
在一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。要为每个异常类型使用一个单独的catch子句,如下例所示:
try
{
code that might throw exception
}
catch(FileNotFoundException e)
{
emergency action for missing files
}
catch(UnknownHostException e)
{
emergency action for unknown host;
}
catch(IOException e)
{
emergency action for all other I/O problems
}
异常对象可能包含有关异常性质的信息。要想获得这个对象的更多信息,可以尝试使用e.getMessage()得到详细的错误信息(如果有的话),或者使用e.getClass().getName()得到异常对象的实际类型。
3.finally语句
代码抛出一个异常时,就会停止处理这个方法中剩余的代码,并退出这个方法。如果这个方法已经获得了只有它自己知道的一些本地资源,而且这些资源必须清理,这就会有问题。一种解决方案是捕获所有异常,完成资源的清理,再重新抛出异常。但是,这种解决方案比较繁琐,这是因为需要在两个地方清理资源分配。一个在正常的代码中;另一个在异常代码中。finally子句可以解决这个问题。
不管是否有异常被捕获,finally子句中的代码都会执行。下面的示例中,所有情况下程序都将关闭输入流。
FileInputStream in=new FileInputStream(...);
try
{
...
}
catch(IOException e)
{
...
}
finally
{
in.close();
}
try语句可以只有finally子句,而没有catch子句。例如,下面这条try语句:
InputStream in=...
try
{
code that might throw exceptions
}
finally
{
in.close();
}
无论在try语句块中是否遇到异常,finally子句中的in.close()语句都会执行。当然,如果真的遇到一个异常,这个异常将会被重新抛出,并且必须由另外一个catch子句捕获。
InputStream in=...
try
{
try
{code that might throw exceptions}
finally
{
in.close();
}
}
catch(IOException e)
{
show error message
}
内层的try语句块只有一个职责,就是确保关闭输入流。外层的try语句块也只有一个职责,就是确保报告出现的错误。这种解决方案不仅清楚,而且功能更强:将会报告finally子句中出现的错误。
注:当finally子句包含return语句时,有可能产生意想不到的结果。假设利用return语句从try语句块中间退出。在方法返回前,会执行finally子句块。如果finally块也有一个return 语句,这个返回值将会屏蔽原来的返回值。来看看下面这个例子:
public static int parseInt(String s)
{
try
{
return Integer.parseInt(s);
}
finally
{
return 0;
}
}
看起来在parseInt(“42”)调用中,try块的体会返回整数42。不过,这个方法真正返回之前,会执行finally子句,这就使得方法最后会返回0,而忽略原先的返回值。
更糟糕的是,考虑调用parseInt(“zero”)。Integer.parseInt方法会抛出一个Number-FormatException。然后执行finally子句,return 语句甚至会“吞掉”这个异常!
finally子句的体要用于清理资源。不要把改变控制流的语句(return,throw,break,continue)放在finally子句中。
(三)使用异常的技巧
- 异常处理不能代替简单的测试
- 不要过分地细化异常
- 充分利用异常层次结构
- 不要压制异常
- 在检测错误时,“苛刻”要比放任更好
- 不要羞于传递异常