转载

Effective Java异常及使用感想

  • 受检异常

    • 除了RuntimeException以外的异常,都属于checkedException、Exception,FileNotFoundException,IOException,SQLException。
    • 可以简单理解为需要手动处理方法抛出的异常。
  • 非受检异常

    • 从java.lang.RuntimeException或java.lang.Error类衍生出来的。例如:NullPointException,ArithmeticException,IndexOutOfBoundsException,ClassCastException。
    • 相对于受检异常,不需要手动处理抛出的异常,但会在程序运行期间可能会抛出的异常。

1、只针对异常的情况才能使用异常

《Effective Java》提出这一条建议的原因是让我们不要用异常去控制程序的流程。以下是使用异常去控制流程的错误示例:

基于异常的循环

try{
    int i = 0;
    while(true) {
        range[i++].climb();
    }
} catch (ArrayIndexOutOfBoundException e) {
}
复制代码

标准模式的循环

// 语法糖
for (Mountain m : range) {
    m.climb();
}
// 等同于
for (int i = 0; i < rang.length; i++) {
    rang[i].clmib();
}
复制代码

使用基于异常的循环理由

  • 认为标准模式的循环时JVM对每次数组的访问都要检查越界情况,效率比较慢

想法错误理由

  • 使用循环的标准模式并不会导致冗余的检查,JVM会这种代码进行优化
  • 把代码放在try-catch块中反而阻止了JVM本来可能要执行的某些特定优化
  • 经过测试,基于异常的模式比标准模式要慢得多。
    • 可以测试当数组有10万个元素时,for循环每次都比try-catch要快。

总结

  • 不要使用异常去控制程序流程。原因如下:
    • 异常机制设计是用于处理不正常的情形
    • 使用异常会比正常流程的代码慢得多
    • try-catch块可能会阻止一些JVM本来要进行的优化
  • 使用异常控制程序流程在开发还是很少见到,但可以让我们学到的是处理异常的地方会阻止JVM本来要进行的优化,导致需要花费处理的时间比正常过程要多,所以慎用异常。

2、对可恢复的情况使用受检异常,使用运行时异常表明编程错误

如果期望调用者当发生异常能够相应catch到异常并进行处理和恢复,让程序继续跑下去,对于这种情况就应该使用受检异常

运行时异常和错误,都是不需要也不应该被捕获的可抛出结构。因为程序跑出未受检的异常或者错误,往往就属于不可恢复的情形,继续执行下去有害无益。

总结

  • 在写业务代码过程,比较常抛出的是受检异常,即我们想让使用的人进行捕获异常然后进行相应的处理,才能保证业务流程完整执行下去。我们并不希望有出现任何异常导致将异常返回到给前端。
  • 对运行时异常表明编程错误,大多出会现在使用组件错误或者配置错误。而这类异常,是我们在编码后把程序相应跑一遍测试后就能迅速处理的异常。比如:
    • 连接数据库配置并不正确。在程序运行时才会连接数据库,失败后抛出异常表明配置错误。
    • 使用Spring为容器注入两个相同名称的bean。在程序启动初始化,会出现失败注入相同bean的编程异常。

3、优先使用标准的异常

Java提供一组基本的未受检异常,能满足绝大多数API的异常抛出需求

  • 使API更易于学习和使用,因为它与程序员已经熟悉的习惯用法一致。
  • 可读性会更好,因为不会出现其他人不熟悉的异常
  • 异常类越少,意味着内存占用越小,装在这些类的时间开销也越少。

常用的异常如下:

异常 使用场合
IllegalArgumentException 非null的参数值不正确
IllegalStateException 对于方法调用而言,对象状态不合适
NullPointerException 在禁止使用null的情况下参数值为null
IndexOutOfBoundsException 下标参数值越界
ConcurrentModificationException 在禁止并发修改的情况下,检查到队形的并发修改
UnsupportedOperationException 对象不支持用户请求的方法

总结

  • 我们在写一些通用的工具类方法时,可以优先使用java提供的异常。这样其他的调用将工具类迁移到其他的项目,并不用再声明自己写的异常,非常方便。
/**
 * 字符串 转 UUID
 * 字符串格式为不带-的32位字符串
 *
 * @param uuidString
 * @return
 */
public static UUID format(String uuidString) {
    if (uuidString.length() != 32) {
        throw new IllegalArgumentException("Invalid UUID string: " + uuidString);
    }
    // ...
}    
复制代码

4、抛出与抽象对应的异常

异常转译: 更高层的实现应该捕获底层的异常,同时抛出可以按照高层抽象进行解释的异常

try {
    ... // user lower-level abstraction to do out bidding
} catch (LowerLevelException e) {
    throw new HigherLevelException(...);
}
复制代码

异常链。如果底层的异常对调试导致高层异常的问题非常有帮助,使用异常链就很合适。

  • 高层的异常提供访问方法(Throwable的getCause方法)来获取底层的异常。

总结

  • 如果不能阻止或处理来自更低层的异常,一般的做法是使用异常转译,除非低层方法碰巧可以保证它抛出的所有异常对高层也合适,才可以将异常从低层传播到高层。
  • 异常链对高层和低层异常都提供了最佳的功能: 它允许抛出适当的高层异常,同时又能捕获低层的原因进行失败分析。

