Java异常处理中的各种细节汇总

前言

今天我们来讨论一下,程序中的错误处理。

在任何一个稳定的程序中,都会有大量的代码在处理错误,有一些业务错误,我们可以通过主动检查判断来规避,可对于一些不能主动判断的错误,例如 RuntimeException,我们就需要使用 try-catch-finally 语句了。

有人说,错误处理并不难啊,try-catch-finally 一把梭,try 放功能代码,在 catch 中捕获异常、处理异常,finally 中写那些无论是否发生异常,都要执行的代码,这很简单啊。

处理错误的代码,确实并不难写,可是想把错误处理写好,也并不是一件容易的事情。

接下来我们就从实现到 JVM 原理,讲清楚 Java 的异常处理。

学东西,我还是推荐要带着问题去探索,提前思考几个问题吧:

  • 一个方法,异常捕获块中,不同的地方的 return 语句,谁会生效?
  • catch 和 finally 中出现异常,会如何处理?
  • try-catch 是否影响效率?
  • Java 异常捕获的原理?

二、Java 异常处理

2.1 概述

既然是异常处理,肯定是区分异常发生和捕获、处理异常,这也正是组成异常处理的两大要素。

在 Java 中,抛出的异常可以分为显示异常和隐式异常,这种区分主要来自抛出异常的主体是什么,显示和隐式也是站在应用程序的视角来区分的。

显示异常的主体是当前我们的应用程序,它指的是在应用程序中使用 “throw” 关键字,主动将异常实例抛出。而隐式异常就不受我们控制, 它触发的主体是 Java 虚拟机,指的是 Java 虚拟机在执行过程中,遇到了无法继续执行的异常状态,续而将异常抛出。

对于隐式异常,在触发时,需要显示捕获(try-catch),或者在方法头上,用 "throw" 关键字声明,交由调用者捕获处理。

2.2 使用异常捕获

在我们编写异常处理代码的时候,主要就是使用前面介绍到的 try-catch-finally 这三种代码块。

  • try 代码块:包含待监控异常的代码。
  • catch 代码块:紧跟 try 块之后,可以指定异常类型。允许指定捕获多种不同的异常,catch 块用来捕获在 try 块中出发的某个指定类型的异常。
  • finally 代码块:紧跟 try 块或 catch 块之后,用来声明一段必定会运行的代码。例如用来清理一些资源。

catch 允许存在多个,用于针对不同的异常做不同的处理。如果使用 catch 捕获多种异常,各个 catch 块是互斥的,和 switch 语句类似,优先级是从上到下,只能选择其一去处理异常。

既然 try-catch-finally 存在多种情况,并且在发生异常和不发生异常时,表现是不一致的,我们就分清楚来单独分析。

1. try块中,未发生异常

不触发异常,当然是我们乐于看见的。在这种情况下,如果有 finally 块,它会在 try 块之后运行,catch 块永远也不会被运行。

2. try块中,发生异常

在发生异常时,会首先检查异常类型,是否存在于我们的 catch 块中指定的待捕获异常。如果存在,则这个异常被捕获,对应的 catch 块代码则开始运行,finally 块代码紧随其后。

例如:我们只监听了空指针(NullPointerException),此时如果发生了除数为 0 的崩溃(ArithmeticException),则是不会被处理的。

当触发了我们未捕获的异常时,finally 代码依然会被执行,在执行完毕后,继续将异常“抛出去”。

3. catch 或者 finally 发生异常

catch 代码块和 finally 代码块,也是我们编写的,理论上也是有出错的可能。

那么这两段代码发生异常,会出现什么情况呢?

当在 catch 代码块中发生异常时,此时的表现取决于 finally 代码块中是否存在 return 语句。如果存在,则 finally 代码块的代码执行完毕直接返回,否则会在 finally 代码块执行完毕后,将 catch 代码中新产生的异常,向外抛出去。

而在极端情况下,finally 代码块发生了异常,则此时会中断 finally 代码块的执行,直接将异常向外抛出。

2.3 异常捕获的返回值

再回头看看第一个问题,假如我们写了一个方法,其中的代码被 try-catch-finally 包裹住进行异常处理,此时如果我们在多个地方都有 return 语句,最终谁的会被执行?

如上图所示,在完整的 try-catch-finally 语句中,finally 都是最后执行的,假设 finally 代码块中存在 return 语句,则直接返回,它是优先级最高的。

一般我们不建议在 finally 代码块中添加 return 语句,因为这会破坏并阻止异常的抛出,导致不宜排查的崩溃。

2.4 异常的类型

在 Java 中,所有的异常,其实都是一个个异常类,它们都是 Throwable 类或其子类的实例。

Throwable 有两大子类,Exception 和 Error。

  • Exception:表示程序可能需要捕获并且处理的异常。
  • Error:表示当触发 Error 时,它的执行状态已经无法恢复了,需要中止线程甚至是中止虚拟机。这是不应该被我们应用程序所捕获的异常。

通常,我们只需要捕获 Exception 就可以了。但 Exception 中,有一个特殊的子类 RuntimeException,即运行时错误,它是在程序运行时,动态出现的一些异常。比较常见的就是 NullPointerException、ArrayIndexOutOfBoundsException 等。
Error 和 RuntimeException 都属于非检查异常(Unchecked Exception),与之相对的就是普通 Exception 这种属于检查异常(Checked Exception)。

所有检查异常都需要在程序中,用代码显式捕获,或者在方法中用 throw 关键字显式标注。其实意思很明显,要不你自己处理了,要不你抛出去让别人处理。

这种检查异常的机制,是在编译期间进行检查的,所以如果不按此规范处理,在编译器编译代码时,就会抛出异常。

2.5 异常处理的性能问题

对于异常处理的性能问题,其实是一个很有争议的问题,有人觉得异常处理是多做了一些工作,肯定对性能是有影响的。但是也有人觉得异常处理的影响,和增加一个 if-else 属于同种量级,对性能的影响其实微乎其微,是在可以接受的范围内的。

既然有争议,最简单的办法是写个 Demo 验证一下。当然,我们这里是需要区分不同的情况,然后根据解决对比的。
一个最简单的 for 循环 100w 次,在其中做一个 a++ 的自增操作。

  • A:无任何 try-catch 语句。
  • B:将 a++ 包在 try 代码块中。
  • C:在 try 代码块中,触发一个异常。

就是一个简单的 for 循环,就不贴代码了,异常通过 5/0 这样的运算,触发除数为 0 的 ArithmeticException 异常,并在 JDK 1.8 的环境下运行。

为了避免影响采样结果,每个例子都单独运行 10 遍之后,取平均值(单位纳秒)。

到这里基本上就可以得出结论了,在没有发生异常的情况下,try-catch 对性能的影响微乎其微。但是一旦发生异常,性能上则是灾难性的。

因此,我们应该尽可能的避免通过异常来处理正常的逻辑检查,这样可以确保不会因为发生异常而导致性能问题。

至于为什么发生异常时,性能差别会有如此之大,就需要从 Java 虚拟机 JVM 的角度来分析了,后面会详细分析。

2.6 异常处理无法覆盖异步回调

try-catch-finally 确实很好用,但是它并不能捕获,异步回调中的异常。try 语句里的方法,如果允许在另外一个线程中,其中抛出的异常,是无法在调用者这个线程中捕获的。

这一点在使用的过程中,需要特别注意。

三、JVM 如何处理异常

3.1 JVM 异常处理概述

接下来我们从 JVM 的角度,分析 JVM 如何处理异常。

当异常发生时,异常实例的构建,是非常消耗性能的。这是由于在构造异常实例时,Java 虚拟机需要生成该异常的异常栈(stack trace)。

异常栈会逐一访问当前线程的 Java 栈帧,以及各种调试信息。包括栈帧所指向的方法名,方法所在的类名、文件名以及在代码中是第几行触发的异常。

