Error Handling with Exceptions【5】

The restriction on exceptions does not apply to constructors. You can see in StormyInning that a constructor can throw anything it wants, regardless of what the base-class constructor throws. However, since a base-class constructor must always be called one way or another (here, the default constructor is called automatically), the derived-class constructor must declare any base-class constructor exceptions in its exception specification. Note that a derived-class constructor cannot catch exceptions thrown by its base-class constructor.

對於異常的約束在構造方法上是無效的,你可以看到在StormyInning中它的構造方法可以拋出任何它想拋出的異常,不管它的基類的構造方法拋出什麼異常信息。單位因爲基類的構造方法總是會通過一種或者其它的方式被調用,這裏默認的構造方法是自動調用的,所以派生類的構造方法必須聲明它的基類構造方法拋出的異常。注意的是派生類的構造方法不能捕獲它的基類的構造方法拋出的異常信息的。

The reason StormyInning.walk( ) will not compile is that it throws an exception, but Inning.walk( ) does not. If this were allowed, then you could write code that called Inning.walk( ) and that didn’t have to handle any exceptions, but then when you substituted an object of a class derived from Inning, exceptions would be thrown so your code would break. By forcing the derived-class methods to conform to the exception specifications of the base-class methods, substitutability of objects is maintained.

StormyInning.walk()之所以不能編譯通過是因爲它會拋出異常,但是Inning.walk()並沒有拋出異常。如果這被允許的話,當你寫代碼調用Inning.walk()的時候就不會作異常處理了,但是當你使用這個類的派生類調用的時候則會拋出異常,那麼程序就終止了。通過強制派生類遵守基類的異常聲明,這樣對象的互換就可以得到保證。

The overridden event( ) method shows that a derived-class version of a method may choose not to throw any exceptions, even if the base-class version does. Again, this is fine since it doesn’t break any code that is written—assuming the base-class version throws exceptions. Similar logic applies to atBat( ), which throws PopFoul, an exception that is derived from Foul thrown by the base-class version of atBat( ). This way, if you write code that works with Inning and calls atBat( ), you must catch the Foul exception. Since PopFoul is derived from Foul, the exception handler will also catch PopFoul.

而覆寫的event()這個方法則表明派生類的某個方法可以選擇不去拋出異常,甚至它的基類中這個方法拋出了異常。這是因爲即使基類拋出異常也不會影響已有的程序。atBat()也是同樣的道理。這個異常信息是繼承的基類中atBat()方法拋出的異常。這樣如果你使用Inning調用atBat()的話,你必須要捕獲Foul異常。因爲PopFoul是繼承的Foul,這樣異常處理程序同樣也可以捕獲PopFoul

The last point of interest is in main( ). Here, you can see that if you’re dealing with exactly a StormyInning object, the compiler forces you to catch only the exceptions that are specific to that class, but if you upcast to the base type, then the compiler (correctly) forces you to catch the exceptions for the base type. All these constraints produce much more robust exception-handling code

我們最後的一個關注點就是main()方法了,這裏你可以看到如果你處理的是一個StormyInning對象的話,編譯器會強制要求你值捕獲這個類拋出的異常,但是如果你上傳轉化爲基類的對象,這時編譯器就要求你捕獲基類的異常信息。這些異常限制使得你的異常處理程序更加強壯。

It’s useful to realize that although exception specifications are enforced by the compiler during inheritance, the exception specifications are not part of the type of a method, which comprises only the method name and argument types. Therefore, you cannot overload methods based on exception specifications. In addition, just because an exception specification exists in a base-class version of a method doesn’t mean that it must exist in the derived-class version of the method. This is quite different from inheritance rules, where a method in the base class must also exist in the derived class. Put another way, the “exception specification interface” for a particular method may narrow during inheritance and overriding, but it may not widen—this is precisely the opposite of the rule for the class interface during inheritance.

在繼承中儘管編譯器會對異常處理作出強制要求,異常處理並不是方法類型的一部分,它是由方法名稱和參數類型組成的。因此你不能通過異常處理來實現重載。換一個說法來說,僅僅因爲在基類中的方法中使用了異常處理程序就必須要在派生類中的這個方法拋出這個異常。這和繼承的規則是有很大的不同的。換一種說法,異常處理程序接口通過繼承和覆寫會變得越來越小這和繼承中接口的規則是正好相反的。

Constructors

