到 Google 资讯主页   
EasyJF首页   资料   源码   软件    论坛   网站    
   使用帮助    
    该信息为本站MyRSS系统缓存内容,部分图片及附件有可能无法正常使用.easyjf.comwww.javaresearch.org无关,不对该信息负责.通过http://www.javaresearch.org/article/77518.htm访问该信息的原始内容.
页面功能  【加入收藏】 【推荐给朋友】 【字体:  】 【关闭】   
异常(整理了一天)
作者:jacky_zz 来源:www.javaresearch.org  发布时间:2007-12-11 11:13:40.007


●       如何创建和使用自定义异常。

●       异常支持链表和本地化。

●       抽象类和接口中的异常用法。

●       覆盖方法的异常要求。

●       如何用字节码表示异常处理代码。

●       应用程序中异常处理操作的效率。

在处理异常时,上述主题并不常用。简单项目根本不使用它们。但在编写较复杂的程序时,这些主题的作用便显示出来了。
3.2  自定义异常
前面讨论了如何处理调用Java API的方法时产生的异常。根据需要,还可创建和使用自定义异常??自我构建表示错误的类。可创建全新异常,并将它们用于应用程序。

使用自定义异常有什么好处呢?为何要定义新异常类型?创建自定义异常是为了表示应用程序的一些错误类型,为代码可能发生的一个或多个问题提供新含义。可以显示代码多个位置之间的错误的相似性,也可区分代码运行时可能出现的相似问题的一个或多个错误,或给出应用程序中一组错误的特定含义。

例如,考虑任何类型的服务器。服务器的基本作用是处理与客户机的通信。若使用标准Java API(如java.io和java.net包中的类)来编写服务器,则可使编写的代码在多个位置抛IOException。 在设置服务器、等待客户机连接和获取通信流时,可抛出IOExceptions;在通信期间及试图断开连接时,也可抛出IOExceptions。简言之,服务器的各个部分都可能引发IOException。

对服务器而言,这些IOException意义不尽相同。虽然由同一异常类型表示,但与各个异常相关的业务含义存在差异,报告和恢复操作亦有不同。可以将一个异常集与服务器配置和启动问题关联,将另一个异常集与客户机通信的实际行动关联,将第三个异常集与服务器关闭任务关联。使用自定义异常,可采用对应用程序有意义的方式来灵活地表示错误。

创建和使用自定义异常并不难。遵循以下3个步骤即可。

3.2.1  定义异常类
一般要定义新类来表示自定义异常。多数情况下,只需创建已有异常类的子类。

1  public class CustomerExistsException extends Exception{

2    public CustomerExistsException(){}

3    public CustomerExistsException(String message){

4      super(message);

5    }

6  }

至少要继承Throwable或Throwable的子类。经常需要定义一个或多个构造函数,以在对象中存储错误消息。如第2-4行所示。在继承任何异常时,将自动继承Throwable类的一些标准特性,如:

●       错误消息

●       栈跟踪

●       异常包装

若要在异常中添加附加信息,则可以为类添加一些变量和方法:

1  public class CustomerExistsException extends Exception{

2    private String customerName;

3    public CustomerExistsException(){}

4     public CustomerExistsException(String message){

5       super(message);

6     }

7     public CustomerExistsException(String message, String customer){

8       super(message);

9       customerName = customer;

10     }

11     public String getCustomerName(){

12       return customerName;

13     }

14  }

由本例可知,可修改CustomerExistsException类,以支持其他属性。例如,可将customerName字符串(引发异常的记录的客户名)与异常联系起来。

3.2.2  声明方法抛出自定义异常
这实际上是“处理或声明”规则的“声明”部分。为了使用自定义异常,必须通知调用代码的类:要准备处理这个异常类型。为此,声明一个或多个方法抛出异常:

public void insertCustomer(Customer c) throws CustomerExistsException{

// The method stores customer information in the database.

// If the customer data already exists, the method creates

// and throws the CustomerExistsException.

}

