Java异常处理机制

jopen 8年前

异常处理:

java中的异常处理机制主要依赖于try,catch,finally,throw,throws五个关键字。其中,

try关键字后紧跟一个花括号括起来的代码块(花括号不可省略)简称为try块。里面放置可能发生异常的代码。

catch后对应异常类型和一个代码块,用于表明该catch块用于处理这种类型的代码块。多个catch块后还可以跟一个finally块。

finally块用于回收再try块里打开的物理资源,异常机制会保证finally块总被执行。

throws关键字主要在方法签名中使用,用于声明该方法可能抛出的异常

throw用于抛出一个实际的异常,throw可以单独作为语句使用,抛出一个具体异常对象。

java异常分为两种:Checked异常和Runtime异常。

java认为Checked异常都是可以在编译阶段被处理的异常,所以它强制程序处理所有的Checked异常,而Runtime异常则无需处理。

Checked异常可以提醒程序员需要处理所有可能发生的异常,但Checked异常也带来一些繁琐之。

对于错误处理机制,主要有如下的两个缺点:

1.无法穷举所有异常情况:因为人类的知识是有限的,异常情况总比可以考虑到的情况多,总有漏网之鱼

2.错误处理代码和业务实现代码混杂严重影响程序的可读性,会增加程序维护的难度。

 

java的异常处理机制:实现将“业务功能代码”和“错误处理代码”的分离!

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

java提出了一种假设,如果程序可以顺利完成,那么一切正常,把系统的业务实现代码放在try块中定义,所有的异常处理逻辑放在catch块中进行处理。

即:try{

//业务实现代码

...

}

catch(Exception e){

输入不合法

}

上面的格式中try块和catch块后的{...}都是不可以省略的!

执行步骤:

1.如果执行try块中的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给java运行环境,这个过程称为抛出(throw)异常。

2.当java运行环境收到异常对象时,会寻找能处理该异常对象的catch块,如果找到合适的cathc块并把该异常对象交给catch块处理,那这个过程称为捕获(catch)异常;如果java运行时环境找不到捕获异常的catch块,则运行时环境终止,jav程序也将退出。

注意1:不管程序代码块是否处于try块中,甚至包括catch块中代码,只要执行该代码时出现了异常,系统都会自动生成一个异常对象,如果程序没有为这段代码定义任何catch块,java运行环境肯定找不到处理该异常的catch块,程序肯定在此退出。

注意2:try块后可以有多个catch块,try块后使用多个catch块是为了针对不同异常类提供的不同的异常处理方式。当系统发生不同意外情况时,系统会生成不同的异常对象,java运行时就会根据该异常对象所属的异常类来决定使用哪个catch块来处理该异常。

注意3:通常情况下,如果try块被执行一次,则try块后只有一个catch块会被执行,绝不可能有多个catch块被执行,除非在循环中使用类continue开始下一次循环,下一次循环又重新运行了try块,这才可能导致多个catch块被执行。

注意4:进行异常捕获时,一定要记住先捕获小的异常,再捕获大的异常。

 

java异常类的继承关系:

 java把所有非正常情况分成两种:异常(Exception)和错误(Error),都是继承自Throwable父类。

 Error错误:一般是指虚拟机相关的问题,如系统崩溃,虚拟机出错误等,这种错误无法恢复或不可能捕获,将导致应用程序中断,通常不处理。

 

访问异常信息:

如果程序需要在catch块中访问异常对象的相关信息,可以通过调用catch后异常形参的方法来获得。当java运行时决定调用某个catch块来处理该异常对象时,会将该异常对象赋给catch块后的异常参数,程序就可以通过该参数来获得该异常的相关信息。

所有异常对象都包含了如下几个常用方法:

getMessage():返回该异常的详细描述字符串

printStackTrace():将该异常的跟踪栈信息输出到标准错误输出

printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流

getStackTrace():返回该异常的跟踪栈信息。

 举例如下:

public class TestException{

public static void main(String[] args)

{

try{

     FileInputStream fis=new FileInputStream("a.txt");

}

catch(IOException ioe)

{

   System.out.println(ioe.getMessage());

   ioe.printStackTrace();

}

}

}

 

使用finally回收资源

 有时候,程序在try块里面打开了一些物力资源(比如数据库连接,网络连接好磁盘文件等),这些物理资源都必须显式回收。

因为:java的垃圾回收机制不会回收任何的物理资源,垃圾回收机制只回收堆内存中对象所占用的内存。

 

问题1:那么在哪边回收这些物理资源呢?

答: 在finally块中,因为如果try块的某条语句引起一场,该语句后的其他语句通常不会被执行,那将导致位于该语句后的资源回收语句得不到执行;如果在 catch块里进行资源回收,但catch块完全有可能得不到执行,这也将导致不能及时回收这些物理资源。所以我们不管try块中的代码是否出现异常,也 不管哪个catch块会被执行,finally块总会被执行。

注意点1:只有try块石必须的,也就是说如果没有try块,则不可能有后面的catch块和finally块;
注意点2:catch块和finally块都是可选的,但catch块和finally块至少出现其中之一,也可以同时出现;
注意点3:可以有多个catch块,捕获父类异常的catch块必须位于捕获子类异常的后面;
注意点4:不能只有try块,既没有catch块,也没有finally块;
注意点5:多个catch块必须位于try块之后,finally块必须位于所有catch块之后。

举例如下:
package day0213;

import java.io.FileInputStream;
import java.io.IOException;

public class TestException {

 /**
  * @param  args
  */
 public static void main(String[] args) {
  // TODO Auto-generated method stub
  FileInputStream fis=null;
  try {
   fis=new FileInputStream("a.txt");
  } catch (IOException ioe) {
   System.out.println(ioe.getMessage());
   //return语句强制方法返回
   return;
   //使用exit来退出虚拟机
   //System.exit(1);
  }finally{
   //关闭磁盘文件,回收资源
   if (fis!=null) {
    try {
     fis.close();
    } catch (IOException ioe) {
     ioe.printStackTrace();
    }
   }
   System.out.println("程序已经执行了finally里德资源回收");
  }
 }

}
运行程序结果:
a.txt (系统找不到指定的文件。)
程序已经执行了finally里德资源回收

如果将catch块中的最后两句注释放入程序,那么结果为:a.txt (系统找不到指定的文件。)

 以上两种情况显示:除非在try块或者catch块中调用了退出虚拟机的方法(即System.exit(1);),否则不管在try块、catch块中执行怎样的代码,出现怎样的情况,异常处理的finally块总是会被执行的。不过,一般情况下,不要再finally块中使用return或throw等导致方法终止的语句,因为一旦使用,将会导致try块、catch块中的return、throw语句失效。

 

总结一下这个小问题:

当程序执行try块,catch块时遇到return语句或者throw语句,这两个语句都会导致该方法立即结束,所以系统并不会立即执行这两个语句,而是 去寻找该异常处理流程中的finally块,如果没有finally块,程序立即执行return语句或者throw语句,方法终止。如果有 finally块,系统立即开始执行finally块,只有当finally块执行完成后,系统才会再次跳回来执行try块、catch块里的 return或throw语句,如果finally块里也使用了return或throw等导致方法终止的语句,则finally块已经终止了方法,不用 再跳回去执行try块、catch块里的任何代码了。

综上:尽量避免在finally块里使用return或throw等导致方法终止的语句,否则可能出现一些很奇怪的情况!

异常处理的嵌套

例如catch块中再次包含了一个完整的异常处理流程,这种在try块,catch块或finally块中包含完整的异常处理流程的情形称为异常处理的嵌 套。异常处理流程的代码可以放在任何可执行代码的地方,因此完整的异常处理流程既可放在try块,也可放在catch块,也可放在finally块里。

嵌套的深度没有很明确的限制,通常没有必要写层次太深的嵌套异常处理,会导致程序可读性降低。

 Checked异常和Runtime异常体系

 java异常被分为两大类:Checked异常和Runtime异常(运行时异常)。

所有RuntimeException类及其子类的实例被称为Runtime异常,不是RuntimeException类及其子类的异常实例则被称为Checked异常。

只有java语言提供了Checked异常,其他语言都没有提供,java认为Checked异常都是可以被处理(修复)的异常如果程序没有处理Checked异常,该程序在编译时就会发生错误,无法通过编译。

Checked异常的处理方式:

①:当方法明确知道如何处理异常,程序应该使用try...catch块来捕获该异常,然后在对应的catch块中修补该异常。

②:当方法不知道如何处理异常,应该在定义该方法时声明抛出该异常。

Runtime异常无须显式声明抛出,如果程序需要捕捉Runtime异常,也可以使用try...catch块来捕获Runtime异常。

问题是:大部分的方法总是不能明确知道如何处理异常,这就只能声明抛出异常了。

使用throws抛出异常

使用throws抛出异常的思路是:当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理,如果main方法也不知道应该如何处理这种类型的异常,也可以使用使用throws声明抛出异常,该异常将交给JVM来处理。

JVM对异常的处理方法:打印异常跟踪栈的信息,并终止程序运行,所以有很多程序遇到异常后自动结束。

使用throws抛出异常的格式:

throws声明的抛出的语法格式紧跟在方法之后,可以声明多个异常类,多个异常类之间以逗号隔开。一旦使用了throws语句声明抛出异常,就不用再使用try...catch来捕获异常了。

如:throws ExceptionClass1,ExceptionClass2...

注意点1:如果某段代码调用了一个带throws声明的方法,该方法声明抛出了Checked异常,这表明该方法希望它的调用者来处理该异常。那么这段代码要么放在try块中显示捕获该异常,要么这段代码处于另一个带throws声明抛出的方法中。

举例如下:

 方法一:

package day0213;

import java.io.FileInputStream;
import java.io.IOException;

public class TestException2 {

//test() 方法抛出了异常,那么test()方法的调用者要么放在try块中显示捕获该异常,要么这段代码处于另一个带throws声明抛出的方法中。

//以下为后者的处理方法

 public static void test() throws IOException {
 FileInputStream fis=new FileInputStream("a.txt");
 }

