java 异常详解

     先看一道题,输出啥?

package Test; 
   
public class TestException { 
    public TestException() { 
    } 
   
    boolean testEx() throws Exception { 
        boolean ret = true; 
        try { 
            ret = testEx1(); 
        } catch (Exception e) { 
            System.out.println("testEx, catch exception"); 
            ret = false; 
            throw e; 
        } finally { 
            System.out.println("testEx, finally; return value=" + ret); 
            return ret; 
        } 
    } 
   
    boolean testEx1() throws Exception { 
        boolean ret = true; 
        try { 
            ret = testEx2(); 
            if (!ret) { 
                return false; 
            } 
            System.out.println("testEx1, at the end of try"); 
            return ret; 
        } catch (Exception e) { 
            System.out.println("testEx1, catch exception"); 
            ret = false; 
            throw e; 
        } finally { 
            System.out.println("testEx1, finally; return value=" + ret); 
            return ret; 
        } 
    } 
   
    boolean testEx2() throws Exception { 
        boolean ret = true; 
        try { 
            int b = 12; 
            int c; 
            for (int i = 2; i >= -2; i--) { 
                c = b / i; 
                System.out.println("i=" + i); 
            } 
            return true; 
        } catch (Exception e) { 
            System.out.println("testEx2, catch exception"); 
            ret = false; 
            throw e; 
        } finally { 
            System.out.println("testEx2, finally; return value=" + ret); 
            return ret; 
        } 
    } 
   
    public static void main(String[] args) { 
        TestException testException1 = new TestException(); 
        try { 
            testException1.testEx(); 
        } catch (Exception e) { 
            e.printStackTrace(); 
        } 
    } 
}

如果你一眼就能看出答案,那么这篇文章你就不用浪费时间看啦。输出结果为:

   i=2
  i=1
  testEx2, catch exception
  testEx2, finally; return value=false
  testEx1, finally; return value=false
  testEx, finally; return value=false

为什么要使用异常?

      在没有异常机制的时候比如C语言,我们是这样处理的:通过函数的返回值来判断是否发生了异常(这个返回值通常是已经约定好了的),调用该函数的程序负责检查并且分析返回值。虽然可以解决异常问题,但是这样做存在几个缺陷:

  1. 容易混淆。如果约定返回值为-11111时表示出现异常,那么当程序最后的计算结果真的为-1111呢?
  2. 代码可读性差。将异常处理代码和程序代码混淆在一起将会降低代码的可读性。
  3. 由调用函数来分析异常,这要求程序员对库函数有很深的了解
          在java中提供了异常处理机制能够有效将正常业务代码和异常代码分离开来。

使用异常机制它能够降低错误处理代码的复杂度,如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方去处理它,而如果使用异常,那就不必在方法调用处进行检查,因为异常机制将保证能够捕获这个错误,并且,只需在一个地方处理错误,即所谓的异常处理程序中。这种方式不仅节约代码,而且把“概述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离。总之,与以前的错误处理方法相比,异常机制使代码的阅读、编写和调试工作更加井井有条。

异常分类

      在java中,所有的异常都有一个共同的祖先,Throwable,它有两个重要的子类,Exception(异常)和Error(错误)。Error是指程序无法处理的错误,通常是指jvm运行时出现的问题,比如当jvm不再具有继续执行操作所需的内存资源时,将抛出OutOfMemory,当error发生时,jvm一般选择终止线程。

      Exception是指由于程序逻辑本身的问题,程序本身可以处理的异常。RuntimeException是其一个重要子类。
      java异常通常又分为可查的异常和不可查的异常,可查的异常在一定程度上是可以预计的,在编译阶段就需要处理,否则编译通不过。在Exception中,除了RuntimeException及其子类外,其他Excetpion及其子类都属于可查异常。不可检查异常包括运行时异常(RuntimeException)和错误(Error)。

try catch finally 字节码分析

