chapter9 异常总结

异常的简介

Java中的异常又称为例外,是一个在程序执行期间发生的事件,它中断正在执行程序的正常指令流。为了能够及时有效地处理程序中的运行错误,必须使用异常类,这可以让程序具有极好的容错性且更加健壮。

在 Java 中一个异常的产生,主要有如下三种原因:
①Java 内部错误发生异常,Java 虚拟机产生的异常。
②编写的程序代码中的错误所产生的异常,例如空指针异常、数组越界异常等。
③通过 throw 语句手动生成的异常,一般用来告知该方法的调用者一些必要信息。

Java 通过面向对象的方法来处理异常。在一个方法的运行过程中,如果发生了异常,则这个方法会产生代表该异常的一个对象,并把它交给运行时的系统,运行时系统寻找相应的代码来处理这一异常。

我们把生成异常对象,并把它提交给运行时系统的过程称为拋出(throw)异常。运行时系统在方法的调用栈中查找,直到找到能够处理该类型异常的对象,这一个过程称为捕获(catch)异常。

异常的类型

为了能够及时有效地处理程序中的运行错误,Java 专门引入了异常类。在 Java 中所有异常类型都是内置类 java.lang.Throwable 类的子类,即 Throwable 位于异常类层次结构的顶层。Throwable 类下有两个异常分支 Exception 和 Error,如下图 所示
在这里插入图片描述
Exception 类用于用户程序可能出现的异常情况,它也是用来创建自定义异常类型类的类,是可以处理的。
Error 定义了在通常环境下不希望被程序捕获的异常,通常是灾难性的致命错误,不是程序可以控制的。一般指的是 JVM 错误,如堆栈溢出。

常见的一些运行时异常和非运行时异常:
在这里插入图片描述
在这里插入图片描述

异常处理机制及异常处理基本结构

java的异常处理通过 5 个关键字来实现:try、catch、throw、throws 和 finally。try catch 语句用于捕获并处理异常,finally 语句用于在任何情况下(除特殊情况外)都必须执行的代码,throw 语句用于拋出异常,throws 语句用于声明可能会出现的异常

异常处理的机制如下:
①在方法中用 try catch 语句捕获并处理异常,只有一个try语句,catch 语句可以有多个,用来匹配多个异常
②对于处理不了的异常或者要转型的异常,在方法的声明处通过 throws 语句拋出异常,即由上层的调用方法来处理。

try-catch语句

格式:

try {
    // 可能发生异常的语句
} catch(ExceptionType e) {
    // 处理异常语句
}

如果 try 语句块中发生异常,那么一个相应的异常对象就会被拋出,然后 catch 语句就会依据所拋出异常对象的类型进行捕获,并处理。处理之后,程序会跳过 try 语句块中剩余的语句,转到 catch 语句块后面的第一条语句开始执行。

如果 try 语句块中没有异常发生,那么 try 块正常结束,后面的 catch 语句块被跳过,程序将从 catch 语句块后的第一条语句开始执行

在上面catch语句中的处理代码块 中,可以使用以下 3 个方法输出相应的异常信息。
①printStackTrace() 方法:指出异常的类型(即异常的类名)、性质(即异常的信息)、栈层次及出现在程序中的位置
②getMessage() 方法:输出错误的性质(即异常的信息)
③toString() 方法:给出异常的类型(异常的类名)与性质(异常的信息)
如:
在这里插入图片描述
实例代码:

public class DemoException {

    public static void main(String[] args) {
        int[] arr = new int[3];
        try {
//一旦监测到异常,那么就直接从try中跳到catch语句中,剩下的语句不在被执行,找到和异常匹配的catch语句
            new DemoException().method(arr, 2);
            new DemoException().method(arr,3);
            new DemoException().method(arr,-3);
        }catch(ArrayIndexOutOfBoundsException e){
            System.out.println(e.toString());//输出输出异常的类名、异常的信息
            e.printStackTrace();//调用方法,从而输出异常的类名、异常的信息、异常的位置
            System.out.println(e.getMessage());//输出异常的信息
        }
    }

    public void method(int[] arr, int index) {
        if(index >= arr.length)
            throw new ArrayIndexOutOfBoundsException("数组发生越界");//throw抛出异常之后,throw语句后面的内容不在被执行,直接返回
        System.out.println("当前的下标为:"+index);//没有异常的时候,就输出
    }

}

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

