Java 中的異常和處理詳解

簡介

程序運行時,發生的不被指望的事件,它阻止了程序按照程序員的預期正常執行,這就是異常。異常發生時,是任程序自生自滅,馬上退出終止,仍是輸出錯誤給用戶?或者用C語言風格:用函數返回值做爲執行狀態?。html

Java提供了更加優秀的解決辦法:異常處理機制。java

異常處理機制能讓程序在異常發生時,按照代碼的預先設定的異常處理邏輯,針對性地處理異常,讓程序盡最大可能恢復正常並繼續執行,且保持代碼的清晰。
Java中的異常能夠是函數中的語句執行時引起的,也能夠是程序員經過throw 語句手動拋出的,只要在Java程序中產生了異常,就會用一個對應類型的異常對象來封裝異常,JRE就會試圖尋找異常處理程序來處理異常。程序員

Throwable類是Java異常類型的頂層父類,一個對象只有是 Throwable 類的(直接或者間接)實例,他纔是一個異常對象,才能被異常處理機制識別。JDK中內建了一些經常使用的異常類,咱們也能夠自定義異常。數據庫

Java異常的分類和類結構圖

Java標準褲內建了一些通用的異常,這些類以Throwable爲頂層父類。編程

Throwable又派生出Error類和Exception類。數組

錯誤:Error類以及他的子類的實例,表明了JVM自己的錯誤。錯誤不能被程序員經過代碼處理,Error不多出現。所以,程序員應該關注Exception爲父類的分支下的各類異常類。多線程

異常:Exception以及他的子類,表明程序運行時發送的各類不指望發生的事件。能夠被Java異常處理機制使用,是異常處理的核心。編程語言

整體上咱們根據Javac對異常的處理要求,將異常類分爲2類。ide

非檢查異常(unckecked exception):Error 和 RuntimeException 以及他們的子類。javac在編譯時,不會提示和發現這樣的異常,不要求在程序處理這些異常。因此若是願意,咱們能夠編寫代碼處理(使用try…catch…finally)這樣的異常,也能夠不處理。對於這些異常,咱們應該修正代碼,而不是去經過異常處理器處理 。這樣的異常發生的緣由多半是代碼寫的有問題。如除0錯誤ArithmeticException,錯誤的強制類型轉換錯誤ClassCastException,數組索引越界ArrayIndexOutOfBoundsException,使用了空對象NullPointerException等等。模塊化

檢查異常(checked exception):除了Error 和 RuntimeException的其它異常。javac強制要求程序員爲這樣的異常作預備處理工做(使用try…catch…finally或者throws)。在方法中要麼用try-catch語句捕獲它並處理,要麼用throws子句聲明拋出它,不然編譯不會經過。這樣的異常通常是由程序的運行環境致使的。由於程序可能被運行在各類未知的環境下,而程序員沒法干預用戶如何使用他編寫的程序,因而程序員就應該爲這樣的異常時刻準備着。如SQLException , IOException,ClassNotFoundException 等。

須要明確的是:檢查和非檢查是對於javac來講的,這樣就很好理解和區分了。

初識異常

下面的代碼會演示2個異常類型:ArithmeticException 和 InputMismatchException。前者因爲整數除0引起,後者是輸入的數據不能被轉換爲int類型引起。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

package com.example;

import java. util .Scanner ;

public class AllDemo

{

      public static void main (String [] args )

      {

            System . out. println( "----歡迎使用命令行除法計算器----" ) ;

            CMDCalculate ();

      }

      public static void CMDCalculate ()

      {

            Scanner scan = new Scanner ( System. in );

            int num1 = scan .nextInt () ;

            int num2 = scan .nextInt () ;

            int result = devide (num1 , num2 ) ;

            System . out. println( "result:" + result) ;

            scan .close () ;

      }

      public static int devide (int num1, int num2 ){

            return num1 / num2 ;

      }

}

/*****************************************

 

----歡迎使用命令行除法計算器----

0

Exception in thread "main" java.lang.ArithmeticException : / by zero

     at com.example.AllDemo.devide( AllDemo.java:30 )

     at com.example.AllDemo.CMDCalculate( AllDemo.java:22 )

     at com.example.AllDemo.main( AllDemo.java:12 )

 

----歡迎使用命令行除法計算器----

r

Exception in thread "main" java.util.InputMismatchException

     at java.util.Scanner.throwFor( Scanner.java:864 )

     at java.util.Scanner.next( Scanner.java:1485 )

     at java.util.Scanner.nextInt( Scanner.java:2117 )

     at java.util.Scanner.nextInt( Scanner.java:2076 )

     at com.example.AllDemo.CMDCalculate( AllDemo.java:20 )

     at com.example.AllDemo.main( AllDemo.java:12 )

*****************************************/

異常是在執行某個函數時引起的,而函數又是層級調用,造成調用棧的,由於,只要一個函數發生了異常,那麼他的全部的caller都會被異常影響。當這些被影響的函數以異常信息輸出時,就造成的了異常追蹤棧。

異常最早發生的地方,叫作異常拋出點。

從上面的例子能夠看出,當devide函數發生除0異常時,devide函數將拋出ArithmeticException異常,所以調用他的CMDCalculate函數也沒法正常完成,所以也發送異常,而CMDCalculate的caller——main 由於CMDCalculate拋出異常,也發生了異常,這樣一直向調用棧的棧底回溯。這種行爲叫作異常的冒泡,異常的冒泡是爲了在當前發生異常的函數或者這個函數的caller中找到最近的異常處理程序。因爲這個例子中沒有使用任何異常處理機制,所以異常最終由main函數拋給JRE,致使程序終止。

上面的代碼不使用異常處理機制,也能夠順利編譯,由於2個異常都是非檢查異常。可是下面的例子就必須使用異常處理機制,由於異常是檢查異常。

代碼中我選擇使用throws聲明異常,讓函數的調用者去處理可能發生的異常。可是爲何只throws了IOException呢?由於FileNotFoundException是IOException的子類,在處理範圍內。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

@Test

public void testException() throws IOException

{

    //FileInputStream的構造函數會拋出FileNotFoundException

    FileInputStream fileIn = new FileInputStream("E:\\a.txt");

     

    int word;

    //read方法會拋出IOException

    while((word =  fileIn.read())!=-1)

    {

        System.out.print((char)word);

    }

    //close方法會拋出IOException

    fileIn.clos

}

異常處理的基本語法

在編寫代碼處理異常時,對於檢查異常,有2種不一樣的處理方式:使用try…catch…finally語句塊處理它。或者,在函數簽名中使用throws 聲明交給函數調用者caller去解決。

try…catch…finally語句塊

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

try{

     //try塊中放可能發生異常的代碼。

     //若是執行完try且不發生異常,則接着去執行finally塊和finally後面的代碼(若是有的話)。

     //若是發生異常,則嘗試去匹配catch塊。

 

}catch(SQLException SQLexception){

    //每個catch塊用於捕獲並處理一個特定的異常,或者這異常類型的子類。Java7中能夠將多個異常聲明在一個catch中。

    //catch後面的括號定義了異常類型和異常參數。若是異常與之匹配且是最早匹配到的,則虛擬機將使用這個catch塊來處理異常。

    //在catch塊中可使用這個塊的異常參數來獲取異常的相關信息。異常參數是這個catch塊中的局部變量,其它塊不能訪問。

    //若是當前try塊中發生的異常在後續的全部catch中都沒捕獲到,則先去執行finally,而後到這個函數的外部caller中去匹配異常處理器。

    //若是try中沒有發生異常,則全部的catch塊將被忽略。

 

}catch(Exception exception){

    //...

}finally{

    

    //finally塊一般是可選的。

   //不管異常是否發生,異常是否匹配被處理,finally都會執行。

   //一個try至少要有一個catch塊,不然, 至少要有1個finally塊。可是finally不是用來處理異常的,finally不會捕獲異常。

  //finally主要作一些清理工做,如流的關閉,數據庫鏈接的關閉等。

}

須要注意的地方

一、try塊中的局部變量和catch塊中的局部變量(包括異常變量),以及finally中的局部變量,他們之間不可共享使用。

二、每個catch塊用於處理一個異常。異常匹配是按照catch塊的順序從上往下尋找的,只有第一個匹配的catch會獲得執行。匹配時,不只運行精確匹配,也支持父類匹配,所以,若是同一個try塊下的多個catch異常類型有父子關係,應該將子類異常放在前面,父類異常放在後面,這樣保證每一個catch塊都有存在的意義。

