一、概念
程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常。异常发生时,是任程序自生自灭,立刻退出终止,还是输出错误给用户?或者用C语言风格:用函数返回值作为执行状态?
Java提供了更加优秀的解决办法:异常处理机制。
异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰。
Java中的异常可以是函数中的语句执行时引发的,也可以是程序员通过throw 语句手动抛出的,只要在Java程序中产生了异常,就会用一个对应类型的异常对象来封装异常,JRE就会试图寻找异常处理程序来处理异常。
Throwable类是Java异常类型的顶层父类,一个对象只有是 Throwable 类的(直接或者间接)实例,他才是一个异常对象,才能被异常处理机制识别。JDK中内建了一些常用的异常类,我们也可以自定义异常。
二、Java异常的分类和类结构图
Java标准裤内建了一些通用的异常,这些类以Throwable为顶层父类。
Throwable又派生出Error类和Exception类。
错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。
异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。
总体上我们根据Javac(Java语言编译器,java compiler)对异常的处理要求,将异常类分为2类。
1.非检查异常(unckecked exception):Error 和 RuntimeException 以及他们的子类。javac在编译时,不会提示和发现这样的异常,不要求在程序处理这些异常。所以如果愿意,我们可以编写代码处理(使用try...catch...finally)这样的异常,也可以不处理。对于这些异常,我们应该修正代码,而不是去通过异常处理器处理 。这样的异常发生的原因多半是代码写的有问题。如除0错误ArithmeticException,错误的强制类型转换错误ClassCastException,数组索引越界ArrayIndexOutOfBoundsException,使用了空对象NullPointerException等等。
2.检查异常(checked exception):除了Error 和 RuntimeException的其它异常。javac强制要求程序员为这样的异常做预备处理工作(使用try...catch...finally或者throws)。在方法中要么用try-catch语句捕获它并处理,要么用throws子句声明抛出它,否则编译不会通过。这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样的异常时刻准备着。如SQLException , IOException,ClassNotFoundException 等。
需要明确的是:检查和非检查是对于javac来说的,这样就很好理解和区分了。
三、初识异常
下面的代码会演示2个异常类型:ArithmeticException 和 InputMismatchException。前者由于整数除0引发,后者是输入的数据不能被转换为int类型引发。
packagemy_test;importjava.util.Scanner;public classTestException {public static voidmain (String [] args )
{
System . out. println("----欢迎使用命令行除法计算器----") ;
CMDCalculate ();
}public static voidCMDCalculate ()
{
Scanner scan= newScanner ( System. in );int num1 =scan .nextInt () ;int num2 =scan .nextInt () ;int result =devide (num1 , num2 ) ;
System . out. println("result:" +result) ;
scan .close () ;
}public static int devide (int num1, intnum2 ){return num1 /num2 ;
}
}
异常是在执行某个函数时引发的,而函数又是层级调用,形成调用栈的,因为,只要一个函数发生了异常,那么他的所有的caller都会被异常影响。当这些被影响的函数以异常信息输出时,就形成的了异常追踪栈。上图的例子在devide方法中出现异常,以栈的形式追踪到CMDCalculate再到main方法。
异常最先发生的地方,叫做异常抛出点。
当devide函数发生除0异常时,devide函数将抛出ArithmeticException异常,因此调用他的CMDCalculate函数也无法正常完成,因此也发送异常,而CMDCalculate的caller——main 因为CMDCalculate抛出异常,也发生了异常,这样一直向调用栈的栈底回溯。这种行为叫做异常的冒泡,异常的冒泡是为了在当前发生异常的函数或者这个函数的caller中找到最近的异常处理程序。由于这个例子中没有使用任何异常处理机制,因此异常最终由main函数抛给JRE,导致程序终止,没有输出result结果。
上面的代码不使用异常处理机制,也可以顺利编译,因为2个异常都是非检查异常。但是下面的例子就必须使用异常处理机制,因为异常是检查异常。
代码中我选择使用throws声明异常,让函数的调用者去处理可能发生的异常。但是为什么只throws了IOException呢?因为FileNotFoundException是IOException的子类,在处理范围内。
还没运行就各种报错,编译不过。
packagemy_test;importjava.io.FileInputStream;importjava.io.IOException;importjava.util.Scanner;public classTestException {public void testException() throwsIOException
{//FileInputStream的构造函数会抛出FileNotFoundException
FileInputStream fileIn = new FileInputStream("E:\\a.txt");intword;//read方法会抛出IOException
while((word = fileIn.read())!=-1)
{
System.out.print((char)word);
}//close方法会抛出IOException
fileIn.close();
}public static void main (String [] args ) throwsIOException {
TestException test=newTestException();
test.testException();
}
}
四、处理异常的基本语法
try...catch...finally捕获异常
try{//逻辑语句
}catch{//处理块1的处理语句
}catch{//处理块2的处理语句
}finally{//finally语句块,主要用来释放资源,必然会执行
}//try必须要有,catch和finally至少要有1个,catch可以有多个。
1.try块中的局部变量和catch块中的局部变量(包括异常变量),以及finally中的局部变量,他们之间不可共享使用。
2.每一个catch块用于处理一个异常。异常匹配是按照catch块的顺序从上往下寻找的,只有第一个匹配的catch会得到执行。匹配时,不仅运行精确匹配,也支持父类匹配,因此,如果同一个try块下的多个catch异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面,这样保证每个catch块都有存在的意义。(老将放后面)
3.异常处理的任务就是将执行控制流从异常发生的地方转移到能够处理这种异常的地方去。也就是说:当一个函数的某条语句发生异常时,这条语句的后面的语句不会再执行,它失去了焦点。执行流跳转到最近的匹配的异常处理catch代码块去执行,异常被处理完后,执行流会接着在“处理了这个异常的catch代码块”后面接着执行。有的编程语言当异常被处理后,控制流会恢复到异常抛出点接着执行,这种策略叫做:resumption model of exception handling(恢复式异常处理模式 );而Java则是让执行流恢复到处理了异常的catch块后接着执行,这种策略叫做:termination model of exception handling(终结式异常处理模式)
4.在catch语句块中通常用printStackTrace() 方法指出异常的类型、性质、栈层次及出现在程序中的位置。
5.finally中的return会覆盖try或者catch中的返回值。
6.在 try块中即便有return,break,continue等改变执行流的语句,finally也会执行。
packagemy_test;importjava.io.FileInputStream;importjava.io.IOException;importjava.util.Scanner;public classTestException {public static void main (String [] args ) throwsIOException {
TestException test=newTestException();int res=test.testf();
System.out.println("res="+res);
}public inttestf() {
System.out.println("测试try-catch-finally方法");try{
System.out.println("try块开始");int c=100/0;//这里出错了
System.out.println("c="+c);//所以出错语句后面的语句不会执行。
System.out.println("try块结束");return 10;//如果try里没有异常并且finally没有return语句才会执行这一句
}catch(ArithmeticException e) {
System.out.println("第1个catch开始");
e.printStackTrace();
System.out.println("第1个catch结束");return 20;
}catch(Exception e) {
System.out.println("第2个catch开始");
e.printStackTrace();
System.out.println("第2个catch结束");return 30;
}finally{
System.out.println("finally块开始");
System.out.println("finally块结束");return 40;//加上这一句main中的res=40;不加这一句res=20;
}
}
}
/*输出结果
测试try-catch-finally方法
try块开始
第1个catch开始
java.lang.ArithmeticException: / by zero
at my_test.TestException.testf(TestException.java:18)
at my_test.TestException.main(TestException.java:10)
第1个catch结束
finally块开始
finally块结束
res=40*/
由上大致可知try-catch-finally语句块的执行过程,尝试调换catch块位置、注释或删除return语句再次运行会有意外的收获。(牛客基础题考点)
throws
如果一个方法内部的代码会抛出检查异常(checked exception),而方法自己又没有完全处理掉,则javac保证你必须在方法的签名上使用throws关键字声明这些可能抛出的异常,否则编译不通过。
throws是另一种处理异常的方式,它不同于try...catch...finally,throws仅仅是将函数中可能出现的异常向调用者声明,而自己则不具体处理。
采取这种异常处理的原因可能是:方法本身不知道如何处理这样的异常,或者说让调用者处理更好,调用者需要为可能发生的异常负责。
packagemy_test;importjava.io.IOException;importjava.util.Scanner;public classTestException {public static void main (String [] args ) throwsException {
TestException test=newTestException();
System.out.println("调用f方法");int res=test.f();
System.out.println("res="+res);
}public int f()throwsException{
System.out.println("f方法开始");int c=100/0;
System.out.println("c="+c);return 5;
}
}
/*输出结果
调用f方法
f方法开始
Exception in thread "main" java.lang.ArithmeticException: / by zero
at my_test.TestException.f(TestException.java:16)
at my_test.TestException.main(TestException.java:10)*/
1.如果f()后面throws Exception,在main中调用f(),也需要在main后面throws Exception。这是为了支持多态。例如,父类方法throws 的是2个异常,子类就不能throws 3个及以上的异常。父类throws IOException,子类就必须throws IOException或者IOException的子类。
2.一旦抛出异常程序就结束了,不会执行。(感觉没啥用,try-catch-finally还会执行下去)
3.throws写在方法名后面,与throw区分开。
throw
一般配合try-catch-finally使用
packagemy_test;importjava.io.IOException;importjava.util.Scanner;public classTestException {public static void main (String [] args ) throwsException {
TestException test=newTestException();int res=test.testf();
System.out.println("1.res="+res);
res=0;
System.out.println("2.res="+res);
}public int testf() throwsException {
System.out.println("测试throw");try{throw new Exception("try中的throw new Exception");/*一旦前面写了throw new Exception(),编译器提示后面代码都无效,需要删除掉
System.out.println("try块开始");
int c=100/0;//这里出错了
System.out.println("c="+c);//所以出错语句后面的语句不会执行。
System.out.println("try块结束");
return 10;//如果try里没有异常并且finally没有return语句才会执行这一句*/}catch(ArithmeticException e) {
System.out.println("第1个catch开始");
e.printStackTrace();
System.out.println("第1个catch结束");return 20;
}catch(Exception e) {
System.out.println("第2个catch开始");
e.printStackTrace();
System.out.println("第2个catch结束");return 30;
}finally{
System.out.println("finally块开始");throw new Exception("我是finally中的throw new Exception");//System.out.println("finally块结束");//此句无效需要删除
}
}
}/*测试throw
第2个catch开始
java.lang.Exception: try中的throw new Exception
第2个catch结束 at my_test.TestException.testf(TestException.java:18)
at my_test.TestException.main(TestException.java:9)
finally块开始
Exception in thread "main" java.lang.Exception: 我是finally中的throw new Exception
at my_test.TestException.testf(TestException.java:37)
at my_test.TestException.main(TestException.java:9)*/
1.只在try里写throw new Exception则testf方法后面不需要throws Exception,在finally里写就需要写。
2.throw new Exception后面的语句无效,不允许存在,需要注释或者删除