Java之异常处理机制

大家可以想一个问题,有没有谁能够做到开发项目时一个错误都不发生?如果谁能够做到这一点,那他可能真的是“天才”!但实际上,任何人都不可能在项目开发时一个错误都不产生,既然错误必不可免,那么Java是怎么处理这些问题的呢?

实际上,Java给我们提供了一套完善的异常处理机制来检查和解决可能出现的错误,以保证程序的可读性和可维护性。但我们要注意,Java中的异常和错误其实是两种不同的概念。异常是指在程序执行期间发生的一些错误或问题,所以异常的范围会更大一些,为此Java给我们提供了专门的异常处理机制来处理这些异常。

在本文中,将带领大家来学习Java的异常处理机制,包括异常机制、异常类型、如何捕获异常、如何抛出异常以及如何创建自定义异常等核心内容。

一. 异常机制简介

1. 异常概念

所谓的异常,其实就是各种“意外”,是指在程序执行期间发生的“意外事件”,它中断了正在执行程序的正常指令流,没有产生我们预期中的结果。就好比我们的生活中,也会经常有一些意外发生。本来你计划今天和女朋友去happy,结果你的领导非要你去加班,不加班就辞退,这就是预期外的异常事件。

Java给我们提供了专门的异常类来处理异常。在一个方法的运行过程中,如果发生了异常,这个方法会产生代表该异常的一个对象,并把它交给运行时系统,运行时系统会寻找相应的代码来处理该异常。Java把异常提交给运行时系统的过程称为拋出(throw)异常,运行时系统在方法的调用栈中查找到处理该异常对象的过程称为捕获(catch)异常

2. 产生原因

在Java中,一个异常的产生主要有以下几种原因:

1)Java内部错误导致了异常,比如Java虚拟机自身出现了问题;

2)我们自己编写的代码有问题,例如空指针异常、数组越界异常等;

3)项目运行所依赖的外部数据、网络环境等有问题,导致项目产生了故障;

4)通过throw等语句主动生成异常,主要用来告知该方法的调用者一些必要信息。

3. 异常类型

我们现在知道,Java中有专门的异常处理类,而这些异常类其实都是java.lang.Throwable的子类,所以Throwable是所有异常类的“老祖宗”。从整体上来看,Throwable类中有两个异常分支,即Exception和Error。如下图所示:

从上图中我们可知,Throwable是所有异常和错误的父类,Error和 Exception两个子类分别表示错误和异常。

其中,Error错误是在程序执行期间发生的严重问题,例如虚拟机崩溃、堆栈溢出错误、内存溢出等。这些错误一般是正常情况下不大可能出现的,绝大部分的Error都会导致程序处于非正常、不可恢复的状态,而我们也无法处理这些错误,只能尽力避免它们的发生,所以一般也不需要被开发者捕获处理。

而Exception类则用于处理用户程序可能出现的异常。根据异常的产生时机,该类可以细分为运行时异常(Runtime Exception)和非运行时异常(UnRuntime Exception);或者根据是否检查,又可以叫做非受检异常(Unchecked Exception)和受检异常(Checked Exception)

运行时异常都是RuntimeException类及其子类,如NullPointerException、IndexOutOfBoundsException、ArithmeticException等。这些异常是在运行时才会被发现的异常,它们通常是由于程序员的错误引起的,例如除零、数组越界等。我们在程序中可以选择捕获处理,也可以不处理,但应该尽力避免它们发生。另外因为这些异常在编译阶段通常不会进行异常的检查,所以也被称为非受检异常

非运行时异常是指RuntimeException以外的异常,它们都是Exception类及其子类。这些异常是在编译阶段就被检查的异常,也称为受检异常。它们通常是由于外部因素引起的,例如输入/输出错误、网络连接问题或文件找不到等。我们必须处理这些异常,否则编译时就会出错而无法通过,如IOException、SQLException、ClassNotFoundException及用户自定义的Exception等异常(我们一般不会自定义检查异常)。

4. 小结

综上述所, java.lang.Throwable是Java中的异常父类,包括Error和Exception两大子类。并且在Java代码中,只有继承了Throwable类的实例,才能被throw抛出或者catch捕获。

4.1 Error与Exception的区别

Error是正常情况下不大可能出现的情况,属于未检查类型,大多数发生在运行时。另外绝大部分的Error都会导致程序处于非正常、不可恢复的状态,任何处理技术都无法恢复,肯定会导致程序的非正常终止,所以不需要被开发者捕获。

而Exception是程序正常运行过程中可以预料到的意外情况,分为受检异常和非受检异常。受检异常在代码中必须进行显式地捕获处理,这是编译期检查的一部分。非受检异常也被称为运行时异常,通常是在编码时就能避免的逻辑错误,具体根据需要来判断是否需要捕获,不会在编译器强制要求,应该被开发者捕获进行相应的处理。

接下来再给大家说一些常见的Error和Exception:

4.2 错误

  • OutOfMemoryError:内存溢出错误;
  • VirtualMachineError:虚拟机错误;
  • NoClassDefFoundError:找不到类定义错误;
  • StackOverflowError:堆栈异常错误

