接上次博客:Java学习(12)(String类、String的查找方法、字符串转化、 替换、拆分、截取、trim方法、字符串的不可变性、StringBuilder和StringBuffer)_di-Dora的博客-CSDN博客
目录
异常的概念
异常的体系结构
异常的分类
1、编译时异常:
2、运行时异常:
异常的处理
防御式编程
异常的抛出
异常的捕获
异常声明throws
try-catch捕获并处理
finally
异常的处理流程
自定义异常类
异常的概念
在计算机编程中,异常是指程序运行过程中发生的错误或异常情况、不正常行为。它打破了程序的正常执行流程。当出现异常时,程序通常会停止执行,同时系统会输出异常信息,帮助程序员定位和解决问题。
我们之前已经遇到过很多了:算数异常、空指针异常、数组越界异常、栈溢出……
上述过程中可以看到,java中不同类型的异常,都有与其对应的类来进行描述。
接下来我们就走进“异常的世界”——我认为这不是一个“混乱的”、“熵值非常大的”世界,相反,这是一共“法制严明的”、“秩序井然的”世界,因为计算机的“眼里容不得任何沙子”!
异常的体系结构
异常种类繁多,为了对不同异常或者错误进行很好的分类管理,Java内部维护了一个异常的体系结构:
如上图,
Throwable是异常体系的顶层类,也是所有异常的父类,它派生出两个重要的子类,分别是Error和Exception。
Error指的是Java虚拟机无法解决的严重问题,比如JVM的内部错误、资源耗尽等。一旦发生这种错误,程序将无法恢复并且必须终止。典型的Error包括StackOverflowError和OutOfMemoryError。
Exception是Throwable的另一个子类,通常表示程序可以处理的异常情况。
Exception包括运行时异常和编译时异常。运行时异常是指在程序运行期间可能会发生的异常情况,这些异常情况可以通过编写程序进行捕捉和处理,但也可以选择不进行处理,由程序自行终止。常见的运行时异常包括NullPointerException、ArrayIndexOutOfBoundsException和ClassCastException等。编译时异常异常是指在程序编译期间就能够发现的异常,这些异常必须在代码中进行捕捉和处理,否则程序无法通过编译。常见的检查时异常包括IOException和ClassNotFoundException等。
异常的分类
异常可以根据其发生的时机被分为编译时异常和运行时异常。
1、编译时异常:
编译时异常是在编译阶段就能够被检测出来的异常,也称为受检查异常(Checked Exception)。编译时异常指的是在编译阶段就可能出现的异常,如果程序中存在受检查异常,那么在程序编译的过程中就会被检查出来,并且要求程序必须在方法声明中使用throws语句声明该异常或在方法内使用try-catch语句进行捕获和处理。
例如,文件操作中的IOException就是一个受检查异常,如果我们要对一个文件进行操作,那么就有可能会发生读写异常等,这就需要在程序中进行处理。
2、运行时异常:
运行时异常是在程序运行过程中发生的异常,也称为非受检查异常(Unchecked Exception)。运行时异常指的是程序在运行阶段才会出现的异常,如果程序中存在运行时异常,编译器在编译的时候并不会强制要求程序必须使用try-catch语句进行捕获和处理,也不需要使用throws语句进行声明。
例如,空指针异常(NullPointerException)、数组下标越界异(ArrayIndexOutOfBoundsException)、算术异常(ArithmeticException)等都是运行时异常。这些异常在程序运行时出现,一般是由于代码逻辑错误或者程序运行环境异常导致的。
注意:RunTimeException以及其子类对应的异常,都称为运行时异常。
还有,编译时出现的语法性错误,不能称之为异常。例如将 System.out.println 拼写错了, 写成了 system.out.println. 此时编译过程中就会出错, 这是 "编译期" 出错。而运行时指的是程序已经编译通过得到 class 文件了, 再由 JVM 执行过程中出现的错误。
总的来说,编译时异常和运行时异常的区别在于是否需要在程序中进行捕获和处理。对于受检查异常,程序必须进行捕获和处理,否则无法通过编译;对于运行时异常,程序可以不进行处理,但建议进行处理,以避免程序出现问题导致系统崩溃。
异常的处理
在编写代码时,我们必须考虑代码中可能会出现的各种错误情况,并且及时通知程序员。为了实现这一目标,我们需要采取防御式编程的策略,即在代码中预先考虑和处理异常。主要有两种方式:
防御式编程
LBYL: Look Before You Leap. 在操作之前做充分的检查,即事前防御。这种方式可以有效地避免出现异常,但是代码中正常流程和错误处理流程会混在一起,使代码整体显得比较混乱。
boolean ret = false;
ret = ……();
if (!ret) {
处理匹配错误;
return;
}
ret = ……();
if (!ret) {
处理匹配错误;
return;
}
ret = 游戏确认();
if (!ret) {
……
return;
}
……
EAFP: It's Easier to Ask Forgiveness than Permission.—— “事后获取原谅比事前获取许可更容易”。也就是先操作,遇到问题再处理,即事后认错型。这种方式的优点是:正常流程和错误流程是分离开的,程序员更关注正常流程,代码更清晰易懂。异常处理的核心思想就是 EAFP。
try {
1();
2();
3();
4();
5();
...
} catch (1异常) {
处理1异常;
} catch (2异常) {
处理2异常;
} catch (3异常) {
处理3异常;
} catch (4异常) {
处理4异常;
} catch (5异常) {
处理6异常;
}
在Java中,异常处理主要使用5个关键字:throw、try、catch、finally、throws。
其中,throw关键字用于抛出异常对象;try、catch和finally关键字用于捕获和处理异常;throws关键字用于声明方法可能抛出的异常。我们可以使用这些关键字来编写代码并处理异常,从而保证程序的稳定性和正确性。接下来我们就来具体看看这些关键词:
异常的抛出
在Java中,可以借助throw关键字,抛出一个指定的异常对象,将错误信息告知给调用者。具体语法如下:
throw new XXXException("异常产生的原因");
举个例子:假设我们编写一个计算器程序,其中有一个除法运算方法divide,现在我们想在调用该方法时,如果除数为0,抛出一个ArithmeticException异常,告知调用者出现了“除数不能为0”的错误信息。
public static double divide(int dividend, int divisor) throws ArithmeticException {
if (divisor == 0) {
throw new ArithmeticException("除数不能为0");
}
return dividend / divisor;
}
在上面的代码中,我们首先通过if语句判断除数是否为0,如果为0,则使用throw关键字抛出一个新的ArithmeticException异常对象,并且传入了一个错误信息字符串"除数不能为0"。在调用该方法时,如果出现了除数为0的情况,该方法就会抛出一个ArithmeticException异常,并将错误信息"除数不能为0"告知给调用者。
注意事项:
1. throw必须写在方法体内部 。
2.必须抛出一个已经存在的异常对象,或者是继承自Exception或RuntimeException的自定义异常对象。
3. 如果抛出的是 RunTimeException 或者 RunTimeException 的子类,则可以不用处理,直接交给JVM来处理 。
4. 如果抛出的是编译时异常,用户必须处理,否则无法通过编译 。
5. 当抛出异常时,会立即停止当前方法的执行,并且不会执行当前方法后面的语句。
6.抛出异常时,应当尽量提供有意义的错误信息,便于调用者进行问题定位和处理。
7.如果抛出的异常不被处理,将会一直沿着调用栈向上抛出,直到被捕获或者程序终止。
8.当使用throw关键字时,必须在方法签名上声明可能抛出的异常类型,或者在方法内部使用try-catch语句捕获并处理异常。
嗯?什么意思?我们马上来看看异常的捕获。
异常的捕获
异常的捕获,也就是异常的具体处理方式,主要有两种:异常声明throws 以及 try-catch捕获处理。
异常声明throws
处在方法声明时参数列表之后,当方法中抛出编译时异常,用户不想处理该异常,此时就可以借助throws将异常抛给方法的调用者来处理。即当前方法不处理异常,提醒方法的调用者处理异常。
语法格式:
修饰符 返回值类型 方法名(参数列表) throws 异常类型1,异常类型2...{
}
import java.io.FileNotFoundException;
import java.io.FileReader;
public class FileProcessor {
public void processFile(String fileName) throws FileNotFoundException {
FileReader reader = new FileReader(fileName);
// do something with the file
}
}
在上述代码中,processFile 方法使用了 FileReader 类来读取指定文件的内容。但是,FileReader 的构造函数可能会抛出 FileNotFoundException 异常,即当指定的文件不存在时会抛出该异常。因此,在 processFile 方法中使用了异常声明 throws FileNotFoundException,告诉调用该方法的代码,可能会抛出该异常,需要进行处理。
这样做的好处是让方法的调用者知道该方法可能会抛出哪些异常,并可以根据需要进行相应的处理。
注意事项:
1. throws必须跟在方法的参数列表之后
2. 声明的异常必须是 Exception 或者 Exception 的子类,即只有在方法签名中声明的受检查异常才需要使用 throws,运行时异常不需要声明。
3. 方法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型具有父子关系,直接声明父类即可。
public void OpenConfig(String filename) throws IOException,FileNotFoundException{
}
//FileNotFoundException 继承自 IOException
public void OpenConfig(String filename) throws IOException{
}
但是要注意,在使用 throws 声明抛出异常时,应该尽量精细化,只抛出必要的异常类型,不要一股脑地抛出所有异常类型。
5.如果一个方法声明抛出了异常,那么该方法内部就必须处理该异常,否则编译器会报错;
如果一个方法声明抛出了某些异常,那么调用该方法时,必须捕获或再次声明该方法抛出的异常(继续使用throws抛出),否则编译器会报错。
我在之前某次博客讲过如何快速处理:将光标放在抛出异常方法上,alt + Insert :
try-catch捕获并处理
throws对异常并没有真正处理,而是将异常报告给抛出异常方法的调用者,由调用者处理。如果真正要对异常进行 处理,就需要try-catch。
语法格式:
try{
// 将可能出现异常的代码放在这里
}catch(要捕获的异常类型 e){
// 如果try中的代码抛出异常了,此处catch捕获时异常类型与try中抛出的异常类型一致时,或者是try中抛出异常的基类时,就会被捕获到
// 对异常就可以正常处理,处理完成后,跳出try-catch结构,继续执行后序代码
}【catch(异常类型 e){
// 对异常进行处理
}finally{
// 此处代码一定会被执行到
}】
// 后序代码
// 当异常被捕获到时,异常就被处理了,这里的后序代码一定会执行
// 如果捕获了,由于捕获时类型不对,那就没有捕获到,这里的代码就不会被执行
注意:
1. 【】中表示可选项,可以添加,也可以不用添加
2. try中的代码可能会抛出异常,也可能不会
注意事项:
1. try块内抛出异常位置之后的代码将不会被执行 。
2. 如果抛出异常类型与catch时异常类型不匹配,即异常不会被成功捕获,也就不会被处理,继续往外抛,直到 JVM收到后中断程序----异常是按照类型来捕获的。
3. try中可能会抛出多个不同的异常对象,则必须用多个catch来捕获----即多种异常,多次捕获。
public static void main(String[] args) {
int[] arr = {1, 2, 3};
try {
System.out.println("before");
// arr = null;
System.out.println(arr[100]);
System.out.println("after");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("这是个数组下标越界异常");
e.printStackTrace();
} catch (NullPointerException e) {
System.out.println("这是个空指针异常");
e.printStackTrace();
}
System.out.println("after try catch");
}
}
如果多个异常的处理方式是完全相同, 也可以写成这样:
catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
...
}
我们知道,当一个异常类继承自另一个异常类时,它们就具有父子关系。在使用 try-catch 块处理异常时,通常应该先捕获子类异常,再捕获父类异常,因为子类异常是父类异常的特殊情况。
举个例子,假设我们有两个自定义异常类:MyExceptionA 和 MyExceptionB,其中 MyExceptionB 继承自 MyExceptionA。现在在一个方法中可能会抛出这两种异常,我们需要使用 try-catch 块来处理它们,应该按照子类在前,父类在后的顺序来写:
try {
// some code that may throw MyExceptionB or MyExceptionA
} catch (MyExceptionB e) {
// handle MyExceptionB
} catch (MyExceptionA e) {
// handle MyExceptionA (including MyExceptionB)
}
这样写是正确的,因为 MyExceptionB 是 MyExceptionA 的子类,如果我们先捕获了 MyExceptionA,那么 MyExceptionB 就永远不会被捕获到,这是不正确的。
4. 可以通过一个catch捕获所有的异常,即多个异常,一次捕获(但是不推荐)。
由于 Exception 类是所有异常类的父类. 因此可以用这个类型表示捕捉所有异常.。
例如: catch 进行类型匹配的时候, 不光会匹配相同类型的异常对象, 也会捕捉目标异常类型的子类对象. 如刚才的代码, NullPointerException 和 ArrayIndexOutOfBoundsException 都是 Exception 的子类, 因此都能被捕获到:
try {
// some code that might throw an exception
} catch ( Exception e){
// handle the exception
}
//catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
// handle the exception
//}
但是我们不推荐在一个 catch 块中捕获多个不同类型的异常!!!因为这样做可能会导致代码的可读性和可维护性降低。当出现异常时,不同类型的异常需要进行不同的处理,将它们混在一起可能会让代码变得复杂和难以理解。
此外,在某些情况下,捕获父类异常可能会掩盖子类异常的细节信息,这可能会导致难以调试和修复问题。因此,最好在 catch 块中只捕获需要处理的特定异常类型,而不是一次捕获多个不同类型的异常。
还有,
我们的异常的处理应该有针对性:不同的异常需要有不同的处理方式,所以在 catch 块中应该根据不同的异常类型进行处理,而不是使用相同的处理方式。
不要忽略异常:即使在 catch 块中没有对异常进行处理,也应该在 catch 块中输出异常信息或记录日志,以便于后期调试和问题排查。
不要滥用异常:异常机制虽然可以方便地处理错误,但是由于异常的抛出和处理会带来额外的系统开销,所以不应该滥用异常机制,避免对程序性能造成影响。
finally
在写程序时,有些特定的代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源:网络连接、数据库 连接、IO流等,在程序正常或者异常退出时,必须要对资源进进行回收。另外,因为异常会引发程序的跳转,可能 导致有些语句执行不到,finally就是用来解决这个问题的。
语法格式:
try{
// 可能会发生异常的代码
}catch(异常类型 e){
// 对捕获到的异常进行处理
}finally{
// 此处的语句无论是否发生异常,都会被执行到
}
// 如果没有抛出异常,或者异常被捕获处理了,这里的代码也会执行
假设有一个人需要进行银行转账,转账过程中需要进行金额验证,如果金额不足会抛出异常。同时,转账过程需要记录日志,无论成功或失败都需要在最后关闭连接。以下是一个使用了 finally 块的代码示例:
public void transferMoney(double amount) throws InsufficientFundsException {
try {
if (balance < amount) {
throw new InsufficientFundsException("Insufficient funds in account");
}
// 进行转账操作
} catch (InsufficientFundsException e) {
System.out.println(e.getMessage());
} finally {
// 记录日志
System.out.println("Transfer complete.");
// 关闭连接
}
}
在这个例子中,try 块中进行了金额验证和转账操作,如果金额不足,会抛出 InsufficientFundsException 异常,同时 catch 块中打印了异常信息。无论转账是否成功,finally 块都会被执行,完成日志记录和关闭连接的操作。这样可以保证无论发生什么错误,都可以及时关闭连接,防止资源泄漏。
注意:finally中的代码一定会执行的,一般在finally中进行一些资源清理的扫尾工作。
由上面的解说,我们自然而然地会提出一个问题:
既然 finally 和 try-catch-finally 后的代码都会执行,那为什么还要有finally呢?
finally 块的主要作用是在程序退出 try-catch 块之前,确保一些代码被执行。
即 finally 执行的时机是在方法返回之前 ( try 或者 catch 中如果有 return ,会在这个 return 之前执行 finally)。但是如果 finally 中也存在 return 语句, 那么就会执行 finally 中的 return, 从而不会执行到 try 中原有的 return。但是,一般我们不建议在 finally 中写 return (因为会被编译器当做一个警告)。
无论是否抛出异常,finally 中的代码都会被执行,包括需要释放的资源,比如打开的文件或数据库连接等。
当一个 try-catch-finally 块执行时,如果异常被抛出,catch 块会捕获异常并处理它,然后 finally 块中的代码会被执行。如果没有异常被抛出,try 块中的代码执行完后,finally 块中的代码也会被执行。
另外,finally 块可以在 try-catch 块中的 return 语句之前执行,因此可以在 finally 块中执行一些清理工作,确保程序退出前的资源释放和状态更新。
因此,虽然在 try-catch 块中的代码和 finally 块中的代码都会被执行,但是 finally 块的作用是确保必须执行的代码被执行,以防止一些潜在的问题。
再举个例子:假设我们有一个 程序,需要从用户输入中读取数据并进行处理:
import java.util.Scanner;
public class Example {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
try {
System.out.print("Please enter a number: ");
int num = scanner.nextInt();
System.out.println("You entered: " + num);
} catch (Exception e) {
System.out.println("An error occurred: " + e.getMessage());
} finally {
scanner.close();
System.out.println("Scanner closed.");
}
System.out.println("Program ended.");
}
}
在上面的示例中,我们使用了 try-catch-finally 块来处理用户输入。在 try 块中,我们读取用户输入并进行处理。在 catch 块中,我们处理任何可能发生的异常。在 finally 块中,我们关闭输入流并输出一条消息。
现在,假设用户输入了一个有效的整数。程序将读取该整数并输出结果。但是,在此之后,程序将继续执行 finally 块中的代码,关闭输入流并输出一条消息。这是因为无论代码是否抛出异常,finally 块中的代码都将被执行。在本例中,这确保了输入流被正确地关闭并清理了资源,避免了资源泄漏问题。
如果我们没有使用 finally 块,而是在 try-catch 块之后直接结束程序,那么输入流将不会被关闭,可能导致资源泄漏。
让我们来看两道面试题:
1.throw 和 throws 的区别?
throw 和 throws 都是与异常处理有关的关键字,但含义不同。
throw 用于在代码中手动抛出异常对象,通常用在方法内部,表示发生了某种异常情况,需要抛出异常以通知调用者。
而 throws 则用于在方法声明中指定该方法可能会抛出的异常类型,以便调用者能够处理这些异常情况。
2. finally中的语句一定会执行吗?
finally 中的语句一般情况下是一定会执行的,但在某些情况下,finally 中的语句可能不会执行,如在 finally 块中使用了 System.exit() 等直接退出程序的方法,或者在 try 块中使用了 Thread.stop() 等会直接导致程序终止的方法。此外,在 finally 块中抛出异常也会导致 finally 中的语句不执行。
异常的处理流程
关于 "调用栈" :
调用栈(Call Stack)是用来记录当前程序在执行过程中各个方法之间调用关系的一种数据结构。当一个方法被调用时,该方法的信息会被保存在栈中,当该方法执行完毕后,它的信息就会从栈中被弹出。在方法执行过程中,如果调用了其他的方法,那么这些方法的信息也会被保存在栈中,形成一个方法调用的栈。
调用栈的操作通常有两种:压栈和弹栈。当一个方法被调用时,它的信息就会被压入调用栈中;当该方法执行完毕后,它的信息就会从调用栈中弹出。调用栈的深度取决于程序的复杂度和递归的层数,如果调用栈太深,就可能会导致栈溢出等问题。
方法之间是存在相互调用关系的, 这种调用关系我们可以用 "调用栈" 来描述。 在 JVM 中有一块内存空间称为 "虚拟机栈" ——调用栈,专门存储方法之间的调用关系。当代码中出现异常的时候, 我们就可以使用 e.printStackTrace(); 的方式查看出现异常代码的调用栈。
我们用一个例子简单演示一下:当某个方法无法处理异常时,该异常会沿着调用栈向上传递的过程。
假设有一个买家购买商品的场景,涉及到多个类的逐层调用:
public class Main {
public static void main(String[] args) {
try {
User user = new User("John");
Order order = new Order(user);
Payment payment = new Payment(order);
Shipping shipping = new Shipping(payment);
shipping.ship();
} catch (Exception e) {
System.out.println("发生了异常:" + e.getMessage());
}
}
}
class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
class Order {
private User user;
public Order(User user) {
this.user = user;
}
public User getUser() {
return user;
}
}
class Payment {
private Order order;
public Payment(Order order) {
this.order = order;
}
public Order getOrder() {
return order;
}
}
class Shipping {
private Payment payment;
public Shipping(Payment payment) {
this.payment = payment;
}
public void ship() throws Exception {
if (Math.random() < 0.5) {
throw new Exception("订单无法发货");
}
System.out.println(payment.getOrder().getUser().getName() + "的订单已发货");
}
}
在这个例子中,我们首先创建了一个名为 User 的类,表示用户。然后,我们创建了一个名为 Order 的类,表示订单。订单有一个用户属性,类型为 User。接着,我们创建了一个名为 Payment 的类,表示付款。付款有一个订单属性,类型为 Order。最后,我们创建了一个名为 Shipping 的类,表示发货。发货有一个付款属性,类型为 Payment。在 Shipping 类中,我们对发货过程进行了简单的模拟。如果 Math.random() 方法生成的随机数小于 0.5,则抛出一个异常表示订单无法发货。否则,输出用户姓名和订单发货信息。
在 Main 类的 main 方法中,我们创建了一个用户,然后创建一个订单,并将该订单作为参数传递给一个付款对象。接着,我们将付款对象传递给一个发货对象,并调用其 ship 方法进行发货。在 ship 方法中,我们可能会抛出一个异常。如果发生异常,该异常会沿着调用栈向上抛出,最终会被 Main 类的 catch 语句捕获。
【异常处理流程总结】:
- 程序先执行 try 中的代码 如果 try 中的代码出现异常, 就会结束 try 中的代码, 看和 catch 中的异常类型是否匹配。
- 如果找到匹配的异常类型, 就会执行 catch 中的代码。
- 如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者。
- 无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行)。
- 如果上层调用者也没有处理的了异常, 就继续向上传递。
- 一直到 main 方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止。
自定义异常类
在开发过程中,有时候标准的异常类并不能满足我们的需求,所以我们需要自定义异常类来处理特定的异常情况。
自定义异常类:自定义异常类是继承于 Exception 或 RuntimeException 类的类,用于表示一种特定的异常情况。通常情况下,我们需要自定义异常类来处理特定的业务逻辑异常,这样可以使代码更加清晰易懂,也更容易定位和处理异常。
具体方式:
1. 自定义异常类,然后继承自Exception 或者 RunTimeException
2. 实现一个带有String类型参数的构造方法(参数含义:出现异常的原因)。
假设我们正在开发一个学生成绩管理系统,系统中有一个查询学生成绩的方法 getScore(String studentId),如果查询不到该学生的成绩,则应该抛出一个自定义的异常类StudentScoreNotFoundException。该异常类可以继承于 Exception 类,并且需要提供一个无参构造方法和一个带有消息参数的构造方法,如下所示:
public class StudentScoreNotFoundException extends Exception {
public StudentScoreNotFoundException() {
super("Student score not found.");
}
public StudentScoreNotFoundException(String message) {
super(message);
}
}
在查询学生成绩的方法中,如果查询不到该学生的成绩,则可以抛出 StudentScoreNotFoundException 异常,代码如下所示:
public int getScore(String studentId) throws StudentScoreNotFoundException {
int score = 0;
// 根据学生 ID 查询成绩
// 如果查询不到,则抛出 StudentScoreNotFoundException 异常
if (score == 0) {
throw new StudentScoreNotFoundException("Student score not found for ID " + studentId);
}
return score;
}
在调用 getScore 方法时,如果捕获到了 StudentScoreNotFoundException 异常,则可以根据业务需要进行相应的处理,如给用户提示信息等等。
在这个类中定义了两个构造函数,它们分别是:
1、public StudentScoreNotFoundException():
这个构造函数没有参数,调用它会调用父类 Exception 的无参构造函数,同时设置异常信息为 "Student score not found."。
2、public StudentScoreNotFoundException(String message):
这个构造函数有一个字符串类型的参数 message,调用它会调用父类 Exception 的带一个字符串参数的构造函数,同时将参数 message 作为异常信息。
这两个构造函数都是为了在创建 StudentScoreNotFoundException 对象时,可以通过不同的方式设置异常信息。通常,我们可以使用第二个构造函数来传递更详细的异常信息,以便更好地提示用户发生了什么错误。
我在学习过程中产生了一些疑问:为什么在实现自定义异常类型的时候,一定要写一个带有String类型参数的构造方法?可以不要这个参数吗?
经查询资料,总结如下:
在实现自定义异常类型时,一定要写一个带有String类型参数的构造方法是因为这个参数可以提供出现异常的具体原因。在抛出异常时,调用这个构造方法传入具体的错误信息,可以让程序员更好地理解出现异常的原因,更方便地调试和排除错误。
如果不写这个带有String类型参数的构造方法,抛出的异常信息就会比较模糊,不利于程序员排查问题。而且Java规定了Throwable类及其子类都必须有一个String类型的构造方法来传递异常信息,因此自定义异常类也应该遵循这个规定。
当然,自定义异常类可以有多个构造方法,但至少要有一个带有String类型参数的构造方法来传递异常信息。
还要注意:自定义异常通常会继承自 Exception 或者 RuntimeException。继承自 Exception 的异常默认是受查异常;继承自 RuntimeException 的异常默认是非受查异常。