一、概览
Java 异常处理的最佳实践通常包括以下几个方面:有效使用 Java 提供的异常类型,创建和使用自定义异常,异常链,异常处理策略,以及记录和传播异常。
二、有效使用 Java 提供的异常类型
检查异常(Checked Exception):这些异常是在编译阶段就会被检查的异常,通常是预期内的问题,比如读取一个不存在的文件(FileNotFoundException),网络连接失败(IOException)等,一般是通过
try/catch
块或者通过 在 method 上抛出异常的方式来处理。这类异常通常是可恢复的,发生时,应妥善处理,以避免程序终止。在 API 文档中清晰地标明可能抛出的检查异常,是良好的编程习惯。运行时异常(Runtime Exception):这些异常通常是程序员的编程错误,会在程序运行过程中抛出,例如空指针访问(NullPointerException),数组索引越界(ArrayIndexOutOfBoundsException)。这类异常一般表示编程错误,一般无需显式的通过代码去捕获。当这类异常发生时,它们会导致程序中止执行,因此,必须在编程时尽量避免这类异常的产生。
错误(Error):这类问题是程序本身无法处理的严重问题,通常是虚拟机相关的系统错误,资源耗尽等严重情况,比如内存溢出错误(OutOfMemoryError)。对于这类异常,程序无法进行恢复,它们在 API 文档中一般不会被列出,而且只能由 JVM 自身去尽可能地进行处理,或者提前做好相应的容灾预案,比如提前预留足够的内存空间,设置JVM的最大可用内存等。
三、创建和使用自定义异常
只在绝对需要时创建自定义的异常:异常处理的设计精神是,只有在必要的时候才自定义异常,不要随意创建新的异常类。 在许多情况下,Java 标准库中定义的异常类(如 IllegalArgumentException,NullPointerException等)已经足够使用。如果确实需要创建自定义的异常类,应确保它为调用者提供了足够的上下文信息,以方便理解问题和修复错误。
一个好的自定义异常能够增加程序的可读性和可维护性:自定义异常类可以提供更清晰,更具有针对性的错误信息,有助于提高代码的可读性。另外,当出现特定错误时,通过抛出特定的自定义异常,可以直接指示出现问题的可能原因和位置,方便定位问题,提高代码的可维护性。
自定义异常应当覆写 toString() 方法以提供对错误的详细描述:当我们创建自定义的异常类时,覆写 toString() 方法是十分有用的。可以通过覆写这个方法,提供更详细,更易于理解的错误描述信息。这样,当异常被抛出时,我们就可以得到足够的信息帮助我们定位并修复问题。同时,在实际的开发过程中,我们也常常会覆写 getMessage() 方法,依情况提供附加的错误信息。
代码示例。在下面的示例中,我将创建一个我们自己的自定义异常类,并覆写
toString()
方法提供详细的错误描述。public class MyCustomException extends Exception { private int errorCode; public MyCustomException(String message, int errorCode) { super(message); //调用超类(Exception)的构造器 this.errorCode = errorCode; } public int getErrorCode() { return errorCode; } @Override public String toString() { return "MyCustomException{" + "message=" + getMessage() + ", errorCode=" + errorCode + '}'; } }
在这个
MyCustomException
类中,我增加了一个errorCode
成员,并提供了重新定义的toString()
方法。这样在抛出这个异常时,可使用MyCustomException.toString()
获取包含错误编码和错误消息的详细异常信息,有助于更准确地理解和修复问题。
四、异常链
- 使用异常链来包装和重新抛出异常:在Java中,我们可以使用异常链(Exception Chaining)来处理异常。这种方法允许我们捕获一个异常,并把它包装成一个新的异常来重新抛出,这样我们可以在抛出新的异常的同时,保留底层原始异常的信息。这对于跟踪和处理复杂的异常情况特别有用。
例如,思考以下一个简单的异常链示例:
public class HighLevelException extends Exception { public HighLevelException(Exception cause) { super(cause); } } public void process() throws HighLevelException { try { // Some low-level operation that may throw an exception lowLevelOp(); } catch (LowLevelException e) { throw new HighLevelException(e); } }
在这个例子中,
LowLevelException
是被HighLevelException
捕获并包装的。这样当HighLevelException
被捕获时,我们可以通过HighLevelException.getCause()
方法获得原始的LowLevelException
, 通过这种方式,底层异常的信息能够被上层捕获到。使用异常链有助于保持原始的错误上下文,便于进行故障诊断和修复。
五、异常处理策略
- 在你知道如何处理异常的地方进行捕获。否则,就把异常传递出去:这是一个很重要的原则。异步处理的方式是,当我们不知道如何正确地处理一个异常时,我们不应该试图去捕获它,而是应当允许它向上抛出到调用栈,在那些知道如何处理它的代码部分进行处理。
public void process() throws ProcessingException { // some operation that may throw an exception operation(); }
如果
operation()
抛出异常,且process()
不知道如何处理,那么我们就让ProcessingException
继续抛出,留给调用process()
的代码来处理。
- 使用 try-with-resources 语句来处理需要关闭的资源:在Java 7中的增加了一个新的特性
try-with-resources
,这个特性可以帮助我们更容易地关闭在try块中打开的资源。// Using try-with-resources try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) { return br.readLine(); } // Here the BufferedReader is automatically closed
无论是否发生异常,try-with-resources 都会自动关闭它所打开的资源,这避免了finally里面关闭资源的繁琐并且更安全。
- 尽可能地提供具体的处理异常的代码,而非一个处理所有异常的代码块:这是一个很好的编程实践,提供更具体的错误处理可以使我们的代码错误诊断更容易,也有助于增强程序的健壮性。
try{ // Some operation that may throw multiple exception operation(); } catch (FileNotFoundException e) { // Handle the case where file not found } catch (IOException e) { // Handle the case where IO error }
在上述代码中,我们针对不同类型的异常提供了各自的处理逻辑,这样可以为不同情况提供更具体的处理,而不是仅仅使用一个处理所有异常的代码块。
六、记录和传播异常
- 尽可能记录关于异常的所有信息:当异常发生时,记录详细的异常信息,包括引发异常的类和方法,异常类型,消息以及堆栈跟踪等,对于诊断问题和修复bug非常有帮助。一个好的方式是,将这些详细信息做为日志进行记录,如下所示:
try{ // Some operation operation(); } catch (Exception e) { logger.error("Exception occurred in method 'methodNameOfOperation' of Class 'ClassOfOperation'", e); }
在上述示例中, 如果
operation()
方法触发了异常, 我们捕获并打协会在methodNameOfOperation
方法内,ClassOfOperation
类中发生的异常信息与堆栈跟踪。
- 利用异常的消息来传达具体的错误原因:受检异常 (Checked Exception) 应该尽可能地提供描述异常发生原因的信息。这会为调试者在理解和修复问题来源上提供帮助。以下面的代码为例:
public void validate(String input) throws ValidationException { if(input == null) { throw new IllegalArgumentException("Input should not be null."); } if(input.length() <= 0){ throw new ValidationException("Input should not be empty."); } }
在这个代码段中, 当我们遇到非法的输入时,我们抛出一个带有描述性错误消息的异常。这个错误信息清楚地描述了问题产生的原因,这样可以给予开发者或者调试者一个明确的方向去定位问题。
七、结论
设计和实现良好的异常处理策略是编写出健壮、可维护和易于调试的Java程序的关键部分。本质上,一个好的异常处理策略应该能够:
- 向上层抛出无法处理的异常,使得可以在你知道如何处理异常的位置去进行处理。
- 明确地关闭在try块中打开的资源,这样既能确保你的资源能及时并且安全地关闭,又能使代码保持清洁和易读。
- 在捕获异常时,为每种类型的异常提供专门的处理程序,以确保在每种情况下都能容易地定位错误并尽可能修复问题。