关于Java异常处理的9条原则

简介: 关于Java异常处理的9条原则

关于Java异常处理的9条原则

在Java编程中,合理有效地处理异常对于保证程序的稳定性和可维护性至关重要

充分发挥异常优点,可以提高程序可读、可靠、可维护性

本文基于Effective Java 异常章节总结9条异常处理原则

image.png

只针对异常情况才使用异常

不要使用异常来做程序的流程控制,只有针对异常情况才使用异常

不主动判断数组下标是否越界,而使用异常控制流程的反例:

        int[] ints = {1, 2, 3, 4, 5};
        int index = 0;
        try {
            while (true) {
                System.out.println(ints[index++]);
            }
        } catch (ArrayIndexOutOfBoundsException e) {

        }

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

广泛的异常指Throwable,它可以分为三种异常:

  1. 受检异常 CheckException:编译时需要处理(捕获/抛出)的异常(比如IOException等)
  2. 运行时异常 RuntimeException:程序运行错误时抛出的异常(比如空指针NullPointerException、非法参数等)
  3. 错误 Error:运行时虚拟机出现的错误(比如OOM等)

处理受检异常时可以捕获或抛出进行处理,如果希望“恢复”则可以在捕获时进行重试

如果要自定义未受检异常(编译时不需要处理),则要为运行时异常的子类

class MyException extends RuntimeException

错误一般不在代码中进行处理,发生错误时需要排查根源再改造代码

API设计时遵循:对于可以恢复的情况抛出受检异常、对于程序错误抛出运行时异常、不确定能不能恢复抛出未受检异常 (未受检异常可以看成运行时异常)

如果在最外层(离用户最近)返回用户能理解的错误信息

避免不必要的使用受检异常

受检异常需要手动进行处理,这往往能够带来可靠

但是多种受检异常会让API难以使用,调用者处理时直接痛苦面具~

try {
    
} catch (SQLException se) {
    // 处理数据库相关异常
} catch (IOException ioe) {
    // 处理文件读写相关异常
} catch (ClassNotFoundException cnfe) {
    // 处理类未找到异常,可能在加载驱动时出现
}

这时候为了偷懒可能会直接使用Exception统一进行处理~

try {
    
} catch (Exception e) {
    // 偷懒
} 

如果不使用catch处理就直接抛出受检异常

如果无法恢复则抛出未受检异常,通常是自定义的业务异常,如调用失败请稍后重试

记得带上异常信息,防止后续打印日志导致异常信息丢失

try {

} catch (IOException e) {
    throw new MyException("请稍后重试", e);
}

优先使用标准的异常

优先复用标准异常,如非法参数、数组下标越界异常

业务开发更多的还是复用自定义的业务异常~

复用已有的异常,不满足再自定义新异常

抛出与抽象对应的异常

当设计抽象层次的方法时,关注抽象层级的异常,而不是底层的具体实现的异常

ArrayList的迭代器在获取下一个元素时,如果越界会抛出NoSuchElementException

public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

AbstractSequentialList 在实现get获取元素时捕获NoSuchElementException,抛出IndexOutOfBoundsException

public E get(int index) {
    try {
        return listIterator(index).next();
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}

抽象层次捕获实现层次的NoSuchElementException异常,并抛出按照抽象层次进行解释的异常IndexOutOfBoundsException

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

如果方法要抛出异常,在文档中使用@throw说明什么情况下会抛出该异常

/**
 * @throws  IOException
 * if an I/O error occurs
 */

避免抛出Exception、Throwable 要抛出更具体的异常

方法的throws只说明可能抛出的受检异常

@throw要记录在哪种情况下可能抛出的受检异常和运行时异常

在异常信息中保留关键信息

异常中会存储字符串保留当时发生异常的现场相关信息,这种信息对于我们的排查是非常有利的

为了能够更容易的保留这种关键信息,可以在自定义异常时写出方便排查的构造

public class IndexOutOfBoundsException extends RuntimeException {
    //下限
    private final int lowerBound;
    //上限
    private final int upperBound;
    //当前越界下标
    private final int index;

    public IndexOutOfBoundsException(int lowerBound, int upperBound,
                                     int index) {
        //关键信息
        super(String.format(
                "Lower bound: %d, Upper bound: %d, Index: %d",
                lowerBound, upperBound, index));

        this.lowerBound = lowerBound;
        this.upperBound = upperBound;
        this.index = index;
    }
}

比如这个下标越界中包含上下界限以及当前下标位置,能够给出关键信息

努力使失败保持原子性

有些情况下发生异常导致失败会让对象的状态不一致,从而导致数据不一致

发生这种情况后,如果再使用数据不一致的对象就会发生错误

在实现方法时应该努力让发生异常导致失败时保持原子性,失败的调用方法应该让对象处于之前的状态

保证原子性的方法有5种:

  1. 使用不可变对象:即使失败导致出错只要不创建/替换对象,对象都是不可变的
  2. 使用前检查入参,提前抛出异常
    比如ArrayList.remove方法,获取下标前要检查入参
public E remove(int index) {
    rangeCheck(index);
  
    modCount++;
    //如果不检查入参,此时抛出下标越界异常导致modCount数据不一致
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; 

    return oldValue;
}
  1. 调整处理顺序,让可能导致程序失败的步骤发生在改变数据操作之前(类似第二种)
    比如TreeSet需要内部元素实现比较器,如果未实现比较器或者元素类型不同,会发生类型转换异常,从而抛出异常不会执行添加操作
  2. 将源对象进行拷贝,如果发生异常错误可以找回源对象(或直接使用拷贝的对象进行处理)
    列表排序时会先拷贝一份数组再进行排序
default void sort(Comparator<? super E> c) {
    //拷贝
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    //排序完设置
    ListIterator<E> i = this.listIterator();
    for (Object e : a) {
        i.next();
        i.set((E) e);
    }
}
  1. 提供回滚操作,发生异常错误时使用回滚操作达到对象的状态一致

不要忽略异常

发生异常时不要忽略(catch块为空)

try{
       
}catch{
    //为空 忽略
}

忽略异常会导致程序继续执行下去可能导致错误发生,错误发生时也会难以排查

处理异常时可以打印日志,保留异常堆栈信息,如果要抛出就不要重复打印日志

如果要忽略可以写下注释说明理由

总结

只有针对异常情况才使用异常,不要使用异常来做程序的流程控制

广泛的异常分为受检异常、运行时异常(非受检异常)和错误,通常只接触前两者,后者排查虚拟机错误时才接触

对于运行恢复的情况抛出受检异常,程序错误或不确定是否允许恢复的情况抛出运行时异常

受检异常必须进行处理,能够带来可靠,但太多会导致复杂,不catch处理受检异常时可以直接抛出

优先复用已有的标准异常,不满足需求时再自定义

设计抽象层次方法时,关注抽象层次异常,而不是具体实现异常,通过捕获具体实现异常再抛出抽象层次异常

方法文档需要说明可能抛出的异常,不要抛出Exception异常,要抛出具体异常

自定义异常时尽量构造出方便排查的关键信息

异常失败可能导致对象状态不一致,可使用不可变对象、检查入参、调整执行顺序、拷贝对象、实现回滚等方案解决

忽略异常会导致程序继续执行从而发生错误结果,难以排查

最后(不要白嫖,一键三连求求拉~)

本篇文章被收入专栏 Effective Java,感兴趣的同学可以持续关注喔

本篇文章笔记以及案例被收入 Gitee-CaiCaiJavaGithub-CaiCaiJava 感兴趣的同学可以stat下持续关注喔~

有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~

关注菜菜,分享更多干货,公众号:菜菜的后端私房菜

相关文章
|
18天前
|
Java 编译器
探索Java中的异常处理机制
【10月更文挑战第35天】在Java的世界中,异常是程序运行过程中不可避免的一部分。本文将通过通俗易懂的语言和生动的比喻,带你了解Java中的异常处理机制,包括异常的类型、如何捕获和处理异常,以及如何在代码中有效地利用异常处理来提升程序的健壮性。让我们一起走进Java的异常世界,学习如何优雅地面对和解决问题吧!
|
6天前
|
安全 Java 程序员
Java中的异常处理:从新手到专家
在Java编程的世界里,异常处理是每个开发者必须面对的挑战。本文将带你从基础的异常概念出发,逐步深入到高级处理技巧,让你在遇到代码中的“意外”时,能够从容应对,甚至化险为夷。
|
11天前
|
Java 数据库连接 开发者
Java中的异常处理:从基础到高级
【10月更文挑战第42天】在Java的世界中,异常处理是维护程序稳定性和健壮性的关键。本文将带你深入了解Java的异常处理机制,从基本的try-catch语句出发,逐步探索更复杂的异常处理策略。我们将通过实际代码示例来演示如何捕获和处理异常,以及如何自定义异常类型来满足特定需求。无论你是Java新手还是有经验的开发者,这篇文章都将帮助你更好地理解和应用Java的异常处理。
|
18天前
|
Java 开发者
Java中的异常处理:从基础到高级
【10月更文挑战第35天】在Java的世界里,异常处理是维护程序健壮性的关键。本文将深入浅出地探讨Java的异常处理机制,从基本的try-catch语句到自定义异常类的实现,带领读者理解并掌握如何在Java中优雅地处理错误和异常。我们将通过实际代码示例,展示如何捕获、处理以及预防潜在的运行时错误,确保程序即使在面临意外情况时也能保持稳定运行。
33 7
|
17天前
|
Java 数据库连接 开发者
Java中的异常处理机制及其最佳实践####
在本文中,我们将探讨Java编程语言中的异常处理机制。通过深入分析try-catch语句、throws关键字以及自定义异常的创建与使用,我们旨在揭示如何有效地管理和响应程序运行中的错误和异常情况。此外,本文还将讨论一些最佳实践,以帮助开发者编写更加健壮和易于维护的代码。 ####
|
21天前
|
Java
Java 异常处理下篇:11 个异常处理最佳实践
本文深入探讨了 Java 异常处理的最佳实践,包括早抛出晚捕获、只捕获可处理的异常、不要忽略捕获的异常、抛出具体检查性异常、正确包装自定义异常、记录或抛出异常但不同时执行、避免在 `finally` 块中抛出异常、避免使用异常进行流程控制、使用模板方法处理重复的 `try-catch`、尽量只抛出与方法相关的异常以及异常处理后清理资源。通过遵循这些实践,可以提高代码的健壮性和可维护性。
|
21天前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
42 2
|
23天前
|
Java 程序员 数据库连接
深入浅出Java异常处理
【10月更文挑战第30天】在Java的世界里,异常处理就像是生活中的急救箱,遇到意外时能及时救治。本文不仅教你如何使用try-catch语句包扎“伤口”,还会深入讲解如何通过自定义异常来应对那些常见的“头疼脑热”。准备好,我们将一起探索Java异常处理的奥秘,让你的程序更加健壮。
|
20天前
|
Java 数据库连接
深入浅出Java异常处理
【10月更文挑战第33天】在Java编程的海洋中,异常处理就像是救生圈,它不仅能够挽救程序于水深火热之中,还能让代码更加优雅地面对意外。本文将带你领略Java异常处理的魅力,从基础概念到高级技巧,让你的程序在遇到问题时能够从容不迫,优雅地解决问题。
|
21天前
|
Java 开发者
Java中的异常处理:从基础到高级
【10月更文挑战第31天】在Java编程世界中,异常处理是一块基石,它确保了程序的健壮性和可靠性。本文将带你从异常处理的基本概念出发,通过实例展示如何在Java中捕获和处理异常,并深入探讨自定义异常、异常链以及如何使用try-with-resources语句优化资源管理。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的见解和技巧,帮助你编写更加稳定和易于维护的代码。
23 0