Java异常体系简析

  最近在阅读《Java编程思想》的时候看到了书中对异常的描述,结合本身阅读源码经历,谈谈本身对异常的理解。首先记住下面两句话:html

  除非你能解决(或必需要处理)这个异常,不然不要捕获它,若是打算记录错误消息,那么别忘了把它再抛出去。java

  异常既表明一种错误,又能够表明一个消息。 程序员

1、为何会有异常

  这个问题其实不难理解,若是一切都按咱们设计好的进行,那么通常(不通常的状况是咱们设计的就是有缺陷的)是不会出现异常的,好比说一个除法操做:编程

public int div(int x,int y){
  return x/y;
}

  固然咱们设计的是除数不能为0,咱们也在方法名上添加了注释,输出不能为0,若是用户按照咱们的要求使用这个方法,固然不会有异常产生。但是不少时候,用户不必定阅读咱们的注释,或者说,输入的数据不是用户主动指定的,而是程序计算的中间结果,这个时候就会致使除数为0的状况出现。api

  如今异常状况出现了,程序应该怎么办呢,直接挂掉确定是不行的,可是程序确实不能本身处理这种突发状况,因此得想办法把这种状况告诉用户,让用户本身来决定,也就是说程序须要把遇到的这种异常状况包装一下发送出去,由用户来决定如何处理。数组

  异常表示着一种信息。熟悉EOFException的程序员通常都会了解,这个异常,表示信息的成分大于表示出现了异常,不熟悉的参照我以前的博客:http://www.cnblogs.com/yiwangzhibujian/p/7107084.html。当这种情形下的异常(包括用户自定义的大部分异常都属于此类)出现时,是不须要解决的。安全

2、Java异常的分类

  在继续讲解下面部分以前,仍是有必要了解下Java的异常分类的,经过Java API能够看到以下继承关系:服务器

  简单介绍一点:网络

  • Throwable是全部异常的父类
  • Error表示很严重的问题发生了,能够捕获可是不要捕获,由于捕获了也解决不了,这个不是由程序产出的,底层出现问题就让他它挂了吧。

3、异常的处理的理解

  再把一开始说的那句话重复一遍,除非你能解决这个异常,不然不要捕获它,若是打算记录错误消息,那么别忘了把它再抛出去。不过说真的,一个异常既然产生了,基本都是不能解决的,由于咱们的程序不能倒退到出现异常的代码,更不能在相同输入(不能改变输入,否则结果还有什么用),相同代码(不能动态改变原有代码)的状况下来来让它再也不出现异常,否则同一段代码,在同一个输入的状况下有两种不一样的结果,谁还敢用呢?架构

  除非咱们的程序须要依赖外部条件,而由外部条件致使的异常,咱们能够改变外部条件使之知足程序要求,不过这种状况基本均可以在程序执行前检测出来。

3.1 怎么才算解决异常

  举两个简单的例子方便理解下,第一个是关于Socket的,具体Socket的知识能够参考我以前的博客:http://www.cnblogs.com/yiwangzhibujian/p/7107785.html

3.1.1 重复尝试解决偶发问题

  在Socket创建链接之后,咱们能够经过Socket发送消息,高效的Socket利用方法是创建一个链接来持续使用,但是在这种状况下,有一个须要注意的问题,那就是我在每次发送消息的时候,要不要检测Socket是否还在链接中,个人在上面博客中介绍了,不须要。伪代码以下:

//有一个链接中的socket
Socket socket=...
//要发送的数据
String data="";
try{
    socket.write(data);
}catch (Excetption e){
    //打印日志,并重连Socket
    socket=new Socket(host,port);
    socket.write(data);
}

  能够看到,假如当前链接不可用(长时间不用被服务器主动断开,或者网络抖动致使的断开),那么咱们捕获这个异常,而后从新创建一个链接来发送。这是最基本的解决方法,再高级一点的就是设置一个重复次数,当出现异常的时候重复发送指定的次数。

  若是咱们仔细想一想,这个链接异常咱们没有真正的解决它,而是经过又新建了一个链接来处理的,咱们解决的不是这个异常,而是发送数据出现了问题,咱们解决的是发送数据没有成功这个问题。

  一样的,重复尝试解决的偶发问题,这个偶发也是外部的条件致使的偶发,而不是程序自身问题。

3.1.2 不想看到错误堆栈

  通常的Web三层架构,action,server,Dao,若是出现异常后,再不知足上面解决条件的状况下,若是都不捕获异常,那么用户将会看到一个500页面,附带着堆栈信息,这种事不友好的表现方式,这种状况下,咱们就须要在action层,用一个最大的try catch包住一个个方法,当出现异常的时候跳转到错误页面。

public String method(String param){
  try {
    //逻辑处理
  } catch (Exception e) {
    e.printStackTrace();
    //跳转到错误页面
  }
}

  实际上,咱们没有解决异常,咱们只是解决了异常致使的问题,异常自己还在那,真正的解决方法就是程序员解决bug而后从新上线。

  这种也算另类的解决,无可奈何不得不这么作,实际上异常是被吞掉了,吞掉前留下了一点点信息。

3.2 咱们应该怎么作

  首要条件仍是那句话,若是不能解决到出现异常的状况,那就不要捕获它,更不要吞掉他。

  固然有的时候你会打算记录异常的日志,可是最开始也说过,异常也表明一个消息,就像IndexOutOfBoundsException、IOException自己的名字已经能够代表异常的大部分信息,也就是说经过异常堆栈基本就能获得关于异常部分的信息,可是有些异常堆栈没有的是什么呢,那就是发生异常条件时的外部信息。

  固然在抛出异常的时候,虚拟机自己会尽量的打印出直接致使异常产生的输入,但是当咱们还想获取额外的环境信息的时候,咱们就须要捕获异常,而后打印出来。

  就像简单的除0异常,以及字符串转数字异常,自己异常堆栈就会提供基本的信息,可是若是咱们在一个用户交互的环境下,假如咱们想要知道是哪一个用户的输入致使了异常的产生,这个时候系统产生的异常堆栈信息就不能知足咱们的要求了,而这个信息在当前类的一个字段中,这时候咱们就要主动捕获而后打印出咱们想要的。

4、异常的处理

  如今就这各类实例来讲明异常怎么处理。

4.1 对认为必定不会出现的异常

  假如说你写了一个工具类,用于字符串和字节数组的UTF-8的转换,假如以下:

package yiwangzhibujian.util;

import java.io.UnsupportedEncodingException;

public class Utils {
  public static String utf8(byte[] bytes) throws UnsupportedEncodingException{
    return new String(bytes,"UTF-8");
  }
  
  public static byte[] utf8(String str) throws UnsupportedEncodingException{
    return str.getBytes("UTF-8");
  }
}

  那么用你工具类的人会头疼死,明明不会有错误的,要么抛出这个异常,要么捕获,实际上使用者根本不能解决这个异常。

  因此有的人可能这么作,他想既然这个异常必定不可能出现(本质上jvm必定能解析UTF-8的编码,若是不能解析jvm也就不须要继续运行了),那么我就吞了它,什么都不作:

package yiwangzhibujian.util;

import java.io.UnsupportedEncodingException;

public class Utils {
  public static String utf8(byte[] bytes){
    try {
      return new String(bytes,"UTF-8");
    } catch (UnsupportedEncodingException e) {
    }
    return null;
  }
  
  public static byte[] utf8(String str){
    try {
      return str.getBytes("UTF-8");
    } catch (UnsupportedEncodingException e) {
      e.printStackTrace();
    }
    return null;
  }
}

  这么作的人也有,不过这么作的人也分为两种,一种是catch内什么都不作,还有一种是catch内把异常信息打印出来,这两种作法我比较倾向于后面那种,由于要考虑如下条件。

  你认为jvm必定能解析UTF-8,我不反对,但是你能保证你没有拼错UTF-8吗,假如你写成UFO-8呢?

public static String utf8(byte[] bytes){
  try {
    return new String(bytes,"UFO-8");
  } catch (UnsupportedEncodingException e) {
  }
  return null;
}

  那么调用你的方法不只没有错误提示,还致使返回了错误的结果,并致使后续一系列问题的产生,最致命的是 ,咱们根部不知道错误的根源在哪。

  再举一个对象克隆的例子。

package yiwangzhibujian.util;

public class CloneTest {
  public static void main(String[] args) {
    Dog d1=new Dog("zhuzhuxia",26);
    Dog d2=d1.clone();//此处要么捕获要么抛出
    System.out.println(d1);
    System.out.println(d2);
  }
}
class Dog{
  public String name;
  public int age;
  public Dog(String name, int age) {
    super();
    this.name = name;
    this.age = age;
  }
  @Override
  protected Object clone() throws CloneNotSupportedException {
    return super.clone();
  }
}

  能够看到用户调用你的对象的克隆方法是否是很痛苦,你既然提供给我克隆方法,就必定要能用,若是不能用,那么拿回去重写吧,我不会给你擦屁股的。因此咱们就会这么作:

package yiwangzhibujian.util;

public class CloneTest {
  public static void main(String[] args) {
    Dog d1=new Dog("zhuzhuxia",26);
    Dog d2=d1.clone();//此处要么捕获要么抛出
    System.out.println(d1);
    System.out.println(d2);
  }
}
class Dog{
  public String name;
  public int age;
  public Dog(String name, int age) {
    super();
    this.name = name;
    this.age = age;
  }
  @Override
  protected Dog clone() {
    try {
      return (Dog) super.clone();
    } catch (Exception e) {
      e.printStackTrace();//不要省
    }
    return null;
  }
}

  若是你运行上面的代码的话,那么就会抛出异常,由于咱们的类没有实现Cloneable接口,缘由就是忘了写,这在测试运行的首次就会发现并纠正。

java.lang.CloneNotSupportedException: yiwangzhibujian.util.Dog
    at java.lang.Object.clone(Native Method)
    at yiwangzhibujian.util.Dog.clone(CloneTest.java:22)
    at yiwangzhibujian.util.CloneTest.main(CloneTest.java:6)
yiwangzhibujian.util.Dog@2a139a55
null

  因此,我能够假定这种状况下不会出异常,可是咱们不能保证咱们没有犯最基本的错误,因此错误堆栈仍是不能省的。

  咱们来看一下jdk8中的HashMap关于克隆的处理:

@SuppressWarnings("unchecked")
@Override
public Object clone() {
    HashMap<K,V> result;
    try {
        result = (HashMap<K,V>)super.clone();
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
    result.reinitialize();
    result.putMapEntries(this, false);
    return result;
}

  是否是不会抛必须捕获的异常,它还作了更高级的事,那就是我抛一个ERROR,通常咱们的程序都是捕获Exception,不会捕捉这个异常,这个异常会一直向上传播。

  那么打印异常堆栈和抛出ERROR哪一种更好呢,个人建议是抛出ERROR:

  • 能出现这种状况也就表明jvm出现了问题,或许其余基本功能也出现了问题,应该当即停掉重启并解决问题,否则数据都有可能出现错误。
  • 若是打印堆栈信息,那么下次调用仍是会出错,不如直接抛ERROR,若是上层没有作具体的应对jvm应该会中止。

4.2 对假定不该该出现的异常

  咱们再拿上面的字符串,字节数组例子来讲明,咱们对它进行了升级,下面是不完整代码:

public static String byteToStr(byte[] bytes, String charsetName) {
  return new String(bytes, charsetName);
}

  应该怎么作,抛异常?捕获异常打印日志?两种作法都很差:

  • 若是抛异常:那么使用你工具类的人依然很头疼,他必须在每次调用你方法的时候作处理,要么抛要么捕获,而他在想我明明传入一个UTF-8,非得给我抛异常,难用死了。
  • 若是捕获打印日志:这个更不可取,若是用户输错了编码类型,那么你将不能给出任何信息给调用者(打印日志只能过后找错),用户认为写的没错而你也给出了返回值,这也会致使一系列错误的产生。

  这种状况下应该怎么作呢,比较推荐的作法就是包装成运行时异常抛出:

public static String byteToStr(byte[] bytes, String charsetName) {
  try {
    return new String(bytes, charsetName);
  } catch (UnsupportedEncodingException e) {
    throw new RuntimeException(e);
  }
}

  这么作就解决了上面的两个问题。

4.3 对假定必定出异常的状况

  你的代码必定会出异常,那你仍是拿回去重写吧。除非你不想让别人调用你的方法,好比说不可变容器的操做类方法都将抛出异常。

5、异常的一些特殊状况

5.1 防止异常丢失

  在你不主动吞并异常的状况下,异常是不会丢失的,可是有一种特殊情形须要注意,那就是finally中有return的状况(代码参照Java编程思想):

public static void ExceptionSilencer(){
  try {
    throw new RuntimeException();
  } finally {
    return;
  }
}

  这种状况下,异常就会丢了,完彻底全消失不见了,因此要避免这么使用,避免finally中使用return。

5.2 线程中ThreadDeath异常

  这个异常是归于ERROR级别的,Java api也对此有相应介绍:

The ThreadDeath error, though a "normal" condition, is also a subclass of Error because most applications should not try to catch it. 

  就是说ThreadDeath自己是一个普通的异常,这个异常出现应该致使线程死亡,可是不把它归于Exception的缘由就是,jdk的开发者也料到Java程序员最喜欢try catch异常而后吞掉了,这样将会致使本该死亡的线程继续运行下去,这是不该该的。并且当这个异常出现时会终结线程,可是不会打印出任何异常堆栈信息

  这个异常比较少见,Thread的stop方法,会产生这个异常。

  若是你的线程常常莫名其妙的消失,而没有任何相关日志,你能够尝试捕获这个异常,可是记住,打印完相关日志再把它从新抛出去。

6、Java编程思想中关于总结的解读

  下面摘自Java编程思想的异常使用指南,特别好,必定要深刻理解一下:

  1. 在恰当的级别处理问题。(在知道该如何处理的状况下了捕获异常。)
  2. 解决问题而且从新调用产生异常的方法。
  3. 进行少量修补,而后绕过异常发生的地方继续执行。
  4. 用别的数据进行计算,以代替方法预计会返回的值。
  5. 把当前运行环境下能作的事尽可能作完,而后把相同的异常重抛到更高层
  6. 把当前运行环境下能作的事尽可能作完,而后把不一样的异常抛到更高层
  7. 终止程序
  8. 进行简化(若是你的异常模式使问题变得太复杂,那么用起来会很是痛苦)。
  9. 让类库和程序更安全。

  下面依次说下个人想法。

  • 第1条:上面章节已经介绍了,此处再也不说明
  • 第2条:上面也介绍过,就是外部条件致使的,能够重复执行可能正常的代码
  • 第3条:这种状况实质上也是吞并异常,好比说网络爬虫,当遇到死连接的时候,可能会抛出链接异常等,此时抛弃这个链接也是能够的,这个偏差能够接收
  • 第4条:有的程序员会这么设计,当出现用户输出错误数据致使异常的时候,就用一个默认的值来代替,我不喜欢这么作,我会直接抛异常让使用者去更改,若是非要这么作必定要打印好相关日志
  • 第5条:这种状况看需求,若是要求要么所有成功,要么都不作,那么就不适合这种状况
  • 第6条:同上,可是我不太理解这个
  • 第7条:这个就不要了吧,出现一个异常程序就挂了,那也太脆了,不过当程序在正常启动过程当中,若是出现异常就直接挂掉仍是合理的,让用户修改外部条件保障启动没有问题,好比说用户指定的配置文件不存在(或许他写错了路径),那么不要使用默认配置,程序直接挂掉就能够了,否则会给用户一种按照他的配置成功启动的错觉。
  • 第8条:这个和上面说到的用运行时异常来包裹捕获异常一个性质。
  • 第9条:这个是终极目标,考虑全部的状况,把异常消灭在萌芽中,过于理想了。通常越安全越健壮的程序考虑的异常条件就越多。通常都会在使用前作各类判断,条件是否知足,输入是否正确等。

 

  以上就是我对异常的理解,但愿能够帮助到有须要的人,若是你能认真看完我相信你会有收获的,若是错误请指出,禁止转载。

相关文章
相关标签/搜索