如何在项目中正确使用异常?

文章目录

      • 异常系列文章
      • 一、异常介绍
      • 二、异常体系
      • 三、异常处理机制
      • 四、异常处理
      • 五、异常调用链

异常系列文章

如何在项目中正确使用异常?
如何优雅的设计Java异常
Java统一异常处理–实战篇
Java 异常处理的误区和经验总结
你要的Java常见异常都在这里
Java中异常抛出后代码还会继续执行吗

一、异常介绍

异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。

比如说,你的代码少了一个分号,那么运行出来结果是提示是错误 java.lang.Error;如果你用System.out.println(11/0),那么你是因为你用0做了除数,会抛出 java.lang.ArithmeticException 的异常。

异常发生的原因有很多,通常包含以下几大类:

  • 用户输入了非法数据。
  • 要打开的文件不存在。
  • 网络通信时连接中断,或者JVM内存溢出。

这些异常有的是因为用户错误引起,有的是程序错误引起的,还有其它一些是因为物理错误引起的。-

要理解Java异常处理是如何工作的,你需要掌握以下三种类型的异常:

  • 检查性异常: 最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
  • 运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
  • 错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。

二、异常体系

注意:编译器对RuntimeException及其子类不做强制捕获要求,不是指应用程序本身不应该捕获并处理RuntimeException。是否需要捕获,具体问题具体分析。

Java中定义了很多现成的异常(叫做内建异常),这些异常又分属不同类别,不同类别的异常具有不同的特点。整个Java的异常体系(类图)如下图所示。

如何在项目中正确使用异常?_第1张图片

下面将详细讲述这些异常之间的区别与联系:

  • ErrorError类对象由 Java 虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。例如,Java虚拟机运行错误(Virtual MachineError),当JVM不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止;还有发生在虚拟机试图执行应用时,如类定义错误(NoClassDefFoundError)、链接错误(LinkageError)。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在Java中,错误通常是使用Error的子类描述。
  • Exception
    • Exception分支中有一个重要的子类RuntimeException(运行时异常),该类型的异常自动为你所编写的程序定义ArrayIndexOutOfBoundsException(数组下标越界)、NullPointerException(空指针异常)、ArithmeticException(算术异常)、MissingResourceException(丢失资源)、ClassNotFoundException(找不到类)等异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生;
    • RuntimeException之外的异常我们统称为非运行时异常,类型上属于Exception类及其子类,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOExceptionSQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

下面的表中列出了 Java 的非检查性异常。

异常 描述
ArithmeticException 当出现异常的运算条件时,抛出此异常。例如,一个整数"除以零"时,抛出此类的一个实例。
ArrayIndexOutOfBoundsException 用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引。
ArrayStoreException 试图将错误类型的对象存储到一个对象数组时抛出的异常。
ClassCastException 当试图将对象强制转换为不是实例的子类时,抛出该异常。
IllegalArgumentException 抛出的异常表明向方法传递了一个不合法或不正确的参数。
IllegalMonitorStateException 抛出的异常表明某一线程已经试图等待对象的监视器,或者试图通知其他正在等待对象的监视器而本身没有指定监视器的线程。
IllegalStateException 在非法或不适当的时间调用方法时产生的信号。换句话说,即 Java 环境或 Java 应用程序没有处于请求操作所要求的适当状态下。
IllegalThreadStateException 线程没有处于请求操作所要求的适当状态时抛出的异常。
IndexOutOfBoundsException 指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。
NegativeArraySizeException 如果应用程序试图创建大小为负的数组,则抛出该异常。
NullPointerException 当应用程序试图在需要对象的地方使用 null 时,抛出该异常
NumberFormatException 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。
SecurityException 由安全管理器抛出的异常,指示存在安全侵犯。
StringIndexOutOfBoundsException 此异常由 String 方法抛出,指示索引或者为负,或者超出字符串的大小。
UnsupportedOperationException 当不支持请求的操作时,抛出该异常。

下面的表中列出了 Java 定义在 java.lang 包中的检查性异常类。

异常 描述
ClassNotFoundException 应用程序试图加载类时,找不到相应的类,抛出该异常。
CloneNotSupportedException 当调用 Object 类中的 clone 方法克隆对象,但该对象的类无法实现 Cloneable 接口时,抛出该异常。
IllegalAccessException 拒绝访问一个类的时候,抛出该异常。
InstantiationException 当试图使用 Class 类中的 newInstance 方法创建一个类的实例,而指定的类对象因为是一个接口或是一个抽象类而无法实例化时,抛出该异常。
InterruptedException 一个线程被另一个线程中断,抛出该异常。
NoSuchFieldException 请求的变量不存在
NoSuchMethodException 请求的方法不存在

自定义异常跟大多数内建异常一样,要么作为受检异常继承自Exception,要么作为非受检异常继承自RuntimeException。那么,在定义某个异常时,我们应该选择让其继承自Exception呢?还是应该选择让其继承自RuntimeException呢?

  • 对于代码bug(比如数组越界)以及不可恢复异常(比如数据库连接失败),即便我们捕获了,也做不了太多事情,所以,我们倾向于使用非受检异常。
  • 对于可恢复异常、业务异常、预期可能发生的异常,比如提现金额大于余额的异常,我们更倾向于使用受检异常,明确告知调用者需要捕获处理。

三、异常处理机制

Java的异常处理本质上是抛出异常捕获异常

  • 抛出异常:要理解抛出异常,首先要明白什么是异常情形(exception condition),它是指阻止当前方法或作用域继续执行的问题。其次把异常情形和普通问题相区分,普通问题是指在当前环境下能得到足够的信息,总能处理这个错误。对于异常情形,已经无法继续下去了,因为在当前环境下无法获得必要的信息来解决问题,你所能做的就是从当前环境中跳出,并把问题提交给上一级环境,这就是抛出异常时所发生的事情。抛出异常后,会有几件事随之发生。首先,是像创建普通的java对象一样将使用new在堆上创建一个异常对象;然后,当前的执行路径(已经无法继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方继续执行程序,这个恰当的地方就是异常处理程序或者异常处理器,它的任务是将程序从错误状态中恢复,以使程序要么换一种方式运行,要么继续运行下去。

举个简单的例子,假使我们创建了一个学生对象Student的一个引用stu,在调用的时候可能还没有初始化。所以在使用这个对象引用调用其他方法之前,要先对它进行检查,可以创建一个代表错误信息的对象,并且将它从当前环境中抛出,这样就把错误信息传播到更大的环境中。

if(stu == null){
    throw new NullPointerException();
}

这就抛出了异常,它将在其他的地方得到执行或者处理,具体是哪个地方后面将很快介绍,代码中出现的 throw 是一个关键字,暂时先不做过多讲解,后面会详细讲解。

  • 捕获异常:在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。

提示

对于运行时异常错误检查异常,Java技术所要求的异常处理方式有所不同。

由于运行时异常及其子类的不可查性,为了更合理、更容易地实现应用程序,Java规定,运行时异常将由Java运行时系统自动抛出,允许应用程序忽略运行时异常

对于方法运行中可能出现的Error,当运行方法不欲捕捉时,Java允许该方法不做任何抛出声明。因为,大多数Error异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。

对于所有的检查异常,Java规定:一个方法必须捕捉,或者声明抛出方法之外。也就是说,当一个方法选择不捕捉检查异常时,它必须声明将抛出异常。

Java异常处理涉及到五个关键字,分别是:trycatchfinallythrowthrows。下面将骤一介绍,通过认识这五个关键字,掌握基本异常处理知识。

try – 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
  • catch – 用于捕获异常。catch用来捕获try语句块中发生的异常。
  • finally – finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
  • throw – 用于抛出异常。
  • throws – 用在方法签名中,用于声明该方法可能抛出的异常。

四、异常处理

当某段程序抛出异常时,我们应该如何处理抛出的异常呢?一般来讲,我们有3种处理方法。

除了下面的处理方式,也可以结合这个一起参考:

总之,如果你捕获了异常打算处理的话,除了通过日志正确记录异常原始信息外,通常还有三种处理模式:(catch 捕获处理异常的一些最佳实践。)

  • 转换,即转换新的异常抛出。对于新抛出的异常,最好具有特定的分类和明确的异常消息,而不是随便抛一个无关或没有任何信息的异常,并最好通过 cause 关联老异常。
  • 重试,即重试之前的操作。比如远程调用服务端过载超时的情况,盲目重试会让问题更严重,需要考虑当前情况是否适合重试。
  • 恢复,即尝试进行降级处理,或使用默认值来替代原始数据

3种错误方式

1、捕获了异常后直接生吞·

2、丢弃异常的原始信息

3、抛出异常时不指定任何消息

正确:

处理异常应该杜绝生吞,并确保异常栈信息得到保留。如果需要重新抛出异常的话,请使用具有意义的异常类型和异常消息

1)捕获后记录日志

