程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 技巧:當不能拋出異常時

技巧:當不能拋出異常時

編輯:關於JAVA

checked 異常的一個問題是,有時候不允許拋出這樣的異常。特別是,如果要覆蓋超類中聲明的方法,或者實現接口中聲明的方法,而那個方法沒有聲明任何 checked 異常,那麼新的實現也不能聲明 checked 異常。因此必須預先處理異常。另外,可以將異常轉換為運行時異常,或者繞過它而不處理它。但是,應該這樣做嗎,這其中是否隱藏著錯誤?

問題

只要看一個例子,問題就清楚了。假設有一個 File 對象的 List,需要按它們的標准路徑以字典順序排序。所謂標准路徑,是指在解析別名、符號鏈接和 /../ 及 /./ 之後得到的完整絕對路徑。本地方法使用一個比較器,如清單 1 所示:

清單 1. 按標准路徑比較兩個文件

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

public class FileComparator implements Comparator<File> {

   public int compare(File f1, File f2) {
     return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
   }

   public static void main(String[] args) {
    ArrayList<File> files = new ArrayList<File>();
    for (String arg : args) {
      files.add(new File(arg));
    }
    Collections.sort(files, new FileComparator());
    for (File f : files) {
      System.out.println(f);
    }
   }

}

不幸的是,該代碼不能通過編譯。問題在於,getCanonicalPath() 方法拋出一個 IOException,因為它需要訪問文件系統。通常,當使用 checked 異常時,可以使用以下兩種方法之一:

將出錯的代碼包裝在一個 try 塊中,並捕捉拋出的異常。

聲明包裝方法(本例為 compare())也拋出 IOException。

通常,至於選擇何種方法,取決於是否能在拋出異常時合理地處理異常。如果能,那麼使用 try-catch 塊。如果不能,那麼聲明包裝方法本身拋出異常。不幸的是,這兩種技巧對於本例都不管用。

在 compare() 方法中無法合理地處理 IOException。從技術上講,似乎可以做到 — 即返回 0、1 或 -1,如清單 2 所示:

清單 2. 拋出異常時返回一個默認值

public int compare(File f1, File f2) {
   try {
     return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
   }
   catch (IOException ex) {
     return -1;
   }
}

然而,這違反了 compare() 方法的約定,因為它不是一個穩定的結果。對於相同的對象,前後兩次調用可能產生不同的結果。如果使用這個比較器來排序,那麼意味著最終列表沒有被正確排序。所以現在試試第 2 個選項 — 聲明 compare() 拋出 IOException:

public int compare(File f1, File f2) throws IOException {
   return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
}

這也不能通過編譯。因為 checked 異常是方法簽名的一部分,在覆蓋方法時,不能增加 checked 異常,就像不能改變 return 類型一樣。那麼最後還剩下一個折中選項:在 compare() 中捕捉異常,將它轉換成運行時異常,然後拋出運行時異常,如清單 3 所示:

清單 3. 將 checked 異常轉換成運行時異常

public int compare(File f1, File f2) {
   try {
     return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
   }
   catch (IOException ex) {
     throw new RuntimeException(ex);
   }
}

不幸的是,雖然這樣可以通過編譯,但是這種方法也不管用,其原因較為微妙。Comparator 接口定義一個合約。這個合約不允許該方法拋出運行時異常(防止因違反泛型類型安全而成為調用代碼中的 bug)。使用這個比較器的方法合理地依靠它來比較兩個文件,而不拋出任何異常。它們沒有准備好處理 compare() 中意外出現的異常。

正是由於這個微妙的原因,讓運行時異常成為代碼要處理的外部狀況是一個壞主意。這樣只是逃避問題,並沒有真正處理問題。不處理異常所帶來的不良後果仍然存在,包括毀壞數據和得到不正確的結果。

這樣便陷入了困境。既不能在 compare() 內真正有效地處理異常,又不能在 compare() 之外處理異常。還剩下什麼地方可以處理異常 — System.exit()? 惟一正確的辦法是完全避免這種困境。幸運的是,至少有兩種方法可以做到這一點。

將問題一分為二

第一種辦法是將問題一分為二。比較本身不會導致異常。比較的只是字符串而已。通過標准路徑將文件轉換成字符串才會導致異常。如果將可能拋出異常的操作與不會拋出異常的操作分開,那麼問題就更容易處理了。也就是說,首先將所有文件對象轉換為字符串,然後通過字符串比較器(甚至可以通過 java.lang.String 的自然排序)對字符串排序,最後使用排序後的字符串列表對原始的文件列表排序。這種方法不太直接,但是優點是在列表被改變之前就拋出 IOException。如果出現異常,它只會出現在預先設計好的地方,不會造成損害,調用代碼可以指定如何處理異常。清單 4 對此作了演示:

清單 4. 先讀取,然後排序

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;

public class FileComparator {

   private static ArrayList<String> getCanonicalPaths(ArrayList<File> files)
       throws IOException {
     ArrayList<String> paths = new ArrayList<String>();
     for (File file : files) paths.add(file.getCanonicalPath());
     return paths;
   }

