Java 异常学习

782 阅读26分钟

为什么要使用异常

在缺乏内置异常处理机制的情况下,我们过去常依赖函数返回值来传达错误或异常信息,比如约定特定值(如-11111)表示异常状态。这种方法虽然能够基本实现错误的传达,但伴随而来的是几个显著的局限性:

  1. 混淆可能性高:当异常标识与合法的程序输出重叠时,区分真正的异常与正常数据变得困难。例如,如果一个函数在遇到错误时返回-11111,而该数值同时也可能是函数某个有效计算结果,这将导致混淆不清,增加误判风险。
  2. 代码清晰度下降:将错误检查逻辑穿插在业务逻辑之中,会大幅降低代码的可读性和整洁度。错误处理代码与核心功能代码混合,使得阅读和理解程序流程变得复杂,不利于维护和后续的开发工作。
  3. 对开发者知识深度的要求提高:这种模式要求调用者必须详细了解被调用函数的所有可能返回值及其含义,包括哪些是成功指示,哪些代表特定的错误情况。这不仅加重了开发者的负担,也容易因为对库函数理解不深而错误处理异常,影响程序的健壮性。

因此,传统的返回值异常标识法,尽管简单直接,却因上述缺陷而在现代软件开发中逐渐被结构化的异常处理机制所取代。后者通过明确的异常抛出与捕获机制,不仅清晰地区分了正常控制流与异常处理路径,还允许更灵活和详细的错误信息传递,提高了代码的模块化、可读性和整体的程序稳定性。

在面向对象编程(Object-Oriented Programming, 简称OO)中,异常处理机制是一种强大工具,用于增强代码的健壮性与稳定性。它通过简化错误处理逻辑,显著提升了程序的可维护性和可读性。若不采用异常处理机制,开发者需在代码的多处手动检查潜在错误并分别处理这些错误情况,这无疑增加了代码的复杂度和冗余度。

相反,利用异常处理,开发者无需在每次方法调用后立即检查错误状态。异常机制自动捕获运行时错误或由开发人员主动抛出的异常,并将其传递给专门的异常处理器。这样一来,错误处理逻辑集中在一个或几个统一的地方,与程序的主要业务逻辑分离。这种设计不仅减少了代码量,还清晰地区分了“正常流程中的业务操作”与“异常情况下的处理策略”,使得代码结构更为清晰,便于理解、编写及后期的调试工作。

因此,相比传统错误处理方式,异常处理机制为程序错误管理提供了一个更有序、高效的框架,正如《Thinking in Java》一书中所强调的那样,它极大改善了软件开发过程中的代码组织、可读性和可维护性。

在《Thinking in Java》中阐述的异常概念,其核心在于强调异常是程序执行过程中遇到的一种特殊情况,它迫使当前方法或代码块无法按预期继续执行。这里的关键点是认识到:异常本质上标志着程序中发生了某些错误或异常状况,它们偏离了程序正常运作的轨道。尽管Java语言为我们配备了强大的异常处理机制,但这并不意味着我们在设计和编写代码时应将异常视为日常或期望的部分;相反,异常处理机制的存在,是为了警示开发者:“此处可能存在或已发生错误,表明程序运行状态非正常,有可能引发程序崩溃。”

异常发生的时机非常关键:当程序在当前执行环境下无法继续正常推进,即遇到它无法克服的问题以至于无法完成既定任务时,异常就会被触发。这一过程具体包括以下几个步骤:

  1. 创建异常对象:系统首先通过new关键字实例化一个异常对象,该对象封装了关于错误的详细信息,如错误类型、发生位置及可能的原因等。
  2. 终止当前执行流:在异常发生点,程序的正常执行流程被立即停止,这意味着从当前执行点开始的后续代码不再执行。
  3. 抛出异常:异常对象被抛出后,控制权从当前环境转移。此时,Java的异常处理机制介入,它负责查找一个合适的地点来接手并尝试恢复程序的执行,这个地方即异常处理程序(通常位于try-catchtry-catch-finally块中)。