When writing code with exceptions, it’s particularly important that you always ask “If an exception occurs, will this be properly cleaned up?” Most of the time you’re fairly safe, but in constructors there’s a problem. The constructor puts the object into a safe starting state, but it might perform some operation—such as opening a file—that doesn’t get cleaned up until the user is finished with the object and calls a special cleanup method. If you throw an exception from inside a constructor, these cleanup behaviors might not occur properly. This means that you must be especially diligent while you write your constructor.

當你使用異常信息來編寫程序的時候,你可能會問“如果發生異常的話會被合理的清理掉嗎”,這個問題很重要,大部分時間來看都是安全的,但是構造方法是個問題。構造方法把對象放到一個安全的起始狀態,但是它可能會執行一些操作-例如打開文件-直到操作員對這個對象的操作結束了並且調用一個特殊的清理的方法之前它是不會得到清理的。如果你在構造方法內拋出了一個異常,那這些清理的方法就可能得不到正常的執行了。

Since you’ve just learned about finally, you might think that it is the correct solution. But it’s not quite that simple, because finally performs the cleanup code every time, even in the situations in which you don’t want the cleanup code executed until the cleanup method runs. Thus, if you do perform cleanup in finally, you must set some kind of flag when the constructor finishes normally so that you don’t do anything in the finally block if the flag is set. Because this isn’t particularly elegant (you are coupling your code from one place to another), it’s best if you try to avoid performing this kind of cleanup in finally unless you are forced to.

因爲你剛剛學習了finally,所以你可能想這是正確的解決辦法。但是遠遠不是那麼簡單,因爲finally每次都會調用cleanup方法,甚至於在你不希望執行的時候它也會執行。因此,如果你使用finally來執行清理的話,你必須要在構造方法正常執行結束之後設置一個標誌位,這樣的話你就可以根據標誌位的值在finally中什麼也不作。但是這不是明智的做法,這樣造成你的代碼之間的耦合增高。所以如果不是必須的話要儘量的去避免使用finally來執行清理工作。

In the following example, a class called InputFile is created that opens a file and allows you to read it one line (converted into a String) at a time. It uses the classes FileReader and BufferedReader from the Java standard I/O library that will be discussed in Chapter 12, but which are simple enough that you probably won’t have any trouble understanding their basic use:

在接下來的例子中,一個InputFile的類來打開一個文件並且讀取一行轉化爲String型,使用了Java標準I/O庫中建立了FileReaderBufferedReader,這些將會在第12章中講解到,但是很簡單,不會影響你理解它們的基本的使用。

//: c09:Cleanup.java

// Paying attention to exceptions in constructors.

import com.bruceeckel.simpletest.*;

import java.io.*;

 

class InputFile {

  private BufferedReader in;

  public InputFile(String fname) throws Exception {

    try {

      in = new BufferedReader(new FileReader(fname));

      // Other code that might throw exceptions

    } catch(FileNotFoundException e) {

      System.err.println("Could not open " + fname);

      // Wasn't open, so don't close it

      throw e;

    } catch(Exception e) {

      // All other exceptions must close it

      try {

        in.close();

      } catch(IOException e2) {

        System.err.println("in.close() unsuccessful");

      }

      throw e; // Rethrow

    } finally {

      // Don't close it here!!!

    }

  }

  public String getLine() {

    String s;

    try {

      s = in.readLine();

    } catch(IOException e) {

      throw new RuntimeException("readLine() failed");

    }

    return s;

  }

  public void dispose() {

    try {

      in.close();

      System.out.println("dispose() successful");

    } catch(IOException e2) {

      throw new RuntimeException("in.close() failed");

    }

  }

}

 

 

public class Cleanup {

  private static Test monitor = new Test();

  public static void main(String[] args) {

    try {

      InputFile in = new InputFile("Cleanup.java");

      String s;

      int i = 1;

      while((s = in.getLine()) != null)

        ; // Perform line-by-line processing here...

      in.dispose();

    } catch(Exception e) {

      System.err.println("Caught Exception in main");

      e.printStackTrace();

    }

    monitor.expect(new String[] {

      "dispose() successful"

    });

  }

} 

The constructor for InputFile takes a String argument, which is the name of the file you want to open. Inside a try block, it creates a FileReader using the file name. A FileReader isn’t particularly useful until you turn around and use it to create a BufferedReader that you can actually talk to—notice that one of the benefits of InputFile is that it combines these two actions.

InputFile類的構造方法使用了一個字符串作爲參數,該參數意義爲你希望打開的文件名。在Try中使用參數創建了一個FileReader類。FileReader並不是很有用,你只是用它去創建一個BufferedReader,而InputFile做的好的地方就是把這兩步結合起來了。

