异常机制可以使得程序中的异常处理代码和正常业务打代码分离,保证程序代码更加优雅,并可以提高程序的健壮性。
一、概述
在程序运行过程中出现错误,导致程序出现非预期场景。异常处理可以保证出现错误后,控制接下来的程序流程,是选择定位错误信息,还是抛出异常或捕获异常、还是避免程序非正常退出,都取决于我们。异常机制已经成为判断一门编程语言是否成熟的标准,异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。
异常:程序在运行过程中发生由于外部问题导致的程序异常事件,发生的异常会中断程序的运行。(在Java
等面向对象的编程语言中)异常本身是一个对象,产生异常就是产生了一个异常对象。注意在Java
中异常不是错误,在下文的异常的分类中有解释。
Java
异常机制主要依赖于try
、catch
、finally
、throw
、throws
五个关键词。
- 1.
try
:后紧跟一个花括号括起来的代码块(花括号不可以省略),简称try块,里面放置可能引发异常的代码; - 2.
catch
:后面对应异常类型和一个代码块,用于表明该catch
块用于处理这种类型的代码块,可以有多个catch
块; - 3.
finally
:主要用于回收在try
块里打开的物理资源(如数据库连接、网络连接和磁盘文件),异常机制总是保证finally
块总是被执行。只有finally
块,执行完成之后,才会回来执行try
或者catch
块中的return
或者throw
语句,如果finally
中使用了return
或者throw
等终止方法的语句,则就不会跳回执行,直接停止; - 4.
throw
:用于抛出一个实际的异常,throw
可以单独作为语句使用,抛出一个具体的异常对象; - 5.
throws
:用在方法签名中,用于声明该方法可能抛出的异常。
Java
的异常分为两种,checked
异常(编译时异常)和Runtime
异常(运行时异常)
Java
异常分为两种,checked
异常和Runtime
异常,Java
认为Checked
异常都是可以在编译阶段被处理的异常,所以它强制程序处理所有的Checked
异常;而Runtime
异常则无需处理,Checked
异常可以提醒程序员需要处理所以可能发生的异常,但是Checked
异常也给编程带来一些繁琐之处。
二、异常与错误
Java
把所有非正常情况分成两种:异常(Exception
)和错误(Error
),都是继承自Throwable
父类(可抛出),如图所示:
异常和错误最大的区别:异常能被程序本身可以处理,错误是无法处理。
Error
(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。一般指的是与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这些错误无法恢复或者不可能捕获,将导致应用程序中断,通常应用程序不应该试图使用catch
块来捕获Error
对象。在定义该方法时,也无须在其throws
字句中声明该方法可能抛出Error
及其任何子类。
例如,Java虚拟机运行错误(Virtual Machine Error
),当JVM
不再有继续执行操作所需的内存资源时,将出现OutOfMemoryError
。这些异常发生时,Java
虚拟机(JVM
)一般会选择线程终止。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError
)、类定义错误(NoClassDefFoundError
)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在Java
中,错误通过Error
的子类描述。
Exception
(异常):是程序本身可以处理的异常。Exception
类有一个重要的子类RuntimeException
。RuntimeException
类及其子类表示“JVM
常用操作”引发的错误。例如,若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException
。
三、检查的异常和非检查的异常
通常,Java
的异常(包括Exception
和Error
)分为检查异常(checked
exceptions
)和非检查的异常(unchecked exceptions
)。
Java
异常(只包括Exception
,不包括Error
)分为两大类:Checked
异常和RuntimeException
异常(运行时异常)。
1、检查异常(编译器要求必须处置的异常)
定义:正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。
对检查异常(checked exception
)的几种处理方式:
① 继续抛出,消极的方法,一直可以抛到Java
虚拟机来处理,就是通过throws exception
抛出。
使用throws
声明抛出异常的思路是:当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理;如果main
方法也不知道如何处理这种类型的异常,也可以使用throws
声明抛出异常,该异常是将交给JVM处理,JVM
对于异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行,这就是前面程序在遇到异常后自动结束的原因。
throws
声明抛出只能在方法签名中使用,throws
可以声明抛出多个异常类,多个异常类之间以逗号隔开,throws
声明抛出的语法格式如下:
throws ExceptionClass1,ExceptionClass2……
上面throws
声明抛出的语法格式仅跟在方法签名之后,如下例子程序使用了throws
来声明抛出IOException
异常,一旦使用throws
语句声明抛出该异常,程序就无须使用try……catch
块来捕获该异常了:
public class demo1 {
public static void main(String[]args)throws IOException{
FileInputStream fis=new FileInputStream("a.txt");
}
}
上面程序声明不处理IOException
异常,将该异常交给JVM
处理,所以程序一旦遇到该异常,JVM
就会打印该异常的跟踪栈信息,并结束程序了,运行上面的程序,会看到如下结果:
Exception in thread "main" java.io.FileNotFoundException: a.txt (系统找不到指定的文件。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at java.io.FileInputStream.<init>(FileInputStream.java:93)
at demo.demo1.main(demo1.java:10)
② 用try...catch
捕获
除了RuntimeException
及其子类以外,其他的Exception
类及其子类都属于可查异常。这种异常的特点是Java
编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch
语句捕获它,要么用throws
子句声明抛出它,否则编译不会通过。
只有Java
语言提供了Checked
异常,Checked
异常体现了Java
的严谨性,它要求程序员必须注意该异常——要么显式声明抛出,要么显式捕获并处理它,总之不允许对于Checked
异常不闻不问,这是一种严谨的设计哲学,可以增加程序的健壮性,但是问题是大部分的方法总是不能明确地知道如何处理异常,因此只能声明抛出该异常,而这种情况又是非常普遍的,所以Checked
异常降低了程序开发的生产率和代码的执行效率。关于Checked
异常的优劣,在Java
领域是一个备受争议的问题。
2、不可查异常(编译器不要求强制处置的异常)
定义:编译器不要求强制处置的异常,不会在编译的时候检查,一个个去检查会使得工作变得更加繁琐,只能在运行时才能检查出来,包括运行时异常(RuntimeException
与其子类)和错误(Error
)。
对未检查的异常(unchecked exception
)的几种处理方式:
- 捕获;
- 继续抛出;
- 不处理。
3、对于Exception
的异常总结
Exception
这种异常分两大类运行时异常和非运行时异常(编译异常)。程序中应当尽可能去处理这些异常。
运行时异常:都是RuntimeException
类及其子类异常,如NullPointerException
(空指针异常)、IndexOutOfBoundsException
(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时异常的特点是Java
编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch
语句捕获它,也没有用throws
子句声明抛出它,也会编译通过。
非运行时异常 (编译异常):是RuntimeException
以外的异常,类型上都属于Exception
类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException
等以及用户自定义的Exception
异常,一般情况下不自定义检查异常。
四、异常处理机制
Java
异常机制可以让程序具有极好的容错性,让程序更加健壮。当程序运行出现意外情形时,系统会自动生成一个Exception
对象来通知程序,从而实现将“业务功能实现代码”和“错误处理代码”分离,提供更好的可读性。
4.1、使用try……catch捕获异常
如果执行try
块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对对象被提交给Java
运行时环境,这个过程被称为抛出(throw
)异常。
当Java运行时环境受到异常对象时,会寻找能处理该异常对象的catch
块,如果找到合适的catch
块,则把该异常对象交给该catch
块处理,这个过程被称为捕获(catch
)异常;如果Java
运行时环境找不到捕获异常的catch
块,则运行时环境终止,Java
程序也将退出。
try {
... //监视代码执行过程,一旦返现异常则直接跳转至catch,
// 如果没有异常则直接跳转至finally
} catch (SomeException e) {
... //可选执行的代码块,如果没有任何异常发生则不会执行;
//如果发现异常则进行处理或向上抛出。
} finally {
... //必选执行的代码块,不管是否有异常发生,
// 即使发生内存溢出异常也会执行,通常用于处理善后清理工作。
}
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
public class DemoTryCatch {
public static void main(String[] args) {
//捕获异常
try {
//可能产生异常的代码
readFile();
} catch (FileNotFoundException e) {
//异常的处理逻辑,将异常记录日志,异常封装后显示
System.out.println("系统找不到指定的路径");
}
System.out.println("后续代码");
}
public static void readFile() throws FileNotFoundException {
InputStream is = new FileInputStream("E:/iodemo/ch01.txt");
}
}
运行结果:
系统找不到指定的路径
后续代码
上面的程序处理读文件时文件路径名称找不到,系统都将抛出一个异常对象,并把这个异常对象交给对应的catch
块处理,catch
块的处理方式是提醒用户系统找不到指定的路径,通常将异常记录在日志中,异常封装后显示。
4.2、异常类的继承体系
当Java
运行时环境接收到的异常对象,每个catch
块都是专门用于处理该异常类及其子类的异常实例。当Java
运行时环境接收到异常对象后,会依次判断该异常对象是否是catch
块后异常或者子类的实例,如果是,Java
运行时环境将调用和下一个catch
块里的异常类进行比较,Java
异常捕获流程示意图如下:
注意:
①如果try
块被执行一次,则try
块后只有一个catch
块会被执行,绝对不可以有多个catch
块被执行,除非在循环中国使用了continue
开始了下一次循环,下一次循环又重新运行了try
块,这才是导致对个catch
块被执行的原因;
②try
块后面的花括号不可以省略,即使try
块只有一行代码,也不可以省略这个花括号,与之类似的是,catch
块后的花括号也是不可以省略的;
③try
块里声明的变量时代码块内局部变量,它只在try
块内有效,在catch
块中不能访问该对象。
4.3、访问异常信息(异常对象包含的常用方法)
如果需要访问异常对象的详细信息,可以在catch
代码块中调用对应的方法来访问。当Java
运行时决定调用某个catch
块来处理该异常对象时,会将异常对象赋给catch
块后的异常参数,程序即可通过该参数来获得异常的相关信息。
getMessage()
;返回该异常的详细描述字符串printStackTrace()
:将该异常的跟踪栈信息输出到标准错误输出printStackTrace(PrintStream s)
:将该异常的跟踪栈信息输出到指定的输出流getStackTrace()
:返回该异常的跟踪栈信息
import java.io.FileInputStream;
import java.io.IOError;
import java.io.IOException;
public class AccessException {
public static void main(String args[]){
try{
FileInputStream fis = new FileInputStream("a.txt");
}catch (IOException ioe){
System.out.println(ioe.getMessage());
ioe.printStackTrace();
}
}
}
输出打印信息为:
a.txt (系统找不到指定的文件。)
上述的代码看到异常的详细描述信息:a.txt
(系统找不到指定的文件),这就是调用异常的getMessage()
方法返回的字符串。下面更加详细的信息是该异常的跟踪栈信息:
java.io.FileNotFoundException: a.txt (系统找不到指定的文件。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at java.io.FileInputStream.<init>(FileInputStream.java:93)
at demo.AccessException.main(AccessException.java:11)
4.4、使用finally回收资源
有些时候,程序杂在try
块里打开了一些物理资源(例如数据库连接、网络连接和自盘文件等),这些物理资源都是必须显式回收。Java
的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存。
为了保证一定能回收try
块中打开的物理资源,异常处理机制提供了finally
块,不管try
块中的代码是否出现,也不管哪一个catch
块被执行,甚至在try
块或者catch
块中执行了return
语句,finally
块总会被执行。完整的Java
异常处理语法结构如下:
try{
可能发生错误的代码
}catch(err){
只有发生错误时才执行的代码
}finally{
无论是否出错,肯定都要执行的代码
}
异常处理结构中只有try
块是必须的,catch
和finally
是可选的,但是catch
块和finally
块至少出现其中之一,也可以同时出现。
try catch finally
块中return
的执行顺序:
①没有异常发生时,优先级别依次为finally->try
;
②有异常发生时,优先级依次为finally->catch->
捕获异常块最外面的return
参考:https://blog.csdn.net/chenmingxu438521/article/details/102536086
4.5、Java7
和Java9
对于异常的增加功能
①Java7
之前的是每个catch
只能捕获一种类型的异常,从Java7
开始,一个catch
块可以捕获多种类型的异常,当捕获多种类型的异常时,多种异常类型之间用竖线(|)隔开;捕获多种类型的异常时,当异常变量有隐式的final
修饰,因此程序不能对于异常变量重新赋值。
public class MultiExceptionTest {
public static void main(String[]args){
try{
int a=Integer.parseInt(args[0]);
int b=Integer.parseInt(args[1]);
int c=a/b;
System.out.println("您输入的两个数相除的结果是:"+c);
}catch(IndexOutOfBoundsException|NumberFormatException|ArithmeticException ie){
System.out.println("程序发生了数组越界,数字格式异常,算术异常之一");
//对于捕获多异常时,异常变量默认有final修饰,所以不能直接赋值,程序有错
ie=new ArithmeticException("test");
}catch (Exception e){
System.out.println("未知异常");
//捕获一种类型的异常时,异常变量没有final修饰
e=new RuntimeException("test");
}
}
}
对于捕获多异常时,异常变量默认有final
修饰,所以不能直接赋值,程序有错;捕获一种类型的异常时,异常变量没有final
修饰,所以对于异常变量的赋值没有错。
②Java7
增强的自动关闭资源的try语句,为了保证可以正常关闭资源,这些资源实现类必须实现AutoCloseable
或者Closeable
接口,实现这两个接口就必须实现close()
方法。Closeable
是AutoCloseable
的子接口,可以被自动关闭的资源类要么实现了AutoCloseable
接口,要么实现了Closeable
接口,Closeable
接口里的close()
方法时只能声明抛出IOException
或其子类;AotoCloseable
接口里的close()
方法声明抛出了Exception
,因此它的实现类在实现close()
方法时可以声明抛出任何异常。
import java.io.*;
public class AutoCloseTest
{
public static void main(String[] args)
throws IOException
{
try (
// 声明、初始化两个可关闭的资源
// try语句会自动关闭这两个资源。
BufferedReader br = new BufferedReader(
new FileReader("AutoCloseTest.java"));
PrintStream ps = new PrintStream(new
FileOutputStream("a.txt")))
{
// 使用两个资源
System.out.println(br.readLine());
ps.println("庄生晓梦迷蝴蝶");
}
}
}
上面的程序分别声明、初始化了两个IO流,由于BufferedReader、PrintStream
都实现了Closeable
接口,而且他们放在try
语句中声明、初始化,所以try
语句会自动关闭它们,因此上面的程序是安全的,自动关闭资源的try
语句相当于包含隐式的finally
块(这个finally
块用于关闭资源),因此这个try
语句可以既没有catch
块,也没有finally
块。
③Java7
增强的throw
语句,即时Java
编译器会执行更细致的检查,会检查throw
语句抛出异常的实际类型,如Java
编译器知道代码处实际上只可能抛出FileNotFoundException
异常,因此在方法签名中只声明抛出FileNotFoundException
异常即可。
五、使用throw抛出异常
很多时候,系统是否需要抛出异常和实际的业务有关,如果程序中的数据、执行与既定的业务需求不符合,这就是一种异常,由于与业务需求不符合而产生的异常,必须由程序员来决定抛出,系统无法抛出这种异常,对于这种程序中自行抛出的异常,则应该使用throw
语句,throw
语句可以单独使用,抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例,
六、异常处理规则
6.1、异常处理原则:
异常信息是提醒“为什么”会抛出——即问题出现的原因,针对于异常的处理利用以下原则,有助于在调试过程中最大限度的使用好异常。
①具体明确
具体明确指的是在抛出异常时需要针对具体问题来抛出异常,抛出的异常要足够具体详细;在捕获异常时需要对捕获的异常进行细分,这时会有多个catch语句块,这几个catch
块中间泛化程度越低的异常需要越放在前面捕获,泛化程度高的异常捕获放在后面,这样的好处是如果出现异常可以近可能得明确异常的具体类型是什么。
例如 FileInputStream
的一个构造方法如下, 对file对象做检查后判断file是否有效,如果无效直接抛出FileNotFoundException
,而不是IOException
或者其他更宽泛的Exception
。
②提早抛出
提早抛出的基本目的还是为了防止问题扩散,这样出现异常的话排查起来会比较耗时,比较典型的一种情况是 NPE(NullPointerException)
,当某个参数对象为null时,如果不提早判断并抛出异常的话,这个null可能会藏的比较深,等到出现NPE
时就需要往回追溯代码了。这样就给排查问题增加了难度。所以我们的处理原则是出现问题就及早抛出异常。
例如 上面FileInputStream
的构造方法,在使用前就对File
的path
做了判断,如果为null
就及早的抛出NullPointerExceptio
n,防止在后面open
方法中传入一个null
,从而简化了出现异常的情况,方便定位问题。
③延迟捕获
延迟捕获说的是对异常的捕获和处理需要根据当前代码的能力来做,如果当前方法内无法对异常做处理,即使出现了检查异常也应该考虑将异常抛出给调用者做处理,如果调用者也无法处理理论上他也应该继续上抛,这样异常最终会在一个适当的位置被catch
下来,而比起异常出现的位置,异常的捕获和处理是延迟了很多。但是也避免了不恰当的处理。
6.2、处理技巧
对于异常的处理,能避免的异常,尽量在事先做判断来避免异常的发生,当判断时发现逻辑上已经不能往下走了,需要停止流程,这时候将异常抛出并准确的提示使用者问题所在。对于事先无法预判的异常需要对其进行处理。异常分运行时异常RuntimeException
和 检查异常Checked Exception, RuntimeException
一般用在由于接口方法使用不当的时候,如: 使用了null获取属性方法, 数组下标越界,除法运算除以0等,
- 如果你调用服务方法的方式不正确,你应该马上修改代码,避免发生
RuntimeException
; - 如果是用户方法调用你的方法的方式不正确,你应该立刻抛出
RuntimeException
,强制让使用者修正代码或改变使用方式,防止问题蔓延; - 一般情况下,不要捕获或声明
RuntimeException
,需要做的是完善程序代码。因为问题在于你的程序本身有问题,如果你用异常流程处理了,反而让正常流程问题一直存在,对于检查异常,一般先看能不能处理,能处理的异常使用try-catch
语句块捕获处理,不能处理使用throws
分类型抛出给上一级处理。
使用try-catch
语句块处理时一般需要注意以下几方面:try
语句块内要分清稳定代码和非稳定代码,对于稳定的不会出现异常的代码不要放到try
语句块中,则有以下几点:
catch
捕获的异常一定要处理;- 若使用了
finally
语句块,在语句块内一定要对资源对象,流对象进行关闭(jdk1.7
之后 可以使用try-with-resources
替代); finally
中不要使用return
语句,因为finally
语句块最后一定会执行,这里的return
语句会覆盖之前的return
语句。