异常机制

代码实际编写后并不一定是完美的,可能会有我们没有考虑到的情况

异常类型

  • 之前其实已经接触过一些异常了,比如数组越界异常,空指针异常等,这些错误都是异常类型,每一个异常也是一个类,他们都继承自Exception类, 但是异常类型支持在程序运行出现问题时抛出(也就是上面出现的红色报错)也可以提前声明,告知使用者需要处理可能会出现的异常!
  • 异常其实就两大类,一个是编译时异常,一个是运行时异常

1. 运行时异常

  • 在编译时(写代码时)无法感知代码是否有异常, 只有在运行时才会知晓会不会有问题(正常情况下是不会出错的);
  • 运行时异常都继承自RuntimeException
public static void main(String[] args) {
    Object object = null;
    object.toString();   //这种情况就会出现运行时异常
}

image-20220924164637887


2. 编译时异常

  • 编译时异常会明确指出可能会出现的异常,在编译阶段就需要进行处理(捕获异常)必须要考虑到出现异常的情况,如果不进行处理,将无法通过编译!
  • 默认继承自Exception类的异常都是编译时异常。

如Object类中定义的clone方法,就明确指出了在运行的时候会出现的异常

protected native Object clone() throws CloneNotSupportedException;

3. 错误

  • 错误比异常更严重,异常就是不同寻常,但不一定会导致致命的问题,而错误是致命问题,一般出现错误可能"JVM"就无法继续正常运行了,比如OutOfMemoryError就是内存溢出错误(内存占用已经超出限制,无法继续申请内存了)

  • 比如这样的一个无限递归的方法,会导致运行过程中无限制地向下调用方法,导致栈溢出:这种情况就是错误了,已经严重到整个程序都无法正常运行了

public static void main(String[] args) {
    test();
}

private static void test(){
    test();
}

image-20220924165500108

  • 以下例子,实际上我们电脑的内存是有限的,不可能无限制地使用内存来存放变量,所以说如果内存不够用了:
  • 此时没有更多的可用内存供我们的程序使用,那么程序也就没办法继续运行下去了,这同样是一个很严重的错误。
public static void main(String[] args) {
    Object[] objects = new Object[Integer.MAX_VALUE];   //这里申请一个超级大数组
}

image-20220924165657392


抛出异常

  • 当别人调用我们的方法时,如果传入了错误的参数导致程序无法正常运行,这时我们就可以手动抛出一个异常来终止程序继续运行下去,同时告知上一级方法执行出现了问题:
public static int test(int a, int b) {
    if(b == 0)
        throw new RuntimeException("被除数不能为0");  //使用throw关键字来抛出异常
    return a / b;
}
  • 异常的抛出同样需要创建一个异常对象出来,我们抛出异常实际上就是将这个异常对象抛出,异常对象携带了我们抛出异常时的一些信息,比如是因为什么原因导致的异常,在RuntimeException的构造方法中我们可以写入原因。

当出现异常时:

image-20220924200817314

程序会终止,并且会打印栈追踪信息;而当异常发生时,方法调用的每一层都会在栈追踪信息中打印出来,比如这里有两个at,实际上就是在告诉我们程序运行到哪个位置时出现的异常,位于最上面的就是发生异常的最核心位置,我们代码的第15行。

并且这里会打印出当前抛出的异常类型和我们刚刚自定义异常信息。


异常处理

程序没有按照我们理想的样子运行而出现异常时(默认会交给JVM来处理,JVM发现任何异常都会立即终止程序运行,并在控制台打印栈追踪信息)现在我们希望能够自己处理出现的问题,让程序继续运行下去,就需要对异常进行捕获,比如:

public static void main(String[] args) {
    try {    //使用try-catch语句进行异常捕获
        Object object = null;
        object.toString();
    } catch (NullPointerException e){   //因为异常本身也是一个对象,catch中实际上就是用一个局部变量去接收异常
    }
    System.out.println("程序继续正常运行!");
}

我们可以将代码编写到try语句块中,只要是在这个范围内发生的异常,都可以被捕获,使用catch关键字对指定的异常进行捕获,这里我们捕获的是NullPointerException空指针异常:

image-20220924195434572

可以看到,当我们捕获异常之后,程序可以继续正常运行,并不会像之前一样直接结束掉。

注意,catch中捕获的类型只能是Throwable的子类,也就是说要么是抛出的异常,要么是错误,不能是其他的任何类型。

我们可以在catch语句块中对捕获到的异常进行处理:

public static void main(String[] args) {
    try {
        Object object = null;
        object.toString();
    } catch (NullPointerException e){
        e.printStackTrace();   //打印栈追踪信息
        System.out.println("异常错误信息:"+e.getMessage());   //获取异常的错误信息
    }
    System.out.println("程序继续正常运行!");
}

如果某个方法明确指出会抛出哪些异常,除非抛出的异常是一个运行时异常,否则我们必须要使用try-catch语句块进行异常的捕获,不然就无法通过编译:

public static void main(String[] args) {
    test(10);    //必须要进行异常的捕获,否则报错
}

private static void test(int a) throws IOException {  //明确会抛出IOException
    throw new IOException();
}

当然,如果我们确实不想在当前这个方法中进行处理,那么我们可以继续踢皮球,抛给上一级:

public static void main(String[] args) throws IOException {  //继续编写throws往上一级抛
    test(10);
}

private static void test(int a) throws IOException {
    throw new IOException();
}

注意,如果已经是主方法了,那么就相当于到顶层了,此时发生异常再往上抛出的话,就会直接交给JVM进行处理,默认会让整个程序终止并打印栈追踪信息。

注意,如果我们要捕获的异常,是某个异常的父类,那么当发生这个异常时,同样可以捕获到:

public static void main(String[] args) throws IOException {
    try {
        int[] arr = new int[1];
        arr[1] = 100;    //这里发生的是数组越界异常,它是运行时异常的子类
    } catch (RuntimeException e){  //使用运行时异常同样可以捕获到
        System.out.println("捕获到异常");
    }
}

当代码可能出现多种类型的异常时,我们希望能够分不同情况处理不同类型的异常,就可以使用多重异常捕获:

但是要注意一下顺序:

try {
  //....
} catch (RuntimeException e){  //父类型在前,会将子类的也捕获

} catch (NullPointerException e) {   //永远都不会被捕获

} catch (IndexOutOfBoundsException e){   //永远都不会被捕获

}

也可以简写为:

try {
     //....
} catch (NullPointerException | IndexOutOfBoundsException e) {  //用|隔开每种类型即可
		
}

当希望,程序运行时,无论是否出现异常,都会在最后执行任务,可以交给finally语句块来处理:

  • finally语句内可以写IO流或其他占用的功能进行关闭
try {
    //....
}catch (Exception e){
            
}finally {
  	System.out.println("lbwnb");   //无论是否出现异常,都会在最后执行
}

try语句块至少要配合catchfinally中的一个:

try {
    int a = 10;
    a /= 0;
} finally {  //不捕获异常,程序会终止,但在最后依然会执行下面的内容
    System.out.println("lbwnb"); 
}

异常机制简易记忆

  • 遇到throws基本是编译时异常,需要及时处理否则无法运行
  • 遇到try-catch基本是运行时异常,需要在catch语句中进行处理

笔记学习引用