public void f() throws LowLevelException { ... }
public void g() {
  try {
    f();
  } catch(LowLevelException e) {
    log.warn("...", e); //使用日志框架记录日志
  }
}

2)原封不动再抛出

public void f() throws LowLevelException { ... }
//如果LowLevelException是非受检异常,则不需要在函数g()定义中声明
public void g() throws LowLevelException {
  f();
}

3)包装成新异常抛出

public void f() throws LowLevelException { ... }
public void g() {
  try {
    f();
  } catch(LowLevelException e) {
    throw new HighLevelException("...", e);
  }
}

以上我们介绍了3种处理异常的方法,那么,当代码抛出异常时,我们应该选择哪一种来处理方法呢?很多程序员对处理方式的选择比较随意,也没有一个原则。实际上,选择哪种处理方法,其实有一个简单的原则可以参考,那就是:函数只抛出跟函数所涉及业务相关的异常。

在函数内部,如果某块代码的异常行为,并不会导致调用此函数的上层代码出现异常行为,也就是说,上层代码并不关心被调用函数内部的这个异常,我们就可以在函数内部将这个异常“消化掉”:将其捕获并打印日志记录。相反,如果函数内部的异常行为会导致调用此函数的上层代码出现异常行为,那么,我们就必须让上层代码感知到此异常的存在。如果此异常跟函数的业务相关,上层代码在调用此函数时,知道如何处理此异常,那么直接将其抛出就可。如果此异常跟函数的业务无关,上层代码无法理解这个异常的含义,不知道如何处理,那么需要将其包裹成新的跟函数业务相关的异常重新抛出。

public byte[] readData(String filePath) throws DataReadException,FileNotFoundException {
  InputStream in = null;
  try {
    Thread.sleep(10);
    in = new FileInputStream(filePath);
    byte[] data = new byte[in.available()];
    in.read(data);
    return data;
  } catch (InterruptedException e) {
    throw new DataReadException("Interrupted when reading: " + filePath, e);
  } catch (IOException e) {
    throw new DataReadException("Failed to read: " + filePath, e);
  } finally {
    if (in != null) {
      try {
        in.close();
      } catch (IOException e) {
        //使用日志框架记录日志
      }
    }
  }
}

参照刚刚给出的3种异常处理方式,以及选择的原则,我们来分析一下上面的代码。

1)调用readData()函数的上层代码并不关心文件关闭失败(对应in.close()语句)导致的IOException异常,因此,我们直接将其捕获并打印日志。

2)对于文件读取失败而抛出的IOException异常,因为IOException异常比较底层,如果原封不动抛出,那么上层代码可能并不知道如何处理,所以,我们将其重新包裹成自定义的DataReadException异常再抛出。

3)同理,对于因sleep()函数被中断而抛出的InterruptedException异常,上层代码也无法理解,因此,我们同样将其包裹为DataReadException异常再抛出。

4)对于文件打开失败而抛出的FileNotFoundException异常,因为跟readData()函数业务相关,毕竟readData()函数中的参数就是文件的路径,所以,我们可以直接将其抛出。当然,如果我们想要减少readData()函数受检异常的个数,那么也可以将FileNotFoundException异常统一包裹为DataReadException异常再抛出。

五、异常调用链

异常最终的宿命终究是被捕获并打印异常信息,以便程序员debug问题,比如将其打印到日志或命令行中。为了给程序员展示充足的异常信息,我们一般需要将异常调用链完整打印出来。

异常调用链记录了异常引起的整个过程,当前被捕获的异常是由哪个异常引起的,跟函数调用一样,一直追溯到引起整个异常调用链的最原始的异常为止。除此之外,异常调用链还会记录每个异常的生命周期内所经历的所有函数。异常的生命周期指的是,异常从创建到被捕获并不再继续抛出的这一过程。

public class Demo17_1 {
  public static class LowLevelException extends Exception {
    public LowLevelException() { super(); }
    public LowLevelException(String msg, Throwable cause) { super(msg, cause); }
    public LowLevelException(String msg) { super(msg); }
    public LowLevelException(Throwable cause) { super(cause); }
  }

