程序执行时,运行环境的许多因素会影响到程序的正常运行,例如网络是否连接、文件是否存在等等,Java中使用异常机制来帮助程序编写者处理这些理想之外的意外情况.
Java中异常类的层次结构
Java中预定义异常的主要机构:
graph BT Error -.-> Throwable Exception -.-> Throwable IOException -.-> Exception RuntimeException -.-> Exception
所有的异常类都继承自Throwable
类,Error
与Exception
是Throwable
的直接子类,其中Error
异常一般是在系统发生内部错误时抛出.
Exception
下又有两大常见的异常RuntimeException
和IOException
,其中:
RuntimeException
即运行时异常,通常在程序本身发生错误时自动抛出,例如:
- 访问一个对象的方法时,对象为
null
,抛出空指针异常NullPointerException
- 访问数组的下标超过了数组的实际长度,抛出数组下标越界异常
ArrayIndexOutOfBoundsException
- 错误的进行强制类型转换,抛出类型转换异常
ClassCastException
IOException
是程序运行时,由于外界因素导致的I/O操作异常,例如:
- 访问的文件不存在,抛出
FileNotFoundException
异常 - 到达文件结尾,继续读取抛出
EOFException
异常
checked与unchecked异常
Java中分为checked与unchecked两种异常,其中unchecked异常可以不进行处理,而checked异常必须进行捕获处理(或者向上层抛出)
Error
与RuntimeException
异常均属于unchecked异常,程序中不强制对它们进行处理- 除此之外的异常属于checked的异常,这些异常必须被处理
异常处理
在进行方法调用时,如果方法声明了可能会抛出unchecked异常,则需要进行异常捕获或者继续向上抛出.
异常捕获
Java中使用try语句包围可能发生异常的语句,再使用catch子句对相应类型的异常进行捕获:
1 | File file = new File("demo.txt"); |
catch中明确表明需要捕获的异常类型,这样可以捕获声明的异常类型及其子类型,如果需要,可以使用多个catch子句来捕获不同的异常:
1 | try { |
使用多个catch子句时,需要注意,当方法抛出异常时,会根据catch中声明的异常类型,按照定义的顺序,挨个寻找异常处理器,一旦寻找到合适的处理器,就立即应用而不再向后继续搜寻,因此,越具体的异常,应该越靠前进行声明.
除此之外,自JDK 7起,如果对待多个异常,希望使用相同的逻辑来处理,可以在catch子句中声明多个异常:
1 | try { |
finally子句
当代码中发生异常时,异常发生位置后面的语句不再运行,此时程序直接跳转到相应的catch子句中. 如果希望有些语句始终会被执行,而不会受异常影响(例如释放被占用的资源),可以使用finally子句:
1 | FileInputStream fileInputStream = null; |
使用finally子句,需要留意几点:
- try语句可以只包含finally子句,而没有catch子句
- 在一个try语句块中,finally始终最后执行
- 如果finally中有返回语句,则其返回的值会始终覆盖try语句中的返回值
- 如果finally中抛出异常,则会覆盖try或者catch语句中抛出的异常
带资源的try语句
我们经常使用try/finally组合语句来打开和关闭一个资源的输入输出流:
1 | try { |
这样的结构可以保证资源被正确的释放,自JDK 7起,可以使用带资源的try语句来简化这一点:
1 | try (IOStream stream = ...) { |
使用带资源的try语句时,资源会自动的被关闭(即资源的close()
方法会自动的被调用),但前提是,这个资源类实现了AutoCloseable
方法,例如对于之前打开文件输入流的例子,现在可以使用带资源try语句对其进行简化:
1 | try (FileInputStream fileInputStream = new FileInputStream("demo.txt")) { |
对于带资源的try语句,当程序离开try语句时,会自动调用资源的close()
方法,还有一点与一般try语句不同的是,带资源try语句后的catch语句同样也可以用来捕获close()
方法执行时抛出的异常:
1 | try (FileInputStream fileInputStream = new FileInputStream("demo.txt")) { |
还有一种情况,如果try语句也同时抛出了异常,调用close()
方法抛出的异常不会覆盖try语句中待抛出的异常,而是被自动的添加到原来异常的suppressed的异常列表中(异常对象有个addSuppressed()
方法,可通过该方法向目标异常中添加suppressed
异常).
向上抛出异常
有时候,发生异常时不应该立即捕获处理,应该交由调用者来决定如果处理该异常,此时可以将异常向上抛出. 此时只需要在方法后声明可能抛出的异常列表即可(多个异常使用,
隔开):
1 | private void method() throws ExceptionA, ExceptionB { |
自定义异常与手动抛出异常
自定义异常
Java预定义的异常经常不能满足实际的业务需求,此时可以根据需要进行自定义异常.
自定义异常时需要继承Throwable
,通常的自定义异常都直接实现Exception
或RuntimeException
:
- 如果自定义的异常需要强制被处理,则可以继承
Exception成为checked异常
- 如果不需要强制处理,则可以继承
RuntimeException
成为unchecked异常
例如自定义一个网络连接失败异常:
1 | class NetworkConnectedFailedException extends Exception { |
手动抛出异常
当发生错误时,我们可以根据需要手动抛出一个异常,来通知调用者进行处理,Java中可以使用throw
关键字来抛出一个异常对象:
1 | if (time > MAX_CONNECT_TIME) { |
当然,在一个方法中手动抛出一个异常后,需要在方法后声明将会抛出的异常列表:
1 | public void connect() throws NetworkConnectedFailedException { |
包装异常
有时候,我们的方法中不需要关注底层抛出了何种异常,而应该将底层的异常根据业务需要,封装成我们自己的异常,对一场进行二次包装,变成符合我们自己需要的异常即为包装异常,包装异常的意义在于,我们想要抛出自己的异常,同时又不想丢失引起异常的信息. 通过异常对象的initCause(Throwable cause)
方法即可将原始异常存入新建的异常中,例如:
1 | public void open() throws OpenFileFailedException { |
断言
异常的存在是为了帮助我们处理程序运行期间发生的意外情况,而断言是为了声明程序能够正确执行的先决条件. 断言只用于程序的开发测试阶段.
使用断言
Java中使用assert
关键字来声明断言,有两种格式的断言
1 | assert condition; |
断言声明语句中,condition
是一个条件表达式,该表达式返回true
或者false
, 返回true,则表示符合条件,程序继续向下执行,返回false则会抛出AssertionError
异常.
expression
用于定义错误信息,它可以是字符串,或者能够转化为字符串的对象.
例如:
1 |
|
注意:断言默认是关闭的,需要在程序运行期间指定虚拟机参数-ea
来打开断言(如使用java -ea
来执行程序).