3.2.3  找到故障点,新建异常并加上关键字throw
最后一步实际上是创建对象,并通过系统传送该对象。为此,需要了解代码将在方法的哪个位置出现故障。根据情况,可能要使用以下部分或所有条件,来指示代码中的故障点。

1. 外部问题

●       应用程序中产生的异常

●       其他方法返回的故障代码

2. 内部问题

●       应用程序状态不一致

●       应用程序中的处理问题

在本例中,当不能新建一个客户时会遇到一个故障场景。结果,创建一个异常来表示问题并抛出该问题。如下面的示例方法所示:

1  public void insertCustomer(Customer c)

2    throws CustomerExistsException, SQLException {

3    String selectSql =

4      "SELECT * FROM Customer WHERE first_name=? AND last_name=?";

5    String insertSql = "INSERT INTO Customer VALUES(?, ?)";

6    try{

7      Connection conn = dbmsConnectionFactory.getConnection();

8      PreparedStatement selStmt = conn.prepareStatement(selectSql);

9      selectStmt.setString(1, c.getFirstName());

10      selectStmt.setString(2, c.getLastName());

11      ResultSet rs = selStmt.executeQuery();

12      if (rs.next()){

13        // In this case, the failure condition is produced if you

14        //  can already locate a metching record in the database.

15       throw new CustomerExistsException("Customer exists:" + c, c);

16      }

17      else{

18        PreparedStatement insStmt = conn.prepareStatement(insertSql);

19        insStmt.setString(1, c.getFirstName());

20        insStmt.setString(2, c.getLastName());

21        int status = insStmt.executeUpdate();

22      }

23   }

24   catch (SQLException exc){

Java关键字throw将这个新异常对象传给该方法的调用者。在执行完这3个步骤后,就创建了自定义异常。除非派生一个非检测异常类(如RuntimeException或Error),否则调用方法的任何对象随后将按照“处理或声明”规则解决该异常。

这引出了一个有趣的问题:在自定义异常时,应如何派生?必须在Throwable类层次结构中派生,否则将不能在应用程序中传播异常。另外,不能从Throwable直接派生。Throwable为两类主要问题(Exception和Error)提供行为基础,不能为这棵继承树定义新分支。一般也不要直接继承Error或其任何子类,因为自定义异常通常不符合错误标准(即适当应用程序不应试图捕获的严重问题)。

需要从Exception类层次结构中派生。一般地,应将自定义异常定义为故障状态更一般的异常类型的子类。例如,ServerConnectionException是java.io.IOException的子类,因为Server- Connec tionException是java.io.IOException的更具体类型。

如果定义的异常从RuntimeException树继承,是否属于正确的编码实践?若如此,就回避了异常机制,即使声明了异常,类也不必显式处理异常。

通过本例,可了解到如何创建基本的自定义异常。很多情况下,这可轻易地满足要求。自定义异常类(可能还有消息)经常是应用程序惟一需要的异常。有时,需要支持更高级的特性,在一些异常中,可能要用到两个属性:链表(chaining)和本地化(localization)。

3.3  链表异常
从JDK 1.4开始,Throwable类开始支持“链表异常”。从本质上讲,链表异常允许设置两个异常之间的关联。在1.4版之前,有些Java API使用链表异常,将相关异常组合在一起。例如,在JDBC中,可链接SQLException对象,以便在相同的数据库操作中,发回与问题相关的多个不同异常。

在代码中,当然可用链表异常来设置一组相关异常。一种更常见的应用程序是设置异常之间的“起因”关联。假设在代码中捕获异常,并抛出一个与错误对应的自定义异常。一般而言,将失去原始异常的所有信息,或者需要做附加工作来存储附加信息。若使用链表异常,则可将原始异常与自定义异常关联起来,作为问题的起因。如果需要任何原始信息,只需调用自定义异常的getCause( )方法即可获得。

要为异常设置起因,可采用两种方法:作为构造函数参数传入,或调用initCause(Throwable)方法在创建对象后设置。不能多次设置起因,只允许设置一次。在设置了起因的值后,创建的异常对象将在所余生命期中存储相同的起因。在构造函数中设置异常的起因一般更常见,这种做法更快捷,代码也较简洁。

1  try{

2     // Lots of risky I/O operations!

3  }

4  catch (java.io.IOException rootCause){

5    throw new ConnectionException(rootCause);

6  }

当然,需要编写一个构造函数版本,它将Throwable作为参数,并传递给超类,以实现链表异常:

1  public class ConnectionException extends java.io.IOException{

2     public ConnectionException(Throwable cause){

3        initCause(cause);

4     }

5  }

使用initCause方法的主要原因是,链接JDK 1.4之前的自定义异常类。如果正在处理遗留异常类,则可能没有支持链表异常的构造函数。因为initCause在Throwable类级别定义,所以任何异常均可调用它,并能在创建后保存相关异常:

1  try{

2     // Even more risky I/O operations!

3  }

4  catch (java.io.IOException rootCause){

5     throw new LegacyException(“Old exception”).initCause(rootCause);

6  }

如上所述,可使用“起因”将一些异常链接在一起。当代码中有一系列错误时,可将各个异常作为起因链接到下一个,从而构建一组相互关联的异常。很自然,采用这些行为的情况不多,不过,对于较复杂的系统,这是一种便捷的做法。在处理链表异常时,可编写处理器代码来“遍历”链表异常集合,并使用各个对象包含的数据:

1  try{

2    troublesomeObject.riskyMethod();

3  }

4  catch (ChainedException exc){

5    System.out.println("We’re in trouble now!!!");

6    Throwable currentException = exc;

7    do{

8       System.out.println(currentException.toString());

9       currentException = currentException.getCause();

10    }

11    while (currentException != null);

12  }

3.4  异常的本地化和国际化
从JDK 1.1开始,Throwable定义了方法getLocalizedMessage()。默认情况下,该方法返回调用getMessage()提供的值,即返回与Throwable对象相关的任何消息。根据需要,可在自定义Exception类中重新定义这个方法,以支持本地专用的错误消息。这允许在代码中设置支持本地化(l10n)和国际化(i18n)的异常。这两项特性允许应用程序在多个国家或地区使用。

很多情况下,大可不必支持异常本地化。一般而言,应用程序只提供GUI或标准程序输出的i18n。即使一个应用程序支持不同语言,很多产品也只为异常使用一种标准语言,因为软件产品经常包含某一国家的开发团队提供的技术支持。

如果需要本地化与异常相关的文本,则可在自定义异常类中覆盖getLocalizedMessage(),以加载本地专用的异常消息。

Java使用java.util和java.text包的类支持l10n和i18n。java.util.ReourceBundle和java.util.Locale是两个关键类。ResourceBundle类是一个本地化集合包装器,允许存储一系列键-值对,并将这些值与基于标准ISO的本地命名方案关联。本地映射允许根据语言和位置的组合码来定义地区。如表3-1所示。

表3-1  利用语言和位置的组合码定义地区的示例

代    码
 语言和地区
 
de
 German(德国)
 
en-CA
 Canadian English(加拿大 英语)
 
en-GB
 British English(英国 英语)
 
en-US
 American English(美国 英语)
 
fr
 French(法国)
 

 

Locale类是表示这些代码的Java方式。Locale对象映射到特定语言或地区,允许您查看该地区的“标准”偏好。下面的步骤演示了如何创建使用本地消息的自定义异常类。

3.4.1  创建ResourceBundle子类来存储消息
ResourceBundle类用作资源的存储位置。若有兴趣存储异常消息,一种简便的方法是继承ListResourceBundle类,ListResourceBundle类是管理元素组的基类。

1  import java.util.ListResourceBundle;

2  public class ExceptionResourceBundle extends ListResourceBundle{

3    private static final Object [][] contents = {

4      {"criticalException", "A critical exception has occurred"}

5    };

6    public Object [][] getContents(){ return contents; }

7  }

3.4.2  为不同地区继承ResourceBundle类
第一个创建的ResourceBundle类是默认的,如果Java找不到更适合本地使用的资源,则使用该类包含的消息。要支持其他语言或地区,只需构建原始ResourceBundle的子类。这样,就可以自由地为新语言或地区覆盖部分或所有消息。

1  public class ExceptionResourceBundle_fr extends ExceptionResourceBundle{

2     private static final Object [][] contents = {

3       {"criticalException", "Il y'a quelque chose qui cloche!"}

4     };

5     public Object [][] getContents(){ return contents; }

6  }

3.4.3  创建覆盖getLocalizedMessage的自定义异常类并用
ResourceBundle检索消息
自定义异常类的关键修改是覆盖getLocalizedMessage方法。若要使用本地化,需要调用ResourceBundle子类的静态方法getBundle。该方法根据用作输入的本地化信息检索适当的ResourceBundle对象。此后,可调用getString方法来检索存储在Bundle中的特定字符串。

1  import java.util.Locale;

2  import java.util.ResourceBundle;

3  public class LocalizedException extends Exception{

4     public static final String DEFAULT_MSG_KEY = "criticalException";

5     private String localeMessageKey = DEFAULT_MSG_KEY ;

6     private String languageCode;

7     private String countryCode;

8     public LocalizedException(String message){

9       super(message);

10     }

11     public LocalizedException(Throwable cause, String messageKey){

12       super(cause);

13       if (isValidString(messageKey)){

14         localeMessageKey = messageKey;

15       }

16     }

17     public LocalizedException(String defaultMessage, Throwable cause,

String messageKey){

18       super(defaultMessage, cause);

19       if (isValidString(messageKey)){

20         localeMessageKey = messageKey;

21       }

22     }

23     public LocalizedException(String defaultMessage, String messageKey,

String language, String country){

24       super(defaultMessage);

25       if (isValidstring(messageKey)){

26         localeMessageKey = messageKey;

27       }

28       if (isValidString(country)){

29         countryCode = country;

30       }

31       if (isValidString(language)){

32         languageCode = language;

33       }

34    }

35    public void setLocaleMessageKey(String messageKey){

36      if (isValidString(messageKey)){

37        localeMessageKey = messageKey;

38      }

39    }

40    public String getLocaleMessageKey(){

41      return localeMessageKey;

42    }

43    public String getLocalizedMessage(){

44      ResourceBundle rb = null;

45      Locale loc = getLocale();

46      rb = ResourceBundle.getBundle("ExceptionResourceBundle", loc);

47      return rb.getString(localeMessageKey);

48    }

49    private Locale getLocale(){

50       Locale locale = Locale.getDefault();

51       if ((languageCode != null) && (countryCode != null)){

52         locale = new Locale(languageCode, countryCode);

53       }

54       else if (languageCode != null){

55         locale = new Locale(languageCode);

56       }

57       return locale;

58    }

59    private boolean isValidString(String input){

60       return (input != null) && (!input.equals(""));

61    }

62  }

这样,就为异常类本地化提供了支持。若要使用大量本地化异常对象,最好定义一个本地化类,以便所有对象继承。ResourceBundle的最佳特性是:在使用任何getBundle方法来执行查找时,ResourceBundle自动与最接近的语言/地区匹配。如果抛出以下3个LocalizedException对象:

throw new LocalizedException(" ","criticalException","fr","FR");

throw new LocalizedException(" ","criticalException","jp"," ");

throw new LocalizedException("Internal application error");

然后调用处理器块的getLocalizedMessage()方法,可能得到3个不同的异常消息值。在第一种情况下,getBundle调用使用French的Locale对象,并与最接近的Exception Resource Bundle_fr匹配。如果定义了ExceptionResourceBundle_fr_FR,则将使用该异常。在第二种情况下,因为没有类的日语版本,所以类默认使用基源Bundle类:ExceptionResourceBundle。在第三种情况下,使用的本地化是定义为创建异常的系统的默认值。如果应用程序运行在为法语用户配置的计算机上,则返回的类将是ExceptionResourceBundle_fr,否则将使用基类Exception- ResourceBundle。

3.5  子类
在构建子类时,可重新定义父类的方法,修改代码,即可以覆盖方法,这样,您就可以自由地修改完成工作的方式,从而满足子类的需要。

在覆盖方法时,必须遵守一些严格的规则。必须复制方法签名:方法名、输入和输出都要匹配。不能使覆盖的方法比父类的方法更私有(private)。还要注意,方法不能抛出父类的方法未声明的异常。也就是说,在覆盖方法时,可抛出与父类的方法相同的异常或异常的子集。如果在父类方法中未定义,则不能抛出不同的异常。

为什么要抛出在父类中声明的相同异常呢?每当一个类定义方法时,该方法声明的异常(非检测异常除外)表示可能错误的完整集合。在编写调用该方法的代码时,根据这个可能集合,相应地编写处理器代码。

子类是父类的孩子,可在代码任何位置用子类替换父类。如果调用覆盖方法,则将运行该方法的子版本。

如果子类方法只能产生父类方法异常的子集,则一切正常。如果调用方法为绝不可能发生的异常编写了处理块,虽然有些累赘,但绝不会导致应用程序出现故障。

反过来,如果子类方法可覆盖父类方法,并抛出一个不同的异常集,则可能生成处理器代码根本不知道如何处理的异常。虽然可以传输无法在代码中恢复的错误,但这可能中止应用程序,并使异常处理丧失作用。为此,编译器在覆盖方法中强制执行“相同或更少”的异常规则。

3.6  接口和抽象类的异常声明
在为类的方法声明异常时,意义十分明显。那么,如果为抽象方法或接口声明异常,情况将如何?抽象方法只提供方法签名,不真正定义任何代码;接口实际上全部由抽象方法组成。

在这些情况下,抛出特定异常的含义是什么?为这类方法定义异常时,实际上是在设置一种期望,描述最终实现的方法可能出现的错误。

1  public interface CommunicationServer{

2     public void processClient() throws ServerConnectionException;

3     public void listen();

4  }

如果创建实现CommunicationServer的类,则将编写自己的processClient方法版本。接口声明该方法可能抛出ServerConnectionException,为此,您可以根据需要在自己的代码中抛出异常。更重要的是,如果确定要抛出异常,则使用CommunicationServer接口的其他任何类将必须处理或声明该异常。

1  public class ServerManager{

2   private CommunicationServer commoServer = new CommunicationServerImpl();

3    private boolean shutdown;

4    public void handlerCycle(){

5      while (!shutdown){

6        commoServer.listen();

7        try{

8          commoServer.processClient();

9        }

10        catch (ServerConnectionException exc){

11           Logger.log("Client connection error", exc);

12        }

13      }

14   }

15 }

3.7  异常栈跟踪
在面向对象的编程中,大多数复杂操作体现为一系列方法调用。这是两个编程目标决定的:定义可重用的代码单元,将复杂任务逐渐分解为更易管理的小型子任务。另外,因为定义很多对象来共同完成编程任务,在最终编程模型中,很多对象将通过一系列方法调用来实现通信,执行任务。在面向对象的应用程序运行时,经常会发生一系列方法调用,即“调用栈”。

为便于讨论,列举服务器可能执行的一个业务示例:服务器根据响应,将新客户记录添加到数据库。这当然可以在一个方法中全部完成,但那样将使代码冗长难懂,难以维护和重用。最好将这些工作分布在若干个对象中:Server管理整个服务器周期,Communication Delegate处理与客户的通信,Business Delegate解释和处理客户请求,Customer Handler存储处理任何Customer业务操作的方法,DataAccessObject与数据库通信。表3-2显示了服务器根据客户请求,新建客户账户时出现的一系列调用。左侧的项显示哪个类包含方法,右侧的项显示已发生的方法调用。

表3-2  新建客户账户的调用栈


 方    法
 
Server
 Main
 
Server
 Configure
 
Server
 Connect
 
CommunicationDelegate
 Run
 
CommunicationWorkThread
 run
 
CommunicationWorkThread
 readInput
 
ShoppingService
 processInput
 
CustomerAccountService
 createCustomer
 
CustomerAccountService
 isCustomerValid
 
CustomerAccountService
 createAccount
 
AccountDAO
 insertCustomer
 
AccountDAO
 insertAddress
 
AccountDAO
 insertPaymentInfo
 
ShoppingService
 processOutput
 
CommunicationWorkThread
 writeOutput
 

 

在本例中,可清楚地看到一系列方法调用,main方法运行处理器方法,然后接收客户请求,再识别这是一个新建客户的请求。在调用栈中,当诸如insertCustomer的方法在运行期间出现异常,将发生什么情况呢?如果异常在该方法的try-catch-finally中处理,则什么也不发生,insertCustomer按一般方式处理异常,代码将继续运行。

如果insertCustomer声明异常,则情况将有所不同。此时,异常将使insertCustomer停止执行,异常将传给该方法的调用者,即createCustomer方法。createCustomer方法也有两种选择:处理或声明异常;若声明异常,则createCustomer方法停止,异常将沿调用栈进一步上传。

开发人员可根据应用程序的需要,在调用栈的任何一点处理异常:可在产生异常的方法中处理问题,也可在调用序列的某一较远位置处理异常。

在异常沿调用链上传时,它维护一个称为“栈跟踪”的结构。栈跟踪记录未处理异常的各个方法,以及发生问题的代码行。当异常传给方法调用者时,它在栈跟踪中添加一行,指示该方法的故障点。在前面的示例中,栈跟踪可能如下:

SQL Exception: The statement was aborted because it would have caused

a duplicate key value in a unique or primary key constraint defined

on 'CUSTOMER(LAST_NAME)'.

at c8e.p.i._f1(Unknown Source)

at c8e.p.q._b84(Unknown Source)

at c8e.p.q.handleException(Unknown Source)

at c8e.p.n.handleException(Unknown Source)

at c8e.p.p.handleException(Unknown Source)

at c8e.p.j.executeStatement(Unknown Source)

at c8e.p.g.execute(Unknown Source)

at c8e.p.g.executeUpdate(Unknown Source)

at

RmiJdbc.RJPreparedStatementServer.executeUpdate(RJPreparedStatementServer.java:

74)

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at

sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java: 39)

at

sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)