  public static class MidLevelException extends Exception {
    //...与LowLevelException实现类似,省略代码实现...
  }

  public static class HighLevelException extends RuntimeException {
    //...与LowLevelException实现类似,省略代码实现...
  }

  public static void fa() throws LowLevelException {
    throw new LowLevelException("LowLevelException-msg");
  }

  public static void fb() throws LowLevelException {
    fa();
  }

  public static void fc() throws MidLevelException {
    try {
      fb();
    } catch (LowLevelException e) {
      throw new MidLevelException("MidLevelException-msg", e);
    }
  }

  public static void fd() {
    try {
      fc();
    } catch (MidLevelException e) {
      throw new HighLevelException("HighLevelException-msg", e);
    }
  }

  public static void fe() {
      fd();
  }
  
  public static void main(String[] args) {
    try {
      fe();
    } catch(HighLevelException e) {
      e.printStackTrace();
    }
  }
}

我们分析一下上述代码。

1)LowLevelException异常为调用链中的第一个异常,在fa()函数中抛出,fb()未捕获直接将其抛出,fc()将其捕获并且重新包装成MidLevelException异常抛出,所以,LowLevelException异常的生命周期经历了3个函数:fa()、fb()、fc()。

2)MidLevelException异常在fc()函数中创建,在fd()函数中被捕获,然后重新包装成HighLevelException异常抛出,所以,MidLevelException()异常的生命周期经历了2个函数:fc()和fd()。

3)HighLevelException异常为运行时异常,在fd()函数中创建,fe()函数没有将其捕获,默认原样抛出,最终被main()函数捕获并输出异常调用链信息,至此异常调用链结束。所以,HighLevelException异常的生命周期经历了3个函数:fd()、fe()、main()。

HighLevelException异常由MidLevelException异常引起,MidLevelException异常又由LowLevelException异常引起,因此,上述代码打印出来的异常调用链,如下所示。

demo.Demo17_1$HighLevelException: HighLevelException-msg
	at demo.Demo17_1.fd(Demo17_1.java:53)
	at demo.Demo17_1.fe(Demo17_1.java:58)
	at demo.Demo17_1.main(Demo17_1.java:27)
Caused by: demo.Demo17_1$MidLevelException: MidLevelException-msg
	at demo.Demo17_1.fc(Demo17_1.java:45)
	at demo.Demo17_1.fd(Demo17_1.java:51)
	... 2 more
Caused by: demo.Demo17_1$LowLevelException: LowLevelException-msg
	at demo.Demo17_1.fa(Demo17_1.java:34)
	at demo.Demo17_1.fb(Demo17_1.java:38)
	at demo.Demo17_1.fc(Demo17_1.java:43)
	... 3 more

异常调用链可以完整的描述异常发生的整个过程。**但需要特别注意的是,捕获异常并包裹成新的异常抛出时,我们一定要将先前的异常通过cause参数传递进新的异常,否则,异常调用链将会断开。**比如,对于上述示例代码,在创建MidLevelException异常时,如果我们没有将LowLevelException异常通过cause参数传递给MidLevelException,那么,通过MidLevelException异常将无法再追踪到LowLevelException异常。最终打印出来的异常调用链将只包含HighLevelException异常信息和MidLevelException异常信息。

//错误做法
try {
  ...
} catch (CausedByException e) {
  throw new NewException("msg..."); // e丢失
}

//正确的做法
try {
  ...
} catch (CausedByException e) {
  throw new NewException("msg...", e);
}

在平时的开发中,我们还需要特别注意,对于异常的处理,要么记录,要么抛出,但两者不能同时执行。错误的做法如下所示。在异常调用链中,我们只需要在最后一个异常生命周期结束时,打印异常调用链即可,没必要像如下所示,重复打印部分异常调用链。既然我们已经抛出了异常,异常就理应由上层函数来负责处理(比如打印)。

//错误的做法
try {
  ...
} catch (CausedByException e) {  
   logger.error("...", e);
   throw new NewException("msg...", e);
}

参考文章:
部分内容参考于 https://www.xzgedu.com/ Java编程之美专栏

你可能感兴趣的:(Java,SE初级,jvm,java,面试,Java异常)