   public static void main(String[] args) throws IOException {
    ArrayList<File> files = new ArrayList<File>();
    for (String arg : args) {
      files.add(new File(arg));
    }

    ArrayList<String> paths = getCanonicalPaths(files);

    // to maintain the original mapping 
    HashMap<String, File> map = new HashMap<String, File>();
    int i = 0;
    for (String path : paths) {
      map.put(path, files.get(i));
      i++;
    }

    Collections.sort(paths);
    files.clear();
    for (String path : paths) {
      files.add(map.get(path));
    }
   }

}

清單 4 並沒有消除出現 I/O 錯誤的可能性。這一點無法做到,因為這裡的代碼無力提供這樣的功能。但是,可以將這個問題交給更合適的地方來處理。

避免問題

前面提到的方法有點復雜,所以我建議另一種方法:不使用內置的 compare() 函數或 Collections.sort()。使用這樣的函數也許比較方便,但是不適合當前情況。Comparable 和 Comparator 是為確定的、可預測的比較操作而設計的。一旦 I/O 不再符合這種情況,很可能常用的算法和接口變得不適用。即使勉強可以使用,其效率也極其低下。

例如,假設不是按標准路徑來比較文件,而是按內容來比較文件。對於所比較的兩個文件,每個比較操作都需要讀文件的內容 — 甚至可能是完整的內容。這樣一來,高效的算法會想要盡量減少讀的次數,並且可能會想緩存每次讀的結果 — 或者,如果文件較大,則可能緩存每個文件的 hashcode — 而不是每次比較時重新讀每個文件。同樣,您會想到首先填充一個比較鍵列表,然後進行排序,而不是進行內聯排序。

可以想象定義一個單獨的、並行的 IOComparator 接口,該接口拋出必要的異常,如清單 5 所示:

清單 5. 獨立的 IOComparator 接口

import java.io.IOException;

public interface IOComparator<T> {

   int compare(T o1, T o2) throws IOException;

}

然後基於這個類定義一個單獨的、相近實用程序樹,由它對集合的臨時副本進行必要的操作,從而允許拋出異常,同時又不會使數據結構處於可能受損害的、中間的狀態。例如,清單 6 提供了一個基本的冒泡排序:

清單 6. 用冒泡算法對文件排序

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class IOSorter {

   public static <T> void sort(List<T> list, IOComparator<? super T> comparator)
    throws IOException {
     List<T> temp = new ArrayList<T>(list.size());
     temp.addAll(list);

     bubblesort(temp, comparator);

     // copy back to original list now that no exceptions have been thrown 
     list.clear();
     list.addAll(temp);
   }

   // of course you can replace this with a better algorithm such as quicksort 
   private static <T> void bubblesort(List<T> list, IOComparator<? super T> comparator)
    throws IOException {
     for (int i = 1; i < list.size(); i++) {
       for (int j = 0; j < list.size() - i; j++) {
         if (comparator.compare(list.get(j), list.get(j + 1)) > 0) {
           swap(list, j);
         }
       }
     }
   }

   private static <T> void swap(List<T> list, int j) {
     T temp = list.get(j);
     list.set(j, list.get(j+1));
     list.set(j + 1, temp);
   }

}

這不是唯一的方法。為了清晰,清單 6 有意模仿已有的 Collections.sort() 方法;但是,也許更有效的方法是返回一個新的列表,而不是直接修改舊列表,以防在修改列表時拋出異常所帶來的損害。

最終,您實際上承認並著手處理可能出現的 I/O 錯誤,而不是逃避它,您甚至可以做更高級的錯誤修正。例如,IOComparator 也許不會被一次 I/O 錯誤難倒 — 因為很多 I/O 問題是暫時的 — 可以重試幾次,如清單 7 所示:

清單 7. 如果一開始不成功,再試幾次(但是別試太多次)

import java.io.File;
import java.io.IOException;

public class CanonicalPathComparator implements IOComparator<File> {

   @Override 
   public int compare(File f1, File f2) throws IOException {
     for (int i = 0; i < 3; i++) {
       try {
         return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
       }
       catch (IOException ex) {
         continue;
       }
     }
     // last chance 
     return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
   }

}

這種技巧不能解決常規的 Comparator 的問題,因為必須重試無數次才能避免拋出異常,而且很多 I/O 問題並不是暫時性的。

checked 異常是壞主意嗎?

如果 java.io.IOException 是運行時異常,而不是 checked 異常,問題是不是有所改觀?答案是否定的。如果 IOException 擴展 RuntimeException 而不是 java.lang.Exception,那麼更容易編寫出有 bug 的、不正確的代碼,這種代碼忽略了真正可能發生的 I/O 錯誤,而在運行時出人意料地失敗。

然而,編寫正確的、有准備並且能夠處理 I/O 錯誤的代碼並不會更容易。是的,相對於不會出現意外 I/O 錯誤,不需要為此做准備的情況,這種方法更加復雜。但是,從 Java 語言中消除 checked 異常無助於我們實現那樣的理想情況。I/O 錯誤和其他環境問題是常態,積極准備比視而不見要好得多。

總之,checked 異常作為方法簽名的一部分並非沒有道理。當您發現自己想要從一個方法拋出一個 checked 異常,而這又是不允許的 — 因而抑制本不該抑制的異常 — 那麼回過頭來,重新組織一下,考慮為什麼一開始要覆蓋那個方法。很可能,您本應該采取完全不同的方式。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved