java的异常处理机制详解

异常机制已经成为判断一门编程语言是否成熟的标准,异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。


Java异常的处理主要依赖于try,catch,finally,throws,throw这五个关键字。下面分别介绍它们:
1. try:try块中主要放置可能会产生异常的代码块。如果执行try块里的业务逻辑代码时出现异常,系统会自动生成一个异常对象,该异常对象被提交给运行环境,这个过程被称为抛出(throw)异常。Java环境收到异常对象时,会寻找合适的catch块(在本方法或是调用方法),如果找不到,java运行环境就会终止,java程序将退出
2. catch:catch块中放置当出现相应的异常类型时,程序需要执行的代码。当try中语句可能发生多个异常的时候可以由多个catch。
3. finally:finally中存放一定会执行的代码,异常机制保证finally代码总是会被执行。当遇到try或catch中return或throw之类可以终止当前方法的代码时,jvm会先去执行finally中的语句,当finally中的语句执行完毕后才会返回来执行try/catch中的return,throw语句。如果finally中有return或throw,那么将执行这些语句,不会在执行try/catch中的return或throw语句。finally块中一般写的是关闭资源之类的代码。
4. throws:在方法的签名中,用于抛出次方法中的异常给调用者,调用者可以选择捕获或者抛出,如果所有方法(包括main)都选择抛出。那么最终将会抛给JVM。JVM打印出栈轨迹(异常链)。
5. throw:用于抛出一个具体的异常对象。


Java对异常的处理是按异常分类处理的,不同异常有不同的分类,每种异常都对应一个类型(class),每个异常都对应一个异常(类的)对象。java的系统定义的大致的异常类的层次图如下(不全面,比如没有SQLException等):


异常类层次图


java中定义的异常(Exception)和错误(Error)都继承自Throwable类。其中错误的产生大多是由于运行环境jvm导致的,这部分错误我们通过程序很难纠正,如果真的出现又必须纠正,那可能就要涉及到jvm调优的问题。如jvm的垃圾回收机制(GC)之类。
* 而Java的异常分为两种,checked异常(编译时异常)和Runtime异常(运行时异常)*
编译时异常: java认为checked异常都是可以再编译阶段被处理的异常,所以它强制程序处理所有的checked异常,java程序必须显式处理checked异常,如果程序没有处理,则在编译时会发生错误,无法通过编译。
运行时异常: 在编译的过程中,Runtime异常无须处理也可以通过编译。所有的Runtime异常原则上都可以通过纠正代码来避免。

既然说java的异常都是一些异常类的对象,那么这些异常类也有一些方法我们应该了解:
1. getMessage();返回该异常的详细描述字符
2. printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。(异常链)
3. printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定的输出流
4. getStackTrace():返回该异常的跟踪栈信息。


再细讲java是如何处理异常之前我们在来重申一下两个重要问题
1. 为什么要有异常?对于构造大型、健壮、可维护的应用系统而言,错误处理是整个应用需要考虑的重要方面。Java异常处理机制,在程序运行出现意外时,系统会生成一个Exception对象,来通知程序,从而实现将“业务功能实现代码”和“错误处理代码”分离,提供更好的可读性。其中checked异常体现了java设计哲学:没有完善处理的代码根本不会被执行,体现了java的严谨性。
2. 异常有什么用? (1)可以对可能出现的异常进行更清晰的处理和说明,比如在finally中关闭资源或连接,或者在catch块中捕获异常打印信息到屏幕和日志等。(2)应用异常来处理业务逻辑,可以这么做,但是这有违背异常设计的初衷(异常实质上可以是一个if else语句,当然可以用作业务处理)。


明确了上面两个问题之后,我们就来看一下java的具体的异常处理机制。

1.使用try…catch捕获异常:

java提出了一种假设,如果try中的语句一切正常那么将不执行catch语句块,如果try中语句出现异常,则会抛出异常对象,由catch语句块根据自己的类型进行捕获。若没有相应的catch块,则抛出。
所以其执行步骤可以总结为以下两点:
(1) 如果执行try块中的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给java运行环境,这个过程称为抛出(throw)异常。
(2) 当java运行环境收到异常对象时,会寻找能处理该异常对象的catch块,如果找到合适的cathc块并把该异常对象交给catch块处理,那这个过程称为捕获(catch)异常;如果java运行时环境找不到捕获异常的catch块,则运行时环境终止,java程序也将退出。
下面还有几点注意事项需要大家注意:
注意一: 不管程序代码块是否处于try块中,甚至包括catch块中代码,只要执行该代码时出现了异常,系统都会自动生成一个异常对象,如果程序没有为这段代码定义任何catch块,java运行环境肯定找不到处理该异常的catch块,程序肯定在此退出。
注意二: 进行异常捕获时,一定要记住先捕获小的异常,再捕获大的异常。
注意三: 看下面一段java程序,我们来说明java对finally的处理方式:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class Test{
    public static void main(String[] args) {
        FileInputStream fis=null;
        try {
            fis=new FileInputStream("a.txt");
        } catch (FileNotFoundException e) {
            System.out.println(e.getMessage());
            // return语句强制方法返回
            return;
            // 使用exit来退出虚拟机
            //System.exit(1);
        }finally{
            if(fis!=null){
                try {
                    fis.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                fis=null;
            }
            System.out.println("fis资源已被回收");
        }
    }
}

运行这个程序,在catch中使用return而不exit可以得到如下结果:

a.txt (系统找不到指定的文件。)
fis资源已被回收

如果使用exit而不是return,那么将会得到如下结果:

a.txt (系统找不到指定的文件。)

以上两种情况显示:除非在try块或者catch块中调用了退出虚拟机的方法(即System.exit(1);),否则不管在try块、catch块中执行怎样的代码,出现怎样的情况,异常处理的finally块总是会被执行的。当程序执行try块,catch块时遇到return语句或者throw语句,这两个语句都会导致该方法立即结束,所以系统并不会立即执行这两个语句,而是去寻找该异常处理流程中的finally块,如果没有finally块,程序立即执行return语句或者throw语句,方法终止。如果有finally块,系统立即开始执行finally块,只有当finally块执行完成后,系统才会再次跳回来执行try块、catch块里的return或throw语句,如果finally块里也使用了return或throw等导致方法终止的语句,则finally块已经终止了方法,不用再跳回去执行try块、catch块里的任何代码了。所以,一般情况下,不要在finally块中使用return或throw等导致方法终止的语句,因为一旦使用,将会导致try块、catch块中的return、throw语句失效。
以下四种情况将会导致finally块不执行:
(1)在finally语句块中发生了异常
(2)在前面的代码中使用了System.exit()退出虚拟机
(3)程序所在线程死亡
(4)关闭cpu
- ps:在论坛上看到有朋友问:如果catch捕获了异常,那么try…catch语句块之后的语句是否会执行?
- 答案是如果catch块或finally块中没有throw语句或者return语句,那么try…catch之后的语句就一定会执行。因为异常已经被捕获和处理了呀~为什么后面的语句为什么不能执行呢。
- ps:可能又会有朋友问,如果try…catch块之后的语句中有使用到try中的引用,而try中的语句失败了,后面的怎么执行?
- 放心,如果真的有这种情况,那java一定会要求你讲这些语句和与那些可能失败的语句一起放入try…catch块中的。


2.使用throw(s)处理异常:

如果当前出现的异常在本方法中无法处理,我们只能抛出异常。
如果每个方法都是简单的抛出异常,那么在方法调用方法的多层嵌套调用中,Java虚拟机会从出现异常的方法代码块中往回找,直到找到处理该异常的代码块为止。然后将异常交给相应的catch语句处理(异常没有在本地处理,逻辑上throw之后的程序不会在进行)。如果Java虚拟机追溯到方法调用栈最底部main()方法时,如果仍然没有找到处理异常的代码块(这属于异常没有得到处理,将终止出现异常的线程),将按照下面的步骤处理:
第一、调用异常的对象的printStackTrace()方法,打印方法调用栈的异常信息。
第二、如果出现异常的线程为主线程,则整个程序运行终止;如果非主线程,则终止该线程,其他线程继续运行。
关于throw的用法我们有几点注意事项要注意:
注意一: throw语句后不允许有紧跟其他语句,因为这些没有机会执行(块外也不行,因为不会执行,无论是否被调用方捕获。如果异常是在本方法内部throw直接捕获,那是可以执行块后面的代码的,记住只要throw论文,throw之后的代码都不会在执行)。我以一段程序来说明这个问题:

public class TestException {  
    public static void exc() throws ArithmeticException{
        int a =1;
        int b=4;
        for (int i=-2;i<3;i++){
                    a=4/i;
                System.out.println("i="+i);
        }
    }
    public static void caexc(){
        try {
            exc();
        } catch (ArithmeticException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {  
        TestException.caexc();      
    }  
}  

输出结果为:

i=-2
i=-1
java.lang.ArithmeticException: / by zero
    at TestException.exc(TestException.java:8)
    at TestException.caexc(TestException.java:14)
    at TestException.main(TestException.java:21)

虽然捕获了异常,但由于原来的线程已经throw,所以后面的代码均不会得到执行。
注意二: 如果一个方法调用了另外一个声明抛出异常的方法,那么这个方法要么处理异常,要么声明抛出。
注意三: throw和throws关键字的区别:
throw用来抛出一个异常,在方法体内。语法格式为:throw 异常对象。
throws用来声明方法可能会抛出什么异常,在方法名后,语法格式为:throws 异常类型1,异常类型2…异常类型n
注意四: throw语句抛出异常的两种情况:
1.当throw语句抛出的异常是Checked异常,则该throw语句要么处于try块里显式捕获该异常,要么放在一个带throws声明抛出的方法中,即把异常交给方法的调用者处理。
2.当throw语句抛出的异常是Runtime异常,则该语句无须放在try块内,也无须放在带throws声明抛出的方法中,程序既可以显式使用try…catch来捕获并处理该异常,也可以完全不理会该异常,把该异常交给方法的调用者处理。
Runtime异常和Checked异常在抛出时的区别见下面这段代码:

public class TestException {  
    public static void throw_checked(int a) throws Exception{
        //Exception默认为checkedExcption
        if(a>0) throw new Exception("Exception:a>0");
    }
    public static void throw_runtime(int a) {
        if(a>0) throw new RuntimeException("runtimeException:a>0");
    }
    public static void main(String[] args) {  
        int a=1;
        try {
            throw_checked(a);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        throw_runtime(a);
    }  
}  

可见Runtime异常的灵活性比Checked的灵活性更强。因为Checked异常必须要被显式捕获或者显式抛出,所以Runtime写的更方便,我们自定义异常一般都是用Runtime异常。

3.自定义异常:

用户自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。异常类通常需要提供两种构造器:一个是无参数的构造器,另一个是带一个字符串的构造器,这个字符串将作为该异常对象的详细说明(也就是异常对象的getMessage方法的返回值)。下面给出一段自定义异常MyException的代码:

public class MyException extends RuntimeException{
    public MyException(){
    }
    public MyException(String s){
        super(s);
    }
}

用throws声明方法可能抛出自定义的异常,并用throw语句在适当的地方抛出自定义的异常。捕获自定义异常的方法与捕获系统异常一致。还可以异常转型。

4.异常链(异常跟踪栈):

异常对象的printStackTrace方法用于打印异常的跟踪栈信息,根据printStackTrace方法的输出结果,我们可以找到异常的源头,并跟踪到异常一路触发的过程。虽然printStackTrace()方法可以很方便地追踪异常的发生状况,可以用它来调试,但是在最后发布的程序中,应该避免使用它。而应该对捕获的异常进行适当的处理,而不是简单的将信息打印出来。


5.总结:

(1) catch块尽量保持一个块捕获一类异常,不要忽略捕获的异常,捕获到后要么处理,要么转译,要么重新抛出新类型的异常。
(2) 不要用try…catch参与控制程序流程,异常控制的根本目的是处理程序的非正常情况。
(3) 避免过大的try块,不要把不会出现异常的代码放到try块里面,尽量保持一个try块对应一个或多个异常。
(4) 细化异常的类型,不要不管什么类型的异常都写成Excetpion。




参考文献:

  1. JDK API 1.7
  2. http://blog.sina.com.cn/s/blog_9d88a5770101gsf4.html
  3. http://lavasoft.blog.51cto.com/62575/18920
  4. Thinking in java
  • 13
    点赞
  • 68
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值