If the FileReader constructor is unsuccessful, it throws a FileNotFoundException, which must be caught separately. This is the one case in which you don’t want to close the file, because it wasn’t successfully opened. Any other catch clauses must close the file because it was opened by the time those catch clauses are entered. (Of course, this is trickier if more than one method can throw a FileNotFoundException. In that case, you might want to break things into several try blocks.) The close( ) method might throw an exception so it is tried and caught even though it’s within the block of another catch clause—it’s just another pair of curly braces to the Java compiler. After performing local operations, the exception is rethrown, which is appropriate because this constructor failed, and you wouldn’t want the calling method to assume that the object had been properly created and was valid.

如果FileReader的構造方法沒有成功,它會拋出一個FileNotFoundException的異常信息,這個是必須要捕獲的異常信息。這是其中的一種情況因爲文件並沒有成功的打開所以不需要關閉這個文件。而其它的捕獲異常中則必需要關閉這個文件,因爲進入到其它的異常中的時候這個文件已經被打開了。(當然如果還有其它的方法也拋出這個異常信息的話這就有點投機取巧的意思了,如果是那種情況的話,你可能會想把代碼拆分到幾個Try中去)。Close()方法可能也會拋出異常信息所以也對其進行了trycatch操作。儘管它們在另外一個trycatchcatch模塊中,那對於編譯器而言只是一對花括號而已。經過這層操作之後,異常再次被拋出,這是很合理的,因爲構造方法失敗了,而你又不希望這個方法被誤認爲這個對象成功的創建了並且是有效的吧。

In this example, which doesn’t use the aforementioned flagging technique, the finally clause is definitely not the place to close( ) the file, since that would close it every time the constructor completed. Because we want the file to be open for the useful lifetime of the InputFile object, this would not be appropriate.

在上面這個例子中,並沒有使用我們前面提到的標誌位的技術,因爲finally語句中並不是關閉這個文件的位置。因爲那樣的話它會每次在構造方法執行完畢後調用。因爲我們希望在InputFile對象的整個聲明週期這個文件一直是打開的,這麼做顯然並不是合適的。

The getLine( ) method returns a String containing the next line in the file. It calls readLine( ), which can throw an exception, but that exception is caught so getLine( ) doesn’t throw any exceptions. One of the design issues with exceptions is whether to handle an exception completely at this level, to handle it partially and pass the same exception (or a different one) on, or whether to simply pass it on. Passing it on, when appropriate, can certainly simplify coding. In this situation, the getLine( ) method converts the exception to a RuntimeException to indicate a programming error.

getLine()方法返回了一個字符串,它包含了文件中的一行。它調用了readLine(),這個方法可能會拋出異常,但是這個異常已經被捕獲了所以getLine()不需要再拋出異常。關於異常的一個設計的概念就是將異常信息在這一層拋出,並且將同樣的或者不同的異常再次拋出,或者直接拋出,如果合理操作的話可以在一定程度上簡化代碼。在這種情況下getLine()方法將異常信息轉化爲了RuntimeException來標識程序錯誤。

The dispose( ) method must be called by the user when finished using the InputFile object. This will release the system resources (such as file handles) that are used by the BufferedReader and/or FileReader objects. You don’t want to do this until you’re finished with the InputFile object, at the point you’re going to let it go. You might think of putting such functionality into a finalize( ) method, but as mentioned in Chapter 4, you can’t always be sure that finalize( ) will be called (even if you can be sure that it will be called, you don’t know when). This is one of the downsides to Java; All cleanup—other than memory cleanup—doesn’t happen automatically, so you must inform the client programmer that they are responsible, and possibly guarantee that cleanup occurs using finalize( ).

InputFile對象使用完畢之後用戶必需要調用dispose()方法,這個方法會釋放BufferedReader或者FileReader使用的系統資源例如文件句柄,在你使用完畢InputFile對象之前你不需要作這些。你可能在想將這些方法放入到finalize()方法中,但是在第四章中我們提到過你不能總是認爲finalize()這個方法會被調用(就算你確信它會被調用,但你也不知道何時被調用)。這就是Java的另一面,除了內存清理之外的清理工作都不是自動發生的,所以你必需要告訴客戶端程序員他們有責任而且必需保證在使用finalize()方法的時候必須要調用清理的動作。

In Cleanup.java an InputFile is created to open the same source file that creates the program, the file is read in a line at a time, and line numbers are added. All exceptions are caught generically in main( ), although you could choose greater granularity.