这些异常输出到 Log 中,就是我们熟悉的崩溃日志(崩溃栈)。

3.2 崩溃实例分析异常处理

当把 Java 代码编译成字节码后,每个方法都会附带一个异常表,其中记录了当前方法的异常处理。

下面直接举个例子,写一个最简单的 try-catch 类。

使用 javap -c 进行反编译成字节码。

可以看到,末尾的 Exceptions Table 就是异常表。异常表中的每一条记录,都代表了一个异常处理器。

异常处理器中,标记了当前异常监控的起始、结束代码索引,和异常处理器的索引。其中 from 指针和 to 指针标识了该异常处理器所监控的代码范围,target 指针则指向异常处理器的起始位置,type 则为最后监听的异常。

例如上面的例子中,main 函数中存在异常表,Exception 的异常监听代码范围分别是 [0,8)(不包括 8),异常处理器的索引为 11。

继续分析异常处理流程,还需要区分是否命中异常。

1. 命中异常

当程序发生异常时,Java 虚拟机会从上到下遍历异常表中所有的记录。当发现触发异常的字节码的索引值,在某个异常表中某个异常监控的范围内。Java 虚拟机会判断所抛出的异常和该条异常监听的异常类型,是否匹配。如果能匹配上,Java 虚拟机会将控制流转向至该此异常处理器的 target 索引指向的字节码,这是命中异常的情况。

2. 未命中异常

而如果遍历完异常表中所有的异常处理器之后,仍未匹配到异常处理器,那么它会弹出当前方法对应的 Java 栈帧。回到它的调用者,在其中重复此过程。

最坏的情况下,Java 虚拟机需要遍历当前线程 Java 栈上所有方法的异常表。

3.3 编译后的 finally 代码块

我们写的代码,其实终归是给人读的,但是编译器干的事儿,都不是人事儿。它会把代码做一些特殊的处理,只是为了让自己更好解析和执行。

编译器对 finally 代码块,就是这样处理的。在当前版本的 Java 编译器中,会将 finally 代码块的内容,复制几份,分别放在所有可能执行的代码路径的出口中。

写个 Demo 验证一下,代码如下。

继续 javap -c 反编译成字节码。

这个例子中,为了更清晰的看到 finally 代码块,我在其中输出的一段 Log “run finally”。可以看到,编译结果中,包含了三份 finally 代码块。

其中,前两份分别位于 try 代码块和 catch 代码块的正常执行路径出口。最后一份则作为全局的异常处理器,监控 try 代码块以及 catch 代码块。它将捕获 try 代码块触发并且未命中 catch 代码块捕获的异常,以及在 catch 代码块触发的异常。
而 finally 的代码,如果出现异常,就不是当前方法所能处理的了,会直接向外抛出。

3.4 异常表中的 any 是什么?

从上图中可以看到,在异常表中,还存在两个 any 的信息。

第一个信息的 from 和 to 的范围就是 try 代码块,等于是对 catch 遗漏异常的一种补充,表示会处理所有种类的异常。

第二个信息的 from 和 to 的范围,仔细看能看到它其实是 catch 代码块,这也正好印证了我们上面的结论,catch 代码块其实也被异常处理器监控着。

只是如果命中了 any 之后,因为没有对应的异常处理器,会继续向上抛出去,交由该方法的调用方法处理。

四、总结

到这里我们就基本上讲清楚了 Java 异常处理的所有内容。

在日常开发当中,应该尽量避免使用异常处理的机制来处理业务逻辑,例如很多代码中,类型转换就使用 try-catch 来处理,其实是很不可取的。

异常捕获对应用程序的性能确实有影响,但也是分情况的。

一旦异常被抛出来,方法也就跟着 return 了,捕获异常栈时会导致性能变得很慢,尤其是调用栈比较深的时候。

但是从另一个角度来说,异常抛出时,基本上表明程序的错误。应用程序在大多数情况下,应该是在没有异常情况的环境下运行的。所以,异常情况应该是少数情况,只要我们不滥用异常处理,基本上不会影响正常处理的性能问题。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • 如何利用Retrofit+RxJava实现网络请求的异常处理

    通常情况下我们在与服务器进行通信的时候,不一定就不会出错,有时会出现其他的错误,这个时候我们只要和服务器约定好各种异常,在返回结果处进行判断,到底是执行错误,还是返回正常数据.具体的思路大致就是这样.这里我们定义ExceptionHandle,这里我参考网上的东西,然后稍微做了一些改动. ExceptionHandle public class ExceptionHandle { private static final int UNAUTHORIZED = 401; private stati

  • 通过实践了解如何处理Java异常

    大多数团队都使用了几种最佳实践.以下是帮助你入门或改进异常处理的9个最重要的内容. 1.在finally块中清理资源或使用Try-With-Resource语句 在try块中使用资源是很频繁的,比如InputStream,之后需要关闭它.这些情况中的一个常见错误是在try块结束时关闭资源. public void doNotCloseResourceInTry() { FileInputStream inputStream = null; try { File file = new File("

  • 详解Java异常处理最佳实践及陷阱防范

    前言 不管在我们的工作还是生活中,总会出现各种"错误",各种突发的"异常".无论我们做了多少准备,多少测试,这些异常总会在某个时间点出现,如果处理不当或是不及时,往往还会导致其他新的问题出现.所以我们要时刻注意这些陷阱以及需要一套"最佳实践"来建立起一个完善的异常处理机制. 异常分类 首先,这里我画了一个异常分类的结构图. 在JDK中,Throwable是所有异常的父类,其下分为"Error"和"Exception&

  • Java异常处理的12条军规总结

    异常的概念 异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的. 比如说,你的代码少了一个分号,那么运行出来结果是提示是错误java.lang.Error:如果你用System.out.println(11/0),那么你是因为你用0做了除数,会抛出java.lang.ArithmeticException的异常. 异常发生的原因有很多,通常包含以下几大类: •用户输入了非法数据. •要打开的文件不存在. •网络通信时连接中断,或者JVM内存溢出. 这些异常有的是因为用

  • Java异常处理与throws关键字用法分析

    本文实例讲述了Java异常处理与throws关键字用法.分享给大家供大家参考,具体如下: Java异常处理 认识异常: 1.异常是导致程序中断运行的一种指令流,如果不对异常进行正确处理,则可能导致程序的中断执行,造成不必要的损失. 2.异常范例 空指针异常 Exc e=null; System.out.println(e.i); 除0异常 int a=10; int b=0; System.out.println(a/b); 3.处理异常 异常格式: try{ 异常语句: } catch(Exc

  • Java异常处理中的各种细节汇总

    前言 今天我们来讨论一下,程序中的错误处理. 在任何一个稳定的程序中,都会有大量的代码在处理错误,有一些业务错误,我们可以通过主动检查判断来规避,可对于一些不能主动判断的错误,例如 RuntimeException,我们就需要使用 try-catch-finally 语句了. 有人说,错误处理并不难啊,try-catch-finally 一把梭,try 放功能代码,在 catch 中捕获异常.处理异常,finally 中写那些无论是否发生异常,都要执行的代码,这很简单啊. 处理错误的代码,确实并

  • Java编程中的一些常见问题汇总

    本文列举了我在周围同事的Java代码中看到的一些比较典型的错误.显然,静态代码分析(我们团队用的是qulice)不可能发现所有的问题,这也是为什么我要在这里列出它们的原因. 如果你觉得少了什么,请不吝赐教,我会很乐意把它们加上. 下面列出的所有这些错误基本都与面向对象编程有关,尤其是Java的OOP. 类名 读下这篇短文"什么是对象".类应该是真实生活中的一个抽象实体,而不是什么"validators","controller", "m

  • Java异常处理中的一些特殊情况举例

    只使用try和finally不使用catch的原因和场景 JDK并发工具包中,很多异常处理都使用了如下的结构,如AbstractExecutorService,即只有try和finally没有catch. class X { private final ReentrantLock lock = new ReentrantLock(); // ... public void m() { lock.lock(); // block until condition holds try { // ...

  • 浅析Java异常处理中断言的使用

    断言的概念 断言用于证明和测试程序的假设,比如"这里的值大于 5". 断言可以在运行时从代码中完全删除,所以对代码的运行速度没有影响. 断言的使用 断言有两种方法: 一种是 assert<<布尔表达式>> : 另一种是 assert<<布尔表达式>> :<<细节描述>>. 如果布尔表达式的值为false , 将抛出AssertionError 异常: 细节描述是AssertionError异常的描述文本使用 jav

  • Java异常处理中同时有finally和return语句的执行问题

    在Java中当try.finally语句中包含return语句时,执行情况到底是怎样的,finally中的代码是否执行,大家众说纷纭,有的说会执行,有的说不会执行,到底哪种说法正确,现在通过下面的例子加以说明:      第一种情况:try中包含return语句,finally中不包含 public class TestTry { static String s=""; public static void main(String args[]){ s = test1(); Syste

  • java开发中遇到的异常汇总详解

    异常 算术异常类:ArithmeticExecption 空指针异常类:NullPointerException 类型强制转换异常:ClassCastException 数组负下标异常:NegativeArrayException 数组下标越界异常:ArrayIndexOutOfBoundsException 违背安全原则异常:SecturityException 文件已结束异常:EOFException 文件未找到异常:FileNotFoundException 字符串转换为数字异常:Numb

  • 深入理解Java编程中异常处理的优劣

    Java编程中的异常处理是一个很常见的话题了,几乎任何一门介绍性的Java课程都会提到异常处理.不过,我认为很多人其实没有真正掌握正确处理异常情况的方法和策略,最多也就不过了解个大概,知道概念.我想对三种不同程度和质量的Java异常处理进行了讨论,所阐述的处理异常的方式按手法的高下分为:好,不好和恶劣三种.同时提供了一些解决这些问题的技巧.首先解释一些java异常处理中必须搞清楚的定义和机制.Java语言规范将自Error类或RuntimeException类衍生出来的任何违例都称作"不可检查&

  • Java异常处理方法汇总

    目录 一.异常是什么 二.异常类型 三.常见异常 四.常用异常方法 五.异常处理 六.关键字 throws/throw 七.自定义异常 前言: 我们在软件开发的过程中,任何语言的开发过程中都离不开异常处理.如果不处理异常,将会导致软件异常中断,崩溃,退出,严重影响用户的使用和体验.如果合理的应用异常处理那将会减少软件出现的错误,可以友好的提示用户,提升用户的体验. 一.异常是什么 异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的.如:用户输入非法数据,做除法运算的时

  • 简单了解Java编程中对异常处理的运用

    Java异常是一个描述在代码段中发生的异常(也就是出错)情况的对象.当异常情况发生,一个代表该异常的对象被创建并且在导致该错误的方法中被抛出(throw).该方法可以选择自己处理异常或传递该异常.两种情况下,该异常被捕获(caught)并处理.异常可能是由Java运行时系统产生,或者是由你的手工代码产生.被Java抛出的异常与违反语言规范或超出Java执行环境限制的基本错误有关.手工编码产生的异常基本上用于报告方法调用程序的出错状况. Java异常处理通过5个关键字控制:try.catch.th

  • Java编程中10个最佳的异常处理技巧

    在实践中,异常处理不单单是知道语法这么简单.编写健壮的代码是更像是一门艺术,在本文中,将讨论Java异常处理最佳实践.这些Java最佳实践遵循标准的JDK库,和几个处理错误和异常的开源代码.这还是一个提供给java程序员编写健壮代码的便利手册.Java 编程中异常处理的最佳实践 这里是我收集的10个Java编程中进行异常处理的10最佳实践.在Java编程中对于检查异常有褒有贬,强制处理异常是一门语言的功能.在本文中,我们将尽量减少使用检查型异常,同时学会在Java编程中使用检查型VS非检查型异常

随机推荐