Java基础补缺5:异常处理、常用工具类
1)Java异常处理全面解析
Error 的出现,意味着程序出现了严重的问题,而这些问题不应该再交给 Java 的异常处理机制来处理,程序应该直接崩溃掉,比如说 OutOfMemoryError,内存溢出了,这就意味着程序在运行时申请的内存大于系统能够提供的内存,导致出现的错误,这种错误的出现,对于程序来说是致命的。
Exception 的出现,意味着程序出现了一些在可控范围内的问题,我们应当采取措施进行挽救。比如说之前提到的 ArithmeticException,很明显是因为除数出现了 0 的情况,我们可以选择捕获异常,然后提示用户不应该进行除 0 操作,当然了,更好的做法是直接对除数进行判断,如果是 0 就不进行除法运算,而是告诉用户换一个非 0 的数进行运算。
checked 异常(检查型异常)在源代码里必须显式地捕获或者抛出,否则编译器会提示你进行相应的操作;
而 unchecked 异常(非检查型异常)就是所谓的运行时异常,通常是可以通过编码进行规避的,并不需要显式地捕获或者抛出。
1.1)基本定义与继承关系
Throwable,它有两个主要子类:Error和 Exception。-
检查型异常 (Checked Exception):指所有继承自
Exception类、但不是RuntimeException子类的异常。编译器在编译阶段会强制检查这类异常是否被妥善处理。常见的例子包括IOException、SQLException和ClassNotFoundException。 -
非检查型异常 (Unchecked Exception):包括两大类:
-
运行时异常 (RuntimeException):继承自
RuntimeException的异常,例如常见的NullPointerException(空指针异常)、ArrayIndexOutOfBoundsException(数组越界异常)和ClassCastException(类型转换异常)。 -
错误 (Error):继承自
Error的异常,如OutOfMemoryError(内存溢出错误)和StackOverflowError(栈溢出错误),通常表示JVM本身的严重问题。
-
🔍 核心区别一览
|
对比维度
|
检查型异常 (Checked Exception)
|
非检查型异常 (Unchecked Exception)
|
|---|---|---|
|
编译期检查
|
编译器强制检查,必须处理,否则编译不通过
|
编译器不强制检查,不处理也能通过编译
|
|
处理要求
|
必须显式处理(try-catch)或声明抛出(throws)
|
不强制处理,但建议捕获以提高健壮性
|
|
产生原因
|
通常由外部因素导致,是程序无法控制但可预见的问题(如文件不存在、网络中断)
|
通常由程序内部逻辑错误导致,是理论上可以避免的BUG(如空指针、数组越界)
|
|
设计初衷
|
用于可恢复的情况,提醒程序员处理可能的意外
|
RuntimeException用于编程错误;Error表示不可恢复的严重系统问题 |
💡 如何选择与处理异常
-
检查型异常的处理:当你调用一个可能抛出检查型异常的方法时,编译器会强制你做出选择,主要有两种方式:
-
捕获(Try-Catch):在当前方法中立即处理异常。
-
声明抛出(Throws):将异常抛给上层调用者处理。这在当前方法不具备处理条件时常用。
-
-
非检查型异常的处理:虽然编译器不强制,但好的程序员会主动防范。尤其是对于
RuntimeException,应通过严谨的代码逻辑和前置条件判断来避免。 -
关于自定义异常:当标准异常无法清晰表达业务错误时,可以考虑自定义异常。
-
如果调用方必须处理这个错误(如“余额不足”),应继承
Exception,定义为检查型异常。 -
如果错误是程序逻辑BUG,调用方无法合理恢复(如“非法订单状态转换”),应继承
RuntimeException,定义为非检查型异常。
-
📝 面试考察重点与备考建议
-
概念理解题(高频)
-
问题:简单直接地问“说说检查型异常和非检查型异常的区别”。
-
准备:确保能清晰阐述表格中的几点核心区别,并能举出常见的例子。
-
-
代码分析题(高频)
-
问题:给出一段有潜在异常风险的代码(如文件操作、类型转换),让你指出可能抛出的异常类型,并说明如何修改。
-
准备:熟悉常见异常的产生场景,写出健壮的代码(如使用 try-with-resources 管理资源、进行参数校验)。
-
-
设计思想题(中高频)
-
问题:“在自定义异常时,你如何决定让它继承
Exception还是RuntimeException?” -
准备:理解两者的设计哲学,能结合业务场景说明选择理由,可以参考Joshua Bloch在《Effective Java》中的主张:对可恢复的情况使用检查型异常,对编程错误使用运行时异常。
-
-
处理原则题(中频)
-
问题:考察异常处理的最佳实践,例如“为什么catch块里不能只写
e.printStackTrace()就了事?”或“finally块通常用来做什么?” -
准备:掌握异常处理的基本原则,如:能明确恢复的才catch、避免空的catch块、不要用异常控制业务流程、使用try-with-resources确保资源关闭等。
-
1.2)throw 和 throws 两个关键字的区别
throw 关键字,用于主动地抛出异常;正常情况下,当除数为 0 的时候,程序会主动抛出 ArithmeticException;但如果我们想要除数为 1 的时候也抛出 ArithmeticException,就可以使用 throw 关键字主动地抛出异常。
语法也非常简单,throw 关键字后跟上 new 关键字,以及异常的类型还有参数即可。
public class ThrowDemo { static void checkEligibilty(int stuage){ if(stuage<18) { throw new ArithmeticException("年纪未满 18 岁,禁止观影"); } else { System.out.println("请认真观影!!"); } } public static void main(String args[]){ checkEligibilty(10); System.out.println("愉快地周末.."); } }
throws 关键字的作用就和 throw 完全不同。前面的小节里已经讲了 checked exception 和 unchecked exception,也就是检查型异常和非检查型异常;对于检查型异常来说,如果你没有做处理,编译器就会提示你。
Class.forName() 方法在执行的时候可能会遇到 java.lang.ClassNotFoundException 异常,一个检查型异常,如果没有做处理,IDEA 就会提示你,要么在方法签名上声明,要么放在 try-catch 中。
什么情况下使用 throws 而不是 try-catch 呢?
假设现在有这么一个方法 myMethod(),可能会出现 ArithmeticException 异常,也可能会出现 NullPointerException。这种情况下,可以使用 try-catch 来处理。
public void myMethod() {
try {
// 可能抛出异常
} catch (ArithmeticException e) {
// 算术异常
} catch (NullPointerException e) {
// 空指针异常
}
}
但假设有好几个类似 myMethod() 的方法,如果为每个方法都加上 try-catch,就会显得非常繁琐。代码就会变得又臭又长,可读性就差了。
一个解决办法就是,使用 throws 关键字,在方法签名上声明可能会抛出的异常,然后在调用该方法的地方使用 try-catch 进行处理。
public static void main(String args[]){
try {
myMethod1();
} catch (ArithmeticException e) {
// 算术异常
} catch (NullPointerException e) {
// 空指针异常
}
}
public static void myMethod1() throws ArithmeticException, NullPointerException{
// 方法签名上声明异常
}
总结下 throw 和 throws 的区别
(1)throws 关键字用于声明异常,它的作用和 try-catch 相似;而 throw 关键字用于显式的抛出异常。
(2)throws 关键字后面跟的是异常的名字;而 throw 关键字后面跟的是异常的对象。
示例。
throws ArithmeticException;
throw new ArithmeticException("算术异常");
(3)throws 关键字出现在方法签名上,而 throw 关键字出现在方法体里。
(4)throws 关键字在声明异常的时候可以跟多个,用逗号隔开;而 throw 关键字每次只能抛出一个异常。
1.3)try-catch-finally
使用 finally 块的时候需要遵守这些规则。”
- finally 块前面必须有 try 块,不要把 finally 块单独拉出来使用。编译器也不允许这样做。
- finally 块不是必选项,有 try 块的时候不一定要有 finally 块。
- 如果 finally 块中的代码可能会发生异常,也应该使用 try-catch 进行包裹。
- 即便是 try 块中执行了 return、break、continue 这些跳转语句,finally 块也会被执行。
不执行 finally 的情况
- 遇到了死循环。
- 执行了
System. exit()这行代码。
System.exit() 和 return 语句不同,前者是用来退出程序的,后者只是回到了上一级方法调用。
1.4)总结
- 使用 try-catch 块捕获并处理异常,可以避免程序因异常而崩溃。
- 可以使用多个 catch 块来捕获不同类型的异常,并进行不同的处理。
- 可以使用 finally 块来执行一些必要的清理工作,无论是否发生异常都会执行。
- 可以使用 throw 关键字手动抛出异常,用于在程序中明确指定某些异常情况。
- 可以使用 throws 关键字将异常抛出给调用者处理,用于在方法签名中声明可能会出现的异常。
- Checked Exception 通常是由于外部因素导致的问题,需要在代码中显式地处理或声明抛出。
- Unchecked Exception 通常是由于程序内部逻辑或数据异常导致的,可以不处理或者在需要时进行处理。
- 在处理异常时,应该根据具体的异常类型进行处理,例如可以尝试重新打开文件、重新建立网络连接等操作。
- 异常处理应该根据具体的业务需求和设计原则进行,避免过度捕获和处理异常,从而降低程序的性能和可维护性。
2)try-with-resources
🔑 核心机制与工作原理
-
自动资源管理:try-with-resources语句能确保在语句执行完毕后,每个声明的资源都会被自动关闭,无需手动调用
close()方法。 -
语法核心 -
AutoCloseable接口:任何实现了java.lang.AutoCloseable接口(或其子接口java.io.Closeable)的类都可以用作资源。你在try关键字后的括号()中声明和初始化资源,资源的作用域仅限于try块内部。 -
工作原理:这本质上是一种语法糖。编译器在编译时会将其转换为包含
finally块的等效代码,在finally块中自动调用资源的close()方法。
⚠️ 异常处理机制(面试高频核心)
-
异常抑制:当
try代码块中的异常(主异常)和资源关闭时(close方法)抛出的异常(抑制异常)同时发生时,try块中的异常会被抛出,而close()方法的异常会被“抑制”。 -
获取被抑制的异常:被抑制的异常并不会丢失,可以通过主异常的
getSuppressed()方法获取到。 -
设计优势:这种机制确保了调试时能首先看到引发问题的原始异常,避免了被后续资源关闭时的异常所掩盖,提高了调试效率。
🔄 为什么要抑制异常
IllegalArgumentException。但在finally块中关闭数据库连接时,偏偏也抛出了一个SQLException。如果只抛出后一个异常,你就会很难判断问题的真正起因:是业务逻辑有bug,还是仅仅是资源清理时的小问题?异常抑制机制就是为了解决这个问题而生的,它能确保你看到的异常堆栈中同时包含这两个异常信息,从而更全面地反映问题全貌。⚙️ 核心机制与操作
Throwable类新增了两个方法来支持这个机制:-
addSuppressed(Throwable exception): 将一个异常添加为当前异常的"被抑制异常"。 -
getSuppressed(): 获取所有被当前异常抑制的异常数组。
try-with-resources语句中,如果try块(主业务逻辑)和资源自动关闭(close()方法)都抛出了异常,JVM会自动将close()方法抛出的异常抑制到主业务逻辑抛出的异常上。这样,最终抛出的异常反映了核心业务问题,同时你也可以通过getSuppressed()方法获取到资源关闭时出现的次要问题。try-catch-finally代码块,如果需要保留多个异常信息,就需要手动使用addSuppressed()方法。🔄 多资源管理与关闭顺序
-
声明多个资源:在
try后的括号内,用分号分隔多个资源声明。 -
逆序关闭:多个资源会按照声明顺序的逆序自动关闭。即最后声明的资源最先被关闭。这主要是为了处理资源间的依赖关系,例如,先关闭
BufferedReader,再关闭其底层依赖的FileReader,这是一种安全的做法。
📝 面试回答策略与要点总结
-
清晰阐述概念:首先说明try-with-resources是什么,以及它旨在解决传统
try-catch-finally手动管理资源带来的代码冗长和潜在资源泄漏问题。 -
强调使用条件:明确指出资源类必须实现
AutoCloseable接口,并可以举例说明如FileInputStream、Connection、Statement等JDK常用类都已实现该接口。 -
重点解释异常抑制:这是区分你是否真正理解该机制的关键。务必说清主异常和抑制异常的关系,以及如何通过
getSuppressed()方法获取。 -
提及Java 9增强:如果你了解Java 9的改进,可以加分地提到:从Java 9开始,如果资源已经在外部被初始化,且变量是
final或等效final的,可以直接在try括号中引用该变量,而不必重新声明。 -
代码对比:如果面试官允许,可以简要对比一段传统写法和try-with-resources写法的代码,直观展示其简洁性。
|
考察角度
|
核心要点
|
|---|---|
|
出现频率
|
高,特别是涉及I/O、JDBC等资源操作的岗位
|
|
主要考察点
|
1. 基本概念与优势(解决了什么问题)
2. 异常抑制机制(原理与如何获取被抑制异常) 3. 多资源关闭顺序 4. 与传统 try-finally的区别 |
|
可能的问题
|
- “谈谈你对try-with-resources的理解。”
- “如果try块和close方法都抛出异常,会怎么样?” - “多个资源时,关闭顺序是怎样的?” - “哪些类可以使用try-with-resources?” |
|
回答重点
|
简洁性、安全性(避免资源泄漏)、异常处理的优越性。
|
3)Java异常处理的20个最佳实践
01、尽量不要捕获 RuntimeException
阿里出品的 Java 开发手册上这样规定:
尽量不要 catch RuntimeException,比如 NullPointerException、IndexOutOfBoundsException 等等,应该用预检查的方式来规避。
正例:
if (obj != null) {
//...
}
反例:
try {
obj.method();
} catch (NullPointerException e) {
//...
}
那如果有些异常预检查不出来呢?
的确会存在这样的情况,比如说 NumberFormatException,虽然也属于 RuntimeException,但没办法预检查,所以还是应该用 catch 捕获处理。
02、尽量使用 try-with-resource 来关闭资源
当需要关闭资源时,尽量不要使用 try-catch-finally,禁止在 try 块中直接关闭资源。
反例:
public void doNotCloseResourceInTry() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
inputStream.close();
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
原因也很简单,因为一旦 close() 之前发生了异常,那么资源就无法关闭。直接使用 try-with-resource 来处理是最佳方式。
public void automaticallyCloseResource() {
File file = new File("./tmp.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
资源没有实现 AutoCloseable 接口,那这种情况下怎么办呢?
就在 finally 块关闭流。
public void closeResourceInFinally() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
} catch (FileNotFoundException e) {
log.error(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error(e);
}
}
}
}
03、不要捕获 Throwable
Throwable 是 exception 和 error 的父类,如果在 catch 子句中捕获了 Throwable,很可能把超出程序处理能力之外的错误也捕获了。
public void doNotCatchThrowable() {
try {
} catch (Throwable t) {
// 不要这样做
}
}
打个比方,一匹马只能拉一车厢的货物,拉两车厢可能就挂了,但一 catch,就发现不了问题了。
04、不要省略异常信息的记录
很多时候,由于疏忽大意,我们很容易捕获了异常却没有记录异常信息,导致程序上线后真的出现了问题却没有记录可查。
public void doNotIgnoreExceptions() {
try {
} catch (NumberFormatException e) {
// 没有记录异常
}
}
应该把错误信息记录下来。
public void logAnException() {
try {
} catch (NumberFormatException e) {
log.error("哦,错误竟然发生了: " + e);
}
}
05、不要记录了异常又抛出了异常
这纯属画蛇添足,并且容易造成错误信息的混乱。
反例:
try {
} catch (NumberFormatException e) {
log.error(e);
throw e;
}
要抛出就抛出,不要记录,记录了又抛出,等于多此一举。
反例:
public void wrapException(String input) throws MyBusinessException {
try {
} catch (NumberFormatException e) {
throw new MyBusinessException("错误信息描述:", e);
}
}
这种也是一样的道理,既然已经捕获了,就不要在方法签名上抛出了。
06、不要在 finally 块中使用 return
阿里出品的 Java 开发手册上这样规定:
try 块中的 return 语句执行成功后,并不会马上返回,而是继续执行 finally 块中的语句,如果 finally 块中也存在 return 语句,那么 try 块中的 return 就将被覆盖。
反例:
private int x = 0;
public int checkReturn() {
try {
return ++x;
} finally {
return ++x;
}
}
try 块中 x 返回的值为 1,到了 finally 块中就返回 2 了
07、抛出具体定义的检查性异常而不是 Exception
public void foo() throws Exception { //错误方式
}
一定要避免出现上面的代码,它破坏了检查性(checked)异常的目的。声明的方法应该尽可能抛出具体的检查性异常。
例如,如果一个方法可能会抛出 SQLException 异常,应该显式地声明抛出 SQLException 而不是 Exception 类型的异常。这样可以让其他开发者更好地理解代码的意图和异常处理的方式,并且可以根据 SQLException 的定义和文档来确定异常的处理方式和策略。
08、捕获具体的子类而不是捕获 Exception 类
try {
someMethod();
} catch (Exception e) { //错误方式
LOGGER.error("method has failed", e);
}
如果在 catch 块中捕获 Exception 类型的异常,会将所有异常都捕获,从而可能会给程序带来不必要的麻烦。具体来说,如果捕获 Exception 类型的异常,可能会导致以下问题:
- 难以识别和定位异常:如果捕获 Exception 类型的异常,可能会捕获到一些不应该被处理的异常,从而导致程序难以识别和定位异常。
- 难以调试和排错:如果捕获 Exception 类型的异常,可能会使得调试和排错变得更加困难,因为无法确定具体的异常类型和异常发生的原因。
下面举一个例子来说明为什么应该尽可能地捕获具体的子类而不是 Exception 类型的异常。
假设我们有一个方法 readFromFile(String filePath),用于从指定文件中读取数据。在方法实现过程中,可能会出现两种异常:FileNotFoundException 和 IOException。
如果在方法中使用以下 catch 块来捕获异常:
try {
// 读取数据的代码
} catch (Exception e) {
// 异常处理的代码
}
这样做会捕获所有类型的异常,包括 Checked Exception 和 Unchecked Exception。这可能会导致以下问题:
- 发生 RuntimeException 类型的异常时,也会被捕获,从而可能会掩盖实际的异常信息。
- 在调试和排错时,无法确定异常的具体类型和发生原因,从而增加了调试和排错的难度。
- 在程序运行时,可能会捕获一些不需要处理的异常(如 NullPointerException、IllegalArgumentException 等),从而降低程序的性能和稳定性。
因此,为了更好地定位和处理异常,应该尽可能地捕获具体的子类,例如:
try {
// 读取数据的代码
} catch (FileNotFoundException e) {
// 处理文件未找到异常的代码
} catch (IOException e) {
// 处理输入输出异常的代码
}
这样做可以更准确地捕获异常,从而提高程序的健壮性和稳定性。
09、自定义异常时不要丢失堆栈跟踪
catch (NoSuchMethodException e) {
throw new MyServiceException("Some information: " + e.getMessage()); //错误方式
}
这破坏了原始异常的堆栈跟踪,正确的做法是:
catch (NoSuchMethodException e) {
throw new MyServiceException("Some information: " , e); //正确方式
}
例如,下面是一个自定义异常类,它重写了 printStackTrace() 方法来打印堆栈跟踪信息:
public class MyException extends Exception {
public MyException(String message, Throwable cause) {
super(message, cause);
}
@Override
public void printStackTrace() {
System.err.println("MyException:");
super.printStackTrace();
}
}
这样做可以保留堆栈跟踪信息,同时也可以提供自定义的异常信息。在抛出 MyException 异常时,可以得到完整的堆栈跟踪信息,从而更好地定位和解决异常。
10、finally 块中不要抛出任何异常
try {
someMethod(); //Throws exceptionOne
} finally {
cleanUp(); //如果finally还抛出异常,那么exceptionOne将永远丢失
}
finally 块用于定义一段代码,无论 try 块中是否出现异常,都会被执行。finally 块通常用于释放资源、关闭文件等必须执行的操作。
如果在 finally 块中抛出异常,可能会导致原始异常被掩盖。比如说上例中,一旦 cleanup 抛出异常,someMethod 中的异常将会被覆盖。
11、不要在生产环境中使用 printStackTrace()
在 Java 中,printStackTrace() 方法用于将异常的堆栈跟踪信息输出到标准错误流中。这个方法对于调试和排错非常有用。但在生产环境中,不应该使用 printStackTrace() 方法,因为它可能会导致以下问题:
printStackTrace()方法将异常的堆栈跟踪信息输出到标准错误流中,这可能会暴露敏感信息,如文件路径、用户名、密码等。printStackTrace()方法会将堆栈跟踪信息输出到标准错误流中,这可能会影响程序的性能和稳定性。在高并发的生产环境中,大量的异常堆栈跟踪信息可能会导致系统崩溃或出现意外的行为。- 由于生产环境中往往是多线程、分布式的复杂系统,
printStackTrace()方法输出的堆栈跟踪信息可能并不完整或准确。
在生产环境中,应该使用日志系统来记录异常信息,例如 log4j、slf4j、logback 等。日志系统可以将异常信息记录到文件或数据库中,而不会暴露敏感信息,也不会影响程序的性能和稳定性。同时,日志系统也提供了更多的功能,如级别控制、滚动日志、邮件通知等。
例如,可以使用 logback 记录异常信息,如下所示:
try {
// some code
} catch (Exception e) {
logger.error("An error occurred: ", e);
}
12、对于不打算处理的异常,直接使用 try-finally,不用 catch
try {
method1(); // 会调用 Method 2
} finally {
cleanUp(); //do cleanup here
}
如果 method1 正在访问 Method 2,而 Method 2 抛出一些你不想在 Method 1 中处理的异常,但是仍然希望在发生异常时进行一些清理,可以直接在 finally 块中进行清理,不要使用 catch 块。
13、记住早 throw 晚 catch 原则
“早 throw, 晚 catch” 是 Java 中的一种异常处理原则。这个原则指的是在代码中尽可能早地抛出异常,以便在异常发生时能够及时地处理异常。同时,在 catch 块中尽可能晚地捕获异常,以便在捕获异常时能够获得更多的上下文信息,从而更好地处理异常。
来举个 “早 throw” 例子,如果一个方法需要传递参数,并且该参数必须满足一定的条件,如果参数不符合条件,则应该立即抛出异常,而不是在方法中进行其他操作。这可以确保异常在发生时能够及时被处理,避免更严重的问题。
再来举个“晚 catch”的例子,如果一个方法调用了其他方法,可能会抛出异常,如果在方法内部立即捕获异常,则可能会导致对异常的处理不充分。
来看这段代码:
public class ExceptionDemo1 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String str = sc.nextLine();
try {
int num = parseInt(str);
System.out.println("转换结果:" + num);
} catch (NumberFormatException e) {
System.out.println("转换失败:" + e.getMessage());
}
}
public static int parseInt(String str) {
if (str == null || "".equals(str)) {
throw new NullPointerException("字符串为空");
}
if (!str.matches("\\d+")) {
throw new NumberFormatException("字符串不是数字");
}
return Integer.parseInt(str);
}
}
这个示例中,定义了一个 parseInt() 方法,用于将字符串转换为整数。在该方法中,首先检测字符串是否为空,如果为空,则立即抛出 NullPointerException 异常。然后,检测字符串是否为数字,如果不是数字,则抛出 NumberFormatException 异常。最后,使用 Integer.parseInt() 方法将字符串转换为整数,并返回。
在示例的 main() 方法中,调用 parseInt() 方法,并使用 try-catch 块捕获可能抛出的 NumberFormatException 异常。如果转换成功,则输出转换结果,否则输出转换失败信息。
这个示例使用了 “早 throw, 晚 catch” 的原则,在 parseInt() 方法中尽可能早地抛出异常,在 main() 方法中尽可能晚地捕获异常,以便在捕获异常时能够获得更多的上下文信息,从而更好地处理异常。
运行该示例,输入一个数字字符串,可以看到输出转换结果。如果输入一个非数字字符串,则输出转换失败信息。
另一种解释
🔍 核心思想解读
-
早抛出 (Early Throw):在检测到错误条件(如参数无效、资源不可用)时,应立即抛出异常。这样做可以避免无效或错误的状态在程序中传播,导致更复杂的错误和更难的调试过程。
-
晚捕获 (Late Catch):异常应尽可能在调用栈的较高层次(如主流程控制层、对外接口层)进行捕获。因为越高层的方法拥有的业务上下文越丰富,越能决定该如何处理这个异常(例如是重试操作、记录日志、向用户展示友好错误信息,还是转换为另一种异常)。
⚙️ 代码实例解析
1. 早抛出:参数校验
2. 晚捕获:集中异常处理
Controller-> Service-> DAO。-
DAO层(数据访问层):职责是执行数据库操作。如果连接数据库失败,它不知道在上层是该重试还是直接给用户报错,因此它选择只抛出,不处理。
-
Service层(业务逻辑层):它调用
DAO层。如果SQLException发生,在此层可能仍然缺乏足够的上下文来决定如何面向用户处理(比如是否要触发告警),而且业务逻辑层通常不应处理具体的数据库异常。因此,它可以选择捕获并转换为业务异常再抛出,或者继续向上声明抛出。 -
Controller层(表现层/入口层):这是面向用户的最终入口。在这里,我们拥有了处理异常的完整上下文:可以记录错误日志、可以向用户返回统一的错误格式(如JSON)。
Controller层被统一捕获和处理,保证了用户收到友好、安全的提示,同时运维人员也能从日志中获取详细的错误信息用于排查。这避免了在底层的DAO或Service中草率地捕获异常却无法有效处理的窘境。💡 面试考察点
-
概念理解:直接让你解释“早 Throw 晚 Catch”原则的含义和目的。
-
代码设计:给出一个多层调用的代码场景,让你分析在哪里抛出异常、在哪里捕获异常最合适,或者让你优化一段不符合该原则的异常处理代码。
-
优劣分析:让你对比“早 Throw 晚 Catch”和“随处捕获”两种方式的优缺点,并说明为何前者能提高代码的健壮性和可维护性。
-
实际应用:结合Spring等框架的
@ControllerAdvice或@RestControllerAdvice注解,让你说明如何实现全局的“晚捕获”异常处理机制。
14、只抛出和方法相关的异常
相关性对于保持代码的整洁非常重要。一种尝试读取文件的方法,如果抛出 NullPointerException,那么它不会给用户提供有价值的信息。相反,如果这种异常被包裹在自定义异常中,则会更好。NoSuchFileFoundException 则对该方法的用户更有用。
public class Demo {
public static void main(String[] args) {
try {
int result = divide(10, 0);
System.out.println("The result is: " + result);
} catch (ArithmeticException e) {
System.err.println("Error: " + e.getMessage());
}
}
public static int divide(int a, int b) throws ArithmeticException {
if (b == 0) {
throw new ArithmeticException("Division by zero");
}
return a / b;
}
}
在该示例中,只抛出了和方法相关的异常 ArithmeticException,这可以使代码更加清晰和易于维护。
15、切勿在代码中使用异常来进行流程控制
在代码中使用异常来进行流程控制会导致代码的可读性、可维护性和性能出现问题。
public class Demo {
public static void main(String[] args) {
String input = "1,2,3,a,5";
String[] values = input.split(",");
for (String value : values) {
try {
int num = Integer.parseInt(value);
System.out.println(num);
} catch (NumberFormatException e) {
System.err.println(value + " is not a valid number");
}
}
}
}
虽然这个示例可以正确地处理输入字符串中的非数字字符,但是它使用异常进行流程控制,这就导致代码变得混乱、难以理解。应该使用其他合适的控制结构(如 if、switch、循环等)来管理程序的流程。
16、尽早验证用户输入以在请求处理的早期捕获异常
例如:在用户注册的业务中,如果按照这样来做:
- 验证用户
- 插入用户
- 验证地址
- 插入地址
- 如果出问题回滚一切
这是不正确的做法,它会使数据库在各种情况下处于不一致的状态,应该首先验证所有内容,然后再进行数据库更新。正确的做法是:
- 验证用户
- 验证地址
- 插入用户
- 插入地址
- 如果问题回滚一切
举个例子,我们用 JDBC 的方式往数据库插入数据,那么最好是先 validate 再 insert,而不是 validateUserInput、insertUserData、validateAddressInput、insertAddressData。
Connection conn = null;
try {
// Connect to the database
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "username", "password");
// Start a transaction
conn.setAutoCommit(false);
// Validate user input
validateUserInput();
// Insert user data
insertUserData(conn);
// Validate address input
validateAddressInput();
// Insert address data
insertAddressData(conn);
// Commit the transaction if everything is successful
conn.commit();
} catch (SQLException e) {
// Rollback the transaction if there is an error
if (conn != null) {
try {
conn.rollback();
} catch (SQLException ex) {
System.err.println("Error: " + ex.getMessage());
}
}
System.err.println("Error: " + e.getMessage());
} finally {
// Close the database connection
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
17、一个异常只能包含在一个日志中
不要这样做:
log.debug("Using cache sector A");
log.debug("Using retry sector B");
在单线程环境中,这样看起来没什么问题,但如果在多线程环境中,这两行紧挨着的代码中间可能会输出很多其他的内容,导致问题查起来会很难受。应该这样做:
LOGGER.debug("Using cache sector A, using retry sector B");
18、将所有相关信息尽可能地传递给异常
有用的异常消息和堆栈跟踪非常重要,如果你的日志不能定位异常位置,那要日志有什么用呢?
// Log exception message and stack trace
LOGGER.debug("Error reading file", e);
应该尽量把 String message, Throwable cause 异常信息和堆栈都输出。
19、终止掉被中断线程
while (true) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {} //别这样做
doSomethingCool();
}
InterruptedException 提示应该停止程序正在做的事情,比如事务超时或线程池被关闭等。
应该尽最大努力完成正在做的事情,并完成当前执行的线程,而不是忽略 InterruptedException。修改后的程序如下:
while (true) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
break;
}
}
doSomethingCool();
20、对于重复的 try-catch,使用模板方法
类似的 catch 块是无用的,只会增加代码的重复性,针对这样的问题可以使用模板方法。
例如,在尝试关闭数据库连接时的异常处理。
class DBUtil{
public static void closeConnection(Connection conn){
try{
conn.close();
} catch(Exception ex){
//Log Exception - Cannot close connection
}
}
}
这类的方法将在应用程序很多地方使用。不要把这块代码放的到处都是,而是定义上面的方法,然后像下面这样使用它:
public void dataAccessCode() {
Connection conn = null;
try{
conn = getConnection();
....
} finally{
DBUtil.closeConnection(conn);
}
}
浙公网安备 33010602011771号