Cleanup.javaInputFile被創建來打開創建這個程序的源碼,這個文件被讀取一行,並且加上了行號。雖然你可以選擇更加細緻的方法,但是所有的異常信息還是在main()方法被捕獲的。

One of the benefits of this example is to show you why exceptions are introduced at this point in the book—there are many libraries (like I/O, mentioned earlier) that you can’t use without dealing with exceptions. Exceptions are so integral to programming in Java, especially because the compiler enforces them, that you can accomplish only so much without knowing how to work with them.

這裏例子中的一個優點就是告訴你爲什麼要介紹異常,在本書中涉及到的很多類庫如果你不處理異常的話根本沒辦法使用。異常信息是Java中密不可分的,特別是編譯器也對這些有強制的要求,如果你對異常信息一點都不瞭解的話那麼也只能到此爲止了。

Exception matching

When an exception is thrown, the exception handling system looks through the “nearest” handlers in the order they are written. When it finds a match, the exception is considered handled, and no further searching occurs.

當一個異常信息被拋出的時候,異常處理程序會按照它們書寫的順序先查找距離最近的異常處理程序。當找到匹配的後,異常就被認爲處理了,其它的就不再繼續尋找了。

Matching an exception doesn’t require a perfect match between the exception and its handler. A derived-class object will match a handler for the base class, as shown in this example:

異常信息的匹配並不需要將異常和它的異常處理程序中的異常完全的匹配,派生類的對象和基類的對象是可以匹配的,就像下面展現的例子一樣。

//: c09:Human.java

// Catching exception hierarchies.

import com.bruceeckel.simpletest.*;

 

class Annoyance extends Exception {}

class Sneeze extends Annoyance {}

 

public class Human {

  private static Test monitor = new Test();

  public static void main(String[] args) {

    try {

      throw new Sneeze();

    } catch(Sneeze s) {

      System.err.println("Caught Sneeze");

    } catch(Annoyance a) {

      System.err.println("Caught Annoyance");

    }

    monitor.expect(new String[] {

      "Caught Sneeze"

    });

  }

} 

 

The Sneeze exception will be caught by the first catch clause that it matches, which is the first one, of course. However, if you remove the first catch clause, leaving only:

因爲匹配了第一個異常,Sneeze異常信息會被捕獲,這是第一個異常,但是如果刪除這個異常匹配語句的話,只是剩下

    try {

      throw new Sneeze();

    } catch(Annoyance a) {

      System.err.println("Caught Annoyance");

    }

the code will still work because it’s catching the base class of Sneeze. Put another way, catch(Annoyance e) will catch an Annoyance or any class derived from it. This is useful because if you decide to add more derived exceptions to a method, then the client programmer’s code will not need changing as long as the client catches the base class exceptions.

程序仍然會繼續運行,因爲它可以捕獲Sneeze的基類異常,換一種說法,捕獲Annoyance異常的異常處理程序會捕獲Annoyance或者它的任何一個派生類的異常。這是很有用的,因爲你可以爲這個方法增加更多的派生類異常信息,這樣如果客戶端程序捕獲基類的異常信息的話代碼就不需要作任何修改了。

If you try to “mask” the derived-class exceptions by putting the base-class catch clause first, like this:

如果你試圖將基類的異常捕獲語句放置在最前面,來捕獲它的派生類的異常的話,像這樣:

    try {

      throw new Sneeze();

    } catch(Annoyance a) {

      System.err.println("Caught Annoyance");

    } catch(Sneeze s) {

      System.err.println("Caught Sneeze");

    }

 

the compiler will give you an error message, since it sees that the Sneeze catch-clause can never be reached.

這樣編譯器會提示一個錯誤信息,因爲它認爲Sneeze的異常處理程序永遠不會被執行。

Alternative approaches

An exception-handling system is a trap door that allows your program to abandon execution of the normal sequence of statements. The trap door is used when an “exceptional condition” occurs, such that normal execution is no longer possible or desirable. Exceptions represent conditions that the current method is unable to handle. The reason exception handling systems were developed is because the approach of dealing with each possible error condition produced by each function call was too onerous, and programmers simply weren’t doing it. As a result, they were ignoring the errors. It’s worth observing that the issue of programmer convenience in handling errors was a prime motivation for exceptions in the first place.

異常處理程序是允許你不按照正常的順序的一扇暗門。這經常被用來當異常發生的時候,正常的執行的話已經是不可能或者不合理的了。異常描述的是這些方法已經不能繼續處理了,而異常處理程序產生的原因就是對每個方法調用都作錯誤檢查實在是太費力了,而程序員也不這麼作,而最後的結果就是他們忽略了這些錯誤信息。需要注意的是異常處理程序產生的初衷就是爲了簡化程序員的工作。

One of the important guidelines in exception handling is “don’t catch an exception unless you know what to do with it.” In fact, one of the important goals of exception handling is to move the error-handling code away from the point where the errors occur. This allows you to focus on what you want to accomplish in one section of your code, and how you’re going to deal with problems in a distinct separate section of your code. As a result, your mainline code is not cluttered with error-handling logic, and it’s much easier to understand and maintain.

異常處理程序的最重要的一個原則就是:除非你知道你要對這個異常作什麼否則的話就不要捕獲這個異常信息。事實上,異常處理程序最重要的一個目的就是異常處理部分的代碼與異常發生地分離開來,這樣的話你可以在某個位置將工作的重點集中到這裏要完成什麼工作,而處理處理這個異常信息解決部分則放到另外一個地方。結果就是你代碼不會因爲異常處理邏輯而混亂,也很容易理解和維護。

Checked exceptions complicate this scenario a bit, because they force you to add catch clauses in places where you may not be ready to handle an error. This results in the “harmful if swallowed” problem:

Checked exceptions使得程序複雜了一些,因爲要求即使你沒準備好捕獲錯誤異常,也強制你增加catch語句,那麼這麼異常可能被私吞了。

try {

  // ... to do something useful

} catch(ObligatoryException e) {} // Gulp!

Programmers (myself included, in the first edition of this book) would just do the simplest thing, and swallow the exception—often unintentionally, but once you do it, the compiler has been satisfied, so unless you remember to revisit and correct the code, the exception will be lost. The exception happens, but it vanishes completely when swallowed. Because the compiler forces you to write code right away to handle the exception, this seems like the easiest solution even though it’s probably the worst thing you can do.

程序員們都會作很簡單的東西,私吞這些異常信息但是經常不是故意這麼作的,但是一旦這麼作了,編譯器是不會提示問題的,所以除非你記得回過頭來修改完善這段代碼,否則這個異常信息將被丟掉。這個異常發生了,但是當被私吞是就消失的無影無蹤了。因爲編譯器要求你必須編寫代碼來處理這個異常信息,儘管這是最壞的辦法但是這看起來確實是最簡單的辦法了。

Horrified upon realizing that I had done this, in the second edition I “fixed” the problem by printing the stack trace inside the handler (as is still seen—appropriately—in a number of examples in this chapter). While this is useful to trace the behavior of exceptions, it still indicates that you don’t really know what to do with the exception at that point in your code. In this section we’ll look at some of the issues and complications arising from checked exceptions, and options that you have when dealing with them.

當我意識到我犯了一個錯誤的時候我嚇了一大跳,在第二個版本中我在異常處理程序中打印堆棧的軌跡來修復了這個問題,本章中還是有很多的例子採用了這個辦法這是很合理的,它可以很好的記錄異常發生的行爲軌跡,但是你可能還是不知道應該如何處理這個異常。在本小節中你可以來看看checked exception所帶來的併發症以及問題,以及通過什麼方法來解決這個問題。

This topic seems simple. But it is not only complicated, it is also an issue of some volatility. There are people who are staunchly rooted on either side of the fence and who feel that the correct answer (theirs) is blatantly obvious. I believe the reason for one of these positions is the distinct benefit seen in going from a poorly-typed language like pre-ANSI C to a strong, statically-typed language (that is, checked at compile-time) like C++ or Java. When you make that transition (as I did), the benefits are so dramatic that it can seem like strong static type checking is always the best answer to most problems. My hope is to relate a little bit of my own evolution, that has brought the absolute value of strong static type checking into question; clearly, it’s very helpful much of the time, but there’s a fuzzy line we cross when it begins to get in the way and become a hindrance (one of my favorite quotes is: “All models are wrong. Some are useful.”).

