Java异常体系

程序在运行时期会出现各种突发状况,有的状况可以通过预先设想的方式去处理,但有的情况则会导致该部分代码无法继续运行,有的情况甚至会导致整个程序无法运行,而java给我们提供了异常处理机制,用于处理代码无法继续运行的情况,这就是异常。异常主要是用于解决运行期间问题的,编译期间出现的问题会导致程序无法正常编译,所以在代码生成可执行文件之前就会告警。

一、什么是异常

异常是指各种在编译期间无法发现的并且在运行时可能会导致程序中断的情况,比如我们最经常见到的空指针异常,数组索引越界异常,类型转换异常等等。这些意外情况都会导致程序无法继续运行下去,所以在程序设计之时就需要考虑各种可能出现的情况,并对其进行妥善处理。使程序返回到一种安全的状态,保存用户工作的结果,并以妥善的方式终止程序。java为我们提供了异常处理机制,可以在程序可能出现异常的地方去捕获并且处理该异常情况,使得程序可以妥善的继续运行,在该点的处理方式可能会是继续向上层抛出异常(比如一个方法会出现异常,则在该方法处向调用点抛出异常,由调用点进行处理),也可能是在自身代码块中捕获并处理该异常。
举一个最简单的例子,当我们需要实现一个除法器时,我们预先并不知道传入的除数会为0,因为可能在调用这个方法之前已经做过层层校验,但事实上由于各种我们没想到的情况,最终调用这个方法时,除数果然为0了,这时我们在程序设计时,就需要预先设想到这种情况,然后对这种情况妥善处理:

public class DivZeroTest {
    public static void main(String[] args) {
    	//在主函数中调用除法器,并将除数传为0值
        divZero(1,0);
        divZero(1,1);
    }

    public static void divZero(int a, int b){
        System.out.println(a/b);
    }

上述代码简单实现了一个不会抛出异常的除法方法,运行时则会抛出异常,且下一个除法运算也不会继续执行,说明未经处理的异常是会掐断整个程序的运行的:
Java异常体系_第1张图片
这就是一个简单的异常。

二、异常继承结构

java中的异常继承自Throwable类,Throwable有两个子类,分别是Error和Exception,Error表示程序已经无法正常继续执行,已经无法自愈的情况,而Exception则还可以抢救一波。

Error

Error比较常见的就是栈溢出,比如我们定义一个没有出口的递归方法,就会引发栈溢出:

public class StackOverFlowTest {
    public static void main(String[] args) {
        overFlow(1);
    }

    public static void overFlow(int a){
        if(a > 0){
            overFlow(a + 1);
        }
    }
}

上述代码定义了一个没有出口的递归方法,运行时则会引发栈溢出错误,由于函数栈帧一直在开辟而未被释放,栈内存已经被占完,程序根本无法通过何种方式自己释放栈内存,这就是一种“无法自愈的错误”:
Java异常体系_第2张图片
本篇博客的重点是Exception,所以就不再赘述Error,他们都是继承自Throwable的子类,整个java异常的继承结构如下所示:
Java异常体系_第3张图片

java异常体系十分庞大,我们也可以通过继承Exception来实现自己的异常,上面只是罗列了一些我们经常见到的异常。
在异常的分类上,根据不同的分类标准有不同的划分方法,比如可以按照受检与否分为受检异常(也叫普通异常)和非受检异常(系统异常),受检异常的意思就是在编译期间需要解决的异常,可以通俗的理解成如果不给定解决方式就会报错。类似于IDEA上的“爆红”,反之则是非受检异常,比如RuntimeException和Error就都是非受检异常。

三、常见Exception

我们在编写程序时会经常遇到不同的异常,了解这些异常的来源有助于我们分析定位问题,接下来就罗列一些我们常见的异常,有助于我们分析定位问题原因。

1、NullPointerException

空指针异常是最常见的异常之一,当对一个指向null的对象对象进行操作的时候就会报这个异常,比如下面这段代码:

import java.util.List;

public class TestNullPointerException {
    public static void main(String[] args) {
        List<String> list = null;
        TestNull(list);
    }

    public static void TestNull(List<String> list){
        list.add("test");
    }
}

在这段代码中,我们尝试对一个没有申请内存空间的列表添加元素,此时就会报出空指针异常。在编程时如果对一个没有申请内存空间的对象进行操作,就会报出这个异常,所以在报出这个异常时,问题原因就知道了,问题定位也变的简单。

2、ArrayIndexOutOfBoundsException

数组越界异常也是比较常见的一个异常,当对一个数组进行越界操作时,就会出现该异常,比如:

public class TestIndexBoundsException {
    public static void main(String[] args) {
        int[] arr = new int[5];
        for(int i = 0; i < 5;++i){
            arr[i] = i + 1;
        }
        TestBounds(arr,10);
    }

    public static void TestBounds(int[] arr,int toAddNum){
    	//尝试对数组添加第六个元素
        arr[5] = toAddNum;
    }
}

在上面这段代码中我们尝试对length为5的数组添加第六个元素,就会报出数组越界错误。另外当对列表等进行越界操作时也会报出越界错误(IndexOutOfBoundsException),比如对一个空列表获取第0号位元素等。

3、ClassCastException

类转换错误在面向对象的编程语言中出现的比较多,尤其是在泛型中的类型擦除中会出现。

4、ArithmeticException

算术异常,最常见的就是当除数为0时会报出该异常,在上面的代码中已经实践过了。

5、IllegalArgumentException

非法参数异常,这个异常就是在传入参数不符合指定类型时会出现。

6、IOException

输入输出异常,IOException一般不会被具体的抛出,取而代之的是其子类,比如文件未找到异常,以到达文件尾异常等。我们假设在不存在的路径下读取一个文件,就会抛出文件不存在异常:

import java.io.*;

public class TestIOException {
    public static void main(String[] args) throws IOException {
        File file = new File("MyException\\testioexception");
        InputStreamReader inputStreamReader = new InputStreamReader(
                new FileInputStream(file));
        BufferedReader bf = new BufferedReader(inputStreamReader);
    }
}

在上述程序中指定了一个不存在的文件路径,执行该程序则会报出文件不存在异常:
Java异常体系_第4张图片

7、SQLException

SQL异常

四、异常处理

当程序出现了异常,我们就需要对异常进行处理,接下来介绍异常的处理机制

1、throws

在某个代码块中若异常无法自己处理需要传递给调用者时,可以在方法首部使用throws关键字将异常传递上去,例如上述的IOException,就是使用了throws来向上层抛出异常。

2、try-catch

不是每个时刻都需要将异常抛给方法调用者,当我们可以在某个模块中自己解决的时候,我们就需要捕获这种异常并将其解决掉,那么就使用try-catch子句来实现这个功能:

3、finally子句

当程序在打开资源时简单的try-catch不能保证资源的正常关闭,Java对这种情况提供了try-catch-finally语句块,可以在出现异常之后将资源正常关闭:

4、try-with-resources机制

提供了try-with-resources机制,这种异常捕获机制可以更好的处理资源关闭的问题:

五、自定义Exception

在我们实际编程过程中标准库提供的异常也许并不足以代表我们实际运行情况中的异常,或者有些相近的,但词不达意,这个时候我们就需要自定义我们自己的异常,来表达我们自己程序中的实际情况。

1、如何自定义异常

之前我们说了Java中所有异常都是Exception的子类,所以在自定义异常时需要去继承Exception(当然你也可以继承Throwable),但推荐继承意思较为相近的异常,尤其是在构建大型系统的时候,我们可能需要构建一个自己的异常体系,这些异常都是Exception的子类。
假设现在需要设计一个年龄校验系统,我们只允许年龄处于20到28岁的人录入系统,当年龄超出这个范围的时候我们就需要抛出异常来让上层执行其他的逻辑,那么我们就自定义一个“AgeNotFitException”:

public class AgeNotFitException extends Exception{
    public AgeNotFitException(){

    }

    public AgeNotFitException(String information){

    }
}

2、抛出定义的异常

在定义好自己的异常后,我们需要在合适的地方抛出这个异常,设计好主程序:

public class VerificationAge {
    public static void main(String[] args) {
        java.util.Scanner s = new java.util.Scanner(System.in);
        int age = s.nextInt();
        try{
            Verification(age);
        }catch(AgeNotFitException e){
            e.printStackTrace();
        }
    }

    public static void Verification(int age)throws AgeNotFitException{
        if(age < 20 || age > 28){
            throw new AgeNotFitException();
        }else{
            System.out.println("age is fit");
        }
    }
}

六、异常的使用规范

1、当我们使用的方法包含在对外提供的类中,就应该使用throws声明这个方法可能抛出异常
2、若一个方法可能抛出多个检查型异常,就应该在方法首部列出所有可能的异常。
3、不应该列出非检查型异常,因为非检查型异常要么是避免不了的,要么是可以在前期修正的。
4、在子类中覆盖了超类的一个方法后,子类方法中声明的检查型异常不能比超类的更通用(简单理解为抛出超类异常的子类吧)。当父类没有抛出异常,那么子类也不能抛出异常。

日拱一卒,功不唐捐。

你可能感兴趣的:(Java,java,开发语言)