1. 程式人生 > >詳解 異常exception(一)

詳解 異常exception(一)

Exception

java程式使用exception處理errors和別的異常事件。

1:什麼是異常:

異常是指在程式執行期間,打亂了正常的程式指令流而出現的異常事件。

exception是 excepitonal  event 的 速記。

當方法中出現一個錯誤的時候,這個方法會建立一個物件切換到正在執行的系統,這個物件 叫做異常(exception)物件,異常物件中包含了出現錯誤的資訊,異常的型別,以及此時執行程式的狀態。建立一個異常物件在執行時系統中處理它的過程叫做丟擲一個異常。

當一個方法丟擲一個異常後,這個執行時系統嘗試著去找處理這個異常的方式,這個一系列處理異常的方式是一系列順序的方法呼叫的 列表,這個方法列表叫做呼叫棧。


                                  上圖是呼叫的方法棧。

執行時系統搜尋呼叫棧,找一個包含這個異常處理程式碼塊的方法。這段程式碼塊叫做異常處理程式(exception  handler),搜尋的順序是:從這個錯我出現的方法開始,依次進行處理,這個順序是方法呼叫的反順序,例如以上呼叫棧---是從上到下查詢的順序。當合適的處理程式被找到,執行時系統傳遞這個異常到異常處理程式,異常處理程式考慮合適是根據丟擲異常物件的型別和這個處理程式的型別匹配。

這個異常處理程式被選擇叫做 捕獲一個異常,如果執行時系統搜尋呼叫棧中所有的方法但是沒有找到合適的異常處理程式,這個執行時系統將終止。


                           上圖為搜尋呼叫棧找對應的異常處理程式的過程

2:異常的分類:

一:異常處理方式

java語言提供兩種處理異常的方式: 1:try語句捕獲一個異常,try必須提供一個異常的處理。 2:一個方法指定丟擲一個異常,方法必須提供throws語句列出需要丟擲的異常,相當於方法丟擲一個指定的異常。 並不是所有的異常都是需要捕獲或者特定的丟擲的,為了理解這句話,看三種不同的異常分類。

二:異常的三種類型

1:檢測性異常(Checked Exception): 一些寫的好的程式應該提前考慮到這些異常,這種異常通常出現在寫好的程式的內部,應用程式能夠提前預測覆蓋和捕獲這種異常並從中恢復。例如:假設一個應用程式中需要使用者輸入檔名,然後通過傳遞這個檔名字到java.io.FileReader類中的構造方法中 開啟這個檔案,正常情況下,使用者提供存在的檔名稱,可讀性的檔案,因此FileReader的物件能夠構造成功,這個應用程式能夠正常的執行,但是有時使用者提供了不存在的使用者名稱稱, 這個構造方法會丟擲一個FileNotFoundException,一個好的應用程式應該捕獲這個異常,通知使用者這個錯誤,有可能提示輸入正確的檔名。
檢測性異常是Catch or Specify Requirement主題,除了Error , RuntimeException以及它們的子類,所有的其他異常都是檢測性異常(Checked Exception)。 2:error 第二種異常是error,這種異常通常出現在應用程式的外部,不能夠進行提前預測和從中恢復。例如,假設一個應用程式成功的開啟一個檔案,但是因為硬碟或者系統故障問題,會出現讀取檔案不成功將丟擲一個java.io.IOError,應用程式可能為了通知使用者出現了問題選擇捕獲這個異常,但是列印一個堆疊資訊並退出也許會更有意義。 Errors不是Catch or Specify Requirement的主題,Error是一種異常表示Error和它的子類。 3:執行時異常 第三種異常時執行時異常,這種異常時應用程式的內部的異常,應用程式通常不能夠預測和從中恢復。這通常表明程式的bug,例如邏輯錯誤或者不正確的使用了API等,例如考慮上面的傳遞檔名到java.io.FileReader類的構造方法中建立物件的FileReader物件的例子,如果因為邏輯錯誤傳遞給構造方法一個null,構造方法將丟擲一個NullPointException空指標異常,應用程式能夠捕獲這個異常,但是消除出現異常的這個bug也許更有意義。 執行時異常不是一種Catch or Specify Requirement的主題。執行時異常顯示RuntimeException以及它的子類。 Error和runtime exception(執行時異常) 都叫做非檢測性異常(unchecked exceptions)。

一些程式設計師認為 Catch or Specify Requirement是一種有嚴重缺陷的異常機制,想通過使用unchecked exception 代替 checked exception,通常是不建議。

3:捕獲處理異常

1:解釋異常

這一小節描述如何使用try ,catch,finally語句塊去實現異常處理程式處理,在Java SE 7 中也介紹了 try-with-resources語句,try-with-resources特別適用於關閉使用的資源,例如流。 下列例子定義和實現了ListOfNumbers類,當構造的時候,包含了一個ArrayList---包含0-9是個Integer元素,這個類也定義了一個writeList方法,寫這些數字進入Output.txt檔案中:例子如下:
package test.exception;

import java.io.FileWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

public class ListOfNumbers {

	private  List<Integer> list;
	
	private static  final  int  SIZE = 10;
	
	public  ListOfNumbers(){
		list = new  ArrayList<Integer>(SIZE);
		for(int i = 0; i<SIZE; i++){
           	 list.add(new Integer(i));
		}
	}
   
	public  void  writeList(){
		PrintWriter  out = new PrintWriter(new FileWriter("output.txt"));
	   
		for(int i =0;i<SIZE;i++){
			out.println("Values at "+i+" = "+list.get(i));
		}
		out.close();
	}
}
以上程式碼中第一次紅體字是呼叫了一個構造方法,這個構造方法初始化了一個檔案輸出流,如果這個檔案不開啟,這個構造方法將丟擲一個IOException, 第二次紅體字是呼叫ArrayList類的get方法,如果 i 引數值小於0或者大於SIZE,將丟擲一個IndexOutOfBoundsException異常。 如果試著編譯ListOfNumbers類,編譯器將打印出一個錯誤資訊:the exception thrown by the FileWriterconstructor,但是它不打印出這個錯誤資訊get異常丟擲的資訊,這是因為FileWriter構造方法丟擲的IOException是一個檢測性異常(checked exception),get方法丟擲的異常IndexOutOfBoundsException是一個執行時異常,即非檢測性異常(unchecked exception)。現在你已經熟悉了ListOfNumbers類,在其內部那些地方會丟擲異常,我們能夠寫異常處理程式去捕獲這些異常。 八卦: 檢測性異常:編譯程式的時候會進行檢測,如果沒有進行處理,不能通過編譯。 非檢測性異常:編譯程式的時候能夠通過檢測,通常是外部環境(硬碟或者系統故障)等不可檢測預見的Error和內部環境(邏輯錯誤等)的RuntimeException。

2:try 塊

構造異常處理程式的第一步是:把有可能丟擲異常的程式碼塊寫在try塊中,通常一個try塊如下所示: try{  code } catch and finally  塊 code是一行或者多行一定丟擲異常的程式碼塊, 把有可能丟擲異常的語句放在try中,以上例子中的關於try塊的構造如下:
	public  void  writeList() {
		try{
			out= new PrintWriter(new FileWriter("output.txt"));
			
			for(int i =0;i<SIZE;i++){
				out.println("Values at "+i+" = "+list.get(i));
			}
		}
		out.close();
	}

為了捕獲異常必須有相關的catch塊

3:catch  塊

在一個應用處理程式中,一個try塊能夠提供一個或者多個catch塊:直接在try塊的後面提供,在try結束和catch開始處沒有其他程式碼,示例如下:
try {

} catch (ExceptionType name) {

} catch (ExceptionType name) {

}
每一個catch塊是一種異常處理程式,處理一類異常,異常型別在()中包含,這個異常型別宣告在catch中的引數必須是一個繼承自Throwable類的類名,處理程式能夠引用異常型別名。 如果異常處理程式被呼叫,catch塊 中包含的程式碼會被執行,執行時系統呼叫異常處理程式當丟擲的異常型別是第一個和呼叫棧中的型別匹配的時候,如果丟擲的異常物件是合法的,並且和catch中的引數型別一致,系統認為它們是匹配的。 下面是writeList方法中的兩種異常處理,兩種的型別的檢測性異常(checked exception)能夠被丟擲在try塊中。 程式碼如下所示:
public  void  writeList() throws SampleException {
		try{
			out= new PrintWriter(new FileWriter("output.txt"));
			
			for(int i =0;i<SIZE;i++){
				out.println("Values at "+i+" = "+list.get(i));
			}
		}catch(FileNotFoundException e1){
			System.err.println("FileNotFoundException: " + e1.getMessage());
			throw  new  SampleException();
		}catch(IOException e2){
			 System.err.println("Caught IOException: " + e2.getMessage());
			 e2.printStackTrace();
		}
		out.close();
	}

SampleException的宣告如下:
package test.exception;

public class SampleException extends Throwable {
  public SampleException() {
	// TODO Auto-generated constructor stub
	  System.out.println("SampleException.............");
}
}


FileNotFoundException 和 IOException都是檢測性異常(checked exception)。 兩個處理都打印出錯誤的資訊,第二個異常處理除了打印出異常資訊什麼也不做,捕獲了IOException說明那不是第一種處理,第二個異常處理允許程式繼續執行。 第一個異常處理除了打印出錯誤資訊,也丟擲了使用者自定義異常,在以上例子中,當FileNotFoundException 被捕獲時,呼叫SampleException 丟擲,這是程式處理異常的特定情況下的特定的方。 異常處理除了打印出錯誤資訊和終止程式還能做許多其他的,能夠做一些錯誤的恢復,提示使用者去做決定,或者使用chained exceptions處理把錯誤交給更高階的處理。

4:finally 塊

當try塊存在的時候,finally塊總是執行,即使一個不希望的異常出現了,finally塊也繼續執行,finally塊不僅僅用於異常處理,它讓程式設計師避免了意外的程式碼清除繞過 return,continue或者break,把清除程式碼(最後必須執行的程式碼)放入finally塊中是一個很好的選擇,即使沒有期望的異常的時候。

注意:當try或者catch塊在執行的時候,JVM退出了,finally塊也許不執行,同樣的,如果執行try或者catch塊的執行緒是中斷了或者被killed了,即使整個應用程式繼續執行,finally塊也許不執行。

回到writeList方法中:

在以下三種方式中的一種,try塊能夠退出。

1:new FileWriter語句失敗了,丟擲一個IOException。

2:get(i)方法失敗了,丟擲一個ArrayIndexOutOfBoundsException

3:方法中的語句都成功執行,程式正常退出。

不管try塊以哪一種方式退出,finally塊總是執行,因此finally塊是一個很好的cleanup的地方。

程式碼如下:

finally {
		    if (out != null) { 
		        System.out.println("Closing PrintWriter");
		        out.close(); 
		    } else { 
		        System.out.println("PrintWriter not open");
		    } 
		} 
重要:finally塊是一個關鍵的工具---防止資源洩露。當關閉一個檔案或者恢復一個資源,把程式碼放在finally塊中確保資源總是被恢復。如果使用JDK7或者以後的版本,可以考慮使用try-with-resources語句在這種情況下,能夠自動的釋放不需要的系統資源。

5:try-with-resources語句(JDK新出功能)

在try-with-resources的使用中在try中聲明瞭一個或者更多的資源(resource),一個資源(resource)是一個物件在程式完成後必須釋放(關閉)的物件,try-with-resources語句確保在語句的末尾每一個資源被釋放(關閉)。資源是指一些實現了java.lang.AutoCloseable或者java.io.Closeable介面。 下面例子是從一個檔案中讀一行,它使用一個BufferedReader的物件從檔案中讀資料,BufferedReader是一個在程式完成後必須被關閉的資源,例子如下:
  static String  readFirstLineFromFile(String path) throws IOException{
	  try( BufferedReader  br = new BufferedReader(new FileReader(path));){
		 
		  return  br.readLine();
	  }
  }

在這個例子中,資源宣告在try-with-resources中的是BufferedReader,這個資源出現在try後的括號中,BufferedReader在JDK7及以後的版本實現了java.lang.AutoCloseable介面,因為BufferedReader例項是宣告在try語句塊中,不管try語句塊是否正常的完成,它都將關閉。 在JDK7以前的版本中,能夠使用finally塊確保資源關閉,不管try塊中如何結束,下面的例子使用finally塊代替 try-with-resources語句:
	static String readFirstLineFromFileWithFinallyBlock(String path)
			throws IOException {
		BufferedReader br = new BufferedReader(new FileReader(path));
		try {
			return br.readLine();
		} finally {
			if (br != null)
				br.close();
		}
	}

在以上例子中,如果readLine()和 close()都丟擲異常,方法readFirstLineFromFileWithFinallyBlock()丟擲異常從finally塊中,從try塊中丟擲的異常將要被掛起。在readFirstLineFromFile例子中相反,如果異常從try塊中和try-with-resources中丟擲,readFirstLineFromFile從try塊中丟擲異常,從try-with-resources中丟擲的異常被掛起,在JDK7及後來的版本,能夠檢索到掛起異常。 也能夠宣告更多的資源在try-with-resources.中,下面的例子檢索包下的zip檔案,建立一個包含這寫名字的文字檔案。
public static void writeToFileZipFileContents(String zipFileName,
			String outputFileName) throws java.io.IOException {

		java.nio.charset.Charset charset = java.nio.charset.StandardCharsets.US_ASCII;
		java.nio.file.Path outputFilePath = java.nio.file.Paths.get(outputFileName);

		// Open zip file and create output file with
		// try-with-resources statement

		try (java.util.zip.ZipFile zf = new java.util.zip.ZipFile(zipFileName);
				java.io.BufferedWriter writer = java.nio.file.Files
						.newBufferedWriter(outputFilePath, charset)) {
			// Enumerate each entry
			for (java.util.Enumeration entries = zf.entries(); entries.hasMoreElements();) {
				// Get the entry name and write it to the output file
				String newLine = System.getProperty("line.separator");
				String zipEntryName = ((java.util.zip.ZipEntry) entries
						.nextElement()).getName() + newLine;
				writer.write(zipEntryName, 0, zipEntryName.length());
			}
		}
	}

在以上例子中,try-with-catch塊使用了兩個申明通過分號分開,ZipFile和BufferedWriter,當這個程式碼塊直接終止,或者因為異常而終止,BufferedWriter和 ZipFile物件的close方法自動被有順序的呼叫,注意Resources的方法呼叫和建立相反的方法。 下列例子使用try-with-resources自動關閉java.sql.Statement的物件。
public static void viewTable(Connection con) throws SQLException {

	    String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES";

	    try (Statement stmt = con.createStatement()) {
	        ResultSet rs = stmt.executeQuery(query);

	        while (rs.next()) {
	            String coffeeName = rs.getString("COF_NAME");
	            int supplierID = rs.getInt("SUP_ID");
	            float price = rs.getFloat("PRICE");
	            int sales = rs.getInt("SALES");
	            int total = rs.getInt("TOTAL");

	            System.out.println(coffeeName + ", " + supplierID + ", " + 
	                               price + ", " + sales + ", " + total);
	        }
	    } catch (SQLException e) {
	        JDBCTutorialUtilities.printSQLException(e);
	    }
	}

java.sql.Statement Resources是使用的JDBC4.1以後的API.
注意:try-with-resources塊可以像普通的try塊一樣有 catch和finally塊,在try-with-resources語句塊中,catch和finally是在resources被關閉之後。 掛起異常: 異常能夠被丟擲從關聯的try-with-resources塊中,在以上例子writeToFileZipFileContents中,異常能夠從try塊中丟擲。兩個異常能夠被丟擲從try-with-resources中,當試著關閉ZipFile和BufferedWriter物件的時候,如果一個異常被丟擲從一個try塊中和一個或者更多的異常被丟擲從try-with-resources中,異常從try-with-resources中丟擲的異常會被掛起,異常通過writeToFileZipFileContents方法向上丟擲,你能訪問這些掛起的異常通過從try塊中丟擲的異常呼叫Throwable.getSuppressed方法。 實現了AutoCloseable或者Closeable介面: Java doc中列出了實現 AutoCloseable 和 Closeable介面的類,Closeable介面繼承自AutoCloseable介面,當AutoCloseable介面的close方法丟擲一個Excepiton時,Closeable介面的close方法丟擲IOException異常,因此AutoCloseable的子類能夠重寫close方法,丟擲特定的異常。 關於開始的例子的完整版:
public void writeList() {
    PrintWriter out = null;

    try {
        System.out.println("Entering" + " try statement");

        out = new PrintWriter(new FileWriter("OutFile.txt"));
        for (int i = 0; i < SIZE; i++)
            out.println("Value at: " + i + " = " + vector.elementAt(i));
                  
    } catch (ArrayIndexOutOfBoundsException e) {
        System.err.println("Caught ArrayIndexOutOfBoundsException: "
                           +  e.getMessage());
                                 
    } catch (IOException e) {
        System.err.println("Caught IOException: " +  e.getMessage());
                                 
    } finally {
        if (out != null) {
            System.out.println("Closing PrintWriter");
            out.close();
        } 
        else {
            System.out.println("PrintWriter not open");
        }
    }
}

兩種執行情況: 第一種執行期間丟擲異常: 建立FileWriter物件失敗有很多原因,如果程式不能建立或者寫入顯示的檔案,將會丟擲一個IOException異常,當FileWriter丟擲一個異常的IOException的時候,執行時系統立刻停止try塊的執行,呼叫的方法不完全執行了,執行時系統開始呼叫方法呼叫棧的頂端找到合適匹配的異常進行處理,在這個例子中,當IOException出現的時候,FileWriter構造方法在呼叫戰鬥頂端,但是FileWriter沒有一個合適的異常處理程式,因此執行時系統檢查下一個方法,即writeList方法,在這個方法的呼叫棧中,writeList方法有兩個異常處理程式,一個IOException和一個IndexOutOfBoundsException,執行時系統順序的檢測在try語句塊後出現的catch種類,第一個處理程式的引數型別是IndexOutOfBoundsException,這和丟擲的異常不匹配,因此執行時系統檢測下一個異常處理程式---IOException,執行時系統找到了匹配的異常處理程式,catch塊中的程式碼被執行。
異常處理程式執行完畢後,執行時系統傳遞控制到finally塊,不管catch是否被呼叫,finally塊中的程式碼執行。在這個例子中FileWriter從來沒有開啟,因此不需要繼續關閉,當finally執行完畢後,程式繼續執行finally塊後的第一個語句。 以下是丟擲IOException的輸出:
Entering try statement
Caught IOException: OutFile.txt
PrintWriter not open 
另外一種情況try塊正常結束: 這種情況下所有的語句成功執行沒有異常發生,執行完try塊以後,執行時系統傳遞控制到finally塊,因為所以的都是成功執行,PrintWriter是開啟的,當執行到finally時,關閉PrintWriter關閉,再一次的,finally塊完成執行,程式繼續執行finally塊結束後的語句塊,下面是沒有丟擲異常的執行結果:
Entering try statement
Closing PrintWriter