注意:try…catch 与 if…else 不一样,try 后面的花括号{ }不可以省略,即使 try 块里只有一行代码,也不可省略这个花括号。与之类似的是,catch 块后的花括号{ }也不可以省略。另外,try 块里声明的变量只是代码块内的局部变量,它只在 try 块内有效,其它地方不能访问该变量

多重catch语句

格式:和上面的try–catch语句相比,只是多了几个catch语句。

try {
    // 可能会发生异常的语句
} catch(ExceptionType e) {
    // 处理异常语句
} catch(ExceptionType e) {
    // 处理异常语句
} catch(ExceptionType e) {
    // 处理异常语句
...
}

用法和try-catch一样,但是有几点需要注意的:
①在多个 catch 代码块的情况下,当一个 catch 代码块捕获到一个异常时,其它的 catch 代码块就不再进行匹配
②当捕获的多个异常类之间存在父子关系时,捕获异常时一般先捕获子类,再捕获父类。所以子类异常必须在父类异常的前面,否则子类捕获不到,发生报错。如果将子类异常放在父类异常的后面,会有一个提示,提示我们将子类对象放在父类对象之前。
③在方法内部声明了有多少个异常,就有可能抛出多少个异常,那么就要有多少个catch语句。
比如说声明了ArrayIndexOutOfBoundsException,那么就要有catch语句,如果有还有NullPointerException,而只有一个catch语句的时候,就会发生报错。

try - catch - finally语句

格式:

try {
    // 可能会发生异常的语句
} catch(ExceptionType e) {
    // 处理异常语句
} finally {
    // 清理代码块
}

使用 try-catch-finally 语句时需注意以下几点:
①异常处理语法结构中只有 try 块是必需的,也就是说,如果没有 try 块,则不能有后面的 catch 块和 finally 块;同时不能只有try,而没有catch或者finally,两者中至少有一个。
②可以有多个 catch 块,捕获父类异常的 catch 块必须位于捕获子类异常的后面
③多个 catch 块必须位于 try 块之后,finally 块必须位于所有的 catch 块之后,即finally放在最后。
④finally 与 try 语句块匹配的语法格式,此种情况会导致异常丢失,所以不常见。

对于以上格式,无论是否发生异常(除特殊情况外),finally 语句块中的代码都会被执行。执行流程:
在这里插入图片描述
try catch finally 语句块的执行情况可以细分为以下 3 种情况:
①如果 try 代码块中没有拋出异常,则执行完 try 代码块之后直接执行 finally 代码块,然后执行 try catch finally 语句块之后的语句。
②如果 try 代码块中拋出异常,并被 catch 子句捕捉,那么在拋出异常的地方终止 try 代码块的执行,转而执行相匹配的 catch 代码块,之后执行 finally 代码块。如果 finally 代码块中没有拋出异常,则继续执行 try catch finally 语句块之后的语句;如果 finally 代码块中拋出异常,则把该异常传递给该方法的调用者。
③如果 try 代码块中拋出的异常没有被任何 catch 子句捕捉到,那么将直接执行 finally 代码块中的语句,并把该异常传递给该方法的调用者

package fundamental;

public class DemoException {

    public static void main(String[] args) {
        //如果在方法中检测到可能会发生自定义异常,那么在方法声明的后面throws这个自定义异常
        int[] arr = new int[3];
        try {
//一旦监测到异常,那么就直接从try中跳到catch语句中,剩下的语句不在被执行,找到和异常匹配的catch语句
            new DemoException().method(arr, 2);
            new DemoException().method(arr,3);
            new DemoException().method(arr,-3);
        }catch(ArrayIndexOutOfBoundsException e){
            //如果只有一个catch语句,那么这里就可以是Exception,但是如果是多重catch语句,那么父类异常Exception要放在最后
            System.out.println(e.toString());//输出输出异常的类名、异常的信息
            e.printStackTrace();//调用方法,从而输出异常的类名、异常的信息、异常的位置
            System.out.println(e.getMessage());//输出异常的信息
        }finally{
 /*finally语句无论是否发生异常,都会执行这个语句,用于清空代码注意如果在
 *finally语句中同样发生异常,那么把该异常传递给该方法的调用者(输出异常的
 *信息时调用printStackTrace()这个方法)
 */
            new DemoException().method(arr,8);//如果这句有异常,后面的语句不在被执行
            System.out.println("这里学习异常");
        }
    }

