原文:https://www.jianshu.com/p/15872cba211d

Java异常控制机制又被称为“违例控制机制”。
捕获程序错误最理想的时机是在编译阶段,这样可以彻底避免错误的代码运行。但并非所有的错误都能在编译期间侦测到,有些问题必须在运行期间解决。

错误在运行期间发生时,我们可能不知道具体应该怎样解决,但我们清楚此时不能不管不顾地继续执行下去。此时应该做的事情是:

  • 暂停程序的运行
  • 指出何时、何地发生了什么样的错误
  • 可能的话应处理此错误并恢复程序的执行

Java异常控制机制的作用流程:

  1. 异常产生
    首先程序引擎需要能够获知异常的产生。Java中预置了一系列基本的异常条件,如数组下标越界、空指针、被零除等等,这些异常是由JVM自动产生的(也被称为运行时异常,见后);另一部分异常则是由Java代码(可能是JDK的代码或开发人员自己编写的代码)产生的(也被称为checked异常,见后)。
    异常产生即是异常对象的实例化,该对象的类型通常就说明了异常条件的类型,实例化的异常对象中还会包含对异常条件的补充说明(message),以及异常发生时的线程调用栈信息(stacktrace)。
    在这个环节中,JAVA完成了对错误的描述,包括错误发生的时间、错误的类型(即异常对象的Class)、对错误的描述(message)和错误发生的位置(stacktrace)。

  2. 异常抛出
    异常抛出是JAVA程序流中的一种特殊流程,当异常产生后,JVM会停止继续执行后面的代码,并将异常对象抛出。抛出的异常对象会进入调用栈的上一层,如果异常对象没有被捕获,它会沿着调用栈的顺序逐层向上抛出,直至调用栈为空,此时该线程的运行也就彻底终止了。
    异常的抛出解决了当前作用域可能不具备处理异常所需的信息的问题,将异常对象在调用栈中逐级向上传递,直至有能力处理异常的作用域将其捕获。

  3. 异常捕获
    在异常对象逐级向上抛出的过程中,如果调用栈中某一层有捕获该类型异常的逻辑,该异常对象便会被捕捉,异常被捕获后JVM会终止抛出异常对象的过程。

  4. 异常处理
    当异常对象被捕获后,JVM会执行捕获后的处理逻辑(处理逻辑是由程序员编写的)。当处理逻辑执行完成后,JVM会继续执行捕获了异常的作用域中接下来的代码(除非异常处理逻辑中将该异常继续抛出,或异常处理逻辑中产生了新的异常)。


try-catch-finally

前文所述的异常控制流程,在JAVA程序中以try-catch-finally结构实现:

 
图片.png

  1. try块也被称为“警戒区”,try块包裹的代码在执行过程如果产生异常,或其调用栈的下层中产生了异常并被抛至本层,则会被与此try块关联的catch命令尝试捕获。若异常产生于警戒区之外,则会直接向上层抛出。
  2. catch命令后的括号内指定希望捕捉的异常对象类型(可以指定多个),如果产生或被抛至此层的异常对象是catch指定的异常类型(或其子类),则异常对象会被捕捉。上例中,所有Exception对象及其子类的对象在此处均会被捕获。
  3. 被捕获后,JVM会执行catch块中的代码,catch块中的代码能够访问被捕捉到的异常对象(即上例中的Exception e)。
    catch块中的代码仍然有可能产生异常,所以也可以在catch块中插入try-catch-finally。
  4. finally块为可选块,如果有,则无论是否有异常被抛出,JVM都会在try-catch块执行完成后执行finally块中的代码。

Exception与Error

前文所述的Java异常控制机制实际上并不仅对“异常”起作用。除了我们所说的异常(Exception)能够被产生、抛出和捕捉之外,还有另一种类型“错误(Error)”。
Java中,Throwable是所有可以被抛出并捕获的类的父类。Throwable有两大子类,分别是Exception和Error。
Java官方并没有给出Error和Exception的严格定义,而是将Error描述为“应用程序不应尝试捕捉处理的严重问题”,Exception则是“应用程序应该尝试捕捉处理的问题”。

我们从几个例子看一下:

  • NoClassDefFoundError:JVM的ClassLoader在尝试加载某个类,但该类在Classpath中并不存在时会产生的错误。例如a.jar依赖b.jar中的某个类,如果我们使用编译完成的a.jar时并没有引入b.jar,编译器并不会发现问题(因为a.jar已经完成了编译,需要编译的代码中只使用了a.jar中的api,并没有直接使用b.jar),但在运行时JVM找不到b.jar中被a所依赖的类,便会发生错误。
  • UnsupportedClassVersionError:当JVM尝试加载一个class但发现该class的版本并不被支持时产生的错误。例如我们使用JDK1.8开发并编译一个类,但在JDK1.7的环境中运行时,便会发生此错误
  • OutOfMemoryError:当JVM内存不足,无法为一个对象分配内存时发生的错误,例如堆区内存溢出、Perm区内存溢出等。
  • StackOverFlowError:当程序的递归调用过深,导致线程调用栈溢出时发生的错误。
  • NoSuchFieldError/NoSuchMethodError:当JVM试图访问某个成员属性或某个方法时,发现目标不存在。一般都是由于class信息在运行时被改变导致的,多见于使用反射时。

通过上面的例子能够看出,Error一般都与程序本身的直接关系不大,更多是由于环境导致的问题。而且Error发生后通常程序都没有再继续执行下去的可能性,所以Java官方将其定义为“应用程序不应尝试捕捉处理的严重问题”。


Exception的分类

Java将Exception分为两类,checked异常和unchecked异常,也被称为非运行时异常和运行时(runtime)异常。
RuntimeException是Exception的一个子类,RuntimeException的子类都属于unchecked异常(也就是运行时异常),其他所有的Exception都是checked异常(也就是非运行时异常)。

这两种异常的区别从字面上即可理解,checked代表“必须被check”,而unchecked代表“无须被check”:
Java要求checked异常必须被在代码编写阶段就调用者了解,unchecked异常则不用。如果一个方法中有可能产生checked异常,则Java编译器会要求该方法定义中必须加入throws定义,明确说明该方法可能会抛出某类checked异常。如下图:

 
图片.png

foo方法可能产生IOException(这是一种checked异常),所以bar方法在调用foo时,编译器会提示错误。此时可以在bar方法的定义行中加入throws:

public void bar() throws IOException

也可以在bar方法内将IOException捕获处理:

 
图片.png

另一个理解checked异常与unchecked异常区别的角度是:所有由JVM自动生成的异常都是unchecked异常,反之,由java程序主动生成的异常是checked异常。
例如:

 
图片.png

上图中f.createNewFile()方法可能会产生checked异常IOException,我们看看File类的源码:

 
这里写图片描述

可以看到红框处,IOException异常是在代码中被主动抛出的,凡是这样在代码中主动抛出的异常,都是checked异常。

相应地,unchecked异常是JVM在运行时自动产生的,例如下图的方法,只要传入的参数b等于0,就会在运行时自动产生ArithmeticException:

 
图片.png

 
图片.png

代码中永远不需要这样写:

 
图片.png


异常处理的原则

异常处理的原则主要有三个:

  • 具体明确
  • 提早抛出
  • 延迟捕获

具体明确:
指抛出的异常应能通过异常类名和message准确说明异常的类型和产生异常的原因。

我们通过例子来看:

代码1:

 
图片.png

代码2:

 
图片.png

这两段代码的处理逻辑是类似的,均是在入参input1或input2为null或空串时抛出异常,但只有第二段符合“具体明确”的标准:
首先,第二段代码通过异常类型【IllegalArgumentException】明确了异常是由于传入了不合法的参数导致的;其次,在message中说明了具体是哪个参数不合法,为什么不合法。这样不仅能够在查阅日志时快速知晓异常产生的原因,也让上层的程序能够针对IllegalArgumentException这一特定类型的异常进行有针对性的捕捉和处理。
相比之下,第一段代码中抛出的异常就不够具体明确,异常类型Exception不具有说明性质,异常message也不够明确,上层程序难以处理,阅读日志时也难以快速定位。

提早抛出:

指应尽可能早的发现并抛出异常,便于精确定位问题。

同样通过例子来看:

代码1:

 
图片.png

代码2:

 
图片.png

在传入的filename为null时,这两段代码都会抛出异常,第一段代码抛出的异常是:

 
图片.png

第二段代码抛出的异常是:

 
图片.png

第一段代码抛出的异常是在标准Java类库【InputFileStream】中抛出的,这首先就提升了问题定位的难度,不过幸好stacktrace中也打印出了前面的调用链,我们可以在标准类库的调用者身上查找问题(可以定位到Test.java的第38行)。
同时NullPointerException是Java中信息量最少的(却也是最常遭遇且让人崩溃的)异常。它压根不提我们最关心的事情:到底哪里是null。在稍微复杂一些的场景中(如一行代码中有多处都可能导致NullPointerException)会让人更加崩溃。

