如何善用Java异常

简介: Java的异常算是Java语言的一个特色了。也是在日常编码中会经常使用到的东西。但你真的了解异常吗?

Java的异常算是Java语言的一个特色了。也是在日常编码中会经常使用到的东西。但你真的了解异常吗?

这里有一些关于异常的经典面试题:

  • Java与异常相关的类结构和主要继承关系是怎样的?
  • Java7在关于异常的语法上做了什么改进?
  • 什么是运行时异常和声明式异常?它们有什么区别?
  • 什么是“异常丢失(异常覆盖)”问题?
  • 什么是异常链?
  • 什么是返回值覆盖?
  • 编写异常时的一些最佳实践?

如果以上问题的答案你都能了然与胸,那么恭喜你,已经很熟悉Java异常这一块了。

如果一些问题还弄不清楚?没关系,看完这篇文章就可以了。


异常的层次结构


先上图

抛开下面那些异常不谈,我们的关注点可能主要在四个类上:

  • Throwable
  • Error
  • Exception
  • RuntimeException

其中,因为Error代表“错误”,多为比较严重的错误。如果你了解JVM,应该对OutOfMemoryErrorStackOverflowError这两个类比较熟悉。

一般我们在写代码时,可能用的比较多的是Exception类和RuntimeException类。

那到底是继承Exception类好还是继承RuntimeException类好呢?后面我们在“编写异常的最佳实践”小节会讲到。


Java7与异常


Java7对异常做了两个改进。第一个是try-with-resources,第二个是catch多个异常


try-with-resources

所谓的try-with-resources,是个语法糖。实际上就是自动调用资源的close()函数。和Python里的with语句差不多。

不使用try-with-resources,我们在使用io等资源对象时,通常是这样写的:

String getReadLine() throws IOException {
    BufferedReader br = new BufferedReader(fileReader);
    try {
        return br.readLine();
    } finally {
        if (br != null) br.close();
    }
}

使用try-with-recources的写法:

String getReadLine() throws IOException {
    try (BufferedReader br = new BufferedReader(fileReader)) {
        return br.readLine();
    }
}

显然,编绎器自动在try-with-resources后面增加了判断对象是否为null,如果不为null,则调用close()函数的的字节码。

只有实现了java.lang.AutoCloseable接口,或者java.io.Closable(实际上继随自java.lang.AutoCloseable)接口的对象,才会自动调用其close()函数。

有点不同的是java.io.Closable要求一实现者保证close函数可以被重复调用。而AutoCloseable的close()函数则不要求是幂等的。具体可以参考Javadoc。

但是,需要注意的是try-with-resources会出现异常覆盖的问题,也就是说catch块抛出的异常可能会被调用close()方法时抛出的异常覆盖掉。我们会在下面的小节讲到异常覆盖。


多异常捕捉

直接上代码:

public static void main(String[] args) {
    try {
        int a = Integer.parseInt(args[0]);
        int b = Integer.parseInt(args[1]);
        int c = a / b;
        System.out.println("result is:" + c);
    } catch (IndexOutOfBoundsException | NumberFormatException | ArithmeticException ie) {
        System.out.println("发生了以上三个异常之一。");
        ie.getMessage();
        // 捕捉多异常时,异常变量默认有final修饰,
        // 所以下面代码有错:
        // ie = new ArithmeticException("test");
    }
}


Suppressed

如果catch块和finally块都抛出了异常怎么办?请看下下小节分析。


运行时异常和声明式异常


所谓运行时异常指的是RuntimeException,你不用去显式的捕捉一个运行时异常,也不用在方法上声明。

反之,如果你的异常只是一个Exception,它就需要显式去捕捉。

示例代码:

