朝阳市网站建设_网站建设公司_过渡效果_seo优化
2026/1/16 21:09:41 网站建设 项目流程

Java基础补缺5:异常处理、常用工具类

1)Java异常处理全面解析

Error 的出现,意味着程序出现了严重的问题,而这些问题不应该再交给 Java 的异常处理机制来处理,程序应该直接崩溃掉,比如说 OutOfMemoryError,内存溢出了,这就意味着程序在运行时申请的内存大于系统能够提供的内存,导致出现的错误,这种错误的出现,对于程序来说是致命的。

Exception 的出现,意味着程序出现了一些在可控范围内的问题,我们应当采取措施进行挽救。比如说之前提到的 ArithmeticException,很明显是因为除数出现了 0 的情况,我们可以选择捕获异常,然后提示用户不应该进行除 0 操作,当然了,更好的做法是直接对除数进行判断,如果是 0 就不进行除法运算,而是告诉用户换一个非 0 的数进行运算。

checked 异常(检查型异常)在源代码里必须显式地捕获或者抛出,否则编译器会提示你进行相应的操作;

而 unchecked 异常(非检查型异常)就是所谓的运行时异常,通常是可以通过编码进行规避的,并不需要显式地捕获或者抛出。

 

1.1)基本定义与继承关系

Java中所有异常类的根类是 Throwable,它有两个主要子类:ErrorException
  • 检查型异常 (Checked Exception):指所有继承自 Exception类、但不是RuntimeException子类的异常。编译器在编译阶段会强制检查这类异常是否被妥善处理。常见的例子包括 IOExceptionSQLExceptionClassNotFoundException
  • 非检查型异常 (Unchecked Exception):包括两大类:
    • 运行时异常 (RuntimeException):继承自 RuntimeException的异常,例如常见的 NullPointerException(空指针异常)、ArrayIndexOutOfBoundsException(数组越界异常)和 ClassCastException(类型转换异常)。
    • 错误 (Error):继承自 Error的异常,如 OutOfMemoryError(内存溢出错误)和 StackOverflowError(栈溢出错误),通常表示JVM本身的严重问题。

🔍 核心区别一览

为了更直观地对比,我将核心差异整理成了下面的表格:
对比维度
检查型异常 (Checked Exception)
非检查型异常 (Unchecked Exception)
编译期检查
编译器强制检查,必须处理,否则编译不通过
编译器不强制检查,不处理也能通过编译
处理要求
必须显式处理(try-catch)或声明抛出(throws)
不强制处理,但建议捕获以提高健壮性
产生原因
通常由外部因素导致,是程序无法控制但可预见的问题(如文件不存在、网络中断)
通常由程序内部逻辑错误导致,是理论上可以避免的BUG(如空指针、数组越界)
设计初衷
用于可恢复的情况,提醒程序员处理可能的意外
RuntimeException用于编程错误;Error表示不可恢复的严重系统问题

💡 如何选择与处理异常