這個話題看起來很簡單,但實際上還不僅僅是複雜,更重要的是它還非常多變。總有人會頑固的堅持自己的立場,聲明正確的答案是不言自明的。我覺得之所以有這種論點,是因爲我們所使用的工具已經不是“ANSI標準出臺前C那樣的弱類型語言了”,而是像C++JAVA這樣強靜態類型語言,也就是編譯時就作類型檢查的語言,這是前者所無法比你的。當你港開始這個轉變的時候,會發現它帶來的好處是那樣生動,好像類型檢查能一勞永逸解決所有的問題。這裏,我想結合自己的認識過程,告訴你我是怎樣從對類型檢查的絕對迷信變成懷疑的;當然,很多的時候還是非常有用的,但是當它阻擋了我們的去路並且稱爲的時候,我們就得跨過去。只是這條界限並不是很清晰,我最喜歡的一句格言就是“所有的模型都是錯誤的,但是有些是能用的”。

History

Exception handling originated in systems like PL/1 and Mesa, and later appeared in CLU, Smalltalk, Modula-3, Ada, Eiffel, C++, Python, Java, and the post-Java languages Ruby and C#. The Java design is similar to C++, except in places where the Java designers felt that the C++ design caused problems.

To provide programmers with a framework that they were more likely to use for error handling and recovery, exception handling was added to C++ rather late in the standardization process, promoted by Bjarne Stroustrup, the language’s original author. The model for C++ exceptions came primarily from CLU. However, other languages existed at that time that also supported exception handling: Ada, Smalltalk (both of which had exceptions but no exception specifications) and Modula-3 (which included both exceptions and specifications).

In their seminal paper[44] on the subject, Liskov and Snyder note that a major defect of languages like C that report errors in a transient fashion is that:

“...every invocation must be followed by a conditional test to determine what the outcome was. This requirement leads to programs that are difficult to read, and probably inefficient as well, thus discouraging programmers from signaling and handling exceptions.”

Note that one of the original motivations of exception handling was to prevent this requirement, but with checked exceptions in Java we commonly see exactly this kind of code. They go on to say:

“...requiring that the text of a handler be attached to the invocation that raises the exception would lead to unreadable programs in which expressions were broken up with handlers.”

Following the CLU approach when designing C++ exceptions, Stroustrup stated that the goal was to reduce the amount of code required to recover from errors. I believe that he was observing that programmers were typically not writing error-handling code in C because the amount and placement of such code was daunting and distracting. As a result, they were used to doing it the C way, ignoring errors in code and using debuggers to track down problems. To use exceptions, these C programmers had to be convinced to write “additional” code that they weren’t normally writing. Thus, to draw them into a better way of handling errors, the amount of code they would need to “add” must not be onerous. I think it’s important to keep this goal in mind when looking at the effects of checked exceptions in Java.

C++ brought an additional idea over from CLU: the exception specification, to programmatically state in the method signature what exceptions may result from calling that method. The exception specification really has two purposes. It can say “I’m originating this exception in my code, you handle it.” But it can also mean “I’m ignoring this exception that can occur as a result of my code, you handle it.” We’ve been focusing on the “you handle it” part when looking at the mechanics and syntax of exceptions, but here I’m particularly interested in the fact that often we ignore exceptions and that’s what the exception specification can state.

In C++ the exception specification is not part of the type information of a function. The only compile-time checking is to ensure that exception specifications are used consistently; for example, if a function or method throws exceptions, then the overloaded or derived versions must also throw those exceptions. Unlike Java, however, no compile-time checking occurs to determine whether or not the function or method will actually throw that exception, or whether the exception specification is complete (that is, whether it accurately describes all exceptions that may be thrown). That validation does happen, but only at run time. If an exception is thrown that violates the exception specification, the C++ program will call the standard library function unexpected( ).

It is interesting to note that, because of the use of templates, exception specifications are not used at all in the standard C++ library. Exception specifications, then, may have a significant impact on the design of Java generics (Java’s version of C++ templates, expected to appear in JDK 1.5).

Perspectives

First, it’s worth noting that Java effectively invented the checked exception (clearly inspired by C++ exception specifications and the fact that C++ programmers typically don’t bother with them). It has been an experiment, which no language since has chosen to duplicate.

Secondly, checked exceptions appear to be an obvious good thing when seen in introductory examples and in small programs. It has been suggested that the subtle difficulties begin to appear when programs start to get large. Of course, largeness usually doesn’t happen overnight; it creeps. Languages that may not be suited for large-scale projects are used for small projects that grow, and at some point we realize that things have gone from manageable to difficult. This is what I’m suggesting may be the case with too much type checking; in particular, with checked exceptions.

The scale of the program seems to be a significant issue. This is a problem because most discussions tend to use small programs as demonstrations. One of the C# designers observed that:

“Examination of small programs leads to the conclusion that requiring exception specifications could both enhance developer productivity and enhance code quality, but experience with large software projects suggests a different result—decreased productivity and little or no increase in code quality.” [45]