void test() {
    hasRuntimeException();
    try {
        hasException();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
void hasException() throws Exception {
    throw new Exception("exception");
}
void hasRuntimeException() {
    throw new RuntimeException("runtime");
}

虽然从异常的结构图我们可以看到,RuntimeException继承自Exception。但Java会“特殊对待”运行时异常。所以如果你的程序里面需要这类异常时,可以继承RuntimeException

而且如果不是明确要求要把异常交给上层去捕获处理的话,我们建议是优先使用运行时异常,因为它会让你的代码更加简洁。


什么是异常覆盖


正如我们前面提到的,在finally块调用资源的close()方法时,是有可能抛出异常的。与此同时我们可能在catch块抛出了另一个异常。那么catch块抛出的异常就会被finally块的异常“吃掉”。

看看这段代码,调用test()方法会输出什么?

void test() {
    try {
        overrideException();
    } catch (Exception e) {
        System.out.println(e.getMessage());
    }
}
void overrideException() throws Exception {
    try {
        throw new Exception("A");
    } catch (Exception e) {
        throw new Exception("B");
    } finally {
        throw new Exception("C");
    }
}

会输出C。可以看到,在catch块的B被吃掉了。

JDK提供了Suppressed的两个方法来解决这个问题:

// 调用test会输出:
// C
// A
void test() {
    try {
        overrideException();
    } catch (Exception e) {
        System.out.println(e.getMessage());
        Arrays.stream(e.getSuppressed())
                .map(Throwable::getMessage)
                .forEach(System.out::println);
    }
}
void overrideException() throws Exception {
    Exception catchException = null;
    try {
        throw new Exception("A");
    } catch (Exception e) {
        catchException = e;
    } finally {
        Exception exception = new Exception("C");
        exception.addSuppressed(catchException);
        throw exception;
    }
}


异常链


你可以在抛出一个新异常的时候,使用initCause方法,指出这个异常是由哪个异常导致的,最终形成一条异常链。

详情请查阅公众号之前的关于异常链的文章。


返回值覆盖


跟之前的“异常覆盖”问题类似,finally块会覆盖掉trycatch块的返回值。

所以最佳实践是不要在finaly块使用return!!!


最佳实践?


  1. 如果可以,尽量使用RuntimeException
  2. 尽量不要在finally块抛出异常或者返回值
  3. 尽量使用Java7对异常的新语法
  4. try-catch块可以单独抽到一个方法里面去,让你的代码更简洁 —— 参考《代码整洁之道》第7章
  5. 记录异常日志可以结合log和printStackTrace


目录
相关文章
|
4天前
|
Java
在 Java 中捕获和处理自定义异常的代码示例
本文提供了一个 Java 代码示例,展示了如何捕获和处理自定义异常。通过创建自定义异常类并使用 try-catch 语句,可以更灵活地处理程序中的错误情况。
|
4天前
|
Java
在 Java 中,如何自定义`NumberFormatException`异常
在Java中,自定义`NumberFormatException`异常可以通过继承`IllegalArgumentException`类并重写其构造方法来实现。自定义异常类可以添加额外的错误信息或行为,以便更精确地处理特定的数字格式转换错误。
|
5天前
|
IDE 前端开发 Java
怎样避免 Java 中的 NoSuchFieldError 异常
在Java中避免NoSuchFieldError异常的关键在于确保类路径下没有不同版本的类文件冲突,避免反射时使用不存在的字段,以及确保所有依赖库版本兼容。编译和运行时使用的类版本应保持一致。
|
7天前
|
Java 编译器
如何避免在 Java 中出现 NoSuchElementException 异常
在Java中,`NoSuchElementException`通常发生在使用迭代器、枚举或流等遍历集合时,尝试访问不存在的元素。为了避免该异常,可以在访问前检查是否有下一个元素(如使用`hasNext()`方法),或者使用`Optional`类处理可能为空的情况。正确管理集合边界和条件判断是关键。
|
10天前
|
Java
Java异常捕捉处理和错误处理
Java异常捕捉处理和错误处理
11 1
|
11天前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
27 2
|
18天前
|
Java
如何在 Java 中处理“Broken Pipe”异常
在Java中处理“Broken Pipe”异常,通常发生在网络通信中,如Socket编程时。该异常表示写入操作的另一端已关闭连接。解决方法包括:检查网络连接、设置超时、使用try-catch捕获异常并进行重试或关闭资源。
|
21天前
|
存储 安全 Java
如何避免 Java 中的“ArrayStoreException”异常
在Java中,ArrayStoreException异常通常发生在尝试将不兼容的对象存储到泛型数组中时。为了避免这种异常,确保在操作数组时遵循以下几点:1. 使用泛型确保类型安全;2. 避免生类型(raw types)的使用;3. 在添加元素前进行类型检查。通过这些方法,可以有效防止 ArrayStoreException 的发生。
|
22天前
|
人工智能 Oracle Java
解决 Java 打印日志吞异常堆栈的问题
前几天有同学找我查一个空指针问题,Java 打印日志时,异常堆栈信息被吞了,导致定位不到出问题的地方。
30 2
|
25天前
|
Java 索引
如何避免在 Java 中引发`StringIndexOutOfBoundsException`异常
在Java中,处理字符串时若访问了不存在的索引,会抛出`StringIndexOutOfBoundsException`异常。为避免此异常,应确保索引值在有效范围内,例如使用`length()`方法检查字符串长度,并确保索引值不小于0且不大于字符串长度减1。