    public void method(int[] arr, int index) {
        if(index >= arr.length)
            throw new ArrayIndexOutOfBoundsException("数组发生越界");//throw抛出异常之后,throw语句后面的内容不在被执行,直接返回
        System.out.println("当前的下标为:"+index);//没有异常的时候,就输出
    }

}

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

throws语句和throw语句

throws语句用于声明可能会发生异常。

使用throws声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理;如果main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常将交给JVM处理。JVM对异常的处理方法是:打印异常的跟踪栈信息(即printStackTrace(),输出异常类的名字、异常信息、异常位置),并终止程序运行,这就是前面程序在遇到异常后自动结束的原因。

使用 throws 声明抛出异常时有一个限制,是方法重写中的一条规则:
子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常类的数量不允许比父类方法声明抛出的异常数量多
。看如下程序:

public class OverrideThrows {
    public void test() throws IOException {
        FileInputStream fis = new FileInputStream("a.txt");
    }
}
class Sub extends OverrideThrows {
    // 子类方法声明抛出了比父类方法更大的异常
    // 所以下面方法出错
    public void test() throws Exception {
    }
}

上面程序中 Sub 子类中的 test() 方法声明抛出 Exception,该 Exception 是其父类声明抛出异常 IOException 类的父类,这将导致程序无法通过编译。因此在编写类继承代码时要注意,子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多

throw语句用于抛出异常。
当 throw 语句执行时,它后面的语句将不执行,此时程序转向调用者程序,寻找与之相匹配的 catch 语句,执行相应的异常处理程序。如果没有找到相匹配的 catch 语句,则再转向上一层的调用程序。这样逐层向上,直到最外层的异常处理程序终止程序并打印出调用栈情况。

throws 关键字和 throw 关键字在使用上的几点区别如下:
①throws 用来声明一个方法可能抛出的所有异常信息,表示出现异常的一种可能性,但并不一定会发生这些异常;throw 则是指拋出的一个具体的异常类型,执行 throw 则一定抛出了某种异常对象
②通常在一个方法(类)的声明处通过 throws 声明方法(类)可能拋出的异常类,比如throws ArrayIndexOutOfBoundsException,如果有多个异常类的时候,要用逗号隔开,而在方法(类)内部通过 throw 声明一个具体的异常信息,即抛出的是一个对象,比如throw new ArrayIndexOutOfBoundsException(“数组越界”)

多异常捕获

我们学习的多 catch 代码块虽然客观上提高了程序的健壮性,但是也导致了程序代码量大大增加。如果有些异常种类不同,但捕获之后的处理是相同的,例如以下代码。

try{
    // 可能会发生异常的语句
} catch (FileNotFoundException e) {
    // 调用方法methodA处理
} catch (IOException e) {
    // 调用方法methodA处理
} catch (ParseException e) {
    // 调用方法methodA处理
}

3 个不同类型的异常,要求捕获之后的处理都是调用 methodA 方法。为了解决这种问题,Java 7 推出了多异常捕获技术,可以把这些异常合并处理。上述代码修改如下:

try{
    // 可能会发生异常的语句
} catch (IOException | ParseException e) {
    // 调用方法methodA处理
}

注意:由于 FileNotFoundException 属于 IOException 异常,IOException 异常可以捕获它的所有子类异常。所以不能写成 FileNotFoundException | IOException | ParseException 。也就是说,如果再catch语句中,如果有某一个异常是另一个异常的子类,那么只要写这个异常父类就可以了,不用写那个异常子类了。

使用一个 catch 块捕获多种类型的异常时需要注意如下两个地方。
①捕获多种类型的异常时,多种异常类型之间用竖线 | 隔开。
②捕获多种类型的异常时,异常变量有隐式的 final 修饰,因此程序不能对异常变量重新赋值。但是捕获一种类型的异常时,异常变量没有 final 修饰,所以可以对异常变量重新赋值

自定义异常

实现自定义异常类需要继承 Exception 类或其子类,比如如果自定义运行时异常类需继承 RuntimeException 类或其子类。