at java.lang.reflect.Method.invoke(Method.java:324)

at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:261)

at sun.rmi.transport.Transport$1.run(Transport.java:148)

at java.security.AccessController.doPrivileged(Native Method)

at sun.rmi.transport.Transport.serviceCall(Transport.java:144)

at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:460)

at

sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:701)

at java.lang.Thread.run(Thread.java:534)

at

sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall

.java:247)

atsun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:223)

at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:133)

at RmiJdbc.RJPreparedStatementServer_Stub.executeUpdate(Unknown Source)

atRmiJdbc.RJPreparedStatement.executeUpdate(RJPreparedStatement.java:80)

at AccountDAO.insertCustomer(AccountDAO.java:69)

at CustomerAccountService.createAccount(CustomerAccountService.java:13)

at CustomerAccountService.createCustomer(CustomerAccountService.java:6)

at ShoppingService.processInput(ShoppingService.java:6)

at CommunicationWorkThread.run(CommunicationWorkThread.java:21)

at java.lang.Thread.run(Thread.java:534)

如前所述,它记录应用程序中执行停止的各个点。乍一看,上面的记录有些繁乱,但是,在阅读了下面的格式分析后,理解起来将非常容易。前几行记录抛出的异常:显示异常类的类型和异常的详细消息。接下来,栈跟踪记录代码中的停止点,各行显示被调用方法中执行停止一个位置,标明类、类中的方法名、与故障点对应的文件的行。一行行地往下看,执行从最内部的被调用方法逐渐上传到业务操作的起点。在本例中,业务操作由线程执行,故最后一项是线程的run方法。

