Java:异常的操作和处理

什么是异常?

简单的一句话来说:在Java中,将程序执行过程中发生的不正常行为称为异常。

在写代码的过程中,我们经常会遇到各种各样的异常,下面就通过三个常见的异常来引出下文。
异常一:算术异常(ArithmeticException)

System.out.println(1/0);

运行结果:
在这里插入图片描述

异常二:数组越界异常(ArrayIndexOutOfBoundsException)

int[] array=new int[3];
array[3]=123;

运行结果:
在这里插入图片描述

异常三:空指针异常(NullPointerException)

int[] array=null;
System.out.println(array.length);

运行结果:
在这里插入图片描述

异常的分类

异常可以分为编译时异常(受查异常)和运行时异常(非受查异常),在讲异常之前,我们先对异常的上层知识进行一个简单的了解。

在这里插入图片描述

  • Throwable:是异常体系的顶层类,它被两个子类(Error和Exception)继承
  • Error:指的是Java虚拟机无法解决的严重问题(比如栈溢出或者堆溢出等),这时候就要对代码逻辑等进行一个详细的检查
  • Exception:异常产生后可以通过代码来对异常进行处理,跳过这个异常,可以继续执行下面的语句

编译时异常

编译时异常也称为受查异常。

有一些异常连编译都不能够通过,更不用说运行了,类似这种异常,必须在编译的时候就将其处理掉(这种异常在IDEA编译器中都会有提示,一般是以红色的下划线为主),才能够正常运行。

class A{
    public int a;

    @Override
    protected A clone() {
        return (A)super.clone();
    }
}

在这里插入图片描述


注意:少写了个分号、写漏了个括号……类似这些不叫编译时异常,而应该是编译时的语法错误,一般这些错误IDEA也都是会自动提醒的。

运行时异常

运行时异常也称为非受查异常。

运行时异常也是我们见得最多的异常了,例如前面开头的三个例子就都是运行时异常。
运行时异常有非常多种,但是既然是运行时才出现的问题,那么也就说明这些程序已经通过编译生成.class文件了,再由JVM执行的过程中才出现的错误。类似这种运行时的异常,IDEA一般都是不会提醒的,只有运行之后才能知道问题所在。

异常的抛出

在Java中,可以借助throw关键字来抛出一个指定的异常对象,将错误信息告知给调用者。

语法为:

throw new XXXException("异常产生的原因");

类似的例子可以看之前“Java:图书管理系统”一文中的代码,这里再重新举一个简单的例子:

public class Main {
    public static void main(String[] args) {
        int index=3;
        int[] array=new int[index];
        if(array==null){
            throw new NullPointerException("传递的数组为null");
        }
        if(index<0||index>array.length){
            throw new ArrayIndexOutOfBoundsException("数组越界");
        }
    }
}

注意:

  • throw必须写在方法体的内部
  • 抛出的对象必须是Exception或其子类对象
  • 如果抛出的是RunTimeException或其子类时,可以不用进行处理,直接交给JVM来处理即可
  • 异常一旦抛出,其后面的代码就不会执行

异常的捕获

异常的捕获(处理方式):一是异常声明throws,二是try - catch捕获并处理。

异常声明throws

当方法中抛出异常的时候,如果不想在这里就进行处理的话,就可以使用异常声明,借助throws将异常抛给方法的调用者,在由方法的调用者进行异常处理。

class A{
    public int a;

    @Override
    protected A clone() throws CloneNotSupportedException {
        return (A)super.clone();
    }
}

(此代码就是上面的例子进行异常声明后,就不会再报错了)
注意:

  • throws必须跟在方法参数的后面
  • 声明的异常必须是Exception或其子类
  • 如果方法的内部同时抛出了多个异常,那么throws后面就必须跟上多个异常类型,之间需要使用逗号隔开
  • 如果抛出的多个异常类型具有父子类关系的,那么只要声明父类即可
  • 当需要调用声明抛出异常的方法的时候,调用者需要对此异常进行处理,或者再次使用throws声明异常

try - catch捕获并处理

由于上面使用throws只是对异常进行声明,实质上并没有对异常进行真正的处理,而是直接将异常抛给方法的调用者,当调用者没有对其进行处理时,它依旧是会报错的。

如果要真正意义上对异常进行处理,不能够只是靠throws来声明,而应该使用try - catch捕获并处理异常。
语法为:

try{
    //这里放入可能会出现异常的代码;
}catch (要捕获的类型 e) {
    //如果try中的代码抛出异常了,并且try中抛出的异常与catch捕获是异常的类型一致(或者是其子类)的话,就会被捕获到
    //然后就会执行这里面的代码
    //对异常处理完成后,就会直接跳出try - catch语句,继续执行后面的代码
}

继续完善上面的例子:

class A{
    public int a;

    @Override
    protected A clone() throws CloneNotSupportedException {
        return (A)super.clone();
    }
}

public class Main {
    public static void main(String[] args) {
        A a=new A();
        try{
            A b=a.clone();
        } catch (CloneNotSupportedException e) {
            System.out.println("异常:CloneNotSupportedException");
            e.printStackTrace();
        }
    }
}

注意:

  • try语块内抛出异常之后的代码是不会被执行的
  • 异常是按照类型来进行捕获的,如果抛出的异常类型捕获的异常类型不匹配,那么就表明异常没有被成功捕获,则会继续往外抛异常,直到JVM收到后才会中断程序
  • 我们一般在捕获异常的时候是通过使用多个catch来捕获的,如果多个异常的处理方法是相同的,那么也可以写成如下格式:
catch(要捕获的类型1 | 要捕获的类型2 e){
    ...
}
  • 如果异常之间具有父子类关系,那么一定是子类的异常先排在前面进行捕获,父类的异常排在后面进行捕获(因为如果是反过来则可能会子类的异常是永远捕获不到的,造成错误)

在前面的学习,我们知道了Exception类是所有异常类的父类,那么这时候就有人有下面这个疑问了:

既然Exception类是所有异常类的父类,那么我们在catch对异常进行获取的时候,直接就写一个 catch(Exception e){…} 语句不就可以解决所有问题了?无论抛出什么异常,全部都是Exception类的子类,都是可以直接就捕获到了。

我的回答是对的。其实这段话本身就没有错误的,但是我们在实际的开发过程中是很少这样去使用的,因为一旦这样做,当捕获到异常的时候,只能够知道try语句中的这行代码是异常的,并不能确切地知道这行代码到底是因为上面而抛出异常的,这样的话,在检查代码的时候就会变得非常困难。

finally

这里补充一个finally语块,其实finally语块应该是结合在捕获并处理那里的,完整的表示应该是下面这样。

语法为:

try{
    //这里放入可能会出现异常的代码;
}catch (要捕获的类型 e) {
    //如果try中的代码抛出异常了,并且try中抛出的异常与catch捕获是异常的类型一致(或者是其子类)的话,就会被捕获到
    //然后就会执行这里面的代码
    //对异常处理完成后,就会直接跳出try - catch语句,继续执行后面的代码
}finally{
    //此处的语句无论是否发生异常,都是会执行的
}
//如果没有抛出异常,否则异常已经被处理了,这里的代码也是会执行的

这时候就又有人问了:上面不是说了不管try语块中是否有异常,有的话就对异常处理完成后,就会直接跳出try - catch语句,继续执行后面的代码;没有的话就直接执行后面的代码。那么这样的话,跟finally又有什么区别呢?为什么还要引出finally语句这个东西呢?
这里我的回答是:写在外面不一定能够执行到,但是写在finally里面的代码是一定会被执行的。下面以一段代码为例就可以解释清楚了:

public class Main {
    public static int func(){
        int[] array=new int[3];
        try{
            array[2]=10;
            return array[2];
        }catch (ArrayIndexOutOfBoundsException e){
            System.out.println("ArrayIndexOutOfBoundsException");
            e.printStackTrace();
        }finally {
            System.out.println("abcd");
        }
        array[0]=20;
        return array[0];
    }

    public static void main(String[] args) {
        int num=func();
        System.out.println(num);
    }
}

运行结果:
在这里插入图片描述

这段代码中try语块内部并没有存在异常的地方,所以会走到return的地方,既然是return了,自然也就不会再执行后面的代码了,但是在运行结果中可以看到是有“abcd”打印出来的,这就说明了:无论如何,finally语块中的代码是一定会被执行的。
注意:虽然finally语块中也是可以写return的,由于finally语块相对于try语块中的return,一般来说是优先执行的,所以返回的值也应该是finally语块中return返回的值。但是我们一般是不建议在finally语句中写return的(编译器可能会将其当做一个警告)。

补充:throw和throws的区别

throw:用于抛出异常,throw new ……
throws:用在方法上来进行异常声明的

总结

异常处理的流程:

  1. 程序向执行try语块中的代码
  2. 如果try中代码出现异常,则会结束try语块中的代码,检验与catch中的异常类型是否匹配
  3. 如果找到匹配的异常类型,就会执行catch中的代码
  4. 如果没有找到匹配的异常类型,就会将异常向上传递到上层调用者
  5. 无论是否找到匹配的异常类型,finally中的代码都是会被执行的
  6. 如果上层调用者也没有处理异常,那么将会继续向上传递,一直到main方法也没有找到合适的代码来处理这个异常,此时就会交给JVM进行处理,整个程序也随之异常终止(只要是交给JVM处理的,都是属于异常终止

自定义异常类

自定义异常类很多时候是开发中使用得最多的。

具体的实现方式是:

  1. 先自定义一个异常类,让其继承Exception或者RunTimeException
  2. 实现一个带有String参数类型的构造方法,其中参数是用来写初夏异常的原因

代码示范:

class A extends Exception {
    public A(String message) {
        super(message);
    }
}

注意:

  • 自定义异常都是会继承Exception类或者是RunTimeException类的
  • 自定义异常继承Exception类默认是受查异常的(也就是编译时异常)
  • 自定义异常继承RunTimeException类默认是非受查异常(也就是运行时异常)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蔡欣致

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值