Java異常處理機制

java

 

本文對Java異常處理機制以及經常使用的異常處理方式進行了大體的描述。鑑於各類有關Java的文檔中對於異常處理有不少的說明,所以,本文主要側重於說明怎樣在面向對象分析和設計(OOA & OOD)中進行異常設計,以及與咱們經常使用的異常處理方式相關但容易出現問題的地方進行了探討。目前,關於異常設計的規範尚未很好的標準,本文拋磚引玉,但願能從你們對異常處理的看法和經驗中,尋求一種好的方式來規範咱們的異常設計。 程序員

 

1     異常處理的重要性

 

異常處理是Java語言自己的一項重要特性。對於建立建立大型、健壯、易於維護的程序來說是很是重要的。在《Thinking in Java》中,對異常的應用指出瞭如下幾點: sql

 

  • 解決問題並再次調用形成違例的方法。
  • 平息事態的發展,並在不從新嘗試方法的前提下繼續。
  •  計算另外一些結果,而不是但願方法產生的結果。
  •  在當前環境中儘量解決問題,以及將相同的違例從新出一個更高級的環境。
  •  在當前環境中儘量解決問題,以及將不一樣的違例從新出一個更高級的環境。
  • 停止程序執行。
  • 簡化編碼。若違例方案使事情變得更加複雜,那就會使人很是煩惱,不如不用。使本身的庫和程序變得更加安全。這既是一種短時間投資(便於調試),也是一種長期投資(改善應用程序的健壯性)

 

 

 

1.1   在編譯期間對錯誤進行捕獲

 

C語言不一樣的是,Java在編譯時就要求對全部可能出現的可檢查異常進行捕獲(關於可檢查異常的解釋見2.1),從而保證了在運行時程序對於這種意外狀況進行了處理,從而保證了程序的健壯性。 數據庫

 

1.2   簡化錯誤控制代碼

 

Java相對比,C語言對於錯誤的控制是經過返回一個值或設置一個標誌(位),接收者會檢查這些值或標誌,判斷具體發生了什麼事情,這種方法致使正常流程和錯誤流程混雜在一塊兒,使程序龐大,形成閱讀和理解時的困難。而異常控制機制則能夠將那些用於描述具體操做的代碼與專門糾正錯誤的代碼分隔開。通常狀況下,用於讀取、寫入以及調試的代碼會變得更富有條理。同時也有效減小了代碼量。 編程

 

2     異常的分類與經常使用的異常處理方法

 

咱們在這裏所說的異常,包括了狹義的異常(Exception)和錯誤(Error)兩種,也就是java.lang.Throwable類,java.lang. Exceptionjava.lang.Error是它的子類。 瀏覽器

 

狹義的異常是指java.lang.Exception及其子類,其中又分爲兩種,第一種,編譯器強制違例規範捕獲它們,就要求咱們在編譯以前對這樣的異常作好應對的處理,例如java.io.IOExceptionjava.sql.SQLException等等;第二種,指java.lang.RuntimeException及其子類,對於這一類異常,編譯器不要求爲這些類型指定違例規範,例如java.lang.NullPointerException 安全

 

2.1   可檢查異常

 

對於那些編譯器強制要求進行捕獲的異常,咱們稱爲可檢查異常。假如程序中,對這些語句沒有使用異常控制的話,會發生編譯錯誤。 服務器

 

2.2   運行時異常

 

運行時異常Java語言並不強制咱們在程序中進行捕獲,例如ClassCastExceptionEmptyStackException IllegalArgumentExceptionIndexOutOfBoundsExceptionSecurityExceptionSystemException UnsupportedOperationException等等。 網絡

 

運行時異常不要求捕獲的緣由是在程序中,不少的操做都有可能會致使運行時異常,編譯器根據已知的信息,並不能確信這種異常不會發生,可是對於程序員來說,又是顯然不會發生異常的。若是對這樣的異常也須要顯式加以捕 多線程

 

 

 

獲或者經過throws語句來拋出,顯然會使程序很混亂。

 

2.3   錯誤

 

錯誤(java.lang.Error)是指比較嚴重的問題,大多數錯誤是由於不正常的條件所引發的。由於它可能會在程序的不少地方出現,並且很難從錯誤中恢復,若是對這樣的錯誤也要在編譯時強制要求捕獲的話,會使程序顯得很是混亂。

 

例如線程死鎖,就是一個Error類的子類,由於大多數程序並不能對之捕獲和處理。

 

例如java.lan4g.VerifyError,當Java虛擬機對字節碼校驗出現錯誤時,拋出這樣的錯誤。

 

對於程序在運行時所遇到的錯誤,能夠採用try...catch語句,進行處理(通常這種錯誤都不是由程序處理,並且這種錯誤出現的不多),對不能匹配到相應catch語句的錯誤則直接拋出。

 

2.4   處理方法

 2.4.1  異常捕獲

 

咱們通常採用下面的方法來處理異常和錯誤:

 

try{

 

         //正常處理流程

 

}catch( Throwable ex){

 

         //異常處理

 

}finally{

 

         //釋放例如輸入輸出流、數據庫鏈接之類的

 

//不會被GC自動回收的資源

 

}

 

其中,catch()語句所catch到的爲 Throwable對象,包括了全部的ExceptionError

 

2.4.2  獲取異常處理信息

 

這是最最廣泛的一種處理方式,以下:

 

try{...}

 

catch( Exception ex ){

 

         //ex.getMessage()就包含了異常的說明信息

 

}

 

這樣作的好處是咱們能夠對這個Exception的信息有比較靈活自由的處理方式,好比打印到標準輸出流,記錄到日誌等等,缺點就是當出現NullPointerException時,異常的getMessage()返回爲null,對之進行處理會拋出一個咱們沒有進行捕獲的NullPointerException。適用於給用戶顯式異常信息。

 

2.4.3  打印調用堆棧

 

在程序的調試過程當中,咱們通常採用這種處理,以下:

 

try{...}

 

catch( Exception ex ){

 

ex.printStackTrace();

 

}

 

這樣作的好處就是能夠從調用堆棧裏找出引起異常的根本緣由,缺點是信息不夠友好。適合用來調試程序。

 

2.4.4  拋出異常

 

這種方法的用法以下:

 

在方法的內部:

 

try{...}

 

catch( Exception ex ) throw ex;        .....方式1

 

或者在方法說明時,就聲明會拋出的異常,例如:

 

public void doSomeThing() throws Exception          .....方式2

 

這種方法的優勢就是將異常一直傳遞到一個適合處理的層級,從而由適合處理這個異常的角色來以爲應該以何種方式來對異常做相應的處理。缺點就是若是沒有通過良好的設計,容易「迷失」引起異常的真正緣由。這種方式適合在API類型的產品中採用。

 

在實際使用中,方式1和方式2經常結合使用,例以下面的方法:

 

 

 

public void method1() throws MyException{

 

       try{

 

 

 

}catch( SomeException ex ){

 

       doSomething();

 

       throw new MyException( ex.getMessage() );

 

}fiinally{

 

       doMyFinally();   //必要的清楚工做

 

}

 

2.4.5  其它處理

 

JSP中,咱們經常採用errorPage機制,若是在當前頁面捕獲到錯誤,則自動轉到出錯頁面,出錯頁面的代碼,通常的寫法以下:

 

<%@ page isErrorPage="true" %>

 

<%

 

              String message = exception.getMessage();

 

              out.println( message );

 

%>

 

JSP中,能夠採用以下的方式來引用

 

<%@ page errorPage="error.jsp"%>

 

這個語句也就指示Web服務器在執行JSP頁面代碼時若是出現異常,則轉到指定的頁進行異常處理,包括打印出異常信息等等。若是沒有指定錯誤頁,則在出現異常時有的WEB服務器會直接拋出錯誤。

 

3     當前異常處理中的薄弱環節

 

3.1   系統設計中沒有異常設計

 

目前,採用UML進行系統的分析和設計已經成爲Java設計和開發中的重要環節,UML中的類圖、用例圖、順序圖等等都對Java開發起到了很好的輔助做用。可是據筆者的經驗,大多數的UML設計都是針對一種相對理想化的狀況來進行設計的,這樣的確也起到了簡化設計的效果,可是這樣就把對系統中異常的處理徹底交給了實現階段,形成的第一個後果就是靈活性太高,每一塊程序的編寫者均可以按照本身的好惡和習慣來定義和處理所涉及的異常。形成代碼中有關異常部分的管理混亂。咱們的設計的出發點和評判標準應該是設計是否對開發過程(主要是編碼)是否是能起到指導做用,而不是它是否符合標準。

 

然而事實上對錯誤的預防和對於運行期錯誤的處理,是一個軟件中很重要的一部分,對於一個健壯的系統,據統計,用於錯誤處理的代碼量,要大大超過理想化狀況下實現功能的代碼。因此,在設計時,沒有對異常的設計(以Rational RoseTogether或者說UML來講,沒有有關異常的設計,在順序圖中也對此隻字不提,筆者覺得,是很荒謬的),所形成的結果就是實現時,只有少數的、按照理想化的環境和操做實現軟件功能的代碼部分是可控制的,而在代碼中佔了很大比重的異常處理部分,則沒有總體設計,徹底由實現者臨時來決定。

 

所以,筆者建議在咱們進行系統OO設計的階段,就對對系統中可能出現的異常進行設計,這個設計主要包括如下的內容:

 

  • 自定義異常類的定義,包括命名、包裝和層次
  • 異常信息的組織
  • 異常拋出的時機
  • 對異常的處理

 

對於這些內容,咱們將在第4部分進行詳細分析。

 

3.2   異常處理流於簡單的形式

 

目前的程序中,咱們經常採用的異常處理都比較簡單,包括調試期間的

 

       ex.printStackTrace();

 

和在JSP中的errorPage,以及API內部的throw,從上邊對幾種方法的分析,咱們能夠知道,在決定一個異常是應該拋出仍是進行處理(如何處理)的時候,必須參考特定程序的上下文環境,包括程序的性質、程序的使用者,以及引起該異常的緣由、異常是否能夠恢復等等具體的緣由。

 

由此也能夠看到,若是咱們在實現一個系統以前的設計環節沒有對異常進行很好的設計,而是由實現者按照本身的實現方式來作,那麼異常部分就很容易失去控制。

 

在異常處理作的很差的程序中,還能夠發現這樣的程序:

 

try{

 

       //...

 

}catch( Exception ex ){

 

       //...

 

}

 

這樣的程序,應該儘可能避免,咱們能夠參照Java源代碼中的一段來看:

 

   public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {

 

          this();

 

 

 

       if (port < 0 || port > 0xFFFF)

 

           throw new IllegalArgumentException(

 

                     "Port value out of range: " + port);

 

       try {

 

           SecurityManager security = System.getSecurityManager();

 

           if (security != null) {

 

              security.checkListen(port);

 

           }

 

 

 

           impl.create(true); // a stream socket

 

           if (bindAddr == null)

 

              bindAddr = InetAddress.anyLocalAddress; 

 

 

 

           impl.bind(bindAddr, port);

 

           impl.listen(backlog);

 

 

 

       } catch(SecurityException e) {

 

           impl.close();

 

           throw e;

 

       } catch(IOException e) {

 

           impl.close();

 

           throw e;

 

       }

 

    }

 

咱們能夠看到,這些程序中對異常都不是簡單的捕獲捕獲Exception,而是針對不一樣的異常,進行了不一樣的處理,這樣是用來處理異常的代碼顯得比較多,也多是由於這個緣由,許多的開發人員都習慣採用簡單的方式來處理,只是靠Exception中的信息提供異常的具體信息,這樣,的確,減小了代碼量,可是也形成了邏輯上的錯誤。由於在現實世界中,就不存在一刀切的錯誤處理方式,而是要根據錯誤的性質和具體狀況來判斷。

 

上面所說的是對異常的「處理」,那麼對於將異常拋出這種狀況,也存在這樣的問題,由於並非全部的出現異常的狀況都要集中到最終的程序層來給出處理措施的,有些異常甚至須要使用重試機制來重試必定的次數,等等。

 

3.3   異常的形式和包含信息不能很好的幫助用戶找到引起異常的緣由

 

這個問題是在程序設計的過程當中看到的,這個問題比較簡單,由於對於開發人員(指API產品)和用戶(甚至包括那些只有測試期間會發生的異常,那異常信息就只是由咱們本身來看),異常信息顯然是不一樣的,你們都要關注的是關注的是引起異常的緣由,可是咱們本身最關心的是代碼中哪一行引起了這個錯誤,從而找到和修改程序中的Bug;對API產品來說,最關心的倒是外層調用API的程序(主要是設置API所須要的環境,提供數據,調用API完成所需的功能)哪裏出現了錯誤,包括是否是提供了正確的運行環境,有沒有加載正確的類庫,提供給API的數據是否是正確等等;對於最終用戶(例如對於JSP程序來說,使用瀏覽器訪問頁面的就是最終用戶)來說,他們所關心的是本身的操做是否是正確,好比操做步驟錯誤、填寫表單錯誤或瀏覽器(或者其它)設置的錯誤等等。

 

異常信息也是程序與用戶之間的一個要完成很重要的交流渠道,咱們要藉助異常信息,來進行輔助調試、功能調用、類庫部署以及使用。因此必須審慎的選擇異常信息,而不是臨時的決定。

 

經常能夠看到這樣的程序:

 

     public void method1(  ){

 

            if( someCondition )

 

                   thow new SomeException( 「Exception in method1」 );

 

     }

 

異常若是都這樣拋出的話,還有什麼意義可言呢?除了能夠中斷程序,不讓錯誤的程序繼續執行下去以外,不能給別人任何有用的信息。固然,假如程序的設計、開發、調試和使用都是一我的的話,他也許內心明白這個someCondition究竟是怎麼樣一個條件,可是咱們建議不要這樣來寫。

 

3.4   轉換舊的錯誤處理方式

 

咱們知道,在之前的C語言編程裏,沒有提供結構化異常處理的方法,對運行時錯誤的處理是經過分支來處理的,例如:

 

FILE* fp;

 

if( !fp = fopen( 「filename.dat」, 「w+b」 )) != NULL ){

 

       //...

 

}

 

或者下面的代碼:

 

       int ret = 0;

 

       ret = doSomeThing( );

 

使用整數ret,返回錯誤碼。不一樣的錯誤碼,就表明了(例若有一個錯誤代碼含義表,或者由程序來翻譯錯誤代碼以向調用層提供文字的錯誤信息)。

 

錯誤代碼這種方式是在沒有結構化異常處理機制以前的一種很天然的方式,可是正是由於這種方式存在的不足,纔有了異常機制的引入。

 

錯誤碼的最大問題就是把錯誤的檢查留到了運行期,而異常機制則是在編譯時就強制要求對可能發生(固然限於可檢查異常)的異常編寫預先的捕獲和處理代碼。

 

另一個就是錯誤代碼沒有層次性,僅僅是在不一樣層次的調用之間可能會有一個傳遞的過程,可是這種傳遞也是很容易把程序的邏輯搞得很混亂的。

 

由於錯誤碼是簡單的數字,與之有關的有兩點,一是所表明的具體含義,二是拋出異常的時機,在OO設計中,沒有一個很好的機制來完成這種邏輯層次、邏輯過程的設計。

 

在面嚮對象語言中,不提倡使用錯誤碼這種方式來進行錯誤的處理,一個好的方法就是採用異常來將這種方式轉換爲面向對象的處理方式,這樣作的一個好處就是能夠進行很好的OO設計,也有很好的錯誤處理層次。

 

4     OOD的過程當中對異常進行規範設計

 

本部分描述了在Java程序的OO設計階段應該如何對系統的異常控制進行設計。

 

4.1   原則

 

l  按照系統的邏輯對出現的異常進行封裝,從而在異常拋出時,能夠從異常的文字信息或異常的堆棧信息,分析出引起異常的緣由和消除該異常的辦法。

 

l  自定義異常類應從程序的業務邏輯出發,異常類命名,異常信息的選擇,異常在哪個層次進行處理,哪些層次對異常僅僅是拋給調用者,都應該明確體現其在業務邏輯上的意義。

 

l  在以上原則的基礎上,對於大多數異常,在API設計時,應該儘量拋出給調用層,而不是內部處理。

 

l  通常狀況下,不該該對異常進行太寬範圍的處理,而是針對不一樣的異常進行處理。

 

在下面的各節,咱們將使用一些實際開發的例子對異常的設計來進行說明。

 

4.2   異常類的命名、包裝和層次

 

異常從本質上來說,也屬於Java類,因此在設計的時候首先要遵循OO中類設計的規範。

 

在一個工程(對各類IDE而言)中,應該將全部使用到的自定義異常與拋出這些異常的類放在一塊兒,而不是做爲一個異常包,包含全部的異常類。應該說,採用一個異常包這種形式是屬於程序員(實現者)的思惟方式的產物,而將異常分開則是更符合客觀世界的邏輯,從Java2的類庫中,咱們也能夠看到,java.lang包內部包含了ExceptionNumberFormatException等異常,而java.io包又包含了java.io.ExceptionFileNotFoundException等等異常。

 

對自定義異常的細化也是一個很重要的環節,原則上講,對於每一類的異常狀況,都應該有本身的異常,可是事實上,這個「每一類」是很難去給出一個好的劃分方式的。咱們能夠參考Java2中有關的例子來做爲參考:

 

        +--java.lang.Exception

 

              |

 

              +--java.io.IOException

 

 已知的子類: ChangedCharSetException, CharConversionException, EOFException, FileNotFoundException, InterruptedIOException, MalformedURLException, ObjectStreamException, ProtocolException, RemoteException, SocketException, SyncFailedException, UnknownHostException, UnknownServiceException, UnsupportedEncodingException, UTFDataFormatException, ZipException

 

 

 

咱們看到,java.io.IOExceptionjava.lang.Exception繼承,咱們同時也能夠看到,從java.io.IOException繼承的異常都有了很是明確的含義,針對某種特定的異常,這一點從異常類的名字就能夠看出。

 

 

 

尤爲重要的是,咱們把本身的異常類也做爲OO設計(例如使用Rational Rose)中的一個部分,包括在UML的類圖中,明確的設計出系統中共包含了什麼異常,異常之間又是怎樣繼承的。

 

由於異常可能不少,這個裏邊是否是有一些更好的方法,在UML圖中具體描述的還須要進一步考慮。並且,考慮到異常類自己的代碼中沒有太多的內容,多半都是繼承父類,因此,對異常類的描述更加側重於類之間的層次關係和異常類的命名。

 

4.3   異常信息的組織

 

異常信息應該統必定義,爲了不一樣的異常,給用戶不少種描述信息,使用戶無所適從,也爲了修改的方便性,咱們對於異常信息經過資源文件的方式來實現統必定義。一個可行的方法就是在一個資源類裏定義異常的字符串常量,也能夠定義資源包,將資源分類,那樣可使單個的資源類不至於太龐大。

 

按照簡化設計的原則,咱們對於某些不多使用的異常的信息,能夠簡單的(簡單指不使用常量的形式)來輸出,例如

 

     throw new FatalException( 「This is a fatal error」 );

 

對於這樣的信息,咱們在開發的過程當中,若是發現會屢次的重用這個信息,則應該按照refactor的要求,將其重構爲一個異常信息類的常量,這樣作,也是簡化異常信息類的一個方式。

 

4.4   異常的拋出時機

 

一般,在軟件OO設計過程當中,順序圖是描述一個方法的實現的。在目前的順序圖中,只是一個方法的理想化實現。可是就象咱們在本文的最開始部分提到的,正常的流程只是比較小的一個部分,由於運行時不少的狀況均可能致使方法出錯。

 

能夠理解,若是把全部的異常拋出也很清楚的畫到順序圖裏,所帶來的結果並非很好,而是整個圖看上去很亂,正確的分支和錯誤的分支相混雜,基本上失去了順序圖的意義。因此,本文並不提倡把異常這樣來詳細的畫在順序圖上。

 

對於try...catch機制來捕獲的異常的說明,咱們能夠利用(Rational Rose)中,咱們能夠利用一個小的Note來在原來的順序圖中,對一個方法出現異常的分支進行一個簡單的說明,或者,對一個方法,給出一個總體的有關異常拋出的說明。

 

對於直接在方法的使用throws語句拋出的異常,咱們應該在方法的說明部分加以具體描述。

 

4.5   對異常的處理,例如從異常中恢復、重試等等

 

在一些程序中,有些異常是能夠簡單的拋出的,由於那些異常的上下文環境不涉及到一些不可回收資源的釋放,可是,大多數狀況下,須要對此做出判斷是否須要回收,處理代碼有可能在catch塊中,也有多是在finally塊中。

 

有些程序甚至須要更加特殊的處理,例如重試網絡鏈接、刪除臨時文件等等,固然,一個很是常見的例子就是對於數據庫程序,必須對當前事務進行回滾。

 

5     幾種比較重要的特殊異常處理

 

5.1   循環遍歷中的異常處理

 

在循環遍歷中,有些異常是須要忽略的,由於循環中的一項有錯誤,不該該使整個的函數終止。例如如下的程序

 

    public Hashtable getCerts(){

 

        Hashtable ht = new Hashtable();

 

        File f = new File( _certDirPath );

 

        File[] certFiles = f.listFiles();

 

 

 

        for( int i = 0; i < certFiles.length; i++ ){

 

            X509Certificate cert = null;

 

            String subject = "";

 

            PublicKey key = null;

 

            try {

 

              FileInputStream fis = new FileInputStream (  certFiles[i] );

 

              CertificateFactory certFact = CertificateFactory.getInstance( "X.509", "BC" );

 

               cert = (X509Certificate)certFact.generateCertificate( fis );

 

                fis.close();

 

                if( cert != null ){

 

                    subject = cert.getSubjectDN().toString();

 

                    key = cert.getPublicKey();

 

                }

 

            }

 

            catch (NoSuchProviderException ex) {

 

                continue;

 

            }catch (IOException ex) {

 

                continue;

 

            }catch (CertificateException ex) {

 

                continue;

 

            }

 

            if( subject != null && key != null )

 

                ht.put( subject, key );

 

        }

 

        return ht;

 

    }

 

 

 

       在這樣的程序裏,假如目錄下的某個文件由於某種緣由損壞而沒法讀出,咱們能夠看到,程序中只是簡單的讓程序continue,直接進行下一個循環項。假如文件都發生了錯誤而沒法讀出所需的信息,那麼返回的HashTable中就會保持運行本方法以前的數據,裏邊不包含數據。

 

在這種狀況下的異常處理的關鍵是是否須要中斷循環遍歷的過程。須要根據程序的具體狀況來進行處理。

 

5.2   多線程中的異常處理

 

與上邊的循環遍歷相似的,咱們在多線程程序中,也經常是要使用一個循環遍從來重複執行某些後臺的操做,經過控制這個循環的條件,達到控制線程的做用。因此這個程序不能簡單的由於拋出異常而中斷程序的運行,而是應該簡單的忽略(對某些特定的異常而言),或者經過其它(例如顯示在控制檯,記錄到日誌)等等,對不少Server程序來說是這樣的。

 

另一個問題就是在主線程由於異常而結束的處理中,一點要加上終止在主線程中啓動的其它線程,不然,對WEB程序而言,這些線程成爲孤立線程,除非你從新啓動WEB服務器(對於應用程序,從新啓動程序就能夠了),這些線程不能正常結束。

 

5.3   空指針異常NullPointerException

 

在一個 Java 程序員所能遇到的全部異常中,空指針異常屬於最恐怖的,這是由於:它是程序能給出的信息最少的異常。例如,不像一個類轉型異常,空指針異常不給出它所須要的內容的任何信息,只有一個空指針。此外,它並不指出在代碼的何處這個空指針被賦值。在許多空指針異常中,真正的錯誤出如今變量被賦爲空值的地方。爲了發現錯誤,咱們必須經過控制流跟蹤,以發現變量在哪裏被賦值,並肯定是否這麼作是不正確的。當賦值出如今包中,而不是出如今發生報錯的地方時,進程會被明顯地破壞。

 

Null(空標誌)在Java中應用很是的普遍,它給程序的設計和編碼帶來了很大的靈活性,可是從另外的一個角度來說,使用空標誌,程序員有效地掩蓋了異常狀況出現位置的跡象。誰知道空指針在被丟棄前從方法到方法傳遞了多遠?這隻能使得診斷錯誤以及肯定怎樣修正它們更加困難。經驗證實這種代碼常常中斷。

 

5.4   Web程序(jsp,servlet)中的異常

 

Web程序(主要是針對JSP而言),咱們通常是經過在頁面的開始部分設置errorPage的方式,來完成處理的,從而儘量避免在頁面裏寫繁複的try...catch,可是須要指出的是,對於拋出須要額外處理(釋放資源,異常信息轉換)的異常,仍是須要使用 try...catch的,這樣,即使是在catch到異常後直接釋放,也能夠有機會在finally塊中進行額外的處理。

 

另外就是這種方法在errorPage中,基本上都是採用

 

String message = exception.getMessage();

 

out.println( message );

 

這種方式來處理的,這種方式在 NullPointerException發生時,exception.getMessage()的返回值爲 null,不但不能有什麼參考價值,反而時異常的引起緣由更加不清晰。

 

5.5   JNI中的異常處理

 

咱們在前邊的部分提到,C中多數是經過返回錯誤碼的機制來完成錯誤處理的,而在C++中,則提供了叫好的異常處理。咱們在實現某些JNI程序時,須要將某些CC++)的程序封裝一個JNI的包,從而在Java中,經過本地方法調用類使用。

 

JNI 程序中,很容易使用C的處理方式(也就是前邊提到的使用返回錯誤碼的方式),遇到問題直接返回一個錯誤代碼,可是考慮到外層程序是從Java中調用,因此咱們建議採用拋出異常給Java程序,從而藉助Java異常處理的優點,使整個系統的異常處理更加規範化。從C中拋異常給Java的方法,請參考JNI方面資料。

 

JNIC程序端也經常要調用Java方法,若是所調用的Java程序包含了異常的拋出,那麼C程序端須要在每次調用了該方法後,調用這樣的語句來清除異常:

 

if( env->ExceptionChecked() )

 

{

 

       //將捕獲到的異常拋給Java虛擬機

 

       env->ThrowNew( env->ExceptionOccurred() );

 

       //清除異常

 

       env->ExceptionClear();

 

//運行C程序端的錯誤處理分支

 

       doSomeThing();}

 

}

 

須要說明的是,JNI中拋出異常須要異常類在包中,並卻,包和類名不能寫錯,不然會形成Java虛擬機的崩潰。在JNI中,出現內存訪問的錯誤,也會形成虛擬機崩潰,若是程序是運行在Web服務器中,通常的狀況下,Web服務器會重啓。

 

6     與異常相關的幾個設計和編程問題

 

6.1   資源的分配與釋放

 

咱們知道,在Java中,除了內存以外的資源都不能由GC自動進行收集清理,於是咱們必須在程序中顯式的進行釋放和處理。固然對正常運行的流程來講這比較簡單。而對於出現異常的狀況,咱們須要時刻記住的就是即使出現異常,咱們也同樣須要釋放它們。

 

請看下邊的程序:

 

        Connection conn = null;

 

        Statement st = null;

 

        ResultSet rs = null;

 

        Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver" );

 

        Connection conn = DriverManager.getConnection( "jdbc:odbc:test", "sa", "" );

 

        Statement  st = conn.createStatement();

 

        ResultSet  rs = st.executeQuery( "select * from table1" );

 

        while( rs.next() ){

 

            String sName = rs.getString(1);

 

            String s = "name=" + sName;

 

            System.out.println( s );

 

        }

 

        rs.close();

 

        st.close();

 

        conn.close();

 

這段程序很是簡單,可是這段程序裏包含了不少拋出異常的語句,對這段程序咱們須要加上異常的捕獲,不然,一是編譯不能經過,二是運行期會有不少的錯誤致使程序由於異常而退出。

 

這是加上異常處理的程序

 

 

 

        Connection conn = null;

 

        Statement st = null;

 

        ResultSet rs = null;

 

        try {

 

            Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver" );

 

            conn = DriverManager.getConnection( "jdbc:odbc:test", "sa", "" );

 

            st = conn.createStatement();

 

            rs = st.executeQuery( "select * from table1" );

 

            while( rs.next() ){

 

                String sName = rs.getString(「name」);

 

                String s = "name=" + sName;

 

                System.out.println( s );

 

            }

 

            rs.close();

 

            st.close();

 

            conn.close();

 

        }

 

        catch (SQLException ex) {

 

        //...

 

}catch (ClassNotFoundException ex) {

 

//...

 

        }catch(Exception ex ){

 

//...

 

}

 

這段程序是能夠編譯經過的,可是咱們看到它有邏輯上的錯誤,對ClassNotFoundException異常的處理沒有問題,由於一開始的加載JDBC驅動的語句若是發生異常,後邊的全部的操做均可以不作,可是對於SQLException來說狀況就複雜了,很明顯的會有可能發生下邊幾種錯誤:

 

l  st 對象在執行後邊的查詢時出錯,則connst不能釋放

 

l  rs.getString()時字段名寫錯,則rsstconn不能釋放

 

無需更多的列舉,每一個人都會想到,將關閉這些資源的語句,從try塊中移到finally塊中,即:

 

finally{

 

     rs.close();

 

     st.close();

 

     conn.close();

 

}

 

在程序編譯時,能夠看到是沒法編譯經過的,這裏我以爲咱們應該感謝編譯器,它阻止了咱們犯錯誤,假如不對此進行阻止,咱們看程序,能夠發現,假如鏈接的建立出現錯誤,那麼strs理所固然的是 null,咱們試圖對一個null對象進行close()操做!這個操做所致使的就是一個簡單的NullPointerException(),咱們將不能從中獲得任何錯誤信息的描述。通過修改,程序能夠寫成

 

finally{

 

     try{rs.close();}catch(Exception ex ){}

 

     try{st.close();}catch(Exception ex ){}

 

     try{conn.close();}catch(Exception ex ){}

 

}

 

能夠看到,咱們的catch語句在這裏就很是簡單,由於咱們能夠預見到可能會發生的異常――假如異常發生,那麼必定是NullPointerException,也就是說,在程序尚未來得及對這個對象進行初始化時發生了異常,因此該對象值爲null,對之關閉失敗,那就不用關閉了,因此咱們能夠簡單的什麼都不作就能夠了。

 

固然還能夠這樣來寫:

 

finally{

 

     try{

 

    if( rs != null )rs.close();

 

    if( st != null )st.close();

 

    if( conn != null )conn.close();

 

}catch( Exception ex ){

 

     }

 

}

 

筆者更傾向於後面這種方式,由於與前邊的代碼相比,它很清楚的表達了代碼自身的含義,至於仍是須要放在 try...catch中,則是編譯器的強制要求,咱們在使用編譯器爲咱們帶來的好處的同時,也不得不接受它帶來的一些形式上的強制要求。沒有這個強制要求,也許咱們會忘記了這個null檢查。

 

對於Socket鏈接、文件流等等,都和數據庫鏈接很類似,須要很是仔細有關它們的釋放。

 

在這裏,咱們還講到了null的檢查,下一節咱們將對有關null的異常進行探討。

 

6.2   從設計和編碼來避免出現NullPointerException

 

看下面的代碼:

 

     String s rs.getString(「name」);

 

     s = s.trim();

 

編譯器須要咱們強制進行檢查的是SQLException,並不要求咱們對字段的值進行NullPointerException,這一點能夠理解,由於若是強制要求,那麼每段代碼都充滿了NullPointerException,由於可能出現這樣的異常的地方實在是太多了。

 

另外對於NullPointerException,咱們很難期望try...catch這種機制所捕獲到的異常能告訴咱們有用的信息,由於getMessage()方法獲得的信息,是一個簡單的」null」

 

一個可行的地方是咱們在代碼裏對可能出現的空指針進行檢查,這種檢查可能不少,可是這也是爲了程序的健壯性的一個必須的方法。

 

例如:

 

     public static boolean method1(Connection conn ,String s1 ){

 

            if( conn == null ){

 

                   //處理分支

 

                   //有多是 throw new IllegalArgumentException(「Argument Error!Conection is null!」);

 

                   return false;//不少的狀況下這樣處理

 

                   }

 

            if( s1 == null ){

 

                   //與上邊類似的處理

 

            }

 

            //..正常的處理

 

     }

 

咱們看到對null的檢查是很重要的,須要指出的是,咱們在程序設計的過程當中,經常採用空指針代替異常狀況,但這實際上卻把控制流限制在方法調用和返回的普通方式,同時也隱藏了異常狀況發生的跡象。

 

Java 程序中,異常狀況一般是經過拋出異常,並在適當的控制點捕獲它們來進行處理。可是常常看到的方法是經過返回一個空指針值來代表這種狀況(以及,可能打印一條消息到System.err)。若是調用方法沒有明確地檢查空指針,它可能會嘗試丟棄返回值並觸發一個空指針異常。

NullPointerException的策略是代碼中儘可能避免使用空標誌,可是事實上,許多 Java 類庫自己,好比HashTable類和BufferReader類都用了空標誌。當使用這樣的類時,您能夠經過在執行前,顯式檢查操做是否將返回空來避免錯誤。

相關文章
相關標籤/搜索