 public static void main(String[] args) throws Exception {
       test();
 }

}

 方法二:

package day0213;

import java.io.FileInputStream;
import java.io.IOException;

public class TestException2 {
   
 public static void test() throws IOException {
 FileInputStream fis=new FileInputStream("a.txt");
 }

 public static void main(String[] args) {
       try {
  test();
 } catch (IOException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
 }
 }

}

使 用throws声明抛出异常时有一个限制:就是方法重写时的“两小”中的一条规则:子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或 或相等,子类方法中不允许比父类方法声明抛出更多异常。即如果子类抛出的异常是父类抛出的异常的父类,那么程序无法通过编译。

因为Checked异常存在一些不便之处,大部分情况,可以使用Runtime异常,如果程序需要在合适的地方捕获异常,并对异常进行处理,程序一样可以用try...catch捕获Runtime异常。

使用throw抛出异常

当程序出现错误时,系统会自动抛出异常,另外,java也允许程序自行抛出异常,自行抛出异常使用throw语句完成!

抛出异常:

如果需要在程序中自行抛出异常,应使用throw语句,throw语句可以单独使用,throw语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。throw语句的格式如下:throw ExceptionInstance;

throw语句抛出异常的两种情况:

1.当throw语句抛出的异常是Checked异常,则该throw语句要么处于try块里显式捕获该异常,要么放在一个带throws声明抛出的方法中,即把异常交给方法的调用者处理。

2.当throw语句抛出的异常是Runtime异常,则该语句无须放在try块内,也无须放在带throws声明抛出的方法中,程序既可以显式使用try...catch来捕获并处理该异常,也可以完全不理会该异常,把该异常交给方法的调用者处理。

举例如下:

package day0213;

public class TestException3 {

 public static void throwChecked(int a) throws Exception {
  if (a < 0) {
   /**
    * 自行抛出Exception异常
    * 改代码必须处于try块里,或处于带throws声明的方法中
    */
   throw new Exception("a的值大于0,不符合要求");
  }
 }

 public static void throwRuntime(int a) {
  if (a < 0) {
   /**
    * 自行抛出RuntimeException异常,既可以显式捕获该异常
    * 也可以完全不用理会该异常,把该异常交给方法的调用者处理
    */
   throw new RuntimeException("a的值大于0,不符合要求");
  }else {
   System.out.println("a的值为:"+a);
  }
 }

 public static void main(String[] args) {
  try {
   /**
    * 此处调用了带throws声明的方法,必须显示捕获该异常(使用try...catch)
    * 否则,要在main方法中再次声明抛出
    */
   throwChecked(-3);
  } catch (Exception e) {
   System.out.println(e.getMessage());
  }
  throwRuntime(3);
 }

}

由上面的代码显式:自行抛出Runtime异常比自行抛出Checked异常的灵活性更好。

自定义异常类

用户自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。

应以异常类通常需要提供两种构造器:一个是无参数的构造器,另一个是带一个字符串的构造器,这个字符串将作为该异常对象的详细说明(也就是异常对象的getMessage方法的返回值)。

 例子如下:

package day0213;

public class TestException4 extends Exception {

 public TestException4() {

 }

 public TestException4(String msg) {
  super(msg);
 }

 }

catch和throw同时使用

前面已有两种异常处理方法:

1.在异常出现的方法内捕获并处理,方法的调用者将不能再次捕获该异常。

2.该方法签名中声明抛出该异常,将该异常完全交给方法调用者处理。

但是在实际应用中往往需要更复杂的处理方式,即异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,可以让该方法的调用者也能捕获到异常。

为了实现这种靠多个方法协作处理同一个异常的情形,可以通过catch块中结合throw来完成。

举例catch和throw同时使用的例子:

package day0213;

public class TestException4 {
 // 以下AuctionException这个异常是自定义的异常类
 private double initPrice = 30.0;

 public void bid(String bidPrice) throws AuctionException {
  double d = 0.0;
  try {
   d = Double.parseDouble(bidPrice);
  } catch (Exception e) {
   e.printStackTrace();
   throw new AuctionException("竞拍价必须是数值,不能包含其他字符!");
  }
  if (initPrice > d) {
   throw new AuctionException("竞拍价比起拍价低,不允许竞拍!");
  }
  initPrice = d;
 }

 public static void main(String[] args) {
  TestException4 ta = new TestException4();
  try {
   ta.bid("df");
  } catch (AuctionException ae) {
   // TODO: handle exception
   System.err.println(ae.getMessage());
  }
 }
}

catch和throw同时使用来处理异常的方法是在大型企业中比较常用的。

java的异常跟踪栈

异常对象的printStackTrace方法用于打印异常的跟踪栈信息,根据printStackTrace方法的输出结果,我们可以找到异常的源头,并跟踪到异常一路触发的过程。

虽然printStackTrace()方法可以很方便地追踪异常的发生状况,可以用它来调试,但是在最后发布的程序中,应该避免使用它。而应该对捕获的异常进行适当的处理,而不是简单的将信息打印出来。

总之,要合理使用异常。

转自:http://www.cnblogs.com/zhouhong/archive/2012/02/15/2349620.html