在实际的编程过程中,异常是不可忽略的一环,目前较为流行的方式是自定义异常然后统一捕获统一处理,那么还有没有其他的异常处理方式,本文介绍一种处理异常的新方法。
场景:程序正在解析CSV文件,程序可能出现以下错误:
无法正确解析数据
导入的CSV文件无法读取
运行的程序耗尽了内存或者磁盘空间等资源
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException:Exception in thread "main" java.nio.file.NoSuchFileException: src/main/resources/bank-data-simple.csvException in thread "main" java.lang.OutOfMemoryError: Java heap space
为什么使用异常
例如,文件中的CSV数据行可能并不是期望的格式:
CSV一行数据包含的列可能超过预期的三列
CSV一行数据包含的列可能少于预期的三列
某些列的数据格式可能不正确,例如,日期可能不正确
在可怕的C语言编程时代,你会添加大量的if条件检查,这些检查将返回一个含义模糊的错误码。这种方式有几个缺点。首先,它依赖全局共享可变状态来查找最近的错误。这使得单独理解代码的各个部分变得更加困难。结果是你的代码变得更难维护。其次,这种方式容易出错,因为你要区分实际值和错误码的值。这个例子中的类型系统还比较弱,可能对程序员更有帮助。最后,控制流和业务逻辑混在一起,这会使得代码更难以维护和单独测试。
为了解决这些问题,Java将异常作为一级语言特性,这带来了许多好处:
记录说明
该语言支持将异常作为方法签名的一部分。
类型安全
类型系统推算出你是否正在处理异常流。
问题分离
try/catch代码块分隔开了业务逻辑和异常恢复处理。
异常分类
受检查的异常
这些错误是你期望能够恢复的。在Java中,必须声明一个方法,该方法可以抛出一个受检查异常列表。如果不声明,则必须为这个特定的异常提供合适的try/catch代码块。
不受检查的异常
这些错误可以在程序执行期间的任何时候抛出。方法不必在其签名中显式地声明这些异常,并且调用者也不必像受检查的异常那样去显式地处理它们。
Java异常类是由定义良好的层次结构组成的。Error和RuntimeException是不受检查的异常,它们都是Throwable的子类。你不应该指望捕获它们并从中恢复过来。Exception类通常表示程序应该能从中恢复错误。(如下图)