4.3 运行时异常

  • NullPropagation:空指针异常;
  • ClassCastException:强制类型转换异常;
  • IllegalArgumentException:非法传参异常;
  • IndexOutOfBoundsException:下标越界异常;
  • NumberFormatException:数字格式化异常

4.4 非运行时异常

  • ClassNotFoundException:找不到指定的类异常;
  • IOException:IO操作异常

二. 异常处理机制

1. 概述

既然异常的产生是不可避免的,那么为了保证程序有效地执行,我们就需要对发生的异常进行相应的处理。否则当Java程序发生异常时,可能会导致程序的停止或故障。而Java给我们提供了一套完整的异常处理机制,来应对可能出现的各种问题。在Java中,异常处理主要是利用5个关键字来实现:

try、catch、throw、throws、finally

其中,try catch语句用于捕获并处理异常,finally语句用于在任何情况下(除特殊情况外)都必须执行的代码,throw语句用于拋出异常,throws语句用于声明可能会出现的异常。

总之,Java的异常处理机制给我们提供了一种结构性和控制性的方式,来处理程序执行期间发生的事件。总体上来说,对异常的处理其实就是“要么捕获,要么抛出”,这好比我们对待一个犯了错误的孩子,要么抓住他修理一顿,要么把他扔给别人处理,而自己不管不问。所以我们对异常的处理思路如下:

  • 捕获异常:在方法中用 try catch语句捕获并处理异常,catch语句可以有多个,用来匹配多个异常;
  • 抛出异常对处理不了的异常或者要转型的异常,在方法的声明处可以通过throws语句拋出异常,即由上层的调用方法进行处理。

接下来再分别细说一下异常处理机制。

2. 捕获异常

很多时候,我们对异常的处理就是进行捕获,所以Java给我们提供了try-catch语句来捕获异常。如果我们编写的某些代码,可能会抛出异常,那你就可以用try语句块把这些代码包裹起来。而catch语句块中包含了处理异常的代码,所以如果一个异常被抛出,它就可以被catch语句块捕获并处理。

try {
    // 可能会抛出异常的代码
} catch (ExceptionType1 e1) {
    // 异常处理代码
} catch (ExceptionType2 e2) {
    // 异常处理代码
} finally {
    // finally块是可选的,并不是必须的,不管是否发生异常,这里的代码都会被执行
}

在try语句块中,我们可以编写多个语句,包括方法调用、循环和条件语句等。如果try语句中的某行代码产生了异常,该异常被抛出,则该语句之后的其他语句就不会再被执行,程序会直接跳转到与该异常匹配的catch语句块中。catch语句块中的异常参数提供了有关异常的详细信息,例如异常类型、异常消息和堆栈跟踪等内容。

我们可以同时使用多个catch语句块来处理不同类型的异常。在这种情况下,catch语句块的顺序就很重要,因为异常的处理会与catch语句块的顺序匹配。如果一个异常可以匹配多个catch语句块,则只有第一个匹配的语句块会被执行。

finally语句块是可选的,不管是否发生异常,finally里的代码都会被执行。我们通常是在finally语句块中执行一些清理操作,比如关闭文件或释放资源等。

3. 抛出异常

我们除了可以捕获异常外,还可以在代码中手动地通过throw语句来抛出异常。

if (x < 0) {
    throw new IllegalArgumentException("x不能为负数");
}

在上面的例子中,我们规定,如果x小于0,就手动抛出一个IllegalArgumentException异常,并将消息“x不能为负数”传递给异常。该异常可以是任何Throwable的子类,包括我们的自定义异常类。

4. 自定义异常

Java也允许我们创建自己的异常类。为了创建一个自定义的异常类,我们一般是继承扩展Exception或RuntimeException类,如下所示。

public class MyException extends Exception {
    public MyException() {}
    
    public MyException(String message) {
        super(message);
    }
}

在上面的例子中,创建了一个自定义的异常类MyException,它扩展了Exception类。我们还提供了两个构造函数,一个不带参数,另一个带有一个字符串参数,用于设置异常消息。

5. 异常处理的最佳实践

在Java中,异常处理是非常重要的,但也容易被滥用,下面给大家总结的一些处理异常的最佳实践。

5.1 不要捕获过多的异常

捕获过多的异常可能会使代码难以理解和维护,我们通常是只捕获必要的异常,并在可能的情况下将它们向上抛出,让更高层次的代码来处理它们。

5.2 不要忽略异常

忽略异常可能会使程序在后续的步骤中出现错误,进而导致数据出现损坏或其他不良后果。我们一般是在catch语句块中处理异常,并使用日志来记录异常信息,以便更好地了解问题。

5.3 不要在finally块中返回值

在finally语句块中返回值可能会导致不可预测的行为。如果一个方法在try语句块中返回了一个值,在finally语句块中又返回了另一个值,这可能会导致错误的结果。我们在finally语句块中只应该执行清理操作,不应该返回值。

5.4 将异常转换