In reference to uncaught exceptions, the CLU creators stated:

“We felt it was unrealistic to require the programmer to provide handlers in situations where no meaningful action can be taken.” [46]

When explaining why a function declaration with no specification means that it can throw any exception, rather than no exceptions, Stroustrup states:

“However, that would require exception specifications for essentially every function, would be a significant cause for recompilation, and would inhibit cooperation with software written in other languages. This would encourage programmers to subvert the exception-handling mechanisms and to write spurious code to suppress exceptions. It would provide a false sense of security to people who failed to notice the exception.” [47]

We see this very behavior—subverting the exceptions—happening with checked exceptions in Java.

Martin Fowler (author of UML Distilled, Refactoring, and Analysis Patterns) wrote the following to me:

“...on the whole I think that exceptions are good, but Java checked exceptions are more trouble than they are worth.”

I now think that Java’s important step was to unify the error reporting model, so that all errors are reported using exceptions. This wasn’t happening with C++, because for backward compatibility with C the old model of just ignoring errors was still available. But if you have consistent reporting with exceptions, then the exceptions can be used if desired, and if not, they will propagate out to the highest level (the console or other container program). When Java changed the C++ model so that exceptions were the only way to report errors, the extra enforcement of checked exceptions may have become less necessary.

In the past, I have been a strong believer that both checked exceptions and strong static type checking were essential to robust program development. However, both anecdotal and direct experience[48] with languages that are more dynamic than static have lead me to think that the great benefits actually come from:

  1. A unified error-reporting model via exceptions, regardless of whether the programmer is forced by the compiler to handle them.
  2. Type checking, regardless of when it takes place. That is, as long as proper use of a type is enforced, it doesn’t matter if it happens at compile time or run time.

On top of this, there are very significant productivity benefits to reducing the compile-time constraints upon the programmer. Indeed, reflection (and eventually, generics) is required to compensate for the over-constraining nature of strong static typing, as you shall see in the next chapter and in a number of examples throughout the book.

I’ve already been told by some that what I say here constitutes blasphemy, and by uttering these words my reputation will be destroyed, civilizations will fall, and a higher percentage of programming projects will fail. The belief that the compiler can save your project by pointing out errors at compile time runs strong, but it’s even more important to realize the limitation of what the compiler is able to do; in Chapter 15, I emphasize the value of an automated build process and unit testing, which give you far more leverage than you get by trying to turn everything into a syntax error. It’s worth keeping in mind that:

A good programming language is one that helps programmers write good programs. No programming language will prevent its users from writing bad programs.[49]

In any event, the likelihood of checked exceptions ever being removed from Java seems dim. It would be too radical of a language change, and proponents within Sun appear to be quite strong. Sun has a history and policy of absolute backwards compatibility—to give you a sense of this, virtually all Sun software runs on all Sun hardware, no matter how old. However, if you find that some checked exceptions are getting in your way, or especially if you find yourself being forced to catch exceptions, but you don’t know what to do with them, there are some alternatives.

Passing exceptions to the console

In simple programs, like many of those in this book, the easiest way to preserve the exceptions without writing a lot of code is to pass them out of main( ) to the console. For example, if you want to open a file for reading (something you’ll learn about in detail in Chapter 12), you must open and close a FileInputStream, which throws exceptions. For a simple program, you can do this (you’ll see this approach used in numerous places throughout this book):

在簡單的程序中,就像本書中很多的例子,最簡單的辦法而又不用寫太多的代碼來阻止異常就是將他們送到main()方法,送到輸出臺。舉例來說,如果你想打開一個文件來讀取,那麼你就必須打開並且關閉FileInputStream對象,它會拋出異常,一個簡單的例子你可以這麼作:

//: c09:MainException.java

import java.io.*;

 

public class MainException {

  // Pass all exceptions to the console:

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

    // Open the file:

    FileInputStream file =

      new FileInputStream("MainException.java");

    // Use the file ...

    // Close the file:

    file.close();

  }

} ///:~

 

Note that main( ) is also a method that may have an exception specification, and here the type of exception is Exception, the root class of all checked exceptions. By passing it out to the console, you are relieved from writing try-catch clauses within the body of main( ). (Unfortunately, file I/O is significantly more complex than it would appear to be from this example, so don’t get too excited until after you’ve read Chapter 12).