如果在一个方法中有涉及到自定义异常,那么需要在方法声明的后面用throws来声明可能抛出的异常,如果不是自定义的,比如ArrayIndexOutOfBoundsException,那么可以不声明,也可以声明。

自定义异常类一般包含两个构造方法:一个是无参的默认构造方法,如果是这样的话,那么它的父类也是无参的构造方法,另一个是带字符串1的构造方法,那么它的父类也是有参的构造方法,并且这个参数就是这个字符串,才可以到最后可以输出这个异常信息,否则如果父类是一个无参的构造方法,那么就不会输出任何异常信息,同时如果父类是带字符串2的构造方法,那么输出的异常信息就是字符串2,而不是原来自己定义的字符串1,这个字符串将作为该异常对象的描述信息(也就是异常对象的getMessage()方法的返回值)。比如下面的throw new DemoZiDingYiException(“角标不可以为负数”);,括号里的字符串就是这个自定义异常类的信息。

自定义异常类的格式:

class IntegerRangeException extends Exception {
/*
自定义异常类无参的构造方法,那么父类也应该是无参,才可以满足最后没有输出异
*常信息,否则如果调用父类有参的构造方法,那么最后会输出异常信息,与原来的
*要求相违背
*/
    public IntegerRangeException() {
        super();
    }
/*
*自定义异常类有参的构造方法,调用父类的构造方法的时候,是调用父类有参的构
*造方法,并且这个参数是s,才可以实现最后输出的异常信息是s,否则不是
*/
    public IntegerRangeException(String s) {
        super(s);
    }
}

其实和ArrayIndexOutOfBounds异常类的定义方法是一样的,因为他们都是相关的异常类的子类嘛,不信,我们可以看看ArrayIndexOutOfBounds这个异常类的代码:

public ArrayIndexOutOfBoundsException(String s) {
        super(s);
    }

自定义代码实例:

自定义异常类代码:

public class DemoZiDingYiException extends Exception {
    public DemoZiDingYiException(String message){
//最后输出的异常信息就是message,如果这个父类的构造方法的参数不是message,那么最后的输出异常信息不是自己原先输入的那个
         super(message);
    }

}

测试代码:

public class DemoException {
   
//如果main方法也不知道如何处理这种类型的异常(个人觉得是一种自定义异常),要使用throws声明抛出异常,该异常将交给JVM处理
   public static void main(String[] args) throws DemoZiDingYiException {
        int[] arr = new int[3];
        try {
//一旦监测到异常,那么就直接从try中跳到catch语句中,剩下的语句不在被执行,找到和异常匹配的catch语句
            new DemoException().method(arr, 2);
            new DemoException().method(arr,3);
            new DemoException().method(arr,-3);
        }catch (DemoZiDingYiException e){
//在函数中有多少个异常,就要catch几个异常,如果在下面方法method中只有一个异常,但是实际这里的catch多于一个异常,那么就会报错
//如果只有一个catch语句,那么就可以是Exception,这里是多重catch语句,所以Exception要放在最后,否则会报错
            System.out.println(e.getMessage());//输出异常的信息
            e.printStackTrace();//调用方法,从而输出异常的类名、异常的信息、异常的位置
            System.out.println(e.toString());//输出输出异常的类名、异常的信息
        }catch(ArrayIndexOutOfBoundsException e){
            System.out.println(e.toString());//输出输出异常的类名、异常的信息
            e.printStackTrace();//调用方法,从而输出异常的类名、异常的信息、异常的位置
            System.out.println(e.getMessage());//输出异常的信息
        }finally{
/**上面的catch语句种无论是否发生异常,都会执行这个finally语句,用于清空代
*码,注意如果在finally语句中同样发生异常,如果这一种异常是一种自定义异常,那么就一定要throws来声明可能会发生这一种异常,否则如果不是自定义异常就不用声明也可以,之后就会回到就利用JVM来处理这一种异常
*/
            new DemoException().method(arr,-3);//如果在method中有throws异常,那么这个函数也要throws异常,然后后面的语句不在被执行
            System.out.println("这里学习异常");
        }
    }