三、java中,異常處理的任務就是將執行控制流從異常發生的地方轉移到可以處理這種異常的地方去。也就是說:當一個函數的某條語句發生異常時,這條語句的後面的語句不會再執行,它失去了焦點。執行流跳轉到最近的匹配的異常處理catch代碼塊去執行,異常被處理完後,執行流會接着在「處理了這個異常的catch代碼塊」後面接着執行。
有的編程語言當異常被處理後,控制流會恢復到異常拋出點接着執行,這種策略叫作:resumption model of exception handling(恢復式異常處理模式 )
而Java則是讓執行流恢復處處理了異常的catch塊後接着執行,這種策略叫作:termination model of exception handling(終結式異常處理模式)

1

2

3

4

5

6

7

8

9

10

11

public static void main(String[] args){

        try {

            foo();

        }catch(ArithmeticException ae) {

            System.out.println("處理異常");

        }

}

public static void foo(){

        int a = 5/0//異常拋出點

        System.out.println("爲何還不給我漲工資!!!");  //////////////////////不會執行

}

throws 函數聲明

throws聲明:若是一個方法內部的代碼會拋出檢查異常(checked exception),而方法本身又沒有徹底處理掉,則javac保證你必須在方法的簽名上使用throws關鍵字聲明這些可能拋出的異常,不然編譯不經過。

throws是另外一種處理異常的方式,它不一樣於try…catch…finally,throws僅僅是將函數中可能出現的異常向調用者聲明,而本身則不具體處理。

採起這種異常處理的緣由多是:方法自己不知道如何處理這樣的異常,或者說讓調用者處理更好,調用者須要爲可能發生的異常負責。

1

2

3

4

public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN

{

     //foo內部能夠拋出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 類的異常,或者他們的子類的異常對象。

}

finally塊

finally塊無論異常是否發生,只要對應的try執行了,則它必定也執行。只有一種方法讓finally塊不執行:System.exit()。所以finally塊一般用來作資源釋放操做:關閉文件,關閉數據庫鏈接等等。

良好的編程習慣是:在try塊中打開資源,在finally塊中清理釋放這些資源。

須要注意的地方:

一、finally塊沒有處理異常的能力。處理異常的只能是catch塊。

二、在同一try…catch…finally塊中 ,若是try中拋出異常,且有匹配的catch塊,則先執行catch塊,再執行finally塊。若是沒有catch塊匹配,則先執行finally,而後去外面的調用者中尋找合適的catch塊。

三、在同一try…catch…finally塊中 ,try發生異常,且匹配的catch塊中處理異常時也拋出異常,那麼後面的finally也會執行:首先執行finally塊,而後去外圍調用者中尋找合適的catch塊。

這是正常的狀況,可是也有特例。關於finally有不少噁心,偏、怪、難的問題,我在本文最後統一介紹了,電梯速達->:finally塊和return

throw 異常拋出語句

throw exceptionObject

程序員也能夠經過throw語句手動顯式的拋出一個異常。throw語句的後面必須是一個異常對象。

throw 語句必須寫在函數中,執行throw 語句的地方就是一個異常拋出點,它和由JRE自動造成的異常拋出點沒有任何差異。

1

2

3

4

5

6

7

public void save(User user)

{

      if(user  == null)

          throw new IllegalArgumentException("User對象爲空");

      //......

         

}

異常的鏈化

在一些大型的,模塊化的軟件開發中,一旦一個地方發生異常,則如骨牌效應同樣,將致使一連串的異常。假設B模塊完成本身的邏輯須要調用A模塊的方法,若是A模塊發生異常,則B也將不能完成而發生異常,可是B在拋出異常時,會將A的異常信息掩蓋掉,這將使得異常的根源信息丟失。異常的鏈化能夠將多個模塊的異常串聯起來,使得異常信息不會丟失。

異常鏈化:以一個異常對象爲參數構造新的異常對象。新的異對象將包含先前異常的信息。這項技術主要是異常類的一個帶Throwable參數的函數來實現的。這個當作參數的異常,咱們叫他根源異常(cause)。

查看Throwable類源碼,能夠發現裏面有一個Throwable字段cause,就是它保存了構造時傳遞的根源異常參數。這種設計和鏈表的結點類設計一模一樣,所以造成鏈也是天然的了。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public class Throwable implements Serializable {

    private Throwable cause = this;

    

    public Throwable(String message, Throwable cause) {

        fillInStackTrace();

        detailMessage = message;

        this.cause = cause;

    }

     public Throwable(Throwable cause) {

        fillInStackTrace();

        detailMessage = (cause==null ? null : cause.toString());

        this.cause = cause;

    }

     

    //........

}

