目录
异常是什么?
就是指 程序在运行过程中所产生的不正常的情况。
Java中它是狭义的概率:它是指程序编译器或运行期,出现了一种叫做Exception的类的错误信息的时候,我们需要对这种Exception类进行处理,我们才叫做“异常处理”。
这里的关键就是,无论是在编译器还是运行期,报错信息里面出现了Exception类,需要我们进行处理,否则会让我们的程序无法通过编译或者运行中断的情况。那么这个时候,对Exception的处理才叫“异常处理"。
Exception是什么?
在Java语言和库的设计当中,先人其实已经预先预料到了我们在开发或运行过程中可能会出现各种各样的问题。所以,他们就提前先把这些问题进行了一个封装设计,对每种问题设计成一个类型来进行说明。比如我们常见的: ArrayIndexOutofBoundsException -- 数组下标越界
NullPointerException -- 空指针异常
ClassCastException -- 类型转换异常
FileNotFoundException -- 文件未找到异常
......
先人设计的这些异常类很多,所以他们根据继承的设计方案设计成了一个继承树结构。
异常的继承树Throwable
Throwable:异常继承树的根类,往上就是Object了。由它衍生了两个子类:
-
Error ---错误
-
Exception ---异常
Exception 下面的子类:
1、编译时异常
- 非 RuntimeException 及其子类异常
2、运行时异常
-
都是 RuntimeException 及其子类异常(现象:1.程序中断 2.控制台打印报错信息)
两种异常报的时机不同,比如:FileNotFoundException 找不到文件异常,就是一个编译时异常。而空指针异常等等都是编译的时候不报,程序运行起来以后真的出了某个问题才中断程序打印异常信息,它们是运行时异常。
它们是两个不同的概率,共性 都是 报错,如果在运行期报,都会中断我们的程序。
区别:
- 错误是不能用代码解决的问题;往往是运行环境问题或硬件问题。
- 异常是可以用代码解决的问题【实际开发中,几乎主要是解决异常】
异常信息的解读
报错信息分3个部分:
-
Exception in thread “main” 它告知的是哪个线程发生了异常。
-
java.lang.ArrayIndexOutOfBoundsException 这里是 异常的类名(见名知意)。
-
at com.xxx.xxx.main(xxxMain.java:xx)异常发生位置 【从上往下找第一行自己写的代码】
异常的传播机制
public class TestMain {
public static void main(String[] args) {
System.err.println("这是一个错误的决定.");
test();
}
public static void test(){
new ClassA().methodA();
}
}
public class ClassA {
public void methodA(){
Scanner scan = new Scanner(System.in);
System.out.println("请输入一个整数:");
int input = scan.nextInt();
System.out.println("输入的是:" + input );
}
}
根据演示代码来进行说明 当程序一旦启动肯定是从main方法开始运行,然后调用了test()。 调用方法就进入该方法内部,然后在test方法内调用methodA(),进入methodA的内部。
依次执行methodA()内部的代码,在nextInt()处程序进入阻塞状态,等待外部的输入。
现在的情况是,外部的输入是一个不能够转换成int的字符串。因此,回车以后在nextInt() 内部出现异常。
这个时候,JVM就会根据这次异常产生一个对应的异常对象。
然后,检查在当前代码处有没有处理这个异常对象,如果没有,那么它会结束这个方法,带着异常对象返回方法调用处。
如果调用处也没有解决这个异常,那么调用处方法也被结束,再返回给它的调用处。
如果这样一层层的返回,直到main方法都没有解决,那么main方法也就被结束,所以我们看到我们的程序被中断结束掉了。
然后,流程返回给了main方法的调用者也就是JVM。这个时候JVM就把这个异常对象的信息用“System.err.pringln()”打印在控制台。
这里有两个知识点:
1、方法结束的方式除了有遇到 } 和 return语句以外,第三种结束方式就是产生了一个异常对象,又没有处理,也会结束方法,流程也会返回方法调用处;
2、方法的调用栈,先进后出的特点决定,我们异常是结束方法,所以是从里面向外面进行返回,所以底层方法在前面调用方法在后面。而异常是由底层产生的,所以打印顺序真正发生异常的位置打印在了前面。
如何处理异常?
手段一:if语句规避异常
提前用if语句规避异常的出现。比如:用if(对象!=null)规避空指针异常;用if(下标 < 数组.length)规避数组下标越界异常;用if(对象 instanceof 类型)规避ClassCastException。
这种处理方式是最好的异常处理手段,因为它可以保证异常根本不会发生。几乎所有的运行时异常都应该用这种方式来处理。
【当然这种方式需要足够丰富的经验,能够预判到某块代码可能会发生某种类型的异常,你也才可能去写if判断语句】
手段二:try-catch
使用 重点语法 try-catch 把这个异常对象给处理掉。一旦处理掉这个异常,就不会再结束方法返回方法调用处了,而是顺着这个方法继续执行下去了。
try-catch
try-catch 其实是由两端代码块组成;一个叫try块(尝试),一个叫catch块(捕获)了。
try{
//正常代码 有可能发生异常
}catch(声明一个 异常 变量){
//处理代码
}
try—catch的执行效果:
-
如果try块中的代码没有发生异常,那么整个try块中的内容会按顺序执行完毕,然后跳过catch块,继续执行本方法剩余的代码,直到方法结束流程返回方法调用处。调用处也继续正常执行;
-
如果try块中的代码发生了异常,那么发生异常的代码处,后续的代码不执行,流程直接跳到catch块。
-
首先,要匹配。当前发生的异常对象是否与catch块()中声明的异常变量是否匹配;如果匹配,那么就算捕获住了该异常;如果不匹配,那么就没有捕获住异常(没有被处理),然后按中断方法,带着异常对象返回方法调用处的流程执行。
-
注意,只要catch()中的异常变量与try块中的异常对象匹配成功,那么就算捕获住了异常,也就中断掉了异常的传播机制。根据多态(父类的引用可以指向子类对象),如果catch()中写的是“Exception ex”,就可以捕获所有异常。
-
catch块的主要作用就是:打断异常传播链。如果想要程序不中断,那么在异常传播给main方法之前任意位置,使用try—catch。效果上会有区别
-
在catch块{ }中 书写 "异常对象.prinStackTrace()",把捕获住的异常内部的信息,主动打印在控制台上供程序员参看。
-
一个try块中有多个异常呢?
-
try块中的代码可以发生多种异常,但是每次运行只可能发生一个
-
解决方案:
-
catch块中用Exception进行捕获;
-
try块后面,跟多个catch块,每个catch块捕获一种异常;
try{
}catch(){
}catch(){
}...
/** try块中发生异常,就会拿这个异常对象按catch从上往下的顺序依次匹配。
匹配上那个catch块就进入其内部执行,执行完以后直接跳到最后一个catch的后面,往下顺序执行。*/
/**
多个catch块的书写顺序
多个异常之间没有继承关系,顺序无所谓;
有继承关系,子类写在前面
*/
finally块的作用:
try{
书写正常逻辑,但是有可能发生异常的代码;让它试着去执行
}catch(捕获异常A){
发生异常A以后,我们需要执行的代码
}catch(捕获异常B){
发生异常B以后,我们需要执行的代码
}...{
}finally{
//不管是否发生异常,都需要执行的代码
}
/** 开发中,finally块里面常常书写的是:资源的清理,管道的关闭...
等收尾性的清理动作。【这个在软件开发中是非常重视的要求!!!】*/
finally最大的特点: 不可被跳过!
在代码级别上,只有“System.exit(0)”关闭虚拟机,可以阻止finally被执行。其他的break、continue、return,就算写在finally的前面,也会先执行finally再break....
后面补充【try—with—resource】自动关闭资源
手段三:主动抛异常对象
我们也可以主动抛异常对象(throw),然后让调用处得到一个明确的要求解决异常的信号(throws)。这个信号可以在编译期就给出,让本方法的调用者在编译期就先去检查,并且给出异常的处理方案。
编译时异常应该根据异常产生的职责来决定到底用手段二还是手段三。
在很多业务场景中,有时候异常并不是JVM产生的,而是可能根据业务需求我们自己主动产生一个异常对象。
- 既然异常都是类,是类就可以产生对象,JVM能产生,所以可以用new的语法产生异常对象。
- 但是光new出异常对象还不够,因为这个时候知识在内存分配空间产生对象,并不具备传播的功能,所以引出关键字:throw
throw关键字
throw的作用:抛出异常;后面跟一个异常对象。
一旦这句代码被执行,就会真的发生一个异常对象进入传播,返回方法调用处的效果。
1.throw 后面跟的是运行时异常,直接书写即可
2.throw 后面跟的是编译时异常,那么要在编译期进行代码级别的处理
代码级别的处理:
- try-catch (无意义,相当于自己主动抛出异常,又把这个异常捕获 阻断传播)
- throws 在方法的声明处增加throws,后面加异常类的类名 (目的:提醒本方法的调用者,本方法可能会发生某种类型的异常,且这个异常是由方法的调用者造成的,应该由它处理)
throw 与 throws 的 区别
1.书写不同
- throw 是书写在方法体当中的 作为一条执行语句
- throws 是书写在方法声明的最后 是声明语句的一部分;(此时才是一个完整的声明)
2.后面跟的东西不同
- throw 后面跟一个 异常对象
- throws 后面跟一个或多个 异常类,中间用逗号分隔
3.意义不同
- throw 语句一旦被执行到,那么就一定会发生一个异常抛出的动作
- throws 只是描述该方法 可能会发生哪些类型的异常,然后在编译期就告知调用者,要求调用者处理
4.相关性
- 有throw语句,且throw语句后面跟的是编译时异常,那么必须要有throws
- 有throws的方法,不一定有throw语句;【如果throws后面跟的是运行时异常,编译期也不会提示】
如何使用throw 和 throws ?
调用者:
我们如果调用别人的某个方法,如果这个方法有throws,那么我们必须用try-catch 或 继续throws的方式处理;选择哪个,取决于职责;异常的发生职责是在哪个方法身上。如果在当前方法上,那就用try-catch解决;如果在当前方法的调用者上,那就继续抛 throws。
设计者:
一般都是在业务场景不允许,但是JVM允许的情况下,我们才主动抛出异常,这个时候用throws。
自定义异常
先人开发的异常类虽然很多,但是实际开发中可能还是不够用,特别是业务异常。比如:工资计算系统,算出工资为负数,抛什么异常?
自定义异常 还可以自定义方法,我们可以给异常类增加额外的行为。比如:书写日志。
在一个项目中可能发生各种各样的异常,到处都是try-catch,而try后面还有多个catch,那么如果我们能够把各种异常统一成一个自定义异常,对于我们来说代码会得到极大的简化。
自定义异常的定义:
1.书写一个自定义类,让它继承Exception;
2.一般都至少要给这个自定义异常类定义3个构造方法;
###
方法重写
完整的方法声明的要求:
1、方法名不能变;
2、参数列表不能变
3、返回类型不能变;
4、访问修饰符可以变大
5、重写后抛出的异常类型 不能比重写前多 (范围)