综上所述,异常处理机制扮演着“安全网”的角色。一旦程序遭遇异常,它会立即中止错误现场的进一步执行,记录下必要的异常信息,并将控制权转移给预设的异常处理器。这不仅帮助开发者识别和诊断问题,还提供了机会来优雅地处理这些错误情况,决定是尝试恢复程序执行、通知用户还是采取其他适当的补救措施。

简而言之,异常处理机制的本质作用在于,当程序遭遇意料之外的问题,即异常时,它能够捕捉这些错误,记录详细的错误信息,并给予程序一个恢复的机会,而不是直接导致程序崩溃。通过使用try-catch或者try-catch-finally结构,开发者可以预见到潜在的错误场景,并为这些异常情况编写专门的处理逻辑,从而提升程序的健壮性和用户体验。因此,面对异常,我们的态度应当是谨慎且准备充分,利用好Java提供的工具来妥善管理和应对程序中的不寻常情况。

异常体系

Throwable是java语言中所有错误和异常的超类(万物即可抛)。它有两个子类:Error、Exception。

Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。

Throwable又派生出Error类和Exception类

  • 错误:Error类及其子类表示的是系统级错误,这类错误通常由Java虚拟机(JVM)生成,用于指示发生了非常严重的异常情况,比如内存溢出(OutOfMemoryError)、线程死锁或是虚拟机内部错误。这类错误通常情况下应用程序不应该去捕获或尝试处理,因为它们表示的是程序无法恢复的情况。
  • 异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。它又分为两大类:受检异常(checked exceptions)和非受检异常(unchecked exceptions)。

受检异常(checked exceptions)

  1. 编译时检查:受检异常在编译时期会被Java编译器强制检查。如果某个方法可能抛出受检异常,要么必须在该方法内使用try-catch语句块捕获并处理这个异常,要么必须在方法签名中使用throws关键字声明这个异常,将其传递给上层调用者处理。这意味着编译器不会允许程序忽略这些异常,不处理就无法通过编译。

  2. 设计意图:受检异常通常用于表示程序运行过程中可能出现的外部错误或异常情况,这些情况往往是程序自身无法控制的,比如文件不存在、网络连接失败等。它们提示开发者需要预先考虑并处理这些潜在的问题。

  3. 典型例子:包括IOException、SQLException、ClassNotFoundException等。

这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样的异常时刻准备着。如SQLException , IOException,ClassNotFoundException 等。

非受检异常(unchecked exceptions)

  1. 编译时不检查:与受检异常相反,非受检异常在编译时期不会被强制要求处理。这意味着你可以选择捕获这些异常,也可以完全不捕获,编译器都不会报错。它们在程序中可以自由地抛出而无需显式地在方法签名中声明。
  2. 设计意图:非受检异常通常用来表示编程错误或违反了某种假设的情况,例如空指针访问(NullPointerException)、数组越界(ArrayIndexOutOfBoundsException)等。这些异常理论上是可以通过良好的编程实践避免的,因此不需要强制性的处理机制。
  3. 典型例子:所有继承自RuntimeException的异常都是非受检异常,包括但不限于上述提到的NullPointerException、ArrayIndexOutOfBoundsException以及IllegalArgumentException等。

对于这些异常,我们应该修正代码,而不是去通过异常处理器处理 。这样的异常发生的原因多半是代码写的有问题。如除0错误ArithmeticException,错误的强制类型转换错误ClassCastException,数组索引越界ArrayIndexOutOfBoundsException,使用了空对象NullPointerException等等。

需要明确的是:检查和非检查是对于javac来说的,这样就很好理解和区分了。

异常DEMO

异常发生在执行特定函数过程中,考虑到函数间的层级调用关系形成了调用链(或称为调用栈)。一旦某个函数中抛出了异常,这不仅影响当前函数的执行流程,还会波及到调用它的所有上级函数,层层上溯。因此,当异常发生时,为了帮助开发者定位问题源头,Java会自动生成一个异常追踪栈。这个追踪栈详细记录了异常发生时的函数调用序列,每一个调用层级作为栈中的一帧,展示了异常传播的完整路径,从而为开发者提供了从异常发生点追溯到最初起源的详细指南,极大地便利了错误诊断和调试工作。