下面是一個例子,演示了異常的鏈化:從命令行輸入2個int,將他們相加,輸出。輸入的數不是int,則致使getInputNumbers異常,從而致使add函數異常,則能夠在add函數中拋出

一個鏈化的異常。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

public static void main(String[] args)

{

     

    System.out.println("請輸入2個加數");

    int result;

    try

    {

        result = add();

        System.out.println("結果:"+result);

    } catch (Exception e){

        e.printStackTrace();

    }

}

//獲取輸入的2個整數返回

private static List<Integer> getInputNumbers()

{

    List<Integer> nums = new ArrayList<>();

    Scanner scan = new Scanner(System.in);

    try {

        int num1 = scan.nextInt();

        int num2 = scan.nextInt();

        nums.add(new Integer(num1));

        nums.add(new Integer(num2));

    }catch(InputMismatchException immExp){

        throw immExp;

    }finally {

        scan.close();

    }

    return nums;

}

 

//執行加法計算

private static int add() throws Exception

{

    int result;

    try {

        List<Integer> nums =getInputNumbers();

        result = nums.get(0)  + nums.get(1);

    }catch(InputMismatchException immExp){

        throw new Exception("計算失敗",immExp);  /////////////////////////////鏈化:以一個異常對象爲參數構造新的異常對象。

    }

    return  result;

}

 

/*

請輸入2個加數

r 1

java.lang.Exception: 計算失敗

    at practise.ExceptionTest.add(ExceptionTest.java:53)

    at practise.ExceptionTest.main(ExceptionTest.java:18)

Caused by: java.util.InputMismatchException

    at java.util.Scanner.throwFor(Scanner.java:864)

    at java.util.Scanner.next(Scanner.java:1485)

    at java.util.Scanner.nextInt(Scanner.java:2117)

    at java.util.Scanner.nextInt(Scanner.java:2076)

    at practise.ExceptionTest.getInputNumbers(ExceptionTest.java:30)

    at practise.ExceptionTest.add(ExceptionTest.java:48)

    ... 1 more

 

*/

自定義異常

若是要自定義異常類,則擴展Exception類便可,所以這樣的自定義異常都屬於檢查異常(checked exception)。若是要自定義非檢查異常,則擴展自RuntimeException。

按照國際慣例,自定義的異常應該老是包含以下的構造函數:

  • 一個無參構造函數
  • 一個帶有String參數的構造函數,並傳遞給父類的構造函數。
  • 一個帶有String參數和Throwable參數,並都傳遞給父類構造函數
  • 一個帶有Throwable 參數的構造函數,並傳遞給父類的構造函數。

下面是IOException類的完整源代碼,能夠借鑑。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

public class IOException extends Exception

{

    static final long serialVersionUID = 7818375828146090155L;

 

    public IOException()

    {

        super();

    }

 

    public IOException(String message)

    {

        super(message);

    }

 

    public IOException(String message, Throwable cause)

    {

        super(message, cause);

    }

 

     

    public IOException(Throwable cause)

    {

        super(cause);

    }

}

異常的注意事項

一、當子類重寫父類的帶有 throws聲明的函數時,其throws聲明的異常必須在父類異常的可控範圍內——用於處理父類的throws方法的異常處理器,必須也適用於子類的這個帶throws方法 。這是爲了支持多態。

例如,父類方法throws 的是2個異常,子類就不能throws 3個及以上的異常。父類throws IOException,子類就必須throws IOException或者IOException的子類。

至於爲何?我想,也許下面的例子能夠說明。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

class Father

{

    public void start() throws IOException

    {

        throw new IOException();

    }

}

 

class Son extends Father

{

    public void start() throws Exception

    {

        throw new SQLException();

    }

}

/**********************假設上面的代碼是容許的(實質是錯誤的)***********************/

class Test

{

    public static void main(String[] args)

    {

        Father[] objs = new Father[2];

        objs[0] = new Father();

        objs[1] = new Son();

 

        for(Father obj:objs)

        {

        //由於Son類拋出的實質是SQLException,而IOException沒法處理它。

        //那麼這裏的try。。catch就不能處理Son中的異常。

        //多態就不能實現了。

            try {

                 obj.start();

            }catch(IOException)

            {

                 //處理IOException

            }

         }

   }

}

