【Java】异常的初步认识

06-02 1136阅读

目录

1. 异常的概念与体系结构

1.1 异常类的总架构

1.2 Exception 狭义体系下的异常分类(异常抛出的时机)

1.3 Throwable 广义体系下的异常分类

1.4 异常对象的创建和打印

2. 与异常相关的5个关键字

2.1 throw关键字 与 throws关键字

1. 异常的手动抛出:throw 

2. 异常抛出的显式声明:throws

2.2 try-catch 语句

2.3 finally 关键字

3. 自定义异常类

4. 防御式编程

4.1 事前防御型

4.2 事后认错型

## 总结:异常的处理流程与影响 ##


1. 异常的概念与体系结构

异常的概念:

在Java中,将程序执行过程中发生的 不正常行为 或 非预期事件 称为异常。

异常对象:

为了表示异常,Java使用了异常类来把不正常事件封装成了异常对象。在运行期间,这些异常对象会由系统根据错误类型自动创建,并把错误信息和上下文一并封装进对象中。

例如:下面的代码会触发空指针异常。

    public static void main(String[] args) {
        int[] arr = null;
        System.out.println(arr[3]);
    }

【Java】异常的初步认识

本次异常信息显示:

  • 触发异常的是主线程main
  • 触发的异常类型是NullPointerException(空指针异常)
  • 给出解释:不能使用arr数组,因为它为空null;
  • 触发语句所在位置是:exception包底下的 Test1类中的 main方法里面的 第10行位置。

    这些异常信息全部都封装在系统创建的异常对象中。

    1.1 异常类的总架构

    刚刚解释了什么是异常和异常对象,它里面大概有什么内容,也了解到异常对象是根据异常类来创建的。那么异常类是什么呢?异常类有多少呢?

    异常种类繁多,请看下图:

    【Java】异常的初步认识·

    异常类的概念:

    广义上,异常类指的是Throwable的子类(严格来说Throwable不是异常类)。狭义上,异常类指的是Exception类及其子类。

    从上图中可以看到,Throwable后面产生了2个分支:

    • Throwable:是异常体系的顶层类,但其本身不是异常类,它派生出两个重要的子类, Error 和 Exception(有且只有这两个分支)。
    • Error(错误):指的是JVM无法解决的严重问题(系统级别的问题)。比如:JVM的内部错误、资源耗尽等,典型代表: StackOverflowError 和 OutOfMemoryError,一旦发生回力乏术。
    • Exception(异常):异常产生后程序员可以通过代码进行处理,使程序继续执行。这种异常类似感冒、发烧,是可以通过一些手段治好的。我们平时所说的异常就是Exception。

      栈溢出错误的举例:StackOverflowError

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

      【Java】异常的初步认识

      1.2 Exception 狭义体系下的异常分类(异常抛出的时机)

      在Exception异常体系下,我们把异常分为 运行时异常 与 编译时异常。

      运行时异常:(Runtime Exceptions)

      定义:指的是 RuntimeException类及其子类。

      特点:该类异常在运行期间才检查。

      编译时异常:(无官方对应术语)

      定义:指的是所有继承自 Exception 但不继承 RuntimeException 的异常类。

      特点:该类异常在编译期间检查。

      注意:对于异常的检查,运行时异常发生在编译期间,编译时异常发生在运行期间。异常的 触发 和 抛出 都发生在运行期间。(3种行为的执行顺序是:检查 ——> 触发 ——> 抛出。)

      • 检查:检查程序员是否存在处理异常的策略。
      • 触发:检查是否发生了异常,如果发生了就是触发。
      • 抛出:确定异常已经发生后,抛出异常是一种处理异常的策略。(异常的处理策略包括抛出 和 捕获处理)

        运行时异常 和 编译时异常的图示:运行时异常是黄色部分,编译时异常是绿色部分。

        【Java】异常的初步认识

        1.3 Throwable 广义体系下的异常分类

        只要是Throwable及其子类,都可以称为异常,所以error这样的系统级错误也是广义上的异常。广义的异常可以分成 受查异常 和 非受查异常。

        受查异常:(Checked Exceptions)

        囊括的范围:编译时异常。

        特点:编译器会强制要求我们处理(例如throws、try-catch)。在IDEA上,如果我们没对受查异常进行处理,那么会在可能触发受查异常的地方画上红色波浪线。

        非受查异常:(Unchecked Exceptions)

        囊括的范围:运行时异常(Runtime Exceptions)和 错误(errors)。

        特点:编译器不会强制要求我们处理。

        补充:

        其实编译时异常是中文口语而非官方叫法,受查异常(Unchecked Exceptions)才是官方叫法。本身 “在编译期间检查” 和 “编译器强制处理” 这两个特点都是受查异常的特点,而且并没有一种错误或异常会在编译期间触发和抛出

        受查异常 和 非受查异常的图示:非受查异常是蓝色部分,受查异常是红色部分。

        【Java】异常的初步认识

        1.4 异常对象的创建和打印

        在 Java 中,异常对象的管理和打印机制是 JVM 和 Java 标准库共同实现的。以下是关于异常对象创建、构造方法选择及打印机制的详细解析:

        JVM 在执行字节码时,会监控特定指令。当代码中发生异常时,JVM 会自动创建异常对象。

        printStackTrace()方法

        作用:用于输出异常的堆栈轨迹 和 错误信息。

        JVM自动打印:当异常对象创建出来后,如果无方法处理,那么异常对象就会一直往上层方法抛,直到抛给JVM。JVM会自动调用异常对象的printStackTrace()方法,并终止该线程。

        例如:JVM自动创建算术异常对象,由JVM打印后终止了程序,后面的代码不执行

            public static void main(String[] args) {
                System.out.println(8 / 0);  //触发算术异常
                //程序终止,后面的内容不执行
                System.out.println(111);
                System.out.println(222);
            }

        【Java】异常的初步认识

        如果异常是从深层方法抛出的,那么堆栈轨迹会把上抛的路线描述出来。 

        除了JVM自动创建异常对象,我们也可以通过异常类的构造方法手动创建异常对象并手动调用printStackTrace()方法,这里介绍2种构造方法:

        1. 无参构造方法:如Exception() 

        显示的错误信息:默认构造方法,无详细错误信息。

        2. 一个参数的构造方法,参数类型为String:如Exception(String message)

        显示的错误信息:message参数就是错误信息的详细描述,由程序员详细描述。

        例如:

            public static void main(String[] args) {
                RuntimeException e1 = new RuntimeException();
                RuntimeException e2 = new RuntimeException("由程序员描述异常信息");
                e1.printStackTrace();
                e2.printStackTrace();
            }

        【Java】异常的初步认识

        这里为了演示才专门创建出异常对象并打印,在实际开发中我们是在抛出异常的同时new出异常对象的,而不是现在这样创建异常对象但不抛出和处理。

        2. 与异常相关的5个关键字

        Error错误是系统级别的错误,无法干预纠正。但Exception异常是我们程序员可以通过代码来处理的,使程序继续正常运行。为了方便处理异常,Java提供了5个关键字:throw、throws、try、catch、finally。

        2.1 throw关键字 与 throws关键字

        1. 异常的手动抛出:throw 

        throw关键字

        作用:

        • 显式抛出一个异常对象,抛出的对象只能是 Exception 或其子类对象。
        • 由于是手动 throw 出异常,所以异常对象也要手动 new 创建。
        • 异常一旦被抛出,其后的代码都不执行。(中断当前执行流)

          注意:throw 必须写在方法体内部。

              public static void main(String[] args) {
                  int[] arr = null;
                  if(arr == null){
                      throw new NullPointerException("数组arr不能为空");
                  }
                  //抛出异常后,后面的代码不执行
                  System.out.println(3+5);
              }

          异常对象抛出后,后面的代码不执行,所以不会打印8:

          【Java】异常的初步认识


          上面的代码如果不显式抛出异常的话,直接访问数组arr的元素,那么JVM也会自动创建空指针异常对象并自动抛出:

              public static void main(String[] args) {
                  int[] arr = null;
                  System.out.println(arr[5]);
                  //抛出异常后,后面的代码不执行
                  System.out.println(3+5);
              }

          【Java】异常的初步认识

          可以看到,就算异常对象是由JVM抛出,后面的代码也是不执行,此处没有打印8。

          2. 异常抛出的显式声明:throws

          throws关键字

          作用:

          • 在方法签名(方法头)后面 声明该方法可能抛出的异常。
          • throws 后面是抛出的异常类型,数量可以多个,用“,”逗号隔开。
          • 如果本层方法(手动或自动)抛出的异常是受查异常,那么必须使用throws关键字声明;非受查异常无强制要求。

            反例: 

            【Java】异常的初步认识

            FileNotFoundException 是IOException的子类,而IOException是受查异常,此处主动抛出受查异常而没有throws声明,所以报错了

            【Java】异常的初步认识

            加上throws就不会编译报错:

            【Java】异常的初步认识

            (这里只需要声明一个FileNotFoundException就行,这里为了演示throws可声明多个异常所以加上了Exception)

            2.2 try-catch 语句

            throws对异常并没有真正处理,而是将异常报告给抛出异常方法的调用者(上层方法),由调用者处理。如果真正要对异常进行处理,就需要try-catch。

            语法格式:

            try {
                // 可能抛出异常的代码
            } catch (异常类型1 变量名1) {
                // 处理异常类型1的逻辑
            } catch (异常类型2 变量名2) {
                // 处理异常类型2的逻辑
            }
            

            注意:try语句只能有一个。

            标准流程:

            1. 先执行try代码块中的代码,如果某一条语句触发了异常,那么try代码块将终止执行。
            2. 触发异常后,会跳转到catch语句从上往下进行匹配。如果异常对象是某个catch异常参数类型的同类或子类,那么程序将进入该catch代码块并对异常进行处理。
            3. 其中一个catch代码块捕捉并处理异常后,直接退出整个try-catch,继续执行后面的代码。

            例如:

                static int getElement(int[] arr, int index){
                    if(arr == null){
                        throw new NullPointerException();
                    }
                    if(index = arr.length){
                        throw new ArrayIndexOutOfBoundsException();
                    }
                    return arr[index];
                }
                public static void main(String[] args) {
                    try {
                        int[] arr = null;
                        getElement(arr, 5); //抛出空指针异常
                    } catch (NullPointerException e) {      //匹配到处理到该catch
                        System.out.println("空指针异常的处理");
                    } catch (ArrayIndexOutOfBoundsException e){ //异常已处理,跳过该catch语句
                        System.out.println("下标越界的异常处理");
                    }
                    //异常处理完毕,程序继续运行
                    System.out.println(3+5);
                }

            【Java】异常的初步认识

            catch 的特点:

            1. 在没有finally的try-catch语句中,catch是必须存在的。
            2. catch语句支持多捕获,即可以有多个catch。
            3. 每个catch都是独立的代码块,括号()中的局部变量都有独立的作用域。
            4. 在Java7及后续版本,catch语句的小括号()不仅可以是单个具体异常,也支持使用逻辑或“|” 来同时捕获多个异常合并处理。
            5. 如果异常之间具有父子关系,一定是子类异常在前catch,父类异常在后catch,否则语法错误。这样可以避免具体问题的掩盖。

            特点5的反例:父类异常在前,子类异常在后

            【Java】异常的初步认识

            【Java】异常的初步认识

            由于Exception是RuntimeException的父类,所以如果捕获到Exception异常就相当于捕获到RuntimeException异常,所以此处报错说“已捕获XXX”,不能重复捕获。


            特点4的举例:用逻辑或来合并处理异常

                static int getElement(int[] arr, int index){
                    if(arr == null){
                        throw new NullPointerException();
                    }
                    if(index = arr.length){
                        throw new ArrayIndexOutOfBoundsException();
                    }
                    return arr[index];
                }
                public static void main(String[] args) {
                    try {
                        int[] arr = new int[3];
                        getElement(arr, 10); //抛出下标越界异常
                    } catch (NullPointerException | ArrayIndexOutOfBoundsException e) { //使用逻辑|,此处可以对于空指针异常和下标越界异常有相同的解决
                        System.out.println("异常已处理");
                    }
                    //异常处理完毕,程序继续运行
                    System.out.println(111);
                }

            【Java】异常的初步认识

            除了标准的流程,现实中还有其他情况的可能。

            情况1:无异常

            1. 把try代码块的内容全部执行完。
            2. 跳过所有catch语句。
            3. 继续执行try-catch后面的代码。

            情况2:无匹配的catch语句

            (在这种情况下,如果触发的是受查异常,那么编译器会强制要求加上throws)

            1. 在try代码块中,执行到异常处便停止。
            2. 匹配catch语句,发现无对应catch可以捕获。
            3. 把异常对象向调用者(上层方法)抛出,终止本层方法,try-catch后面的语句不执行。

            2.3 finally 关键字

            在写程序时,有些特定的代码,不论程序是否发生异常,都需要执行。比如程序中打开的资源:网络连接、数据库 连接、IO流等,在程序正常或者异常退出时,必须要对资源进进行回收。

            另外,因为异常会引发程序的跳转,可能 导致有些语句执行不到,finally就是用来解决这个问题的。

            语法格式:

            try {
                // 可能抛出异常的代码
            } catch (异常类型1 变量名1) {
                // 处理异常类型1的逻辑
            } catch (异常类型2 变量名2) {
                // 处理异常类型2的逻辑
            } finally {
                // 无论是否抛出异常,最终执行的代码
            }

            finally的作用:无论是否有异常,finally代码块的内容都会执行。(除非线程中断)

            补充:无论是try、catch还是finally,它们的大括号都不能省略,它们是独立的代码块。

            执行流程:

            【Java】异常的初步认识

            例如:

                static int getElement(int[] arr, int index){
                    if(arr == null){
                        throw new NullPointerException();
                    }
                    if(index = arr.length){
                        throw new ArrayIndexOutOfBoundsException();
                    }
                    return arr[index];
                }
                public static void main(String[] args) {
                    Scanner sc = null;
                    try {
                        sc = new Scanner(System.in);
                        System.out.println("请输入:");
                        int n = sc.nextInt();
                        int[] arr = new int[n];
                        getElement(arr, 10); //抛出下标越界异常
                    } catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
                        System.out.println("异常已处理");    //处理异常
                    } finally {
                        sc.close();
                        System.out.println("标准输入流已关闭");
                    }
                    //异常处理完毕,程序继续运行
                    System.out.println(111);
                }

            【Java】异常的初步认识

            需要注意的是,try代码块与finally代码块都是相互独立的。如果吧sc变量声明在try代码块中,那么sc变量的作用域只在try代码块中,finally语句将无法关闭输入流:

            【Java】异常的初步认识

            【Java】异常的初步认识

            finally 的特点:

            1. 在没有catch的 try-finally 语句中,finally是必须存在的。
            2. finally代码块是独立的区域,里面的局部变量拥有独立的作用域。
            3. 若finally块中有return语句,它会覆盖 try 或 catch 中的返回值,导致返回结果是finally块中的返回值。(不建议在finally语句中使用return)

            4. 如果在 finally 块中抛出异常,它会覆盖 try 或 catch 块中抛出的原始异常,导致原始异常丢失。(不建议在finally语句中抛出异常,而且finally只能抛出运行时异常

            特点3的举例:

                public static int func(){
                    try{
                       return 1;
                    }catch (Exception e){
                        return 2;
                    }finally {
                        return 3;
                    }
                }
                public static void main(String[] args) {
                    System.out.println("返回值是:" + func());
                }

            【Java】异常的初步认识

            特点4的举例:

                public static int func(int[] arr){
                    try{
                       if(arr == null)          //如果arr为null,抛出异常1;
                           throw new Exception("异常1");
                    }catch (Exception e){       //如果arr不为null,抛出异常2
                        throw new Exception("异常2");
                    }finally {                  //无论前面抛出了什么异常,最终都是抛出异常3
                        throw new RuntimeException("异常3");
                    }
                }
                public static void main(String[] args) {
                    //测试是否抛出异常1
                    int[] arr = null;
                    try {
                        func(arr);  //结果抛出异常3
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                    //测试是否抛出异常2
                    arr = new int[1];
                    try {
                        func(arr);  //结果抛出异常3
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }

            【Java】异常的初步认识

            3. 自定义异常类

            java中虽然已经内置丰富的异常类,但是并不能完全表示实际开发中所遇到的一些异常。所以java语法支持我们自定义异常类。

            注意:虽然技术上支持Error类的继承,但是Error类表示系统级的错误,强烈不推荐这样使用。而且try-catch语句不会捕获Error异常,自定义Error可能会忽略了异常处理,导致程序意外终止。

            Java官方规范明确了Error类是给 JVM 或系统使用的错误类型,开发者不应主动抛出或处理。仅在 极端底层场景 中可能需要自定义 Error,例如JVM的扩展开发。

            我们自定义异常类一般继承自Exception类 或 RuntimeException类。需要注意的是,Exception类是所有受查异常的基类,所以继承自Exception类的自定义异常是受查异常,继承自RuntimeException类的自定义异常是非受查异常。

            自定义异常的具体步骤:

            1. 自定义异常类,类名一般带有"Exception"字样。然后继承自Exception 或者 RunTimeException。
            2. 重写一个带有String类型参数的构造方法,用来提供异常原因的描述。

            例如写出一个登录异常类,用来辅助用户登录功能:

            public class LoginException extends RuntimeException{ //自定义:登录异常类
                public LoginException(String message) {  //可供描述的构造方法
                    super(message);
                }
            }
            public class Login extends RuntimeException{
                private String userName = "amy";    //被设置的用户名
                private String password = "123456"; //被设置的密码
                public Login(String userName, String password){
                    if(this.userName.equals(userName) && !this.password.equals(password)){    //匹配账户密码
                        throw new LoginException("密码输入错误 或 用户名不存在!");
                    }
                    System.out.println("登录成功");
                }
            }
            public class Demo3 {
                public static void main(String[] args) {
                    try{
                        Login login = new Login("amy", "654321");
                    }catch (LoginException e){
                        e.printStackTrace();
                    }
                    //成功登录后的后续操作
                    //…………
                }
            }

            【Java】异常的初步认识

            4. 防御式编程

            错误在代码中是客观存在的,因此我们要让程序出现问题的时候及时通知程序员。此时防御式编程的思维就显得很重要了。

            防御式编程是一种以预防为核心的编程方法论,旨在通过提前识别和处理潜在错误,提升代码的安全性和可靠性。其核心思想是“假设错误必然发生”,并通过设计防线来减少或消除错误的影响。

            例如:拦截非法输入,预防逻辑错误扩散,减少因外部依赖故障导致的系统崩溃。

            主要的方式可以分为事前防御型和事后认错型。

            4.1 事前防御型

            事前防御:(以预防为核心)

            在问题发生前采取行动,通过预判风险、制定规则、建立机制等手段,主动阻止问题发生。

            在Java代码中,常见的事前防御句式是 if + throw 。

            例如:抛出空指针异常

                public void func(int[] arr){
                    if(arr == null){
                        throw new NullPointerException("数组arr为空");
                    }
                    //后续对数组arr的使用…………
                }

            抛出断言错误:

                public static void func(int[] arr){
                    if(arr == null){
                        throw new AssertionError("参数不能为空引用");
                    }
                    //后续对数组arr的使用…………
                }

            用前置条件 if 判断该代码运行的逻辑是否错误,如果错误就会执行throw语句。throw语句后面的语句都不再执行,把出现的问题给拦截了。

            抛出的异常还能解决一下,要是抛出错误(例如此处的断言),则直接结束程序,让程序员解决完这个断言错误才能测试后面的代码运行。

            4.2 事后认错型

            事后防御:(以应对为核心)

            在问题发生后采取行动,通过检测、修复、补救等手段,减少问题造成的损失或影响。

            在Java代码中,常见的事后防御句式是 try-catch 。(前面已经讲解过try-catch-finally语句,这里不再详细介绍)

            ## 总结:异常的处理流程与影响 ##

            对于受查异常和非受查异常都有两种处理方案:

            受查异常:

            a. 在本层方法使用try-catch捕获处理。

            b. 给上层方法解决,但方法头必须显式throws异常类型。

            非受查异常:

            a. 在本层方法使用try-catch捕获处理。

            b. 给上层方法解决,但方法头不需要显式 throws。

            这两种方案有对应的流程和影响:

            方案a的最大影响:

            假如现在有3层方法,第1层无try-catch语句,第2层有try-catch语句,第3层无try-catch语句但是有可能抛出异常。

            • 如果第3层中执行了一半的语句抛出了异常,然后被第2层的方法try-catch捕捉了,那第3层后一半的语句都不执行。【影响整个方法】
            • 然后第2层try代码块中,处于第3层方法的调用后面的语句都不执行。【影响try代码块】
            • 然后对第1层方法无语句跳过的影响。【无影响】

              方案b的最大影响:

              在极端情况下,异常一直上抛,但无方法try-catch。那么最终由JVM抛出异常并终止该线程。

              其他易混淆点:

              Exception类属于什么异常?

              Exception是受查异常的基类,要当作受查异常处理,所以抛出Exception异常前要在方法标签用throws声明。

              什么样的异常(exceptions)会导致程序结束?

              • JVM抛出的异常 === 始终未被捕获的异常 ===  无方法处理的异常 : 在单线程环境下,这种异常会导致程序提前结束。

                本期分享完毕,感谢大家的支持Thanks♪(・ω・)ノ

                【Java】异常的初步认识

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

相关阅读

目录[+]

取消
微信二维码
微信二维码
支付宝二维码