在复杂的企业应用中,调用栈的级别非常多。在本例中,业务代码使用JDBC驱动程序与数据库通信,JDBC驱动程序是一个适配器,管理与DBMS的通信。调用栈中的大多数项与数据库驱动程序的内部代码对应。在调用栈底部,才能看到应用程序代码的故障点:insertCustomer是应用程序代码的第一个包含错误的方法。

在调试代码时,如果了解如何解释信息,则栈跟踪可能是一个极其有用的工具。例如,仔细分析前面示例的输出,可看出问题的很多方面。首先,抛出异常的原因是数据库插入将导致数据库的主键(具体地讲,是Customer表定义的last_name字段)重复。其次,应用程序代码的起点是insertCustomer方法。可以推出:之所以抛出异常,是因为账户的客户项已经存在。此后,可根据这个判断开发一个测试,并更正代码中的问题。

3.8  低级异常处理
在使用异常时,Java中究竟发生了什么?在代码中处理或声明异常时,实际在做什么?为回答这些问题,必须分析编译Java类时生成的字节码。Java提供了一个javap实用程序,这是一个剖析“反编译”类文件的通用工具,若使用正确切换运行,javap也可解释字节码。为正确理解字节码中不同异常处理选择的含义,需比较3种场景。

●       不产生异常的场景

●       声明异常的场景

●       处理异常的场景

为在一个统一代码示例中看到区别,将列举一个简单的非检测异常示例,它使用来自标准输入的参数,并试图用Integer.parseInt方法将其转换为整数。若将任何非整数值作为参数,该方法将生成NumberFormatException,这是一种RuntimeException类型的异常。

1  public class BytecodeExample{

2     public static void main(String [] args){

3       BytecodeExample app = new BytecodeExample();

4       if (args.length > 0){

5         app.readIntTryCatch(args[0]);

6         app.readIntDeclare(args[0]);

7            app.readInt(args[0]);

8       }

9     }

10

11     public int readInt(String input){

12       int returnValue = 0;

13       returnValue = Integer.parseInt(input);

14       return returnValue;

15     }

16

17     public int readIntTryCatch(String input){

18       int returnValue = 0;

19       try{

20          returnValue = Integer.parseInt(input);

21       }

22       catch(NumberFormatException exc){}

23       return returnValue;

24     }

25

26     public int readIntDeclare(String input) throws NumberFormatException{

27       int returnValue = 0;

28       returnValue = Integer.parseInt(input);

29       return returnValue;

30     }

31  }

在编译代码后,可通过执行下列命令,用解释性的字节码生成一个文本文件:

javap -c -verbose BytecodeExample > bytecodes.txt

该命令在BytecodeExample.class上运行javap实用程序,-c切换解释字节码,-verbose切换提供详细输出,并将输出保存到bytecodes.txt文件。运行这个命令,可看到如下结果:

Compiled from BytecodeExample.java

public class BytecodeExample extends java.lang.Object {

public BytecodeExample();

/* Stack=1, Locals=1, Args_size=1 */

public static void main(java.lang.String[]);

/* Stack=3, Locals=2, Args_size=1 */

public int readInt(java.lang.String);

/* Stack=1, Locals=3, Args_size=2 */

public int readIntTryCatch(java.lang.String);

/* Stack=1, Locals=4, Args_size=2 */

public int readIntDeclare(java.lang.String) throws

java.lang.NumberFormatException;

/* Stack=1, Locals=3, Args_size=2 */

}

Method int readInt(java.lang.String)

0 iconst_0

1 istore_2

2 aload_1

3 invokestatic #7 <Method int parseInt(java.lang.String)>

6 istore_2

7 iload_2

8 ireturn

Method int readIntTryCatch(java.lang.String)

0 iconst_0

1 istore_2

2 aload_1

3 invokestatic #7 <Method int parseInt(java.lang.String)>

6 istore_2

7 goto 14

10 astore_3

11 goto 14

14 iload_2

15 ireturn

Exception table:

From     to   target   type

2       7      10     <Class java.lang.NumberFormatException>

Method int readIntDeclare(java.lang.String)

0 iconst_0

1 istore_2

2 aload_1

3 invokestatic #7 <Method int parseInt(java.lang.String)>

6 istore_2

7 iload_2

8 ireturn

分析程序清单可知,直接解决NumberFormatException的方法有几点不同,这很有趣。readIntDeclare方法与标准readInt方法产生的字节码相同,但异常抛出语句在类的方法查找表中声明。在处理示例readIntTryCatch中,确实有几个附加的字节码指令,以及一个与该方法相关的异常表。若产生异常,异常表控制代码路由。此时,代码不加通告地处理异常,所以该方法的行为基本上与其他两个方法的行为相同。

也许有人会问,这会对性能产生什么影响?可以看到,处理或声明异常实际生成的字节码确有不同,那么,运行效率的成本有多高?其实,成本极低。可以形象地讲:您虽然去一家高级饭店赴约,但饭菜价格并不高!

为了粗略了解使用异常的相对成本,可用方便的剖析器(profiler)来运行BytecodeExample。剖析器不必过于复杂,只要能跟踪执行方法需要的时间即可。示例剖析器的源代码如下所示:

1  import java.util.Date;