异常最先发生的地方,叫做异常抛出点。

package com.evn.exception;

import java.util.Scanner;

/**
 * @Description
 * @ClassName Test
 * @Author Evan
 * @date 2020.07.19 21:38
 */
public class Test {
    public static void main(String[] args) {
        System.out.println("----欢迎使用命令行除法计算器----");
        CMDCalculate();

    }

    public static void CMDCalculate() {
        Scanner scan = new Scanner(System.in);
        int num1 = scan.nextInt();
        int num2 = scan.nextInt();
        int result = devide(num1, num2);
        System.out.println("result:" + result);
        scan.close();
    }

    public static int devide(int num1, int num2) {
        return num1 / num2;
    }

}
----欢迎使用命令行除法计算器----
1
0
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.evn.exception.Test.devide(Test.java:28)
	at com.evn.exception.Test.CMDCalculate(Test.java:22)
	at com.evn.exception.Test.main(Test.java:14)

Process finished with exit code 1
----欢迎使用命令行除法计算器----
r
Exception in thread "main" java.util.InputMismatchException
	at java.util.Scanner.throwFor(Scanner.java:864)
	at java.util.Scanner.next(Scanner.java:1485)
	at java.util.Scanner.nextInt(Scanner.java:2117)
	at java.util.Scanner.nextInt(Scanner.java:2076)
	at com.evn.exception.Test.CMDCalculate(Test.java:20)
	at com.evn.exception.Test.main(Test.java:14)

Process finished with exit code 1

从上面的例子可以看出,当devide函数发生除0异常时,devide函数将抛出ArithmeticException异常,因此调用他的CMDCalculate函数也无法正常完成,因此也发送异常,而CMDCalculate的调用者main方法因为CMDCalculate抛出异常也发生了异常,这样一直向调用栈的栈底回溯。

这种行为叫做异常的冒泡,异常的冒泡是为了在当前发生异常的函数或者这个函数的中找到最近的异常处理程序。由于这个例子中没有使用任何异常处理机制,因此异常最终由main函数抛给JRE,导致程序终止。

上面的代码不使用异常处理机制,也可以顺利编译,因为2个异常都是非检查异常。但是下面的例子就必须使用异常处理机制,因为异常是检查异常。

package com.core.throwable;

//错误即error一般指jvm无法处理的错误
//异常是Java定义的用于简化错误处理流程和定位错误的一种工具。
public class Test2 {