5、每个方法抛出的所有异常都要建立文档

始终要单独地声明受检异常。在方法文档的@throws标签,准确地记录下跑出每个异常的条件。

  • 如果一个公有方法可能抛出多个异常类,则不要使用"快捷方式"声明它会抛出这些异常类的某个超类。
    • 不要声明一个公有方法直接"throws Excpetion" 或 "throws Throwable"。除了main方法,因为只通过虚拟机调用。
  • 可以在@throws标签记录出现的受检异常、非受检异常(不需要用throws关键字抛出)。可以有效地描述这个方法被成功执行的前提条件。

总结

  • 不要直接抛出Excpetion或者Throwable这类的异常或错误,不然调用方不知道如何将抛出什么异常,针对异常进行处理。
  • 我们在业务代码中是会有相应的抛出异常,但是很少在@throws写上相应的执行成功的前提条件。因为大多数都是自己即作为该方法的书写方又作为代码的调用方,所以经常会省略这个过程;但如果有别人想复用该方法,没有注释描述就可能会出现疑惑,可读性会很差。比如下面的例子:
/**
 * 创建meeting的对象
 *
 * @param meetingCreateForm
 * @return
 * @throws InterruptedException 未进行说明什么条件下会发生该异常,让调用方产生疑惑
 */
private Meeting buildMeeting(MeetingCreateForm meetingCreateForm) throws ParseException {
}
复制代码

6、在细节消息中包含失败 - 捕获信息

为了捕获失败,异常的细节信息应该包含"对该异常有贡献"的所有参数和域的值。

  • 例如, IndexOutOfBoundsException 异常的细节消息应该包含下界、上界以及没有落在界内的下标值。

总结

  • 这种情况比较多会出现业务方法在执行过程中,对参数校验或其他导致正常流程无法走通的时候,就会抛出异常,并在异常的message属性写上流程无法正常走通的原因,让接口调用方知悉。
public ContactPersonView add(UUID userId, ContactPersonCreateForm contactPersonCreateForm) {
    UserInfoView userInfo = userClientRemote.getUserInfo(contactPersonCreateForm.getId());
    if (userInfo == null) {
        throw new RestException(ErrorCode.NOT_FOUND, "user not found");
    }
    // ...
}
复制代码

7、努力使失败保持原子性

失败的方法调用应该使对象保持在被调用之前的状态,达到上述效果有如下方法

  • 1)在执行操作之前检查参数的有效性。使得在对象的状态被修改之前,先抛出适当的异常。例如stack.pop()方法
public Object pop() {
    // 当栈的大小为0,没有元素可以出栈,则进行抛出异常,防止栈被修改
    if (size == 0) {
        throw new EmptyStackExcpetion();
    }
    Object result = element[--size];
    element[size] = null;
    return result;
}
复制代码
  • 2)在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容。例如下面的手动事务回滚:
// 1、查库存
Store beforeStore = storeDao.findByProductId(productId);
Store afterStore = new Store();
// 2、保存之前的一份数据镜像
BeanUtils.copyProperties(afterStore, beforeStore);
// 3、扣库存
afterStore.setStoreNum(afterStore.getStoreNum() - num);
try{
    // 4、更新库存
    storeDao.update(afterStore);
    Order order = new Order();
    // 一系列order.set...();
    // 5、订单保存
    orderDao.save(order);
}catch(Exception ex){
    // 更新库存或者或者订单保存发生异常,手动回滚
    storeDao.update(beforeStore);
}
复制代码

总结

  • 针对这一条建议,在我们的业务代码应用如下场景
    • 业务表单参数校验判断,再执行下面的逻辑。例如用户注册功能会插入用户数据,会先判断用户的手机是否属于正常手机,如果不是正常手机则不给予插入数据。
    • 手动事务回滚。

8、不要忽略异常

如果选择忽略异常,catch块中应该包含一条注释,说明为什么可以这么做,并且变量应该命名为ignored

try {
    numColors = f.get(1L, TimeUnit.SECONDS)
} catch(TimeoutException | ExecutionException ignored) {
    // User default: minimal coloring is desirable, not required
}
复制代码

总结

  • 这一条主要建议在捕获异常之后不要忽略相关的恢复或处理。

  • 现在在代码层面,有一些业务逻辑是有捕获异常,然后进行相应的恢复或处理。但有一些只是打印异常的相关日志,并没有进行相应的恢复或处理,例如我们调用远端服务请求失败后,建议我们还是按照忽略异常的做法写上相应的注释。

try {
    ResponseEntity<ShareRemoteView> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, ShareRemoteView.class);
    return responseEntity.getBody();
} catch (Exception ignored) {
    // 注释说明不处理的原因
    LOGGER.error("createShare error.", ignored);
    return null;
}
复制代码

以上感想

Effective Java异常这一章的内容,对我们写业务代码及业务流程的异常抛出和捕获是有一定指示和帮助的,指导我们写好异常发生条件说明、利用底层异常帮助调试。

但如果我们写的是一些公共组件,我想这一章的帮助会更大,因为这里的每一条都对组件的可读性、可用性、健壮性都有要求。

原文  https://juejin.im/post/5e881d73f265da47ce6cac30
正文到此结束
Loading...