static int inc(){
    int x;
    try {
        x = 1;
        return x;
    } catch (Exception e){
        x = 2;
        return x;
    } finally {
        x = 3;
    }
}
static int inc();
descriptor: ()I
flags: ACC_STATIC
Code:
  stack=1, locals=4, args_size=0
     0: iconst_1  //try 块中的 x = 1;
     1: istore_0  //保存栈顶元素到局部变量表中索引为 0 的 slot 中
     2: iload_0   //加载局部变量表中索引为 0 的值到栈中
     3: istore_1  //保存栈顶元素到局部变量表中索引为 1 的 slot 中
     4: iconst_3  //finally 块中的 x = 3;
     5: istore_0  //保存栈顶元素到局部变量表中索引为 0 的 slot 中,x 的值存在这里。
     6: iload_1  //加载局部变量表中索引为 1 的值到栈中
     7: ireturn  //返回栈顶元素,即 x = 1;正常情况下函数运行到这里就结束了,如果出现异常根据异常表跳转到指定的位置
     8: astore_1 //给 catch 块中定义的 Exception e 赋值,存储在 slot1 中。
     9: iconst_2 //catch 块中的 x = 2;
    10: istore_0
    11: iload_0
    12: istore_2
    13: iconst_3 //finally 块中的 x = 3;
    14: istore_0
    15: iload_2
    16: ireturn //此时返回的是 slot2 中的值,即 x = 2
    17: astore_3 //如果出现不属于 java.lang.Exception 及其子类的异常,才会根据异常表中的规则跳转到这里。
    18: iconst_3 //finally 块中的 x = 3;
    19: istore_0
    20: aload_3 //将异常加载到栈顶,
    21: athrow //抛出栈顶的异常
  Exception table:
     from    to  target type
         0     4     8   Class java/lang/Exception
         0     4    17   any
         8    13    17   any

      可以看到,Java 的异常处理是通过异常表的方式来决定代码执行的路径。而finally的实现是通过在每个路径的最后加入finally块中的字节码实现的。 这里如果在finally直接返回,最终返回的是finally的 而不是catch的返回

try catch 会不会影响性能

try {
    for(int i = 0; i < max; i++) {
        String myString = ...;
        float myNum = Float.parseFloat(myString);
        myFloats[i] = myNum;
    }
} catch (NumberFormatException ex) {
    return null;
}
for(int i = 0; i < max; i++) {
    String myString = ...;
    try {
        float myNum = Float.parseFloat(myString);
    } catch (NumberFormatException ex) {
        return null;
    }
    myFloats[i] = myNum;
}

      上述性能区别在哪?想当然就觉得try catch重复执行了这么多次肯定比只执行了一次跑得肯定慢,空间消耗肯定更大,实际情况是不是就是这样呢?
       从上小节字节码可知,每个类会跟随一张异常表(exception table),每一个try catch都会在这个表里添加行记录,每一个记录都有4个信息(try catch的开始地址,结束地址,异常的处理起始位,异常类名称)。
      当代码在运行时抛出了异常时,首先拿着抛出位置到异常表中查找是否可以被catch(例如看位置是不是处于任何一栏中的开始和结束位置之间),如果可以则跑到异常处理的起始位置开始处理,如果没有找到则原地return,并且copy异常的引用给父调用方,接着看父调用的异常表。。。以此类推。
      也就是说,异常如果没发生,也就不会去查表,也就是说你写不写try catch 也就是有没有这个异常表的问题,如果没有发生异常,写try catch对性能是木有消耗的,所以不会让程序跑得更慢。try 的范围大小其实就是异常表中两个值(开始地址和结束地址)的差异而已,也是不会影响性能的。

异常处理机制

      当一个方法出现错误引发异常时,运行时系统创建异常对象,该对象包括了异常类型和堆栈信息等,接着运行时系统负责寻找合适的异常处理器(exception handler),潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合,当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,就是合适的异常处理器,运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适 的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。

异常的使用建议