    //下面这四个异常或者错误有着不同的处理方法
    public void error1() {
        //Throwable 是所有错误和异常的根,因此编译器要求必须在catch块中处理它。一旦捕获到Throwable实例,它将被打印出来。
        try {
            throw new Throwable();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }

    // 这个方法使用throws关键字将可能抛出Throwable类的异常声明出来。与error1()方法类似,这也是一个最顶层的异常。
    // 在方法体内直接使用throw关键字抛出Throwable对象,因为异常声明出去了,所以在方法内部不需要捕获该异常。
    public void error2() throws Throwable {
        throw new Throwable();
    }

    //Exception也必须处理。否则报错,因为检查异常都继承自exception,所以默认需要捕捉。
    public void error3() {
        try {
            throw new Exception();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //由于Error类表示严重的问题,通常应用程序不应捕获它们,所以编译器不要求处理。
    public void error4() {
        throw new Error();
    }

    //RuntimeException及其子类是未受检查的异常,通常不要求在代码中进行捕获或声明。
    public void error5() {
        throw new RuntimeException();
    }
}

class Test2Client {
    public static void main(String[] args) {

        Test2 test2 = new Test2();

        test2.error1();
        try {
            test2.error2();
        } catch (Throwable e) {
            System.out.println("捕获了异常:" + e.getMessage());
        }
        System.out.println();
        test2.error3();
        try {
            test2.error4();
        } catch (Throwable e) {
            System.out.println("捕获了Error:" + e.getMessage());
        }
        try {
            test2.error5();
        } catch (Throwable e) {
            System.out.println("捕获了RuntimeException:" + e.getMessage());
        }
        System.out.println("执行结束 End");
    }
}
java.lang.Throwable
	at com.core.throwable.Test2.error1(Test2.java:11)
	at com.core.throwable.Test2Client.main(Test2.java:48)
java.lang.Exception
	at com.core.throwable.Test2.error3(Test2.java:26)
	at com.core.throwable.Test2Client.main(Test2.java:55)

捕获了异常:null
捕获了Errornull
捕获了RuntimeExceptionnull
执行结束 End

Process finished with exit code 0

异常的处理方式

Java中的异常处理机制是一种结构化的错误管理方式,它允许程序在遇到错误条件时优雅地进行处理,而不是直接终止执行。这一机制基于try-catch-finally语句块和throw关键字,以及异常类的层次结构来实现。下面是Java异常处理机制的核心组成部分和工作原理:

1. try块

  • 用途:try块用于包裹可能会抛出异常的代码段。
  • 行为:如果在try块内的代码执行时没有发生异常,则正常执行完毕后继续。如果遇到异常,则立即停止try块中剩余代码的执行,并寻找合适的catch块来处理这个异常。

2. catch块

  • 用途:catch块用来捕获并处理try块中抛出的异常。
  • 语法:紧跟在try块后面,可以有一个或多个catch块,每个catch块可以指定它能处理的异常类型。
  • 行为:当异常被抛出时,Java会从上至下检查每个catch块,将异常对象传递给第一个匹配该异常类型的catch块执行。

3. finally块

  • 用途:无论是否发生异常,finally块中的代码都会被执行。
  • 适用场景:通常用于释放资源,如关闭文件、网络连接等。
  • 行为:即使try块中有return语句或者异常未被捕获导致方法提前结束,finally块中的代码依然会执行。

4. throw关键字

  • 用途:用于显式抛出一个异常。
  • 语法:可以在任何代码块中使用throw new ExceptionType("异常信息")来抛出一个异常对象。
  • 作用:可以手动抛出异常来表明某个特定条件未满足或发生了错误。

5. 异常类层次结构

  • 基类:所有异常都继承自java.lang.Throwable类。
  • 两大分支:Throwable类的两个直接子类分别是Exception(大部分可处理的异常)和Error(一般不被捕获,代表严重错误,如内存溢出)。
  • 自定义异常:可以通过继承Exception或其子类来创建自定义异常类,以适应特定应用的需求。

工作流程总结

  1. 程序进入try块执行。
  2. 如果在try块中遇到异常,该异常会被抛出。
  3. Java运行时系统寻找合适的catch块来匹配并处理该异常。
  4. 如果找到匹配的catch块,执行其中的异常处理代码。
  5. 不论异常是否被捕获,都会执行finally块(如果有的话)。
  6. 如果没有匹配的catch块处理异常,且当前方法未声明抛出此异常,则异常会向上传递给调用者,这个过程会一直持续到异常被处理或导致程序终止。

Java的异常处理机制通过这种方式,提供了处理错误的灵活性和程序的健壮性,同时保持了代码的清晰和可维护性。

基于try-catch-finally语句块

下面看几个具体的例子,包括error,exception和throwable

上面的例子是运行时异常,不需要显示捕获。 下面这个例子是可检查异常需,要显示捕获或者抛出。

   public void testException(){
        File file =new File("C:\a.txt");
        FileInputStream fis = new FileInputStream(file);
    }

处理方法

public void testException() throws IOException {
        File file = new File("C:\a.txt");

        FileInputStream fis = new FileInputStream(file);

        int word;

        //read方法会抛出IOException
        while ((word = fis.read()) != -1) {
            System.out.print((char) word);
        }
        //close方法会抛出IOException
        fis.close();
    }

一般情况下的处理方式 try catch finally

    public void test2() {
        try {
            //try块中放可能发生异常的代码。
            InputStream inputStream = new FileInputStream("C:\Go\robots.txt");
            //如果执行完try且不发生异常,则接着去执行finally块和finally后面的代码(如果有的话)。
            int i = 1 / 0;
            //如果发生异常,则尝试去匹配catch块。
            throw new SQLException();
            //使用1.8jdk同时捕获多个异常,runtimeexception也可以捕获。只是捕获后虚拟机也无法处理,所以不建议捕获。
        } catch (SQLException | IOException | ArrayIndexOutOfBoundsException exception) {
            System.out.println(exception.getMessage());
            //每一个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7中可以将多个异常声明在一个catch中。
            //catch后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个catch块来处理异常。
            //在catch块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。
            //如果当前try块中发生的异常在后续的所有catch中都没捕获到,则先去执行finally,然后到这个函数的外部caller中去匹配异常处理器。
            //如果try中没有发生异常,则所有的catch块将被忽略。
        } catch (Exception exception) {
            System.out.println(exception.getMessage());
            //...
        } finally {
            //finally块通常是可选的。
            //无论异常是否发生,异常是否匹配被处理,finally都会执行。
            //finally主要做一些清理工作,如流的关闭,数据库连接的关闭等。
        }
    }

一个try至少要跟一个catch或者finally

try {
	 int i = 1;
 }finally {
 		//一个try至少要有一个catch块,否则, 至少要有1finally块。但是finally不是用来处理异常的,finally不会捕获异常。
 }
}

异常出现时该方法后面的代码不会运行,即使异常已经被捕获。这里举出一个奇特的例子,在catch里再次使用try catch finally

    public void Test3() {
        try {
            throwE();
            System.out.println("我前面抛出异常了");
            System.out.println("我不会执行了");
        } catch (StringIndexOutOfBoundsException e) {
            System.out.println(e.getMessage());
        } catch (Exception ex) {
            //在catch块中仍然可以使用try catch finally
            ex.printStackTrace();
            try {
                throw new Exception();
            } catch (Exception ee) {
                // catch 中可以把捕获的异常抛出,也可以不做任何操作
                // ee.printStackTrace();
            } finally {
                System.out.println("我所在的catch块没有执行,我也不会执行的");
            }
        }
    }

    // 在方法声明中抛出的异常必须由调用方法处理或者继续往上抛,
    // 当抛到jre时由于无法处理终止程序
    public void throwE() {
//        Socket socket = new Socket("127.0.0.1", 80);
        //手动抛出异常时,不会报错,但是调用该方法的方法需要处理这个异常,否则会出错。
//        java.lang.StringIndexOutOfBoundsException
//        at com.javase.异常.异常处理方式.throwE(异常处理方式.java:75)
//        at com.javase.异常.异常处理方式.test(异常处理方式.java:62)
        throw new RuntimeException("我发生了异常,且抛出");
    }
java.lang.RuntimeException: 我发生了异常,且抛出
	at com.evn.exception.Test2.throwE(Test2.java:114)
	at com.evn.exception.Test2.Test3(Test2.java:87)
	at com.evn.exception.Test2Client.main(Test2.java:125)
我所在的catch块没有执行,我也不会执行的

其实有的语言在遇到异常后仍然可以继续运行

有的编程语言当异常被处理后,控制流会恢复到异常抛出点接着执行,这种策略叫做:resumption model of exception handling(恢复式异常处理模式 )而Java则是让执行流恢复到处理了异常的catch块后接着执行,这种策略叫做:termination model of exception handling(终结式异常处理模式)

throws关键字

throws是另一种处理异常的方式,它不同于try…catch…finally,throws仅仅是将函数中可能出现的异常向调用者声明,而自己则不具体处理。

采取这种异常处理的原因可能是:方法本身不知道如何处理这样的异常,或者说让调用者处理更好,调用者需要为可能发生的异常负责。

public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN
{ 
 //foo内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常,或者他们的子类的异常对象。
}

finally代码块

finally块不管异常是否发生,只要对应的try执行了,则它一定也执行。只有一种方法让finally块不执行:System.exit()。因此finally块通常用来做资源释放操作:关闭文件,关闭数据库连接等等。

良好的编程习惯是:在try块中打开资源,在finally块中清理释放这些资源。

需要注意的地方:

  1. finally块没有处理异常的能力。处理异常的只能是catch块。
  2. 在同一try…catch…finally块中 ,如果try中抛出异常,且有匹配的catch块,则先执行catch块,再执行finally块。如果没有catch块匹配,则先执行finally,然后去外面的调用者中寻找合适的catch块。
  3. 在同一try…catch…finally块中 ,try发生异常,且匹配的catch块中处理异常时也抛出异常,那么后面的finally也会执行:首先执行finally块,然后去外围调用者中寻找合适的catch块。
package com.evn.exception;

public class FinallyDemo {
    public static void main(String[] args) {
        System.out.println("start ...");
        try {
            throw new IllegalAccessException();
        } catch (IllegalAccessException e) {
            // throw new Throwable();
            //此时如果再抛异常,finally无法执行,只能报错。
            //finally无论何时都会执行
            //除非我显示调用。此时finally才不会执行
            System.exit(0);
        } finally {
            System.out.println("算你狠");
        }
    }
}
start ...

Process finished with exit code 0

throw 关键字

程序员也可以通过throw语句手动显式的抛出一个异常。throw语句的后面必须是一个异常对象。

throw 语句必须写在函数中,执行throw 语句的地方就是一个异常抛出点,它和由JRE自动形成的异常抛出点没有任何差别。

    public void save(String user) {

        if (user == null || user.equals("")) {
            throw new IllegalArgumentException("User对象为空");
        } else {
            System.out.println("user ===》" + user);
        }
    }
Exception in thread "main" java.lang.IllegalArgumentException: User对象为空
	at com.evn.exception.Test2.save(Test2.java:120)
	at com.evn.exception.Test2Client.main(Test2.java:134)

异常调用链

异常的链化

在一些大型的,模块化的软件开发中,一旦一个地方发生异常,则如骨牌效应一样,将导致一连串的异常。假设模块B完成自己的逻辑需要调用模块A的方法,如果模块A模块发生异常,则模块B也将不能完成而发生异常。但是模块B在抛出异常时,会将模块A的异常信息掩盖掉,这将使得异常的根源信息丢失。异常的链化可以将多个模块的异常串联起来,使得异常信息不会丢失。

异常链化:以一个异常对象为参数构造新的异常对象。新的异对象将包含先前异常的信息。这项技术主要是异常类的一个带Throwable参数的函数来实现的。这个当做参数的异常,我们叫他根源异常(cause)。

查看Throwable类源码,可以发现里面有一个Throwable字段cause,就是它保存了构造时传递的根源异常参数。这种设计和链表的结点类设计如出一辙,因此形成链也是自然的了。

public class Throwable implements Serializable {
    
    private transient Object backtrace;

    private String detailMessage;

    private Throwable cause = this;

    
    public Throwable() {
        fillInStackTrace();
    }

    public Throwable(String message) {
        fillInStackTrace();
        detailMessage = message;
    }

    public Throwable(Throwable cause) {
        fillInStackTrace();
        detailMessage = (cause==null ? null : cause.toString());
        this.cause = cause;
    }

    
}

一个异常链例子

package com.core.throwable;

public class ExceptionDemo1 {

    public void A1() {
        try {
            int i = 1 / 0;
        } catch (ArithmeticException e) {
            //当捕获到ArithmeticException时,该方法通过创建并抛出一个新的RuntimeException,
            // 同时将原始异常e作为新异常的cause。这样的处理方式确保了异常能够被上层调用者感知(因为RuntimeException是未检查异常,不需要在方法签名中声明),
            // 同时也保留了原始异常的详细信息。这使得调用者可以决定如何处理这个运行时错误,而不仅仅是停留在方法A内部。
            throw new RuntimeException("A方法计算错误", e);
        }
        System.out.println("A1 不可以继续执行");
    }


    public void A2() {
        try {
            int i = 1 / 0;
        } catch (ArithmeticException e) {
            // 当捕获到ArithmeticException时,通过调用e.printStackTrace()直接在控制台打印异常的堆栈跟踪信息,然后继续执行方法余下的部分(如果有的话)。
            // 这种方法没有重新抛出异常,因此对于方法的调用者而言,除非它们有其他机制监控程序的输出流,否则它们无法直接得知A方法内部发生了异常。程序也不会因为这个异常而停止执行或向上层抛出错误,
            // 这可能不符合某些情况下对错误处理的期望,尤其是当希望上层逻辑能够根据异常做出响应时。
            e.printStackTrace();
        }
        System.out.println("A2 可以继续执行");
    }

    public void A3() {
        try {
            int i = 1;
            i = i / 0;
        } catch (ArithmeticException e) {
            //ignore
        }
        System.out.println("A3 可以继续执行");
    }

    public void B() throws Exception, Error {
        try {
            //接收到A的异常,
            A1();
            throw new RuntimeException("B 抛出一个异常");
        } catch (Exception e) {
            System.out.println("方法B 的catch 块执行");
            throw e;
        } catch (Error error) {
            throw new Error("B也犯了个错误", error);
        }
    }

    public void C() {
        try {
            B();
            System.out.println("try 中的 代码");
        } catch (Exception | Error e) {
            System.out.println("方法C 的catch 块执行");
            e.printStackTrace();
        }
        System.out.println("c 方法执行结束");
    }
}

class TestClient {
    public static void main(String[] args) {
        ExceptionDemo1 test = new ExceptionDemo1();
        test.C();
    }

}

调用A1方法

方法B 的catch 块执行
方法C 的catch 块执行
c 方法执行结束
java.lang.RuntimeException: A方法计算错误
	at com.core.throwable.ExceptionDemo1.A(ExceptionDemo1.java:11)
	at com.core.throwable.ExceptionDemo1.B(ExceptionDemo1.java:18)
	at com.core.throwable.ExceptionDemo1.C(ExceptionDemo1.java:30)
	at com.core.throwable.TestClient.main(ExceptionDemo1.java:43)
Caused by: java.lang.ArithmeticException: / by zero
	at com.core.throwable.ExceptionDemo1.A(ExceptionDemo1.java:8)
	... 3 more

调用A2方法

java.lang.ArithmeticException: / by zero
	at com.core.throwable.ExceptionDemo1.A2(ExceptionDemo1.java:19)
	at com.core.throwable.ExceptionDemo1.B(ExceptionDemo1.java:43)
	at com.core.throwable.ExceptionDemo1.C(ExceptionDemo1.java:55)
	at com.core.throwable.TestClient.main(ExceptionDemo1.java:68)
java.lang.RuntimeException: B 抛出一个异常
	at com.core.throwable.ExceptionDemo1.B(ExceptionDemo1.java:44)
	at com.core.throwable.ExceptionDemo1.C(ExceptionDemo1.java:55)
	at com.core.throwable.TestClient.main(ExceptionDemo1.java:68)
A2 可以继续执行
方法B 的catch 块执行
方法C 的catch 块执行
c 方法执行结束

调用A3方法

A3 可以继续执行
方法B 的catch 块执行
方法C 的catch 块执行
c 方法执行结束
java.lang.RuntimeException: B 抛出一个异常
	at com.core.throwable.ExceptionDemo1.B(ExceptionDemo1.java:44)
	at com.core.throwable.ExceptionDemo1.C(ExceptionDemo1.java:55)
	at com.core.throwable.TestClient.main(ExceptionDemo1.java:68)

自定义异常

自定义异常是Java编程中的一种良好实践,它允许你创建符合特定应用程序需求的异常类。这不仅提高了代码的可读性和可维护性,还使得异常处理更加精确,能够更好地表达程序中遇到的问题。

  • 如果要自定义异常类,则扩展Exception类即可,因此这样的自定义异常都属于检查异常(checked exception)。
  • 如果要自定义非检查异常,则扩展自RuntimeException。

自定义的异常应该总是包含如下的构造函数:

  • 一个无参构造函数
  • 一个带有String参数的构造函数,并传递给父类的构造函数。
  • 一个带有String参数和Throwable参数,并都传递给父类构造函数
  • 一个带有Throwable 参数的构造函数,并传递给父类的构造函数。

下面是IOException类的完整源代码,可以借鉴。

package java.io;


public class IOException extends Exception {

    public IOException() {
        super();
    }

 
    public IOException(String message) {
        super(message);
    }

    
    public IOException(String message, Throwable cause) {
        super(message, cause);
    }


    public IOException(Throwable cause) {
        super(cause);
    }
}

自定义异常通常会继承自现有的异常类,最常见的基类是Exception或RuntimeException。如果你希望异常能够在编译时被检查,则应继承自Exception;如果希望它是未检查异常,则继承自RuntimeException。

自定义异常类

// 继承自Exception,这是一个受检异常
public class MyCustomException extends Exception {
    
    public MyCustomException() {
        super(); // 调用父类构造器
    }

    public MyCustomException(String message) {
        super(message); // 传递异常信息给父类
    }

    public MyCustomException(String message, Throwable cause) {
        super(message, cause); // 同时传递消息和原因
    }

    public MyCustomException(Throwable cause) {
        super(cause);
    }
}

// 如果你想创建一个未检查异常,则继承自RuntimeException
public class MyCustomRuntimeException extends RuntimeException {

    public MyCustomRuntimeException() {
        super(); // 调用父类构造器
    }

    public MyCustomRuntimeException(String message) {
        super(message); // 传递异常信息给父类
    }

    public MyCustomRuntimeException(String message, Throwable cause) {
        super(message, cause); // 同时传递消息和原因
    }

    public MyCustomRuntimeException(Throwable cause) {
        super(cause);
    }
}

抛出自定义异常

public void someMethod() throws MyCustomException {
    if (someConditionIsTrue()) {
        throw new MyCustomException("发生了一个特定的错误");
    }
    // 或者,如果是运行时异常
    // throw new MyCustomRuntimeException("发生了一个特定的错误");
}

捕获和处理自定义异常

和其他异常一样,自定义异常也可以被捕获并处理。

try {
    someMethod();
} catch (MyCustomException e) {
    System.out.println("捕获到了自定义异常:" + e.getMessage());
    // 这里可以进行特定的错误处理逻辑
} catch (MyCustomRuntimeException e) {
    // 处理运行时异常
}

为什么使用自定义异常?

  • 提高代码清晰度:自定义异常能够更准确地描述发生的问题,使阅读代码的人更容易理解异常的具体含义。

  • 增强错误处理的灵活性:你可以设计异常类包含更多与错误相关的数据成员,提供更多的错误处理信息。

  • 遵循设计原则:遵循“针对接口编程,而不是针对实现编程”的原则,自定义异常也是接口(即异常的类型)的一部分,使代码更易于维护和扩展。

异常的注意事项

异常的注意事项

  • 当子类重写父类的带有 throws声明的函数时,其throws声明的异常必须在父类异常的可控范围内——用于处理父类的throws方法的异常处理器,必须也适用于子类的这个带throws方法 。这是为了支持多态。 例如,父类方法throws 的是2个异常,子类就不能throws 3个及以上的异常。父类throws IOException,子类就必须throws IOException或者IOException的子类。

至于为什么?我想,也许下面的例子可以说明。

class Father {
    public void start() throws IOException {
        throw new IOException();
    }
}

class Son extends Father {
    //报错
    @Override
    public void start() throws Exception {
        throw new SQLException();
    }
}
class TestClientDemo {
    public static void main(String[] args) {
        
        Father[] objs = new Father[2];
        objs[0] = new Father();
        objs[1] = new Son();
        for (Father obj : objs) {
            //因为Son类抛出的实质是SQLException,而IOException无法处理它。
            //那么这里的try。。catch就不能处理Son中的异常。
            //多态就不能实现了。
            try {
                obj.start();
            } catch (IOException e) {
                //处理IOException
            }
        }
    }
}

Java的异常执行流程是线程独立的,线程之间没有影响