一.前提知识
首先当运行出错的时候,有两种情况,一种叫做“错误”,另一种叫做“异常”。错误指的是运行过程中遇到了硬件或操作系统出错,这种情况程序员是没办法处理的,因为这是硬件和系统的问题,不能靠代码的修改而处理出错,所以错误这一块我们不需要操心。异常指的就是我们经常写完代码运行后,控制台报错这一情况。我们主要处理的就是这个异常
而这个异常就是一个类,所以它满足类的相关特性,再然后这个类又有许多子类,我们大体把这些子类分为两大类,第一大类称为:运行时异常类,第二大类称为:编译时异常(注意:编译时候出现的语法性错误,不能称为异常。例如System拼写成system)
整体来说这个过程一共分四个阶段:异常声明——>异常抛出——>异常捕获——>异常处理
所以现在我们现在要形成这么一种大致感觉:这个东西不是主体那么重要的组成部分,而像是一副盔甲,如果不加也没什么太大的问题,就像我们之前没学过异常时写的代码,如果加了那么我们这个程序就更加完善,用专业语言形容就是“提高代码的健壮性”。(给我的感受就是类似于C语言的assert断言一样)
异常类的层次结构
其中Error类及其分支就是我们刚刚说的我们程序员解决不了的错误,我们程序员主要解决的就是Exception,其中RuntimeException就是我们刚刚讲的第一大类(运行时异常),而与RuntimeException同级的这些异常类统称为第二大类(编译时异常),我们现阶段主要学习的是RuntimeException及其分支
二.异常格式
try { //将可能会出现异常的代码放在这里 }catch (){ //如果try里面的代码抛出异常了,如果catch里面的异常和try抛出的异常类型一致的话,就会捕获到,然后执行catch里面的代码 }catch (){ //可以写多个catch,catch中的代码用来解决捕获异常后的解决措施 }finally { //此处代码不管怎么样,最后都会执行finally里面的代码,一般用在finally中进行一些资源清理的扫尾工作 }
一段涵盖异常知识点比较全面代码:
public class Test {
public static int getData(){
Scanner sc=null;
try{
sc=new Scanner(System.in);
int data=sc.nextInt();
return data;
}catch (InputMismatchException e){
e.printStackTrace();
}finally {
System.out.println("finally中代码");
}
System.out.println("try-catch-finally之后的代码");
if (null!=sc){
sc.close();
}
return 0;
}
public static void main(String[] args) {
int data=getData();
System.out.println(data);
}
}
现在我们将这个格式一部分一部分的解析
1.try部分(抛出异常)
这个部分主要就是抛出异常这一步,而抛出异常有两种方式,一种是JVM自动抛出异常,一种是我们手动抛出异常,如我们上面这段代码,因为输入类型是nextInt,所以如果我们输入的不是整数,就会有问题,又因为我们没有手动抛出异常,所以此时会交给JVM来解决,JVM就会给我们抛出异常(也就是InputMismatchException这个异常类)。而如果我们手动抛出异常就会需要throw这个关键字,例如我们上面这段代码就可以更改为这样
try{
sc=new Scanner(System.in);
int data=sc.nextInt();
if (/*如果data不为整数*/){
throw new InputMismatchException(); //因为是类,所以我们还要new来创建一个新类
}
return data;
2.catch(捕获异常)
这一部分主要就是捕获异常这一步,我们可以使用多个catch来捕获,如果发现抛出的异常和catch要捕获的异常类型一致,那么就被捕获了,此时会执行catch里面的语句,这里面的语句大致为这三个:
e.getMessage(),e.toString(),e.printStackTrace()
其中getMessage描述异常信息最简洁的,其次是toString,最全面的是printStackTrace(也是我们最常用的),例如我们上面catch这段代码就捕获了InputMismatchException这个异常,然后通过printStackTrace打印错误信息
注:如果多个catch中,出现了异常类之间具有父子类关系,那么父类必须写在最下面的catch;
如果多个异常的处理方式是完全相同的,可以catch(异常1 | 异常2)
3.finally(扫尾工作)
finally里面的代码是一定会执行的,不管try里面有没有异常,甚至直接被return,都会在return之前先把finally里面的代码执行完
如上面这行代码,如果运行正常,那么就直接return了,try-catch-finally之后的代码根本就没有执行,即输入流就没有被释放,造成资源泄漏,所以就需要finally,即便运行正常,在执行return之前,也会先跳到finally这里先把finally里面的代码执行,再跳回return
4.throws(声明异常)
还有一个知识点:throws,也就是异常声明,它的位置必须放在main方法那一行,例如
public static void main(String[] args) throws FileNotFoundException {
它的主要作用就在于处理编译时异常,虽然throws后面既可以跟运行时异常也可以跟编译时异常,但我们一般省略写运行时异常,其中的原因就是运行时异常会在编译器编译后运行的过程中发现问题进行报错,而编译时异常会在编译器编译的时候就报错,用人话来说就是如果throws后面不写运行时异常至少还能先让代码跑起来然后再报错,throws后面要是不写编译时异常,代码连跑都跑不起来就报错了。这时我们才用throws来解决这个问题,即我们知道它可能会有编译的问题,但是我们现在不想解决它,又想让代码先跑起来再说,等到以后提醒方法的调用者处理异常
注:如果声明多个异常,之间要用,隔开;如果声明的多个异常之间存在父子类关系,那么只用写父类的异常就好,子类不用写
三.自定义异常
如果有时候在这些类中没找到合适的异常类,例如密码错误,用户名错误这些,我们就需要自定义异常,格式一般为定义一个类然后继承异常,然后在构造方法中调用父类的构造方法即可
例子如下
class PasswordException extends Exception{
public PasswordException(String message){
super(message);
}
}
调用例子如下
if (!password.equals(password)){
throw new PasswordException("密码错误");
}
注:自定义异常通常会继承自Exception或者RuntimeException,继承Exception的异常默认是编译时异常,继承RuntimeException的异常默认是运行时异常
四:灵魂四问
现在我们提出四个问题,来理清楚代码执行顺序流程是怎么样的
1.如果try中没有遇到问题,怎么执行?
2.如果try中可能会遇到多个问题,怎么执行?
3.如果try中遇到的问题没有被捕获,怎么执行?
4.如果try中遇到了问题,那么在try中之后面的其他代码还会执行吗?
答案:
1.会把try里面所有的代码全部执行完毕,不会执行catch里面的代码,然后执行finally里面的代码,最后执行try-catch-finally之后的代码
2.当遇到第一个异常时,就会中止后面的try代码,然后去catch里面找,找到对应的异常,执行该catch里面的代码,最后执行finally里面的代码,try-catch-finally后面的代码不会执行
3.当遇到第一个异常时,就会中止后面的try代码,然后去catch里面找,如果没有对应的catch,那么就相当于我们try-catch白写了,这时JVM会自动帮我们解决报错,最后执行finally的代码,try-catch-finally后面的代码不会执行
4.就如我们刚刚分析的过程一样,是不会执行的