OutputStreamWriter out = null;
        java.sql.Connection conn = null;
        try {            //   ---------1
            Statement stat = conn.createStatement();
            ResultSet rs = stat.executeQuery("select *from user");
            while (rs.next()){
                out.println("name:" + rs.getString("name") + "sex:"
                        + rs.getString("sex"));
            }
            conn.close();         //------2
            out.close();
        } 
        catch (Exception ex){    //------3
            ex.printStackTrace();    //------4
        }
  • 对于这个try…catch块,他真正目的是捕获SQL的异常,但是这个try块包含了太多的信息了。这是我们为了偷懒而养成的代码坏习惯。有些人喜欢将一大块的代码全部包含在一个try块里面,因为这样省事,反正有异常抛出它就会捕获,而不愿意花时间来分析这个大代码块有哪几块会产生异常,产生什么类型的异常,反正就是一篓子全部搞定。这就想我们出去旅游将所有的东西全部装进一个箱子里面,而不是分类来装,虽然装进去容易,但找出来难!!!所有对于一个异常块,我们应该仔细分清楚每块的抛出异常,因为一个大代码块有太多的地方会出现异常了。所以应该尽可能减小try块范围!!!
  • 在这里你发现了什么?异常改变了运行流程!!如果该程序发生了异常那么conn.close(); out.close();是不可能执行得到的,这样势必会导致资源不能释放掉。所以如果程序用到了文件、Socket、JDBC连接之类的资源,即使遇到了异常,我们也要确保能够正确释放占用的资源。这里finally就有用武之地了:不管是否出现了异常,finally总会执行的,所以应当充分利用finally释放资源。
  • 使用这样代码的人都有这样一个心理,一个catch解决所有异常,这样是可以,但是不推荐!为什么!首先我们需要明白catch块所表示是它预期会出现何种异常,并且需要做何种处理。catch语句应当尽量指定具体的异常类型,而不应该指定涵盖范围太广的Exception类。
  • 捕获异常不做处理,就是我们所谓的丢弃异常。我们都知道异常意味着程序出现了不可预期的问题,程序它希望我们能够做出处理来拯救它,但是你呢?一句ex.printStackTrace()搞定,这是多么的不负责任对程序的异常情况不理不顾。既然捕获了异常,就要对它进行适当的处理,比如:
    1. 对所发生的的异常进行一番处理,如修正错误、提醒.
    2. 重新抛出异常。既然你认为你没有能力处理该异常,那么你就尽情向上抛吧!!!
    3. 封装异常。对异常信息进行分类,然后进行封装处理。
    4. 打印出明确的异常信息。在出现异常后,我们最好能够提供一些文字信息,例如当前正在执行的类、方法和其他状态信息,包括以一种更适合阅读的方式整理和组织printStackTrace提供的信息。

异常处理最佳实践

  1. 不要 在catch语句块中压制异常
public class ExceptionExample {
    public FileInputStream testMethod1(){
        File file = new File("test.txt");
        FileInputStream fileInputStream = null;
        try{
            fileInputStream = new FileInputStream(file);
            fileInputStream.read();
        }catch (IOException e){         
            return null;
        }
        return fileInputStream;
    }
    public static void main(String[] args){
        ExceptionExample instance1 = new ExceptionExample();
        instance1.testMethod1();
    }
}

在异常处理时进行异常压制是非常不好的编程习惯,上面的例子中,无论抛出什么异常都会被忽略,以至没有留下任何问题线索.如果在这一层次不知道如何处理异常,最好将异常重新抛出,由上层决定如何处理异常。

  1. 不要在方法定义分句中定义具体的异常
    按照public FileInputStream testMethod1() throws Exception{这种写法,表示该方法会抛出所有受检查异常,这不是一个良好的编程习惯。在这种情况下,我们最好抛出足够具体的异常,以便调用者进行合适的捕获和处理,例如public FileInputStream testMethod1() throws IOException{。

  2. 捕获具体的异常
    在调用其他模块时,最好捕获由该模块抛出的具体的异常。如果某个被调用模块抛出了多个异常,那么只捕获这些异常的父类是不好的编程习惯。
    例如,如果一个模块抛出FileNotFoundException和IOException,那么调用这个模块的代码最好写两个catch语句块分别捕获这两个异常,而不要只写一个捕获Exception的catch语句块。
    正确的写法如下:

try {
   //some statements
catch(FileNotFoundException e){
//handle here
}
catch(IOException e){
//handle here
} 

最好不要这么写:

try {
   //some statements
catch(Exception e){
//handle here
}
  • 永远不要捕获Throwable类
    这是一个更严重的麻烦。 因为java错误也是Throwable的子类。 错误是JVM本身无法处理的不可逆转的条件。 对于某些JVM的实现,JVM可能实际上甚至不会在错误上调用catch子句。

  • 始终正确包装自定义异常中的异常,以便堆栈跟踪不会丢失

catch (NoSuchMethodException e) {
   throw new MyServiceException("Some information: " + e.getMessage());  //错误方式
}

这破坏了原始异常的堆栈跟踪,并且始终是错误的。 正确的做法是:

catch (NoSuchMethodException e) {
   throw new MyServiceException("Some information: " , e);  //正确方式
}
  • 要么记录异常要么抛出异常,但不要一起执行
catch (NoSuchMethodException e) {  
//错误方式 
   LOGGER.error("Some information", e);
   throw e;
}

正如在上面的示例代码中,记录和抛出异常会在日志文件中产生多条日志消息.

  • finally块中永远不要抛出任何异常
try {
  someMethod();  //Throws exceptionOne
} finally {
  cleanUp();    //如果finally还抛出异常,那么exceptionOne将永远丢失
}

只要cleanUp()永远不会抛出任何异常,上面的代码没有问题。 但是如果someMethod()抛出一个异常,并且在finally块中,cleanUp()也抛出另一个异常,那么程序只会把第二个异常抛出来,原来的第一个异常(正确的原因)将永远丢失。 如果你在finally块中调用的代码可能会引发异常,请确保你要么处理它,要么将其记录下来。 永远不要让它从finally块中抛出来。

  • 不要使用printStackTrace()语句或类似的方法,
    完成代码后,切勿忽略printStackTrace()。 你的同事可能会最终得到这些堆栈,并且对于如何处理它完全没有任何知识,因为它不会附加任何上下文信息。

  • 记住“早throw晚catch”原则
    这可能是关于异常处理最著名的原则。 它基本上说,你应该尽快抛出(throw)异常,并尽可能晚地捕获(catch)它。 你应该等到你有足够的信息来妥善处理它。

  • 终止掉被中断线程

while (true) {
  try {
    Thread.sleep(100000);
  } catch (InterruptedException e) {} //别这样做
  doSomethingCool();
}

InterruptedException是你的代码的一个提示,它应该停止它正在做的事情。 线程中断的一些常见用例是active事务超时或线程池关闭。 你的代码应该尽最大努力完成它正在做的事情,并且完成当前的执行线程,而不是忽略InterruptedException。

异常机制是现代主流语言的标配,但是异常处理问题虽然已经被讨论很多,也有很多经典书籍的论述,却一直都充满争议。很多人都觉得异常处理很难拿捏,同时也难以理解一些语言或库的异常处理设计.
Exception和Error的区别

谈异常处理的第一个问题是:什么是异常?什么又不是异常?这个问题看似简单,其实很多人都没有分辨清楚,尤其是人们经常混用异常(Exception)、错误(Error)、失败(Failure)、缺陷(Bug)这些接近又有区别的词。

这里最需要对比区分的是Failure/Exception和Bug/Error。用例子来说,你尝试打开一个文件失败了,触发了一个IOException,这是一种运行时遇到的操作失败,它并不代表你的程序本身有问题。但是,Bug就不一样了,假设你有一个sort函数对数组进行排序,如果发现调用sort之后居然还有乱序情况,导致整个系统行为出错最后crash,那么这就不是异常而是错误,唯一的解决办法是修改程序解决Bug。所以,在Java中我们可以这样区分,异常(Exception)是一种非程序原因的操作失败(Failure),而错误(Error)则意味着程序有缺陷(Bug)。注意:其他语言术语可能不同,重要的是能从概念上区分它们。

Java的类继承体系非常清楚地区分了Exception和Error。

java.lang.Object
    java.lang.Throwable
        java.lang.Error
        java.lang.Exception

Exception下面是我们常见的各种异常类,Error下面最著名的就是AssertionError,它可以通过throw new AssertionError(...)显式抛出,也可以通过assert操作符产生。Java文档中明确说到:

An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch.

就是说一般情况下不应该尝试用catch(Throwable)或者catch(Error)去捕获Error,因为抛出这个Error就是希望整个程序马上停下来。可能有人会疑惑:“如果不捕获Error,程序crash了后果很严重啊”?这个就要靠自己结合具体情况去判断了,让程序带着已经发作的Bug跑还是立刻停下来,到底哪个后果更严重?有时是前者,有时是后者。

声明异常和未声明异常的区别

Java可以在方法签名上显式地声明可能抛出的异常,但也允许抛出某些未声明的异常。那么,二者有何区别呢?我们自己在设计一个方法时如何决定是否在方法上声明某个异常呢?本质上讲,在方法签名上声明的异常属于方法接口的一部分,它和方法的返回值处于同一抽象层次,不随具体实现的变化而改变。比如,Integer类用于解析一个字符串到Integer型整数的valueOf方法:

public static Integer valueOf(String s) throws NumberFormatException

它声明抛出的NumberFormatException属于这个方法接口层面的一种失败情况,不管内部实现采用什么解析方法,都必然存在输入字符串不是合法整数这种情况,所以这时把这个异常声明出来就非常合理。相反,下面这个从帐户a向帐户b转账的transfer方法:

public boolean transfer(Account a, Account b, Money money) throws SQLException

它抛出SQLException就不对了,因为SQLException不属于这个transfer接口层面的概念,而属于具体实现,很有可能未来某个实现不用SQL了那么这个异常也就不存在了。这种情况下,就应该捕获SQLException,然后抛出自定义异常TransferException,其中TransferException可以定义几种和业务相关的典型错误情况,比如金额不足,帐户失效,通信故障,同时它还可以引用SQLException作为触发原因(Cause)。

public boolean transfer(Account a, Account b, Money money) throws TransferException {
    try {
        ...
        executeSQL(...);
    } catch (SQLException e) {
        throw new TransferException("...", e);
    }
}
什么情况下方法应该抛出未声明的异常?

前面谈到在编写一个方法时,声明异常属于接口的一部分,不随着具体实现而改变,但是我们知道Java允许抛出未声明的RuntimeException,那么什么情况下会这样做呢?比如,下面的例子中方法f声明了FException,但是它的实现中可能抛出RuntimeException,这是什么意思呢?

void f() throws FException {
    if (...) {
        throw new RuntimeException("...");
    }
}

根据上面提到的原理,未声明异常是和实现相关的,有可能随着不同实现而出现或消失,同时它又对应不到FException。比如,f方法依赖于对象a,结果在运行时a居然是null,导致本方法无法完成相应功能,这就可以成为一种未声明的RuntimeException了(当然,更常见的是直接调用a的方法,然后触发NullPointerException)。

其实,很多情况下抛出未声明的RuntimeException的语义和Error非常接近,只是没有Error那么强烈,方法的使用者可以根据情况来处理,不是一定要停止整个程序。我们最常见的RuntimeException可能要算NullPointerException了,通常都是程序Bug引起的,如果是C/C++就已经crash了,Java给了你一个选择如何处理的机会。

所以,抛出未声明异常表示遇到了和具体实现相关的运行时错误,它不是在设计时就考虑到的方法接口的一部分.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,451评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,172评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,782评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,709评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,733评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,578评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,320评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,241评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,686评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,878评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,992评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,715评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,336评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,912评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,040评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,173评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,947评论 2 355