二、Java程序能夠是多線程的。每個線程都是一個獨立的執行流,獨立的函數調用棧。若是程序只有一個線程,那麼沒有被任何代碼處理的異常 會致使程序終止。若是是多線程的,那麼沒有被任何代碼處理的異常僅僅會致使異常所在的線程結束。

也就是說,Java中的異常是線程獨立的,線程的問題應該由線程本身來解決,而不要委託到外部,也不會直接影響到其它線程的執行。

finally塊和return

首先一個不容易理解的事實:在 try塊中即使有return,break,continue等改變執行流的語句,finally也會執行。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public static void main(String[] args)

{

    int re = bar();

    System.out.println(re);

}

private static int bar()

{

    try{

        return 5;

    } finally{

        System.out.println("finally");

    }

}

/*輸出:

finally

*/

不少人面對這個問題時,老是在概括執行的順序和規律,不過我以爲仍是很難理解。我本身總結了一個方法。用以下GIF圖說明。

也就是說:try…catch…finally中的return 只要能執行,就都執行了,他們共同向同一個內存地址(假設地址是0×80)寫入返回值,後執行的將覆蓋先執行的數據,而真正被調用者取的返回值就是最後一次寫入的。那麼,按照這個思想,下面的這個例子也就不難理解了。

finally中的return 會覆蓋 try 或者catch中的返回值。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

public static void main(String[] args)

    {

        int result;

         

        result  =  foo();

        System.out.println(result);     /////////2

         

        result = bar();

        System.out.println(result);    /////////2

    }

 

    @SuppressWarnings("finally")

    public static int foo()

    {

        trz{

            int a = 5 / 0;

        } catch (Exception e){

            return 1;

        } finally{

            return 2;

        }

 

    }

 

    @SuppressWarnings("finally")

    public static int bar()

    {

        try {

            return 1;

        }finally {

            return 2;

        }

    }

finally中的return會抑制(消滅)前面try或者catch塊中的異常

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

class TestException

{

    public static void main(String[] args)

    {

        int result;

        try{

            result = foo();

            System.out.println(result);           //輸出100

        } catch (Exception e){

            System.out.println(e.getMessage());    //沒有捕獲到異常

        }

         

         

        try{

            result  = bar();

            System.out.println(result);           //輸出100

        } catch (Exception e){

            System.out.println(e.getMessage());    //沒有捕獲到異常

        }

    }

     

    //catch中的異常被抑制

    @SuppressWarnings("finally")

    public static int foo() throws Exception

    {

        try {

            int a = 5/0;

            return 1;

        }catch(ArithmeticException amExp) {

            throw new Exception("我將被忽略,由於下面的finally中使用了return");

        }finally {

            return 100;

        }

    }

     

    //try中的異常被抑制

    @SuppressWarnings("finally")

    public static int bar() throws Exception

    {

        try {

            int a = 5/0;

            return 1;

        }finally {

            return 100;

        }

    }

}

finally中的異常會覆蓋(消滅)前面try或者catch中的異常

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

class TestException

{

    public static void main(String[] args)

    {

        int result;

        try{

            result = foo();

        } catch (Exception e){

            System.out.println(e.getMessage());    //輸出:我是finaly中的Exception

        }

         

         

        try{

            result  = bar();

        } catch (Exception e){

            System.out.println(e.getMessage());    //輸出:我是finaly中的Exception

        }

    }

     

    //catch中的異常被抑制

    @SuppressWarnings("finally")

    public static int foo() throws Exception

    {

        try {

            int a = 5/0;

            return 1;

        }catch(ArithmeticException amExp) {

            throw new Exception("我將被忽略,由於下面的finally中拋出了新的異常");

        }finally {

            throw new Exception("我是finaly中的Exception");

        }

    }

     

    //try中的異常被抑制

    @SuppressWarnings("finally")

    public static int bar() throws Exception

    {

        try {

            int a = 5/0;

            return 1;

        }finally {

            throw new Exception("我是finaly中的Exception");

        }

         

    }

}

上面的3個例子都異於常人的編碼思惟,所以我建議:

  • 不要在fianlly中使用return。
  • 不要在finally中拋出異常。
  • 減輕finally的任務,不要在finally中作一些其它的事情,finally塊僅僅用來釋放資源是最合適的。
  • 將盡可能將全部的return寫在函數的最後面,而不是try … catch … finally中。
相關文章
相關標籤/搜索