首頁  >  文章  >  Java  >  分享幾個Java異常處理的建議(收藏)

分享幾個Java異常處理的建議(收藏)

黄舟
黄舟原創
2017-06-18 09:47:401516瀏覽

Java提供了拋出異常、捕捉異常和finally語句的使用來處理程序異常,下面就來具體看一下關於Java異常處理的幾條建議

第1條: 只針對不正常的情況才使用異常

#建議:異常只應該被用於不正常的條件,它們永遠不應該被用於正常的控制流。

透過比較下面的兩份程式碼來說明。

程式碼1


try {
  int i=0;
  while (true) {
    arr[i]=0;
    i++;
  }
} catch (IndexOutOfBoundsException e) {
}



##
for (int i=0; i<arr.length; i++) {
  arr[i]=0;
}

兩份程式碼的作用都是遍歷arr數組,並設定數組中每個元素的值為0。代碼1的是透過異常來終止,看起來非常難懂,代碼2是透過數組邊界來終止。我們應該避免使用程式碼1這種方式,主要原因有三點:

  1.  異常機制的設計初衷是用於不正常的情況,所以很少會JVM實作試圖對它們的性能進行優化。所以,創建、拋出和捕獲異常的開銷是很昂貴的。

  2.  把程式碼放在try-catch中回傳阻止了JVM實作原本可能要執行的某些特定的最佳化。

  3.  對陣列進行遍歷的標準模式並不會導致冗餘的檢查,有些現代的JVM實作會將它們優化掉。

實際上,基於例外的模式比標準模式慢得多。測試程式碼如下:


public class Advice1 {

  private static int[] arr = new int[]{1,2,3,4,5};
  private static int SIZE = 10000;

  public static void main(String[] args) {

    long s1 = System.currentTimeMillis();
    for (int i=0; i<SIZE; i++)
      endByRange(arr);
    long e1 = System.currentTimeMillis();
    System.out.println("endByRange time:"+(e1-s1)+"ms" );

    long s2 = System.currentTimeMillis();
    for (int i=0; i<SIZE; i++)
      endByException(arr);
    long e2 = System.currentTimeMillis();
    System.out.println("endByException time:"+(e2-s2)+"ms" );
  }

  // 遍历arr数组: 通过异常的方式
  private static void endByException(int[] arr) {
    try {
      int i=0;
      while (true) {
        arr[i]=0;
        i++;
        //System.out.println("endByRange: arr["+i+"]="+arr[i]);
      }
    } catch (IndexOutOfBoundsException e) {
    }
  }

  // 遍历arr数组: 通过边界的方式
  private static void endByRange(int[] arr) {
    for (int i=0; i<arr.length; i++) {
      arr[i]=0;
      //System.out.println("endByException: arr["+i+"]="+arr[i]);
    }
  }
}

運行結果:


endByRange time:8ms
endByException time:16ms

結果說明:透過異常遍歷的速度比普通方式遍歷陣列慢很多!

第2條: 對於可恢復的條件使用被檢查的異常,對於程式錯誤使用運行時異常

  1.  運作時異常     -- RuntimeException類別及其子類別都稱為執行時例外。

  2.  被檢查的異常 -- Exception類別本身,以及Exception的子類別中除了"運行時異常"之外的其它子類別都屬於被檢查異常。

它們的差異是:Java編譯器會對"被檢查的異常"進行檢查,而對"運行時異常"不會檢查。也就是說,對於被檢查的異常,要么透過throws進行聲明拋出,要么通過try-catch進行捕獲處理,否則不能通過編譯。而對於運行時異常,倘若既"沒有透過throws聲明拋出它",也"沒有用try-catch語句捕獲它",還是會編譯通過。當然,雖然說Java編譯器不會檢查執行時間異常,但是,我們同樣可以透過throws對該異常進行說明,或透過try-catch進行擷取。

ArithmeticException(例如,除數為0),IndexOutOfBoundsException(例如,陣列越界)等都屬於執行時期例外。對於這種異常,我們應該透過修改程式碼來避免它的產生。而對於被檢查的異常,則可以透過處理讓程式恢復運作。例如,假設因為一個用戶沒有儲存足夠數量的前,所以他在企圖在一個收費電話上進行呼叫就會失敗;於是就將一個被檢查異常拋出。

第3條: 避免不必要的使用被檢查的異常

"被檢查的異常"是Java語言的一個很好的特性。與回傳代碼不同,"被檢查的異常"會強迫程式設計師處理例外的條件,大大提高了程式的可靠性。

但是,過度使用被檢查異常會使API使用起來非常不方便。如果一個方法拋出一個或多個被檢查的異常,那麼呼叫該方法的程式碼則必須在一個或多個catch語句區塊中處理這些異常,或者必須透過throws聲明拋出這些異常。 無論是透過catch處理,或是透過throws聲明拋出,都給程式設計師添加了不可忽略的負擔。

適用於"被檢查的異常"必須同時滿足兩個條件:第一,即使正確使用API​​並不能阻止異常條件的發生。第二,一旦產生了異常,使用API​​的程式設計師可以採取有用的動作對程式進行處理

第4條: 盡量使用標準的例外

#程式碼重用是值得提倡的,這是一個通用規則,異常也不例外。重複使用現有的例外有幾個好處:

第一,它讓你的API更容易學習和使用,因為它與程式設計師原來已經熟悉的習慣用法是一致的。

第二,對於使用這些API的程式而言,它們的可讀性更好,因為它們不會充斥著程式設計師不熟悉的例外。