在实际编码中,理解何时该使用或如何处理不同类型的异常至关重要。
  • 检查型异常的处理:当你调用一个可能抛出检查型异常的方法时,编译器会强制你做出选择,主要有两种方式:
    1. 捕获(Try-Catch):在当前方法中立即处理异常。
       
      try {FileInputStream file = new FileInputStream("somefile.txt");
      } catch (FileNotFoundException e) {System.out.println("文件未找到,请检查路径。");e.printStackTrace(); // 记录日志,便于调试
      }
       
       
    2. 声明抛出(Throws):将异常抛给上层调用者处理。这在当前方法不具备处理条件时常用
       
      public void readFile() throws FileNotFoundException {FileInputStream file = new FileInputStream("somefile.txt");
      }
       
       
  • 非检查型异常的处理:虽然编译器不强制,但好的程序员会主动防范。尤其是对于 RuntimeException,应通过严谨的代码逻辑和前置条件判断来避免。
     
    // 好的做法:在操作前检查参数,避免NullPointerException
    public void printLength(String str) {if (str != null) { // 前置校验System.out.println(str.length());} else {System.out.println("输入字符串不能为null!");}
    }
     
     
  • 关于自定义异常:当标准异常无法清晰表达业务错误时,可以考虑自定义异常。
    • 如果调用方必须处理这个错误(如“余额不足”),应继承 Exception,定义为检查型异常。
    • 如果错误是程序逻辑BUG,调用方无法合理恢复(如“非法订单状态转换”),应继承 RuntimeException,定义为非检查型异常。

📝 面试考察重点与备考建议

在笔试和面试中,异常相关的知识点出现频率非常高。面试官可能会从以下角度考察:
  1. 概念理解题(高频)
    • 问题:简单直接地问“说说检查型异常和非检查型异常的区别”。
    • 准备:确保能清晰阐述表格中的几点核心区别,并能举出常见的例子。
  2. 代码分析题(高频)
    • 问题:给出一段有潜在异常风险的代码(如文件操作、类型转换),让你指出可能抛出的异常类型,并说明如何修改。
    • 准备:熟悉常见异常的产生场景,写出健壮的代码(如使用 try-with-resources 管理资源、进行参数校验)。
  3. 设计思想题(中高频)
    • 问题:“在自定义异常时,你如何决定让它继承 Exception还是 RuntimeException?”
    • 准备:理解两者的设计哲学,能结合业务场景说明选择理由,可以参考Joshua Bloch在《Effective Java》中的主张:对可恢复的情况使用检查型异常,对编程错误使用运行时异常
  4. 处理原则题(中频)
    • 问题:考察异常处理的最佳实践,例如“为什么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,还是仅仅是资源清理时的小问题?异常抑制机制就是为了解决这个问题而生的,它能确保你看到的异常堆栈中同时包含这两个异常信息,从而更全面地反映问题全貌。
 

⚙️ 核心机制与操作

Java 7 开始,Throwable类新增了两个方法来支持这个机制:
  • addSuppressed(Throwable exception): 将一个异常添加为当前异常的"被抑制异常"。
  • getSuppressed(): 获取所有被当前异常抑制的异常数组。
try-with-resources语句中,如果try块(主业务逻辑)和资源自动关闭(close()方法)都抛出了异常,JVM会自动close()方法抛出的异常抑制到主业务逻辑抛出的异常上。这样,最终抛出的异常反映了核心业务问题,同时你也可以通过getSuppressed()方法获取到资源关闭时出现的次要问题。
对于传统的try-catch-finally代码块,如果需要保留多个异常信息,就需要手动使用addSuppressed()方法。
 

🔄 多资源管理与关闭顺序

当需要管理多个资源时,try-with-resources也有明确的行为规则。
  • 声明多个资源:在try后的括号内,用分号分隔多个资源声明。
  • 逆序关闭:多个资源会按照声明顺序的逆序自动关闭。即最后声明的资源最先被关闭。这主要是为了处理资源间的依赖关系,例如,先关闭BufferedReader,再关闭其底层依赖的FileReader,这是一种安全的做法。

📝 面试回答策略与要点总结

在面试中,你可以按照以下思路组织答案,并参考总结表格抓住核心。
  1. 清晰阐述概念:首先说明try-with-resources是什么,以及它旨在解决传统try-catch-finally手动管理资源带来的代码冗长和潜在资源泄漏问题
  2. 强调使用条件:明确指出资源类必须实现AutoCloseable接口,并可以举例说明如FileInputStreamConnectionStatement等JDK常用类都已实现该接口。
  3. 重点解释异常抑制:这是区分你是否真正理解该机制的关键。务必说清主异常和抑制异常的关系,以及如何通过getSuppressed()方法获取。
  4. 提及Java 9增强:如果你了解Java 9的改进,可以加分地提到:从Java 9开始,如果资源已经在外部被初始化,且变量是final或等效final的,可以直接在try括号中引用该变量,而不必重新声明。
  5. 代码对比:如果面试官允许,可以简要对比一段传统写法和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) {
        // 不要这样做
    }
}
 因为有些 error 是不需要程序来处理,程序可能也处理不了,比如说 OutOfMemoryError 或者 StackOverflowError,前者是因为 Java 虚拟机无法申请到足够的内存空间时出现的非正常的错误,后者是因为线程申请的栈深度超过了允许的最大深度出现的非正常错误,如果捕获了,就掩盖了程序应该被发现的严重错误。

打个比方,一匹马只能拉一车厢的货物,拉两车厢可能就挂了,但一 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() 方法中尽可能晚地捕获异常,以便在捕获异常时能够获得更多的上下文信息,从而更好地处理异常。

运行该示例,输入一个数字字符串,可以看到输出转换结果。如果输入一个非数字字符串,则输出转换失败信息。

 

另一种解释

“早 Throw 晚 Catch”是 Java 异常处理中一个重要的设计原则,简单来说就是在发现问题的地方尽早抛出(Throw)异常,而在拥有足够上下文信息、能够妥善处理的地方才捕获(Catch)异常。下面我们结合代码来深入理解。

🔍 核心思想解读

  • 早抛出 (Early Throw):在检测到错误条件(如参数无效、资源不可用)时,应立即抛出异常。这样做可以避免无效或错误的状态在程序中传播,导致更复杂的错误和更难的调试过程。
  • 晚捕获 (Late Catch):异常应尽可能在调用栈的较高层次(如主流程控制层、对外接口层)进行捕获。因为越高层的方法拥有的业务上下文越丰富,越能决定该如何处理这个异常(例如是重试操作、记录日志、向用户展示友好错误信息,还是转换为另一种异常)。

⚙️ 代码实例解析

1. 早抛出:参数校验

下面的代码演示了如何在方法开始时进行参数校验,一旦不符合预期就立即抛出异常:
 
public void processUserOrder(String userId, Order order) {// 早抛出:在业务逻辑执行前,检查参数有效性if (userId == null || userId.trim().isEmpty()) {throw new IllegalArgumentException("用户ID不能为空");}if (order == null) {throw new IllegalArgumentException("订单信息不能为空");}if (order.getTotalAmount().compareTo(BigDecimal.ZERO) <= 0) {throw new IllegalArgumentException("订单金额必须大于零");}// 只有当参数全部有效时,才继续执行核心业务逻辑// ... 处理订单的后续操作
}
 
 
优势:如果传入空订单,异常会立刻在方法开头抛出,调用者能清晰、快速地定位到问题所在,而不是等到业务逻辑执行到一半才因某个诡异的空指针错误而失败。

2. 晚捕获:集中异常处理

假设有一个简单的三层调用关系:Controller-> Service-> DAO
  • DAO层(数据访问层):职责是执行数据库操作。如果连接数据库失败,它不知道在上层是该重试还是直接给用户报错,因此它选择只抛出,不处理
     
    public class OrderDao {public Order findOrderById(String orderId) throws SQLException {// 执行数据库查询,可能抛出SQLException// ... }
    }
     
     
  • Service层(业务逻辑层):它调用DAO层。如果SQLException发生,在此层可能仍然缺乏足够的上下文来决定如何面向用户处理(比如是否要触发告警),而且业务逻辑层通常不应处理具体的数据库异常。因此,它可以选择捕获并转换为业务异常再抛出,或者继续向上声明抛出
     
    public class OrderService {private OrderDao orderDao;public Order getOrderDetails(String orderId) throws OrderNotFoundException {try {return orderDao.findOrderById(orderId);} catch (SQLException e) {// 将底层的技术异常(SQLException)包装成业务层能理解的异常// 这就是一种“晚捕获”的体现,但这里捕获是为了转换,并非最终处理throw new OrderNotFoundException("未找到ID为 " + orderId + " 的订单", e);}}
    }
     
     
  • Controller层(表现层/入口层):这是面向用户的最终入口。在这里,我们拥有了处理异常的完整上下文:可以记录错误日志、可以向用户返回统一的错误格式(如JSON)。
     
    @RestController
    public class OrderController {@Autowiredprivate OrderService orderService;@GetMapping("/orders/{orderId}")public ResponseEntity<?> getOrder(@PathVariable String orderId) {try {Order order = orderService.getOrderDetails(orderId);return ResponseEntity.ok(order);} catch (OrderNotFoundException e) {// 真正的“晚捕获”:在这里进行最终处理// 1. 记录错误日志(对于运维人员)logger.warn("查询订单失败: {}", e.getMessage());// 2. 返回用户友好的错误信息return ResponseEntity.status(HttpStatus.NOT_FOUND).body("订单不存在,请检查订单ID");} catch (Exception e) {// 捕获其他未预期的异常,避免服务器直接抛出不友好的错误logger.error("系统内部错误,订单ID: {}", orderId, e);return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("系统繁忙,请稍后重试");}}
    }
     
     
优势:异常在最终的Controller层被统一捕获和处理,保证了用户收到友好、安全的提示,同时运维人员也能从日志中获取详细的错误信息用于排查。这避免了在底层的DAOService中草率地捕获异常却无法有效处理的窘境。

💡 面试考察点

在面试中,面试官可能会从以下角度考察你对这个原则的理解:
  • 概念理解:直接让你解释“早 Throw 晚 Catch”原则的含义和目的。
  • 代码设计:给出一个多层调用的代码场景,让你分析在哪里抛出异常、在哪里捕获异常最合适,或者让你优化一段不符合该原则的异常处理代码。
  • 优劣分析:让你对比“早 Throw 晚 Catch”和“随处捕获”两种方式的优缺点,并说明为何前者能提高代码的健壮性和可维护性。
  • 实际应用:结合Spring等框架的@ControllerAdvice@RestControllerAdvice注解,让你说明如何实现全局的“晚捕获”异常处理机制。
遵循“早 Throw 晚 Catch”原则,能使你的代码错误处理路径清晰,模块职责分明,大大提升代码的质量和可维护性。

 

 

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、尽早验证用户输入以在请求处理的早期捕获异常

例如:在用户注册的业务中,如果按照这样来做:

  1. 验证用户
  2. 插入用户
  3. 验证地址
  4. 插入地址
  5. 如果出问题回滚一切

这是不正确的做法,它会使数据库在各种情况下处于不一致的状态,应该首先验证所有内容,然后再进行数据库更新。正确的做法是:

  1. 验证用户
  2. 验证地址
  3. 插入用户
  4. 插入地址
  5. 如果问题回滚一切

举个例子,我们用 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);
    }
}

 

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询