2  import java.text.SimpleDateFormat;

3  public class Profile{

4     private Runtime runtime;

5     private long startTime, stopTime, timeElapsed;

6     private SimpleDateFormat sdf = new SimpleDateFormat("mm:ss.SSSS");

7     public Profile(){

8       runtime = Runtime.getRuntime();

9     }

10     public void startTimer(){

11       startTime = System.currentTimeMillis();

12     }

13     public void stopTimer(){

14       stopTime = System.currentTimeMillis();

15       timeElapsed += stopTime - startTime;

16     }

17     public void clearTimer(){

18       startTime = stopTime = timeElapsed = 0;

19     }

20     public long getStartTimeMillis(){

21       return startTime;

22     }

23     public long getStopTimeMillis(){

24       return stopTime;

25     }

26     public long getStartStopTimeMillis(){

27       return stopTime - startTime;

28     }

29       public long getTimeElapsedMillis(){

30         return timeElapsed;

31     }

32     public Date getStartTime(){

33       return new Date(startTime)

34     }

35     public Date getStopTime(){

36       return new Date(stopTime);

37    }

38     public Date getStartStopTime(){

39       return new Date(stopTime - startTime);

40     }

41     public Date getTimeElapsed(){

42       return new Date(stopTime);

43     }

44    public String getTimeElapsedAsString(){

45       StringBuffer buffer = new StringBuffer();

46        return sdf.format(new Date(timeElapsed));

47    }

48    public String getStartStopTimeAsString(){

49       StringBuffer buffer = new StringBuffer();

50       return sdf.format(new Date(stopTime - startTime));

51     }

52   }

为了解各个异常处理选项的相对成本,对于各个选项,运行100万次剖析器,则可看到如下输出:

1,000,000 iterations, no exception produced

Handle: 0.433 s total, 0.433 s/method call

Declare: 0.411 s total, 0.411 s/method call

No Handling: 0.414 s total, 0.414 s/method call

1,000,000 iterations, NumberFormatException produced

Handle: 10.93 s total, 10.93 s/method call

Declare: 10.52 s total, 10.52 s/method call

No Handling: 10.49 s total, 10.49 s/method call

由测试结果知,3个不同选项的性能区别并不明显。即使将相同代码执行很多次(如100万次),3种异常处理选择的性能差别也还是很小。但是,比较产生异常与未产生异常的情况,可以看到应用程序性能的较大差别。

这也就说,您应该遵循最佳编程实践,不必在有效代码和性能间折衷。解决异常的基本规则是成立的。在处理产生异常的代码时,原来的优先级保持不变:

●       尽量避免抛出异常。

●       如果条件允许就处理异常。

●       如果条件不允许就声明异常。






 
相关文章
 
页面功能  【加入收藏】 【推荐给朋友】 【字体:  】 【关闭】   


EasyJF.com 2006 隐私政策 使用EasyJF前必读