    public void method(int[] arr, int index) throws DemoZiDingYiException{
 //如果当前方法中不知道怎么处理异常(即没有catch,那么就要声明异常),如果不是自定义异常类,那么可以不声明,但是如果是自定义异常类,那么一定要声明
        if(index < 0)
            //自定义异常DemoZiDingYiException
            throw new DemoZiDingYiException("角标不可以为负数");//throw用于抛出异常对象
        if(index >= arr.length)
            throw new ArrayIndexOutOfBoundsException("数组发生越界");//throw抛出异常之后,throw语句后面的内容不在被执行,直接返回
        System.out.println("当前的下标为:"+index);//没有异常的时候,就输出
    }
}

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

finally和return执行顺序

如果在try–catch–finally语句中含有return,那么执行顺序为:
①执行try/catch语句中return后面的操作,
②执行finally语句
③然后再执行这个try/catch语句中的语句return语句,直接返回退出当前这个方法,而不管后面的语句了。

代码如下:

public class DemoException2 {
    static int b = 3;
    public static void main(String[] args){
        System.out.println("main: "+new DemoException2().method(b));
    }
    public int method(int b){
        try {
            System.out.println("try "+b);//步骤1,输出try 3
 /*
 *先进行try中的return语句,此时b为4,不过这时候并没有直接返回,而是要进行
 *finally这一步操作,此时将finally进行完毕之后,将返回到try这个return中,
 *直接返回,try-catch-finally语句块后面的语句并没有进行
 */
            return b++;//步骤2,将这个return语句后的语句b++完成,此时b = 4
        }catch(Exception e){
            System.out.println(e.getMessage());
        }finally {
            System.out.println("finally " + b);//步骤3,输出finally 4
            b += 10;//步骤4,b进行相关的操作,此时b为14
 /*
 *步骤5,完成finally之后,已经完成了清理内存的步骤,此时在try-catch-finally
 *语句中对于b的更改一切归零,返回到了一开始的时候b刚进入到try语句时的值3,然后从
 *finally中返回到try中的return,然后直接返回,从而退出了这个方法
 */
        }
        b += 10;
        System.out.println("try--catch--finally完毕之后: "+b);
        return b;
    }
}

结果为:
在这里插入图片描述
当然也可以通过debug会更加直观哦

finally中也有return,那么执行顺序有所不同,在执行完finally语句中的相关操作后,遇到了ruturn,直接返回退出这个方法。
代码如下:

public class DemoException2 {
    static int b = 3;
    public static void main(String[] args){
        System.out.println("main: "+new DemoException2().method(b));
    }
    public int method(int b){
        try {
            System.out.println("try "+b);//步骤1 输出try 3
            //先进性try中的return后面的b++,完毕之后,并没有直接返回,而是要进行finally这一步操作,此时将finally进行完毕之后,将返回到try这个return中,直接返回
            return b++;//步骤2,进行return后面的语句,此时b为4
        }catch(Exception e){
            System.out.println(e.getMessage());
        }finally {
 //如果再finally中有return的话,那么try-catch-finally语句快后面不能有语句了,否则会发生报错
            System.out.println("finally " + b);//步骤3,输出 finally 4
            b+= 10;//步骤5 b = 14
            return b;//步骤6 finally中有return,所以直接返回b即14,而不用再回到try中的return中了
        }
/*
*由于finally语句中含有return,那么try-catch-finally语句块后面不能有语句,
*所以要注释掉,否则会发生报错,如果运行的话,会发现它提示这些语句是无法访
*问到的语句,因此这时候不可以在try-catch-finally语句块后面不能有语句
*/
//        b += 10;
//        System.out.println("try--catch--finally完毕之后: "+b);
//        return b;
    }
}

结果:
在这里插入图片描述
注意此时对比前面代码的结果,发现最后输出main的结果竟然不同,这是为什么呢?下面的代码和上面的又有什么区别呢?如果大家有想法的,请大家指教哈

public class DemoException2 {
    static int b = 3;
    public static void main(String[] args){
        System.out.println("main: "+new DemoException2().method(b));
    }
    public int method(int b){
        int num = b;
        try {
            System.out.println("try "+num);
        }catch(Exception e){
            System.out.println(e.getMessage());
        }finally {
            System.out.println("finally " + num);
            num = 100;
        }
        num += 10;
        System.out.println("try--catch--finally完毕之后: "+num);
        return num;
    }
}

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

上面哪些知识点有错误的地方,请大家指正哈

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值