需要說明的main()方法同樣也是一個方法,也可以拋出異常信息,而這裏異常拋出的是Exception的對象,所有異常信息的基類。通過將它輸出到控制檯,你就無需在main()方法中再寫try-catch語句了。

Converting checked to unchecked exceptions

Throwing an exception from main( ) is convenient when you’re writing a main( ), but not generally useful. The real problem is when you are writing an ordinary method body, and you call another method and realize “I have no idea what to do with this exception here, but I don’t want to swallow it or print some banal message.” With JDK 1.4 chained exceptions, a new and simple solution prevents itself. You simply “wrap” a checked exception inside a RuntimeException, like this:

當你在寫main()方法的時候,在該方法中拋出異常是很簡單的,但是一般講沒有太大的用處,當你書寫代碼的時候遇到的問題就是,你會調用一個其它的方法並且意識到如果這裏發生異常的話我不知道該怎麼做,但是我不想私吞這個異常,也不想打印出一些通俗的錯誤信息。在JDK1.4中一個新的簡單的方法解決了,你直接把checked exception放到RuntimeException中去,像下面這樣:

try {

  // ... to do something useful

} catch(IDontKnowWhatToDoWithThisCheckedException e) {

  throw new RuntimeException(e);

}

 

This seems to be an ideal solution if you want to “turn off” the checked exception—you don’t swallow it, and you don’t have to put it in your method’s exception specification, but because of exception chaining you don’t lose any information from the original exception.

如果你想把checked exception關閉的話看起來像是個不錯的辦法,不會私吞異常,也不必把它放到方法的異常說明裏面,因爲異常鏈你也不會丟失任何原始異常的信息。

This technique provides the option to ignore the exception and let it bubble up the call stack without being required to write try-catch clauses and/or exception specifications. However, you may still catch and handle the specific exception by using getCause( ), as seen here:

下面這種技術提供了一種辦法來忽略遺產並且不需要寫trycatch語句也不需要異常聲明但是你仍然可以通過使用getCause()捕獲這個異常。

//: c09:TurnOffChecking.java

// "Turning off" Checked exceptions.

import com.bruceeckel.simpletest.*;

import java.io.*;

 

class WrapCheckedException {

  void throwRuntimeException(int type) {

    try {

      switch(type) {

        case 0: throw new FileNotFoundException();

        case 1: throw new IOException();

        case 2: throw new RuntimeException("Where am I?");

        default: return;

      }

    } catch(Exception e) { // Adapt to unchecked:

      throw new RuntimeException(e);

    }

  }

}

 

class SomeOtherException extends Exception {}

 

public class TurnOffChecking {

  private static Test monitor = new Test();

  public static void main(String[] args) {

    WrapCheckedException wce = new WrapCheckedException();

    // You can call f() without a try block, and let

    // RuntimeExceptions go out of the method:

    wce.throwRuntimeException(3);

    // Or you can choose to catch exceptions:

    for(int i = 0; i < 4; i++)

      try {

        if(i < 3)

          wce.throwRuntimeException(i);

        else

          throw new SomeOtherException();

      } catch(SomeOtherException e) {

          System.out.println("SomeOtherException: " + e);

      } catch(RuntimeException re) {

        try {

          throw re.getCause();

        } catch(FileNotFoundException e) {

          System.out.println(

            "FileNotFoundException: " + e);

        } catch(IOException e) {

          System.out.println("IOException: " + e);

        } catch(Throwable e) {

          System.out.println("Throwable: " + e);

        }

      }

    monitor.expect(new String[] {

      "FileNotFoundException: " +

      "java.io.FileNotFoundException",

      "IOException: java.io.IOException",

      "Throwable: java.lang.RuntimeException: Where am I?",

      "SomeOtherException: SomeOtherException"

    });

  }

} ///:~

 

WrapCheckedException.throwRuntimeException( ) contains code that generates different types of exceptions. These are caught and wrapped inside RuntimeException objects, so they become the “cause” of those exceptions.

In TurnOffChecking, you can see that it’s possible to call throwRuntimeException( ) with no try block because the method does not throw any checked exceptions. However, when you’re ready to catch exceptions, you still have the ability to catch any exception you want by putting your code inside a try block. You start by catching all the exceptions you explicitly know might emerge from the code in your try block—in this case, SomeOtherException is caught first. Lastly, you catch RuntimeException and throw the result of getCause( ) (the wrapped exception). This extracts the originating exceptions, which can then be handled in their own catch clauses.

The technique of wrapping a checked exception in a RuntimeException will be used when appropriate throughout the rest of this book.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章