而相比之下第二段代码对filename提前进行了校验,并以IllegalArgumentException的形式抛出,这样在第一段代码中遇到的两个问题都可以得到解决,这便是提早抛出的好处。

延迟捕获:

指异常的捕获和处理应尽可能延迟,让掌握更多信息的作用域来处理异常。

代码1:

 
图片.png

上面的代码中,readSomeFile方法将new FileInputStream处有可能产生的FileNotFoundException捕获,并将异常信息记录到了日志中。
这么做看起来似乎没什么问题,但readSomeFile这个方法有可能是一个通用的底层方法,会在各种业务场景下被调用,不同的业务场景下,发生FileNotFoundException时的处理策略可能不一样(例如某些场景要求记录异常并告警,某些场景会使用其他文件名重试),但readSomeFile方法并不知道自己所处的业务场景是什么样的,这一信息只有更上层的作用域才了解,所以在方法内部直接捕获并处理异常的做法就显得有问题了,程序将无法通过甄别业务场景来执行不同的异常处理逻辑。

代码2:

 
图片.png

第二段代码看起来反而更加简单了,没有对FileNotFoundException加以处理,而是直接在方法定义中将其抛出。然而在上面所述的场景下,这种处理方式反而是正确的。将异常抛出交由掌握了足够多信息的上层调用者捕获,这样就可以根据异常产生所处的具体业务流程来进行不同的处理。

例如我们可以在一个业务逻辑中这样处理:

 
图片.png

同时在另一个业务逻辑中这样处理:

 
图片.png


其他重要原则

  1. 不要让异常逃掉
    当一个异常在整个调用栈中的任意一层都没有被捕获,这个异常就“逃掉”了。这对于任何程序来说都是一个灾难性的事件。
    对于B/S系统,从请求处理线程中逃掉的异常很可能会被B/S框架(如Struts/SpringMVC等)捕捉到。如果没有正确配置,这些逃掉的异常很可能就被框架“吃掉”了,即框架捕获了从业务代码层抛出的异常,且没有记录或没有完整记录异常信息。这样的异常来无影去无踪,完全无迹可寻,堪称程序员的大敌。
    某些情况下,异常会被抛到中间件或容器(Tomcat/Jboss/Weblogic/Websphere等)层(可能是没有使用B/S框架或B/S框架没有“吃掉”异常)。被中间件或容器捕获到的异常,一般情况下会被记录在中间件或容器自己的日志中(也有可能不会记),但问题在于,这种情况下,用户会看到中间件或容器提供的错误页,这些错误页基本没有用户友好型可言,而且有可能会把异常堆栈的信息直接显示在页面上,在开放性的系统中,暴露堆栈信息极有可能引发严重的安全问题。
    而在后台进程中,如果异常逃掉了,将会导致线程的退出。如果没有守护线程及时补充异常退出的线程,那么将有可能发生整个进程因为异常而中止的灾难性后果。
    所以说,在编程时应绝对避免异常“逃逸”的情况,对于B/S系统来说,我们可以在每个Action中都加入try-catch块,捕获所有Exception,也可以利用B/S框架的特性来实现从Action层抛出的异常的统一处理(如Struts2和SpringMVC都有的拦截器机制)。对于后台进程来说,可以利用try-catch块避免异常导致线程中止,也可以通过添加守护线程来及时补充因异常而退出的线程,同时还应使用Thread.setDefaultUncaughtExceptionHandler来确保未捕获异常的正确记录。

  2. 正确记录异常信息
    即在异常的stacktrace信息完整、未缺失的基础上,确保异常的stacktrace被正确记录到日志中

错误的做法:

 
图片.png

上面的5种处理全都是错误的,前两种将异常信息输出到了控制台而不是日志文件中。后三种错误的使用了log4j的error方法,均没有正确记录异常的stacktrace

正确的方法:

 
图片.png

注意应使用正确的error方法,传入两个参数,参数1是对异常的附加描述,参数2是未被篡改过的异常对象
在某些情况下,可能需要在处理异常后继续抛出,让上层捕获后继续处理,在这种情况下,需要注意抛出的异常对象未被篡改。

错误的:

 
图片.png

如果像上图这样写的话,下层的异常stacktrace会全部被吃掉。

正确的写法:

 
图片.png