异常的模式与反模式
在什么场景下应该使用哪类异常?
在不受检查与受检查之间做选择
如果希望客户强制恢复,则是受检查的异常,例如:客户端没有按照格式输入。
如果是系统错误客户端无能为力,则是不受检查的异常,例如:磁盘耗尽,内存溢出
过于具体
想到的问题是应该在哪里添加校验逻辑?我们建议创建一个专门的Validator类,原因如下:
需要重用它时,无须复制这个校验逻辑
可以确信系统的不同部分以相同的方式进行校验
可以很容易单独对这个逻辑进行单元测试
它遵循SRP原则,这让维护更简单,并且容易理解程序
例子:
public class OverlySpecificBankStatementValidator {private String description;private String date;private String amount;public OverlySpecificBankStatementValidator(final String description, final String date, final String amount) {this.description = Objects.requireNonNull(description);this.date = Objects.requireNonNull(description);this.amount = Objects.requireNonNull(description);}public boolean validate() throws DescriptionTooLongException,InvalidDateFormat,DateInTheFutureException,InvalidAmountException {if(this.description.length() > 100) {throw new DescriptionTooLongException();}final LocalDate parsedDate;try {parsedDate = LocalDate.parse(this.date);}catch (DateTimeParseException e) {throw new InvalidDateFormat();}if (parsedDate.isAfter(LocalDate.now()))throw new DateInTheFutureException();try {Double.parseDouble(this.amount);}catch (NumberFormatException e) {throw new InvalidAmountException();}return true;}}
过于笼统
让所有异常都成为不受检查的异常。例如,都使用IllegalArgumentException。
public boolean validate() {if(this.description.length() > 100) {throw new IllegalArgumentException("The description is too long");}final LocalDate parsedDate;try {parsedDate = LocalDate.parse(this.date);}catch (DateTimeParseException e) {throw new IllegalArgumentException("Invalid format for date", e);}if (parsedDate.isAfter(LocalDate.now())) throw new IllegalArgumentException ("date cannot be in the future");try {Double.parseDouble(this.amount);}catch (NumberFormatException e) {throw new IllegalArgumentException("Invalid format for amount", e);}return true;}
Notification模式
Notification模式的目的是在你使用太多不受检查的异常情况时提供一个解决方案。该解决方案引入了一个领域类来收集错误信息(这一模式最早是由Martin Fowler提出的)。
首先你需要的是一个Notification类,它负责收集错误信息。
public class Notification {private final List<String> errors = new ArrayList<>();public void addError(final String message) {errors.add(message);}public boolean hasErrors() {return !errors.isEmpty();}public String errorMessage() {return errors.toString();}public List<String> getErrors() {return this.errors;}}
引入这样一个类的好处是,你现在可以定义一个校验器,它能够在一次传递中收集多个错误信息。前面讨论的两种方式是不可能办到的。你现在可以简单地将消息添加到Notification对象中,而不是抛出异常
public Notification validate() {final Notification notification = new Notification();if(this.description.length() > 100) {notification.addError("The description is too long");}final LocalDate parsedDate;try {parsedDate = LocalDate.parse(this.date);if (parsedDate.isAfter(LocalDate.now())) {notification.addError("date cannot be in the future");}}catch (DateTimeParseException e) {notification.addError("Invalid format for date");}final double amount;try {amount = Double.parseDouble(this.amount);}catch (NumberFormatException e) {notification.addError("Invalid format for amount");}return notification;}
异常使用准则
不要忽略异常
忽略一个异常永远不是一个好主意,因为你会无法诊断出问题的根源。如果没有一个明确的处理机制,那就抛出一个不受检查的异常来代替。这样,如果你确实需要处理受检查的异常,那么在运行时看到问题后,你将被迫返回并处理它。
不要捕获通用的异常
尽可能多地捕获一个特定的异常,以便提高可读性,以及支持更多特定的异常处理。如果你捕获了通用的Exception,它还包含了RuntimeException[2]。一些IDE会生成太过通用的catch字句,因此你需要考虑让catch字句更加具体。
记录异常
记录API级别的异常,包括不受检查的异常,这有助于排错。事实上,不受检查的异常会上报应该解决的问题的根源。
@throws NoSuchFileException if the file does not exist@throws DirectoryNotEmptyException if the file is a directory and could not otherwise be deleted because the directory is not empty@throws IOException if an I/O error occurs@throws SecurityException In the case of the default provider,and a security manager is installed, the {@link SecurityManager #checkDelete(String)} method is invoked to check delete access to the file
注意特定实现的异常
不要抛出特定实现的异常,因为它会打破你的API的封装性。例如:read()的定义强制任何未来的实现抛出OracleException,而read()显然可以支持与Oracle完全无关的数据源!
public String read(final Source source) throws OracleException { ... }
异常与流程控制
不要为了流程控制而使用异常。例如:依赖一个异常来退出用于读取数据的循环
try {
while (true) {
System.out.println(source.read());
}
} catch(NoDataException e) {
}
try {while (true) {System.out.println(source.read());}} catch(NoDataException e) {}
异常的替代品
使用null
返回null,而不是抛出一个特定的异常。
final String[] columns = line.split(",");if(columns.length < EXPECTED_ATTRIBUTES_LENGTH) {return null;}
空对象模式
有时会看到Java中的一种做法是空对象模式。简单来说,就是不要返回一个空引用来表示对象的缺失,而是返回一个实现了预期接口但其方法主体为空的对象。这种策略的优点是不用处理意外的空指针异常和一长串的空检查。事实上,这个空对象非常容易可预测,因为它不实现任何功能!尽管如此,这个模式也可能存在问题,因为你可能使用了一个对象隐藏掉了数据中的潜在问题,该对象只是忽略了真正的问题,从而使故障排除变得更加困难。
Optional<T>
Java 8中引入了一个内置的数据类型java.util.Optional<T>,它专用于表示某个值存在与否。Optional<T>提供了一组方法来明确地处理没有值的情况,这有助于减少bug的范围。它还允许你将各种Optional对象组合在一起,这些Optional对象可以作为你使用的不同API的返回类型返回。




