在理想状态下
,
用户输人数据的格式永远都是正确的
,
选择打开的文件也一定存在
,
并
且永远不会出现 bug。在现实世界中却充满了不良的数据和带有问题的代码,现在是讨论 Java 程序设计语育处理这些问题的机制的时候了
人们在遇到错误时会感觉不爽
。
如果一个用户在运行程序期间
,
由于程序的错误或一些
外部环境的影响造成用户数据的丢失
,
用户就有可能不再使用这个程序了
,
为了避免这类事
情的发生, 至少应该做到以下几点:
向用户通告错误
;
•
保存所有的工作结果
;
•
允许用户以妥善的形式退出程序
对于异常情况
,
例如
,
可能造成程序崩溃的错误输入
,
Java
使 用 一 种 称 为 异 常 处 理
(
exception
handing
)
的错误捕获机制处理
。
Java
中的异常处理与
C
+
+
或
Delphi
中的异常处理
十分类似
在测试期间
,
需要进行大量的检测以验证程序操作的正确性
。
然而
,
这些检测可能非常耗
时
,
在测试完成后也不必保留它们
,
因此
,
可以将这些检测删掉
,
并在其他测试需要时将它们粘
贴回来
,
这是一件很乏味的事情
。
本章的第
2
部分将介绍如何使用断言来有选择地启用检测
。
当程序出现错误时
,
并不总是能够与用户或终端进行沟通
。
此时
,
可能希望记录下出现
的问题
,
以备日后进行分析
。
本章的第
3
部分将讨论标准
Java
日志框架
处 理 错 误
假设在一个 Java
程序运行期间出现了一个错误
。
这个错误可能是由于文件包含了错误
信息
,
或者网络连接出现问题造成的
,
也有可能是因为使用无效的数组下标
,
或者试图使用
一个没有被赋值的对象引用而造成的
。
用户期望在出现错误时
,
程序能够采用一些理智的行
为
。
如果由于出现错误而使得某些操作没有完成
,
程序应该
:
•返回到一种安全状态,并能够让用户执行一些其他的命令
;
或者
•
允许用户保存所有操作的结果
,
并以妥善的方式终止程序
要做到这些并不是一件很容易的事情
。
其原因是检测
(
或引发
)
错误条件的代码通常离
那些能够让数据恢复到安全状态
,
或者能够保存用户的操作结果
,
并正常地退出程序的代码
很远
。
异常处理的任务就是将控制权从错误产生的地方转移给能够处理这种情况的错误处理
器
。
为了能够在程序中处理异常情况
,
必须研究程序中可能会出现的错误和问题
,
以及哪类
问题需要关注
1
.
用户输入错误
除了那些不可避免的键盘输人错误外
,
有些用户喜欢各行其是
,
不遵守程序的要求
。
例
如
,
假设有一个用户请求连接一个
URL
,
而语法却不正确
。
在程序代码中应该对此进行检
查
,
如果没有检査
,
网络层就会给出警告
。
2
.
设备错误
硬件并不总是让它做什么
,
它就做什么
。
打印机可能被关掉了
。
网页可能临时性地不能浏
览
。
在一个任务的处理过程中
,
硬件经常出现问题
。
例如
,
打印机在打印过程中可能没有纸了
。
3
.
物理限制
磁盘满了
,
可用存储空间已被用完
4
.
代码错误
程序方法有可能无法正确执行
。
例如
,
方法可能返回了一个错误的答案
,
或者错误地调
用了其他的方法
。
计算的数组索引不合法
,
试图在散列表中查找一个不存在的记录
,
或者试
图让一个空找执行弹出操作
,
这些都属于代码错误
在
Java
中
,
如果某个方法不能够采用正常的途径完整它的
任务
,
就可以通过另外一个路径退出方法
。
在这种情况下
,
方法并不返回任何值
,
而是抛出
(
throw
)
一个封装了错误信息的对象
。
需要注意的是
,
这个方法将会立刻退出
,
并不返回任
何值
。
此外
,
调用这个方法的代码也将无法继续执行
,
取而代之的是
,
异常处理机制开始搜
索能够处理这种异常状况的异常处理器
(
exception
handler
)
。
异常分类
在
Java
程序设计语言中
,
异常对象都是派生于 Throwable
类的一个实例。
![](https://i-blog.csdnimg.cn/blog_migrate/c6c97394b841d8f211cae05262b70d3d.png)
图 7-1 Java 中的异常层次结构
需要注意的是
,
所有的异常都是由
Throwable
继承而来
,
但在下一层立即分解为两个分
支
:
Error
和
Exception
:
'
Error
类层次结构描述了
Java
运行时系统的内部错误和资源耗尽错误
。
应用程序不应该
抛出这种类型的对象
。
如果出现了这样的内部错误
,
除了通告给用户
,
并尽力使程序安全地
终止之外
,
再也无能为力了
。
这种情况很少出现
。
在设计
Java
程序时
,
需要关注
Exception
层次结构
。
这个层次结构又分解为两个分支
:
一个分支派生于
RuntimeException
;
另一个分支包含其他异常
。
划分两个分支的规则是
:
由
程序错误导致的异常属于
RuntimeException
;
而程序本身没有问题
,
但由于像
I
/
O
错误这类
问题导致的异常属于其他异常
:
派生于
RuntimeException
的异常包含下面几种情况
:
•
错误的类型转换
。
•数组访问越界
•
访问
null
指针
不是派生于
RuntimeException
的异常包括
:
•
试图在文件尾部后面读取数据
。
•
试图打开一个不存在的文件
。
•
试图根据给定的字符串查找
Class
对象
,
而这个字符串表示的类并不存在
,
“
如果出现
RuntimeException
异常
,
那么就一定是你的问题
”
是一条相当有道理的规则
。
应该通过检测数组下标是否越界来避免
ArraylndexOutOfBoundsException
异常
;
应该通过在
使用变量之前检测是否为
null
来杜绝
NullPointerException
异常的发生
:
声明受查异常
如果遇到了无法处理的情况
,
那么
Java
的方法可以抛出一个异常
。
这个道理很简单
:
一
个方法不仅需要告诉编译器将要返回什么值
,
还要告诉编译器有可能发生什么错误
。
例如
,
一段读取文件的代码知道有可能读取的文件不存在
,
或者内容为空
,
因此
,
试图处理文件信
息的代码就需要通知编译器可能会抛出
IOException
类的异常
方法应该在其首部声明所有可能抛出的异常
。
需要记住在遇到下面
4
种
情况时应该抛出异常
:
1
)
调用一个抛出受査异常的方法
,
例如
,
FilelnputStream
构造器
。
2
)
程序运行过程中发现错误
,
并且利用
throw语句抛出一个受查异常
(
下一节将详细地
介绍
throw
语句
)
。
3 )
程序出现错误
,
例如
,
a
[
-
l
]
=
0 会抛出一个
ArraylndexOutOffloundsException
这样的
非受查异常
。
4
)
Java
虚拟机和运行时库出现的内部错误
对于那些可能被他人使用的
Java
方法
,
应该根据异常规范
(
exception
specification
)
,
在
方法的首部声明这个方法可能抛出的异常
。
如果一个方法有可能抛出多个受查异常类型
,
那么就必须在方法的首部列出所有的异常
类
。
每个异常类之间用逗号隔开
。
如下面这个例子所示
:
![](https://i-blog.csdnimg.cn/blog_migrate/c3452c93043c55b969e82a48b7d0aa75.png)
同样
,
也不应该声明从
RuntimeException
继承的那些非受查异常
这些运行时错误完全在我们的控制之下
。
如果特别关注数组下标引发的错误
,
就应该将
更多的时间花费在修正程序中的错误上
,
而不是说明这些错误发生的可能性上
。
总之
,
一个方法必须声明所有可能抛出的受查异常
,
而非受查异常要么不可控制
(
Error
)
,
要么就应该避免发生
(
RuntimeException
)
。
如果方法没有声明所有可能发生的受查异常
,
编
译器就会发出一个错误消息。
当然
,
从前面的示例中可以知道
:
除了声明异常之外
,
还可以捕获异常
。
这样会使异常
不被抛到方法之外
,
也不需要
throws
规范
如何抛出异常
在前面已经看到
,
对于一个已经存在的异常类
,
将其抛出非常容易
在这种情况下
:
1
)
找到一个合适的异常类
。
2
)
创建这个类的一个对象
。
3
)
将对象抛出
。
一旦方法抛出了异常
,
这个方法就不可能返回到调用者
。
也就是说
,
不必为返回的默认
值或错误代码担忧
。
捕获异常
如果某个异常发生的时候没有在任何地方进行捕获
,
那程序就会终止执行
,
并在控制台
上打印出异常信息
,
其中包括异常的类型和堆栈的内容
。
要想捕获一个异常
,
必须设置
try
/
catch
语句块
。
最简单的
try
语句块如下所示
![](https://i-blog.csdnimg.cn/blog_migrate/44a17fdb7c2b2db30cb2ec7d2b0a7924.png)
如果在 try语句块中的任何代码抛出了一个在 catch 子句中说明的异常类, 那么
1
)
程序将跳过
try语句块的其余代码
2
)
程序将执行
catch
子句中的处理器代码
。
如果在
try
语句块中的代码没有拋出任何异常
,
那么程序将跳过
catch
子句
。
如果方法中的任何代码拋出了一个在
catch
子句中没有声明的异常类型
,那么
这个方法就会立刻退出(
希望调用者为这种类型的异常设
catch
子句
。
)
通常
,
最好的选择是什么也不做
,
而是将异常传递给调用者
。
如果
read
方法出现了错
误
,
就 让
read
方法的调用者去操心
!
如果采用这种处理方式
,
就必须声明这个方法可能会拋
出一个
K
)
Exception
。
![](https://i-blog.csdnimg.cn/blog_migrate/1b6b4b2b1a3a28bf6e9b960cd9ccfd4a.png)
请记住
,
编译器严格地执行
throws
说明符
。
如果调用了一个抛出受查异常的方法
,
就必
须对它进行处理
,
或者继续传递
。
记录曰志
每个
Java
程序员都很熟悉在有问题的代码中插入一些
System
.
out
.
println
方法调用来帮助
观察程序运行的操作过程
。
当然
,
一旦发现问题的根源
,
就要将这些语句从代码中删去
。
如
果接下来又出现了问题
,
就需要再插入几个调用
System
.
out
.
println
方法的语句
。
记录日志
API
就是为了解决这个问题而设计的
。
见日志 专题