Java异常控制机制和异常处理原则【转】的更多相关文章

  1. Java异常的分类

    1. 异常机制       异常机制是指当程序出现错误后,程序如何处理.具体来说,异常机制提供了程序退出的安全通道.当出现错误后,程序执行的流程发生改变,程序的控制权转移到异常处理器.       传 ...

  2. java异常总结(转载)

    转至 Java常见异常(Runtime Exception )小结 http://www.apkbus.com/android-58405-1-1.html 本文重在Java中异常机制的一些概念.写本 ...

  3. Java异常机制及异常处理建议

    1.Java异常机制 异常指不期而至的各种状况,如:文件找不到.网络连接失败.非法参数等.异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程.Java通过API中Throwable类的众多子类 ...

  4. Atititjs javascript异常处理机制与java异常的转换.js exception process

    Atititjs javascript异常处理机制与java异常的转换.js exception process 1. javascript异常处理机制 Throw str Not throw err ...

  5. Atitit.js javascript异常处理机制与java异常的转换 多重catc hDWR 环境 .js exception process Vob7

    Atitit.js javascript异常处理机制与java异常的转换 多重catc hDWR 环境 .js exception processVob7 1. 1. javascript异常处理机制 ...

  6. Atitit.js javascript异常处理机制与java异常的转换.js exception process Voae

    Atitit.js javascript异常处理机制与java异常的转换.js exception processVoae 1. 1. javascript异常处理机制 1 2. 2. Web前后台异 ...

  7. Atititjs javascript异常处理机制java异常转换.js exception process

    Atititjs javascript异常处理机制java异常的转换.js exception process 1. javascript异常处理机制 Throw str Not throw erro ...

  8. JAVA异常处理原则和log4j输出详细异常分析

    1.多用try,catch;不要一个try,catch包含所有内容 好处:不同模块抓取不同异常,某一模块异常挂了,不影响其他模块的程序的进行 2.多写几个catche:尽量不要使用Exception这 ...

  9. 全面理解Java异常的运行机制

    1. 引子 try…catch…finally恐怕是大家再熟悉不过的语句了,而且感觉用起来也是很简单,逻辑上似乎也是很容易理解.不过,我亲自体验的“教训”告诉我,这个东西可不是想象中的那么简单.听话. ...

随机推荐

  1. Grunt实战 --- 通过nodejs和Grunt实现项目在线构建

    本文主要说明,实现在线自动构建项目的实现方法.

  2. Pentaho BIServer Community Edtion 6.1 使用教程 第一篇 软件安装

    一.简介: Pentaho BI Server 分为企业版和社区版两个版本.其中 社区版 CE(community edtion) 为免费版本. 二.下载CE版(CentOS): 后台下载命令: no ...

  3. 如何修改硬盘挂载的名字LABEL

    ➜ ~ df -h Filesystem Size Used Avail Use% Mounted on/dev/sda2 114G 97G 12G 90% /media/brian/4ef34b75 ...

  4. POJ - 3278 Catch That Cow 【BFS】

    题目链接 http://poj.org/problem?id=3278 题意 给出两个数字 N K 每次 都可以用三个操作 + 1 - 1 * 2 求 最少的操作次数 使得 N 变成 K 思路 BFS ...

  5. Form表单插件

    jQuery Form是一个优秀的表单插件,它可以非常容易地,无侵入地升级HTML表单以支持Ajax jQuery Form表单插件的下载地址为 http://jquery.malsup.com/fo ...

  6. 简单学习github代码托管

    之前尝试使用阿里云code做代码托管 egret+git+阿里云code搭建团队开发 ,现在来学习一下使用 Github做代码托管服务. 总体上看使用的步骤差不多,都需要使用GIT客户端来进行相关的操 ...

  7. KafkaSpout 重复消费问题解决

    使用https://github.com/nathanmarz/storm-contrib来对接Kafka0.7.2时, 发现kafkaSpout总会进行数据重读, 配置都无问题, 也没报错 进行de ...

  8. Spark- Linux下安装Spark

    Spark- Linux下安装Spark 前期部署 1.JDK安装,配置PATH 可以参考之前配置hadoop等配置 2.下载spark-1.6.1-bin-hadoop2.6.tgz,并上传到服务器 ...

  9. BZOJ 1601 [Usaco2008 Oct]灌水:最小生成树

    题目链接:http://www.lydsy.com/JudgeOnline/problem.php?id=1601 题意: Farmer John已经决定把水灌到他的n(1<=n<=300 ...

  10. .net中后台c#数组与前台js数组交互

    第一步:定义cs数组  cs文件里后台程序中要有数组,这个数组要定义成公共的数组.  public string[] lat = null;  public string[] lng = null; ...