Java 异常
Java 提供了异常机制,使得程序有更好的容错性和健壮性。
一、概述
如果没有异常机制,为了尽可能地保证程序地正常运行,应该这么做:
1 |
|
这样的做法有两个问题:
- 无法穷举所有的异常情况,总会有“漏网之鱼”,程序的健壮性差
- 错误处理代码和业务实现代码混杂在一起,程序的可读性、可维护性差
Java 的异常机制可以很好地解决异常。
二、异常类
Throwable:所有错误和异常的父类
Error:错误;一般是与虚拟机相关的问题,无法被处理
例如:虚拟机运行错误、类定义错误、内存不足错误、栈溢出错误等
Exception:异常;表示程序自身的问题,应该被处理
Runtime 异常:运行时异常;可以选择处理也可以选择不处理;出现的原因一般是程序自身错误,应该通过修改代码逻辑来解决
例如:空指针异常、下标越界错误
Checked 异常:检查性异常;必须显式处理,可以通过
try...catch
或throws
处理;出现的原因一般是外界错误,无法通过修改代码逻辑解决例如:
一段读取文件的代码,即使代码逻辑没有任何问题,但可能在执行时因为文件找不到而出错。因此,编译器提示程序员捕捉并处理这些可能出现的异常
三、Exception 的不同类型如何理解?
可以这样理解,
- 编译器不负责检查程序的逻辑错误。首先,程序逻辑的书写是程序员的工作,程序员应该保证代码的正确性;其次,编译器不可能 “模拟” 运行代码以检查是否会出现空指针、越界等错误。
- 程序中存在很多(与代码逻辑无关而与外部有关的)预期会出现的问题,编译器会检查并要求程序进行处理。
四、Try…Catch
1. 语法
1 |
|
2. 处理机制
如果在执行时出现了异常,系统会自动生成一个异常对象,该异常对象被抛出(提交给 JRE)
当 JRE 收到异常对象时,会首先寻找能够处理该异常对象的 catch 块
- 如果找到合适的 catch 块,就会将异常对象交给 catch 块处理,这个过程被称为抛出异常
- 如果找不到合适的 catch 块,则程序终止
进入 catch 块后,方法中的剩余代码都将不会被执行,并且其它 catch 块也不会被执行
3. 排布规则
1 |
|
出现错误时,Java 总是从上到下检索 catch 块,如果将子异常类写在父异常类之后,则子异常类永远不会被执行。
因此,应当遵循这样的规则:
- 将子异常类放置在前,父异常类放置在后
- 将 Exception 放置在最后用于捕捉“漏网之鱼”
4. 多异常捕获
Java 7 之后,一个 catch 可以用于捕获多种类型的异常,需要注意以下两点:
- 多种异常类型之间用
|
隔开 - 异常变量会被隐形 final 修饰,无法再被重新赋值
1 |
|
5. 异常信息的访问
1 |
|
6. finally
(1) 收尾工作
在程序中往往需要进行”收尾工作”,例如:关闭数据库连接、关闭网络连接、关闭文件等。
根据异常类的处理机制,如果在 try 中放置”收尾动作”,假设在“收尾工作”之前出现异常,程序会跳至对应的 catch 块,”收尾工作无法被执行”;如果在 try 和 catch 中都放置”收尾动作”,这的确能够实现”收尾”的要求,但代码大量重复,不是一个好的处理方式。
因此,我们希望有个地方可以放置收尾工作,并且希望收尾工作能保证始终执行。
(2) finally
Java 提供了 finally ,专门用于”收尾”。
1 |
|
无论 try 中是否出现异常,无论哪个 catch 块被执行,无论 try 和 catch 中是否执行了 return
,finally 总会被执行。
(3) 注意
- finally 应该放置在所有 catch 的后面
- finally 始终都会执行,除非虚拟机关闭
- 如果在 try、catch 中执行了 return/throw,则其值将会被缓存,待到 finally 执行完后再次 return/throw。但是如果在 finally 中也执行了 return/throw,则将发生覆盖,try、catch 中的 return/throw 失效,finally 中的 return/throw 得到执行
7. 自动收尾
(1) Java 7
Java 7 允许在 try 之后紧跟一对圆括号,可以在其中声明、初始化资源(例如数据库连接、网络连接、文件等),系统会在 try 语句结束后自动关闭这些资源。
需要注意的是,为保证资源可以被正常关闭,这些资源实现类应该实现 AutoCloseable 或 Closeable 接口。
1 |
|
(2) Java 9
Java 9 再次对这种语法进行了增强,不再要求必须在圆括号中声明、初始化资源,只需要在圆括号中填入以 final 修饰的资源变量或仅被赋值一次的资源变量即可。
1 |
|
五、throws
1. 说明
某些异常并不能在当前方法中处理,而是应该由上级方法进行处理,通过 throws 将异常抛给上级。
在上级中,可以对异常进行处理,或将异常继续向上抛出。如果 main 方法中也未处理异常并将异常向上抛出,则异常将交由 JVM 处理,JVM 会打印信息并终止程序运行。
2. 语法
1 |
|
3. 方法重写时对 throws 的限制
子类方法抛出的异常类型应该是父类方法抛出的异常类型或其子类,
子类方法抛出的异常类型应该与父类方法抛出的异常类型相等或更少
六、throw
1. 说明
可以通过 throw 向外手动抛出异常。
2. 语法
1 |
|
3. 示例
1 |
|
七、自定义异常类
可以自定义异常类,并且
- 如果希望自定义 Checked 异常类,需要继承 Exception 类
- 如果希望自定义 Runtime 异常类,需要继承 RuntimeException 类
通常情况下,提供两个构造器:
- 无参构造器
- 带一个字符串参数的构造器,字符串将作为描述信息
示例:
1 |
|
八、嵌套使用
try…catch、throws、throw 可以嵌套使用,例如:
- 对异常进行部分处理后再抛出,由外层继续处理
- 获取异常信息后,抛出一个新的异常,在新的异常中对异常信息进行部分隐藏
九、注意事项
不要过度使用异常
- 对于代码能够解决的错误,应该通过修改代码的方式解决
- 对于外部的、不能预知的错误,才通过异常解决
异常机制的性能较差,能通过代码解决的问题应该尽量通过代码解决
不要使用过于庞大的 try 块,应该分别捕获并处理
不要直接用
catch(Exception e)
捕获所有异常,应该使用对应的异常类进行拦截不要忽略异常
参考
疯狂 Java 讲义