第三,异常类越少,意味着内存占用越小,并且转载这些类的时间开销也越小。

虽然它们是Java平台库迄今为止最常被重用的异常,但是,在许可的条件下,其它的异常也可以被重用。例如,如果你要实现诸如复数或者矩阵之类的算术对象,那么重用ArithmeticException和NumberFormatException将是非常合适的。如果一个异常满足你的需要,则不要犹豫,使用就可以,不过你一定要确保抛出异常的条件与该异常的文档中描述的条件一致。这种重用必须建立在语义的基础上,而不是名字的基础上!

最后,一定要清楚,选择重用哪一种异常并没有必须遵循的规则。例如,考虑纸牌对象的情形,假设有一个用于发牌操作的方法,它的参数(handSize)是发一手牌的纸牌张数。假设调用者在这个参数中传递的值大于整副牌的剩余张数。那么这种情形既可以被解释为IllegalArgumentException(handSize的值太大),也可以被解释为IllegalStateException(相对客户的请求而言,纸牌对象的纸牌太少)。

第5条: 抛出的异常要适合于相应的抽象

如果一个方法抛出的异常与它执行的任务没有明显的关联关系,这种情形会让人不知所措。当一个方法传递一个由低层抽象抛出的异常时,往往会发生这种情况。这种情况发生时,不仅让人困惑,而且也"污染"了高层API。

为了避免这个问题,高层实现应该捕获低层的异常,同时抛出一个可以按照高层抽象进行介绍的异常。这种做法被称为"异常转译(exception translation)"。

例如,在Java的集合框架AbstractSequentialList的get()方法如下(基于JDK1.7.0_40):


public E get(int index) {
  try {
    return listIterator(index).next();
  } catch (NoSuchElementException exc) {
    throw new IndexOutOfBoundsException("Index: "+index);
  }
}

listIterator(index)会返回ListIterator对象,调用该对象的next()方法可能会抛出NoSuchElementException异常。而在get()方法中,抛出NoSuchElementException异常会让人感到困惑。所以,get()对NoSuchElementException进行了捕获,并抛出了IndexOutOfBoundsException异常。即,相当于将NoSuchElementException转译成了IndexOutOfBoundsException异常。

第6条: 每个方法抛出的异常都要有文档

要单独的声明被检查的异常,并且利用Javadoc的@throws标记,准确地记录下每个异常被抛出的条件。

如果一个类中的许多方法处于同样的原因而抛出同一个异常,那么在该类的文档注释中对这个异常做文档,而不是为每个方法单独做文档,这是可以接受的。

第7条: 在细节消息中包含失败 -- 捕获消息

简而言之,当我们自定义异常或者抛出异常时,应该包含失败相关的信息。

当一个程序由于一个未被捕获的异常而失败的时候,系统会自动打印出该异常的栈轨迹。在栈轨迹中包含该异常的字符串表示。典型情况下它包含该异常类的类名,以及紧随其后的细节消息。

第8条: 努力使失败保持原子性

当一个对象抛出一个异常之后,我们总期望这个对象仍然保持在一种定义良好的可用状态之中。对于被检查的异常而言,这尤为重要,因为调用者通常期望从被检查的异常中恢复过来。

一般而言,一个失败的方法调用应该保持使对象保持在"它在被调用之前的状态"。具有这种属性的方法被称为具有"失败原子性(failure atomic)"。可以理解为,失败了还保持着原子性。对象保持"失败原子性"的方式有几种:

(01) 设计一个非可变对象。

(02) 对于在可变对象上执行操作的方法,获得"失败原子性"的最常见方法是,在执行操作之前检查参数的有效性。如下(Stack.java中的pop方法):


public Object pop() {
  if (size==0)
    throw new EmptyStackException();
  Object result = elements[--size];
  elements[size] = null;
  return result;
}

(03) 与上一种方法类似,可以对计算处理过程调整顺序,使得任何可能会失败的计算部分都发生在对象状态被修改之前。 

(04) 编写一段恢复代码,由它来解释操作过程中发生的失败,以及使对象回滚到操作开始之前的状态上。

(05) 在对象的一份临时拷贝上执行操作,当操作完成之后再把临时拷贝中的结果复制给原来的对象。
虽然"保持对象的失败原子性"是期望目标,但它并不总是可以做得到。例如,如果多个线程企图在没有适当的同步机制的情况下,并发的访问一个对象,那么该对象就有可能被留在不一致的状态中。

即使在可以实现"失败原子性"的场合,它也不是总被期望的。对于某些操作,它会显著的增加开销或者复杂性。

总的规则是:作为方法规范的一部分,任何一个异常都不应该改变对象调用该方法之前的状态,如果这条规则被违反,则API文档中应该清楚的指明对象将会处于什么样的状态

第9条: 不要忽略异常

当一个API的设计者声明一个方法会抛出某个异常的时候,他们正在试图说明某些事情。所以,请不要忽略它!忽略异常的代码如下:


try {
  ...
} catch (SomeException e) {
}

空的catch块会使异常达不到应有的目的,异常的目的是强迫你处理不正常的条件。忽略一个异常,就如同忽略一个火警信号一样 -- 若把火警信号器关闭了,那么当真正的火灾发生时,就没有人看到火警信号了。所以,至少catch块应该包含一条说明,用来解释为什么忽略这个异常是合适的。

以上是分享幾個Java異常處理的建議(收藏)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn