在《java編程思想》中這樣定義 異常:阻止當前方法或做用域繼續執行的問題。雖然java中有異常處理機制,可是要明確一點,決不該該用"正常"的態度來看待異常。絕對一點說異常就是某種意義上的錯誤,就是問題,它可能會致使程序失敗。之因此java要提出異常處理機制,就是要告訴開發人員,你的程序出現了不正常的狀況,請注意。html
記得當初學習java的時候,異常老是搞不太清楚,不知道這個異常是什麼意思,爲何會有這個機制?可是隨着知識的積累逐漸也對異常有一點感受了。舉一個例子來講明一下異常的用途。java
public class Calculator {
public int devide(int num1, int num2) {
//判斷除數是否爲0
if(num2 == 0) {
throw new IllegalArgumentException("除數不能爲零");
}
return num1/num2;
}
}
複製代碼
看一下這個類中關於除運算的方法,若是你是新手你可能會直接返回計算結果,根本不去考慮什麼參數是否正確,是否合法(固然能夠原諒,誰都是這樣過來的)。可是咱們應儘量的考慮周全,把可能致使程序失敗的"苗頭"扼殺在搖籃中,因此進行參數的合法性檢查就頗有必要了。其中執行參數檢查拋出來的那個參數非法異常,這就屬於這個方法的不正常狀況。正常狀況下咱們會正確的使用計算器,可是不排除粗枝大葉把除數賦值爲0。若是你以前沒有考慮到這種狀況,而且恰巧用戶數學基礎很差,那麼你完了。可是若是你以前考慮到了這種狀況,那麼很顯然錯誤已在你的掌控之中。程序員
今天和別人聊天時看到一個笑話:世界上最真情的相依,是你在try我在catch。不管你發神馬脾氣,我都默默承受,靜靜處理。 大多數新手對java異常的感受就是:try...catch...。沒錯,這是用的最多的,也是最實用的。個人感受就是:java異常是從"try...catch..."走來。數據庫
首先來熟悉一下java的異常體系:編程
Throwable
類是 Java 語言中全部錯誤或異常的超類(這就是一切皆可拋的東西)。它有兩個子類:Error
和Exception
。數組
Error
:用於指示合理的應用程序不該該試圖捕獲的嚴重問題。這種狀況是很大的問題,大到你不能處理了,因此聽之任之就好了,你不用管它。好比說VirtualMachineError
:當 Java 虛擬機崩潰或用盡了它繼續操做所需的資源時,拋出該錯誤。好吧,就算這個異常的存在了,那麼應該什麼時候,如何處理它呢??交給JVM吧,沒有比它更專業的了。bash
Exception
:它指出了合理的應用程序想要捕獲的條件。Exception又分爲兩類:一種是CheckedException
,一種是UncheckedException
。這兩種Exception的區別主要是CheckedException須要用try...catch...顯示的捕獲,而UncheckedException不須要捕獲。一般UncheckedException又叫作RuntimeException
。《effective java》指出:對於可恢復的條件使用被檢查的異常(CheckedException),對於程序錯誤(言外之意不可恢復,大錯已經釀成)使用運行時異常(RuntimeException)。微信
咱們常見的RuntimeExcepiton
有IllegalArgumentException
、IllegalStateException
、NullPointerException
、IndexOutOfBoundsException
等等。對於那些CheckedException就不勝枚舉了,咱們在編寫程序過程當中try...catch...捕捉的異常都是CheckedException
。io包中的IOException及其子類,這些都是CheckedException。網絡
在Java中若是須要處理異常,必須先對異常進行捕獲,而後再對異常狀況進行處理。如何對可能發生異常的代碼進行異常捕獲和處理呢?使用try和catch關鍵字便可,以下面一段代碼所示:jvm
try {
File file = new File("d:/a.txt");
if(!file.exists())
file.createNewFile();
} catch (IOException e) {
// TODO: handle exception
}
複製代碼
被try塊包圍的代碼說明這段代碼可能會發生異常,一旦發生異常,異常便會被catch捕獲到,而後須要在catch塊中進行異常處理。
這是一種處理異常的方式。在Java中還提供了另外一種異常處理方式即拋出異常,顧名思義,也就是說一旦發生異常,我把這個異常拋出去,讓調用者去進行處理,本身不進行具體的處理,此時須要用到throw和throws關鍵字。
下面看一個示例:
public class Main {
public static void main(String[] args) {
try {
createFile();
} catch (Exception e) {
// TODO: handle exception
}
}
public static void createFile() throws IOException{
File file = new File("d:/a.txt");
if(!file.exists())
file.createNewFile();
}
}
複製代碼
這段代碼和上面一段代碼的區別是,在實際的createFile方法中並無捕獲異常,而是用throws關鍵字聲明拋出異常,即告知這個方法的調用者此方法可能會拋出IOException。那麼在main方法中調用createFile方法的時候,採用try...catch塊進行了異常捕獲處理。
固然還能夠採用throw關鍵字手動來拋出異常對象。下面看一個例子:
public class Main {
public static void main(String[] args) {
try {
int[] data = new int[]{1,2,3};
System.out.println(getDataByIndex(-1,data));
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
public static int getDataByIndex(int index,int[] data) {
if(index<0||index>=data.length)
throw new ArrayIndexOutOfBoundsException("數組下標越界");
return data[index];
}
}
複製代碼
而後在catch塊中進行捕獲。
也就說在Java中進行異常處理的話,對於可能會發生異常的代碼,能夠選擇三種方法來進行異常處理:
1)對代碼塊用try..catch進行異常捕獲處理;
2)在 該代碼的方法體外用throws進行拋出聲明,告知此方法的調用者這段代碼可能會出現這些異常,你須要謹慎處理。此時有兩種狀況:
若是聲明拋出的異常是非運行時異常,此方法的調用者必須顯示地用try..catch塊進行捕獲或者繼續向上層拋出異常。
若是聲明拋出的異常是運行時異常,此方法的調用者能夠選擇地進行異常捕獲處理。
3)在代碼塊用throw手動拋出一個異常對象,此時也有兩種狀況,跟2)中的相似:
若是拋出的異常對象是非運行時異常,此方法的調用者必須顯示地用try..catch塊進行捕獲或者繼續向上層拋出異常。
若是拋出的異常對象是運行時異常,此方法的調用者能夠選擇地進行異常捕獲處理。
(若是最終將異常拋給main方法,則至關於交給jvm自動處理,此時jvm會簡單地打印異常信息)
下面咱們來看一下異常機制中五個關鍵字的用法以及須要注意的地方。
try關鍵字用來包圍可能會出現異常的邏輯代碼,它單獨沒法使用,必須配合catch或者finally使用。Java編譯器容許的組合使用形式只有如下三種形式:
  try...catch...; try....finally......; try....catch...finally...
複製代碼
固然catch塊能夠有多個,注意try塊只能有一個,finally塊是可選的(可是最多隻能有一個finally塊)。
三個塊執行的順序爲try—>catch—>finally。
固然若是沒有發生異常,則catch塊不會執行。可是finally塊不管在什麼狀況下都是會執行的(這點要很是注意,所以部分狀況下,都會將釋放資源的操做放在finally塊中進行)。
在有多個catch塊的時候,是按照catch塊的前後順序進行匹配的,一旦異常類型被一個catch塊匹配,則不會與後面的catch塊進行匹配。
在使用try..catch..finally塊的時候,注意千萬不要在finally塊中使用return,由於finally中的return會覆蓋已有的返回值。下面看一個例子:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
String str = new Main().openFile();
System.out.println(str);
}
public String openFile() {
try {
FileInputStream inputStream = new FileInputStream("d:/a.txt");
int ch = inputStream.read();
System.out.println("aaa");
return "step1";
} catch (FileNotFoundException e) {
System.out.println("file not found");
return "step2";
}catch (IOException e) {
System.out.println("io exception");
return "step3";
}finally{
System.out.println("finally block");
//return "finally";
}
}
}
複製代碼
這段程序的輸出結果爲:
從這個例子說明,不管try塊或者catch塊中是否包含return語句,都會執行finally塊。
若是將這個程序稍微修改一下,將finally塊中的return語句註釋去掉,運行結果是:
最後打印出的是"finally",返回值被從新覆蓋了。
所以若是方法有返回值,切忌不要再finally中使用return,這樣會使得程序結構變得混亂。
1)throws出如今方法的聲明中,表示該方法可能會拋出的異常,而後交給上層調用它的方法程序處理,容許throws後面跟着多個異常類型;
2)通常會用於程序出現某種邏輯時程序員主動拋出某種特定類型的異常。throw只會出如今方法體中,當方法在執行過程當中遇到異常狀況時,將異常信息封裝爲異常對象,而後throw出去。throw關鍵字的一個很是重要的做用就是 異常類型的轉換(會在後面闡述道)。
throws表示出現異常的一種可能性,並不必定會發生這些異常;throw則是拋出了異常,執行throw則必定拋出了某種異常對象。二者都是消極處理異常的方式(這裏的消極並非說這種方式很差),只是拋出或者可能拋出異常,可是不會由方法去處理異常,真正的處理異常由此方法的上層調用處理。
本小節討論子類重寫父類方法的時候,如何肯定異常拋出聲明的類型。下面是三點原則:
1)父類的方法沒有聲明異常,子類在重寫該方法的時候不能聲明異常;
2)若是父類的方法聲明一個異常exception1,則子類在重寫該方法的時候聲明的異常不能是exception1的父類;
3)若是父類的方法聲明的異常類型只有非運行時異常(運行時異常),則子類在重寫該方法的時候聲明的異常也只能有非運行時異常(運行時異常),不能含有運行時異常(非運行時異常)。
在異常的使用這一部分主要是演示代碼,都是咱們日常寫代碼的過程當中會遇到的(固然只是一小部分),拋磚引玉嗎!
public static void testException1() {
int[] ints = new int[] { 1, 2, 3, 4 };
System.out.println("異常出現前");
try {
System.out.println(ints[4]);
System.out.println("我還有幸執行到嗎");// 發生異常之後,後面的代碼不能被執行
} catch (IndexOutOfBoundsException e) {
System.out.println("數組越界錯誤");
}
System.out.println("異常出現後");
}
/*output:
異常出現前
數組越界錯誤
4
異常出現後
*/
複製代碼
public static void testException2() {
int[] ints = new int[] { 1, 2, 3, 4 };
System.out.println("異常出現前");
System.out.println(ints[4]);
System.out.println("我還有幸執行到嗎");// 發生異常之後,他後面的代碼不能被執行
}
複製代碼
首先指出例子中的不足之處,IndexOutofBoundsException
是一個非受檢異常,因此不用try...catch...顯示捕捉,可是個人目的是對同一個異經常使用不一樣的處理方式,看它會有什麼不一樣的而結果(這裏也就只能用它將就一下了)。異常出現時第一個方法只是跳出了try塊,可是它後面的代碼會照樣執行的。可是第二種就不同了直接跳出了方法,比較強硬。從第一個方法中咱們看到,try...catch...
是一種"事務性"的保障,它的目的是保證程序在異常的狀況下運行完畢,同時它還會告知程序員程序中出錯的詳細信息(這種詳細信息有時要依賴於程序員設計)。
public class Rethrow {
public static void readFile(String file) throws FileNotFoundException {
try {
BufferedInputStream in = new BufferedInputStream(new FileInputStream(file));
} catch (FileNotFoundException e) {
e.printStackTrace();
System.err.println("不知道如何處理該異常或者根本不想處理它,可是不作處理又不合適,這是從新拋出異常交給上一級處理");
//從新拋出異常
throw e;
}
}
public static void printFile(String file) {
try {
readFile(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
printFile("D:/file");
}
}
複製代碼
**異常的本意是好的,讓咱們試圖修復程序,可是現實中咱們修復的概率很小,咱們不少時候就是用它來記錄出錯的信息。若是你厭倦了不停的處理異常,從新拋出異常對你來講多是一個很好的解脫。原封不動的把這個異常拋給上一級,拋給調用這個方法的人,讓他來費腦筋吧。**這樣看來,java異常(固然指的是受檢異常)又給咱們平添不少麻煩,儘管它的出發點是好的。
定義三個異常類:ExceptionA,ExceptionB,ExceptionC
public class ExceptionA extends Exception {
public ExceptionA(String str) {
super();
}
}
public class ExceptionB extends ExceptionA {
public ExceptionB(String str) {
super(str);
}
}
public class ExceptionC extends ExceptionA {
public ExceptionC(String str) {
super(str);
}
}
複製代碼
異常丟失的狀況:
public class NeverCaught {
static void f() throws ExceptionB{
throw new ExceptionB("exception b");
}
static void g() throws ExceptionC {
try {
f();
} catch (ExceptionB e) {
ExceptionC c = new ExceptionC("exception a");
throw c;
}
}
public static void main(String[] args) {
try {
g();
} catch (ExceptionC e) {
e.printStackTrace();
}
}
}
/*
exception.ExceptionC
at exception.NeverCaught.g(NeverCaught.java:12)
at exception.NeverCaught.main(NeverCaught.java:19)
*/
複製代碼
爲何只是打印出來了ExceptionC而沒有打印出ExceptionB呢?這個仍是本身分析一下吧!
上面的狀況至關於少了一種異常,這在咱們排錯的過程當中很是的不利。那咱們遇到上面的狀況應該怎麼辦呢?這就是異常鏈的用武之地:保存異常信息,在拋出另一個異常的同時不丟失原來的異常。
public class NeverCaught {
static void f() throws ExceptionB{
throw new ExceptionB("exception b");
}
static void g() throws ExceptionC {
try {
f();
} catch (ExceptionB e) {
ExceptionC c = new ExceptionC("exception a");
//異常連
c.initCause(e);
throw c;
}
}
public static void main(String[] args) {
try {
g();
} catch (ExceptionC e) {
e.printStackTrace();
}
}
}
/*
exception.ExceptionC
at exception.NeverCaught.g(NeverCaught.java:12)
at exception.NeverCaught.main(NeverCaught.java:21)
Caused by: exception.ExceptionB
at exception.NeverCaught.f(NeverCaught.java:5)
at exception.NeverCaught.g(NeverCaught.java:10)
... 1 more
*/
複製代碼
這個異常鏈的特性是全部異常均具有的,由於這個initCause()
方法是從Throwable
繼承的。
清理工做對於咱們來講是必不可少的,由於若是一些消耗資源的操做,好比IO,JDBC。若是咱們用完之後沒有及時正確的關閉,那後果會很嚴重,這意味着內存泄露。異常的出現要求咱們必須設計一種機制不論什麼狀況下,資源都能及時正確的清理。這就是finally。
public void readFile(String file) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(
new FileInputStream(file)));
// do some other work
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
複製代碼
例子很是的簡單,是一個讀取文件的例子。這樣的例子在JDBC操做中也很是的常見。(因此,我以爲對於資源的及時正確清理是一個程序員的基本素質之一。)
Try...finally結構也是保證資源正確關閉的一個手段。若是你不清楚代碼執行過程當中會發生什麼異常狀況會致使資源不能獲得清理,那麼你就用try對這段"可疑"代碼進行包裝,而後在finally中進行資源的清理。舉一個例子:
public void readFile() {
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(
new FileInputStream("file")));
// do some other work
//close reader
reader.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
複製代碼
咱們注意一下這個方法和上一個方法的區別,下一我的可能習慣更好一點,及早的關閉reader。可是每每事與願違,由於在reader.close()之前異常隨時可能發生,這樣的代碼結構不能預防任何異常的出現。由於程序會在異常出現的地方跳出,後面的代碼不能執行(這在上面應經用實例證實過)。這時咱們就能夠用try...finally來改造:
public void readFile() {
BufferedReader reader = null;
try {
try {
reader = new BufferedReader(new InputStreamReader(
new FileInputStream("file")));
// do some other work
// close reader
} finally {
reader.close();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
複製代碼
及早的關閉資源是一種良好的行爲,由於時間越長你忘記關閉的可能性越大。這樣在配合上try...finally就保證萬無一失了(不要嫌麻煩,java就是這麼中規中矩)。
再說一種狀況,假如我想在構造方法中打開一個文件或者建立一個JDBC鏈接,由於咱們要在其餘的方法中使用這個資源,因此不能在構造方法中及早的將這個資源關閉。那咱們是否是就沒轍了呢?答案是否認的。看一下下面的例子:
public class ResourceInConstructor {
BufferedReader reader = null;
public ResourceInConstructor() {
try {
reader = new BufferedReader(new InputStreamReader(new FileInputStream("")));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
public void readFile() {
try {
while(reader.readLine()!=null) {
//do some work
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void dispose() {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
複製代碼
這一部分講的多了一點,可是異常確實是看起來容易用起來難的東西呀,java中仍是有好多的東西須要深挖的。
對於異常的誤用着實很常見,上一部分中已經列舉了幾個,你們仔細的看一下。下面再說兩個其餘的。
例1. 用一個Exception來捕捉全部的異常,很有"一夫當關萬夫莫開"的氣魄。不過這也是最傻的行爲。
public void readFile(String file) {
BufferedReader reader = null;
Connection conn = null;
try {
reader = new BufferedReader(new InputStreamReader(
new FileInputStream(file)));
// do some other work
conn = DriverManager.getConnection("");
//...
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
reader.close();
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製代碼
從異常角度來講這樣嚴格的程序確實是萬無一失,全部的異常都能捕獲。可是站在編程人員的角度,萬一這個程序出錯了咱們該如何分辨是究竟是那引發的呢,IO仍是JDBC...因此,這種寫法很值得當作一個反例。你們不要覺得這種作法很幼稚,傻子纔會作。我在公司實習時確實看見了相似的狀況:只不過是人家沒有用Exception而是用了Throwable。
例2. 這裏就不舉例子了,上面的程序都是反例。異常是程序處理意外狀況的機制,當程序發生意外時,咱們須要儘量多的獲得意外的信息,包括髮生的位置,描述,緣由等等。這些都是咱們解決問題的線索。可是上面的例子都只是簡單的printStackTrace()。若是咱們本身寫代碼,就要儘量多的對這個異常進行描述。好比說爲何會出現這個異常,什麼狀況下會發生這個異常。若是傳入方法的參數不正確,告知什麼樣的參數是合法的參數,或者給出一個sample。
例3. 將try block寫的簡短,不要全部的東西都扔在這裏,咱們儘量的分析出到底哪幾行程序可能出現異常,只是對可能出現異常的代碼進行try。儘可能爲每個異常寫一個try...catch,避免異常丟失。在IO操做中,一個IOException也具備"一夫當關萬夫莫開"的氣魄。
如下是根據前人總結的一些異常處理的建議:
謹慎地使用異常,異常捕獲的代價很是高昂,異常使用過多會嚴重影響程序的性能。若是在程序中可以用if語句和Boolean變量來進行邏輯判斷,那麼儘可能減小異常的使用,從而避免沒必要要的異常捕獲和處理。好比下面這段經典的程序:
public void useExceptionsForFlowControl() {
try {
while (true) {
increaseCount();
}
} catch (MaximumCountReachedException ex) {
}
//Continue execution
}
public void increaseCount() throws MaximumCountReachedException {
if (count >= 5000)
throw new MaximumCountReachedException();
}
複製代碼
上邊的useExceptionsForFlowControl()用一個無限循環來增長count直到拋出異常,這種作法並無說讓代碼不易讀,而是使得程序執行效率下降。
在捕獲了異常以後什麼都不作,至關於忽略了這個異常。千萬不要使用空的catch塊,空的catch塊意味着你在程序中隱藏了錯誤和異常,而且極可能致使程序出現不可控的執行結果。若是你很是確定捕獲到的異常不會以任何方式對程序形成影響,最好用Log日誌將該異常進行記錄,以便往後方便更新和維護。
一旦你決定拋出異常,你就要決定拋出什麼異常。這裏面的主要問題就是拋出檢查異常仍是非檢查異常。
檢查異常致使了太多的try…catch代碼,可能有不少檢查異常對開發人員來講是沒法合理地進行處理的,好比SQLException,而開發人員卻不得不去進行try…catch,這樣就會致使常常出現這樣一種狀況:邏輯代碼只有不多的幾行,而進行異常捕獲和處理的代碼卻有不少行。這樣不只致使邏輯代碼閱讀起來晦澀難懂,並且下降了程序的性能。
我我的建議儘可能避免檢查異常的使用,若是確實該異常狀況的出現很廣泛,須要提醒調用者注意處理的話,就使用檢查異常;不然使用非檢查異常。
所以,在通常狀況下,我以爲儘可能將檢查異常轉變爲非檢查異常交給上層處理。
不要把上層類的異常放在最前面的catch塊。好比下面這段代碼:
try {
FileInputStream inputStream = new FileInputStream("d:/a.txt");
int ch = inputStream.read();
System.out.println("aaa");
return "step1";
} catch (IOException e) {
   System.out.println("io exception");  
return "step2";
}catch (FileNotFoundException e) {
System.out.println("file not found");    
return "step3";
}finally{
System.out.println("finally block");
//return "finally";
}
複製代碼
第二個catch的FileNotFoundException將永遠不會被捕獲到,由於FileNotFoundException是IOException的子類。
好比下面這段代碼:
public class Main {
public static void main(String[] args) {
try {
String user = null;
String pwd = null;
login(user,pwd);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
public static void login(String user,String pwd) {
if(user==null||pwd==null)
throw new NullPointerException("用戶名或者密碼爲空");
//...
}
}
複製代碼
展現給用戶錯誤提示信息最好不要跟程序混淆一塊兒,比較好的方式是將全部錯誤提示信息放在一個配置文件中統一管理。
只在異常最開始發生的地方進行日誌信息記錄。不少狀況下異常都是層層向上跑出的,若是在每次向上拋出的時候,都Log到日誌系統中,則會致使無從查找異常發生的根源。
儘可能將異常統一拋給上層調用者,由上層調用者統一之時如何進行處理。若是在每一個出現異常的地方都直接進行處理,會致使程序異常處理流程混亂,不利於後期維護和異常錯誤排查。由上層統一進行處理會使得整個程序的流程清晰易懂。
若是有使用文件讀取、網絡操做以及數據庫操做等,記得在finally中釋放資源。這樣不只會使得程序佔用更少的資源,也會避免沒必要要的因爲資源未釋放而發生的異常狀況。
總結很是簡單,不要爲了使用異常而使用異常。異常是程序設計的一部分,對它的設計也要考究點。
文章有不當之處,歡迎指正,你也能夠關注個人微信公衆號:
好好學java
,獲取優質學習資源,也能夠加入QQ技術交流羣:766946816
,咋們來聊聊java。