在某些情况下,我们可能需要将一个异常转换为另一个异常。例如,我们想编写一个库,并且我们希望隐藏该库的某些内部实现细节,此时可以将一个异常转换为另一个更通用的异常类型,以便客户端代码可以更容易地处理它。比如在下面的这个案例中,就用MyLibraryException替换了IOException。

public class MyLibrary {
    public void doSomething() throws MyLibraryException {
        try {
            // 执行一些操作
        } catch (IOException e) {
            //抛出一个更通用的异常,用MyLibraryException替换IOException
            throw new MyLibraryException("发生了错误", e);
        }
    }
}

//自定义一个更通用的异常
public class MyLibraryException extends Exception {
    public MyLibraryException() {}
    public MyLibraryException(String message) {
        super(message);
    }
    public MyLibraryException(String message, Throwable cause) {
        super(message, cause);
    }
}

在上面的例子中,创建了一个自定义的异常类MyLibraryException,它扩展了Exception类。在MyLibrary类中,如果发生了IOException,我们就将其转换为MyLibraryException,并将原始异常作为原因传递。

5.5 不要在构造函数中抛出异常

在Java中,如果我们在构造函数中抛出异常,则对象可能就无法完全初始化了,这可能会导致在后续的步骤中出现错误。所以我们最好避免在构造函数中抛出异常,并使用工厂方法或静态工厂方法来创建对象。

public class MyClass {
    private int x;
    private int y;
    
    public MyClass(int x, int y) {
        this.x = x;
        if (y < 0) {
            throw new IllegalArgumentException("y不能为负数");
        }
        this.y = y;
    }
    
    public static MyClass create(int x, int y) {
        if (y < 0) {
            throw new IllegalArgumentException("y不能为负数");
        }
        return new MyClass(x, y);
    }
}

在上面的例子中,使用了一个create工厂方法来创建MyClass对象。如果y为负数,则在创建对象之前会抛出一个IllegalArgumentException异常。

6. 异常处理时的常见问题

我们在进行异常处理时,也有一些常见的问题需要注意。

6.1 不要捕获Throwable

Throwable是所有异常的超类,包括Error和Exception。开发时我们不能捕获Throwable,因为它可能会捕获到一些严重的错误,如OutOfMemoryError,这些错误是无法通过代码处理的。所以在开发时,我们应该去捕获某个具体的异常类型。

6.2 不要在finally中抛出异常

如果我们在finally语句块中抛出异常,有可能会导致之前抛出的异常被覆盖,这可能会导致其他的一些错误。所以我们在finally语句块中,一般只是进行清理操作,而不是抛出异常。

6.3 不要忽略InterruptedException

InterruptedException是一个非常特殊的异常,它通常表示线程被中断。如果我们要在执行长时间操作的线程上捕获InterruptedException,应该尽快停止线程并退出。如果继续执行,可能会导致一些无法预料的行为。

6.4 不要使用异常来控制流程

在某些情况下,我们可能会使用异常机制来控制程序的流程,但这并不是一种好的实践。这会使代码难以理解,还可能会导致性能问题。例如,下面的代码中就使用了异常机制来控制循环的流程:

try {
    while (true) {
        // 一些操作
        if (someCondition) {
            throw new RuntimeException("循环已经完成");
        }
    }
} catch (RuntimeException e) {
    // 处理异常
}

这种做法虽然在语法上是可行的,但如果我们想结束while循环,更好的办法是使用一个标志位来控制循环的流程,而不是使用异常机制。

boolean done = false;
while (!done) {
    // 一些操作
    if (someCondition) {
        done = true;
    }
}

6.5 不要忽略异常

我们在编写代码时,有可能会忽略掉异常,但这会导致代码无法处理一些错误情况。在捕获异常时,我们应该始终记录异常或打印异常堆栈跟踪,以便更好地弄清楚产生错误的原因。

try {
    // 一些操作
} catch (Exception e) {
    logger.error("发生了错误", e);
}

在上面的例子中,使用了日志记录器来记录异常,并将异常堆栈跟踪作为额外信息传递。

6.6 不要在循环中捕获异常

如果我们在循环内部捕获异常可能会导致性能问题,并让代码更难理解。通常是将循环体封装在一个try语句块中,而不是在循环内部捕获异常。

try {
    for (int i = 0; i < list.size(); i++) {
        // 一些操作
    }
} catch (Exception e) {
    logger.error("发生了错误", e);
}

在上面的例子中,我们使用try语句块来包装循环体,而不是在循环内部捕获异常。

四. 结语

Java的异常处理机制是一种强大的工具,可以帮助我们处理各种异常情况。所以熟练掌握异常处理机制的基础知识和最佳实践,可以帮助我们编写更健壮、可靠的代码。

我们在编写代码时,应该提前考虑到各种异常情况,并在可能的情况下使用异常机制来处理这些情况。要注意,异常处理机制应该是代码的一部分,而不是控制流程的一部分。同时,还应该注意避免常见的异常处理问题,例如捕获Throwable、在finally块中抛出异常、忽略InterruptedException等。并且我们还要注意保持代码的简洁性和可读性。

转自:从零开始学Java之异常处理机制简介 - 知乎

猜你喜欢

转载自blog.csdn.net/fuhanghang/article/details/135008682
今日推荐