Java 中的異常和處理詳解
簡介
程序運行時,發生的不被期望的事件,它阻止了程序按照程序員的預期正常執行,這就是異常。異常發生時,是任程序自生自滅,立刻退出終止,還是輸出錯誤給用戶?或者用C語言風格:用函數返回值作為執行狀態?。
Java提供了更加優秀的解決辦法:異常處理機制。
異常處理機制能讓程序在異常發生時,按照代碼的預先設定的異常處理邏輯,針對性地處理異常,讓程序盡最大可能恢復正常並繼續執行,且保持代碼的清晰。
Java中的異常可以是函數中的語句執行時引發的,也可以是程序員通過throw 語句手動拋出的,只要在Java程序中產生了異常,就會用一個對應類型的異常對象來封裝異常,JRE就會試圖尋找異常處理程序來處理異常。
Throwable類是Java異常類型的頂層父類,一個對象只有是 Throwable 類的(直接或者間接)實例,他才是一個異常對象,才能被異常處理機制識別。JDK中內建了一些常用的異常類,我們也可以自定義異常。
Java異常的分類和類結構圖
Java標准庫內建了一些通用的異常,這些類以Throwable為頂層父類。
Throwable又派生出Error類和Exception類。
錯誤:Error類以及他的子類的實例,代表了JVM本身的錯誤。錯誤不能被程序員通過代碼處理,Error很少出現。因此,程序員應該關注Exception為父類的分支下的各種異常類。
異常:Exception以及他的子類,代表程序運行時發送的各種不期望發生的事件。可以被Java異常處理機制使用,是異常處理的核心。
總體上我們根據Javac對異常的處理要求,將異常類分為2類。
非檢查異常(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主要做一些清理工作,如流的關閉,數據庫連接的關閉等。
}
|
需要注意的地方
1、try塊中的局部變量和catch塊中的局部變量(包括異常變量),以及finally中的局部變量,他們之間不可共享使用。
2、每一個catch塊用於處理一個異常。異常匹配是按照catch塊的順序從上往下尋找的,只有第一個匹配的catch會得到執行。匹配時,不僅運行精確匹配,也支持父類匹配,因此,如果同一個try塊下的多個catch異常類型有父子關系,應該將子類異常放在前面,父類異常放在后面,這樣保證每個catch塊都有存在的意義。
3、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塊中清理釋放這些資源。
需要注意的地方:
1、finally塊沒有處理異常的能力。處理異常的只能是catch塊。
2、在同一try…catch…finally塊中 ,如果try中拋出異常,且有匹配的catch塊,則先執行catch塊,再執行finally塊。如果沒有catch塊匹配,則先執行finally,然后去外面的調用者中尋找合適的catch塊。
3、在同一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
|
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);
}
}
|
異常的注意事項
1、當子類重寫父類的帶有 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
}
}
}
}
|
2、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
|
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
|
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中。
一, 什么是java里的異常
1.1 c語言里的錯誤
- #include <stdio.h>
- int f(int a, int b){
- return a/b;
- }
- int main(){
- int i = f(8,0);
- printf("i is %d\n",i);
- return 0;
- }
上面的例子編譯時是無錯的, 但是一旦執行就會提示吐核錯誤了.
1.2 java里運行時出現的錯誤
- package Exception_kng;
- class Exp1{
- public int f(int a, int b){
- return a/b;
- }
- }
- public class Expt_1{
- public static void g(){
- Exp1 e = new Exp1();
- int i = e.f(8,0);
- System.out.printf("i is %d\n", i);
- }
- }
- [java] Caused by: java.lang.ArithmeticException: / by zero
- [java] at Exception_kng.Exp1.f(Expt_1.java:5)
- [java] at Exception_kng.Expt_1.g(Expt_1.java:12)
- [java] at Enter_1.main(Enter_1.java:31)
二, java里的異常的分類.
3.1 程序猿對有可能出現的異常使用try catch處理.
- package Exception_kng;
- class Exp2{
- public int f(int a, int b){
- int i = 0;
- try{
- i = a/b;
- }
- catch(Exception e){
- System.out.printf("Exception occurs!!\n");
- System.out.println(e.getMessage()); //print the root cause
- System.out.printf("===========================\n");
- e.printStackTrace(); //print the info of function stuck.
- }
- return i;
- }
- }
- public class Expt_2{
- public static void g(){
- Exp2 ex = new Exp2();
- int i = ex.f(8,0); //call f()
- System.out.printf("i is %d\n", i); //successfully executed
- }
- }
- [java] Exception occurs!!
- [java] / by zero
- [java] ===========================
- [java] java.lang.ArithmeticException: / by zero
- [java] at Exception_kng.Exp2.f(Expt_2.java:7)
- [java] at Exception_kng.Expt_2.g(Expt_2.java:23)
- [java] at Enter_1.main(Enter_1.java:31)
- [java] i is 0
3.2 函數里並不處理異常, 使用throw or throws 關鍵字 把可能出現的異常拋給調用該函數的上級函數處理.
- package Exception_kng;
- class Exp3{
- public int f(int a, int b){
- if (0 == b){
- throw new ArithmeticException("Shit !!! / by zero!");
- }
- return a/b;
- }
- }
- public class Expt_3{
- public static void g() throws ArithmeticException{
- Exp3 ex = new Exp3();
- int i = 22;
- i = ex.f(8,0); //throw excetpion
- System.out.printf("i is %d\n", i); //failed executed
- System.out.printf("g() is done!!\n"); //failed executed
- }
- public static void h(){
- try{
- g();
- }catch(ArithmeticException e){
- System.out.printf("Exception occurs!!\n");
- System.out.println(e.getMessage()); //print the root cause
- System.out.printf("===========================\n");
- e.printStackTrace(); //print the info of function stuck.
- }
- System.out.printf("h() is done!!\n"); //successfully executed
- }
- }
- [java] Exception occurs!!
- [java] Shit !!! / by zero!
- [java] ===========================
- [java] java.lang.ArithmeticException: Shit !!! / by zero!
- [java] at Exception_kng.Exp3.f(Expt_3.java:6)
- [java] at Exception_kng.Expt_3.g(Expt_3.java:18)
- [java] at Exception_kng.Expt_3.h(Expt_3.java:25)
- [java] at Enter_1.main(Enter_1.java:31)
- [java] h() is done!!
注意這個程序沒有執行g() 最后的代碼.
3.3 交給jvm虛擬機處理
實際上, 當java程序的1個函數拋出異常時,
首先會檢查當前函數有沒有try catch處理, 如果無檢查上一級函數有無try..catch處理....
這樣在函數棧里一級一級向上檢查, 如果直至main函數都無try..catch, 則拋給jvm..
4.1 try catch finally的執行路線.
下面用個例子來說明:- try{
- f();
- ff();
- }
- catch(ArithmeticException e){
- g();
- }
- catch(IOException e){
- gg();
- }
- catch(AuthorizedException e){
- ggg();
- }
- finally{
- h();
- }
- k();
4.1.1 當try里面的f()拋出了IOException
當f()拋出了異常, 那么ff()就不會執行了. 程序會嘗試捕捉異常.首先捕捉ArithmeticException, 捕捉失敗.
接下來捕捉IOException, 捕捉成功, 執行gg();
一旦捕捉到一個異常, 不會再嘗試捕捉其他異常, 直接執行finally里的h();
執行后面的函數k().
也就是說路線是:
f() -> gg() -> h() -> k()
有2點要注意的.
1. f()函數極有可能未完整執行, 因為它拋出了異常, 拋出異常的語句執行失敗, 之后的語句放棄執行.
2. try{} 里面, f()之后的語句, 例如ff()放棄執行.
4.1.2 沒有任何異常拋出
這種情況很簡單, 就是try{}里面的代碼被完整執行, 因為沒有拋出任何異常, 就不會嘗試執行catch里的部分, 直接到finally部分了.
路線是:
f() -> ff() -> h() -> k()
4.2 如何確定要捕捉的異常名字.
也許有人會問, 我們怎么知道到底會拋出什么異常?
下面有3個解決方案.
1.看代碼憑經驗, 例如看到1段除法的代碼, 則有可能拋出算術異常.
2.在catch的括號里寫上Exception e, 畢竟Exception 是所有其他異常的超類, 這里涉及多態的知識, 至於什么是多態可以看看本人的另一篇文章.
3. 觀察被調用函數的函數定義, 如果有throws后綴, 則可以嘗試捕捉throws 后綴拋出的異常
4.3 為什么需要finally
包括我在內很多人會覺得finally語句簡直多勾余, 既然是否捕捉到異常都會執行, 上面那個例子里的h()為什么不跟下面的k() 寫在一起呢.
上面的例子的確看不出區別.
但下面兩種情況下就體現了finally獨特的重要性.
4.3.1 拋出了1個異常, 但是沒有被任何catch子句捕捉成功.
例如try里面拋出了1個A異常, 但是只有后面只有捕捉B異常, 和C異常的子句.
這種情況下, 程序直接執行finally{}里的子句, 然后中斷當前函數, 把異常拋給上一級函數, 所以當前函數finally后面的語句不會被執行.
例子:
- package Exception_kng;
- import java.net.*;
- import java.io.*;
- class Exp4{
- public int f(int a, int b) throws IOException, BindException{
- return a/b;
- }
- }
- public class Expt_4{
- public static void g(){
- Exp4 ex = new Exp4();
- int i = 22;
- try{
- System.out.printf("g() : try!!\n"); //failed
- i = ex.f(8,0); //call f()
- }
- catch(BindException e){
- System.out.printf("g() : BindException!!\n"); //failed
- }
- catch(IOException e){
- System.out.printf("g() : IOException!!\n"); //failed
- }
- finally{
- System.out.printf("g() : finaly!!\n"); //successfully executed
- }
- System.out.printf("g() is done!!\n"); //failed
- }
- public static void h(){
- try{
- g();
- }catch(ArithmeticException e){
- System.out.printf("Exception occurs!!\n");
- System.out.println(e.getMessage()); //print the root cause
- System.out.printf("===========================\n");
- e.printStackTrace(); //print the info of function stuck.
- }
- System.out.printf("h() is done!!\n"); //successfully executed
- }
- }
我所說的情況, 就在上面例子里的g()函數, g()函數里嘗試捕捉兩個異常, 但是拋出了第3個異常(ArithmeticException 算術異常).
所以這個異常會中斷g()的執行, 因為沒有被捕捉到, 然后拋給調用g()的 h()函數處理, 而在h()捕捉到了, 所以h()函數是能完整執行的.
也就是說g()里的
- System.out.printf("g() is done!!\n"); //failed
執行失敗
而h()里的
- System.out.printf("h() is done!!\n"); //successfully executed
執行成功
但是無論如何, g()里的finally{}部分還是被執行了
執行結果如下:
- [java] g() : try!!
- [java] g() : finaly!!
- [java] Exception occurs!!
- [java] / by zero
- [java] ===========================
- [java] java.lang.ArithmeticException: / by zero
- [java] at Exception_kng.Exp4.f(Expt_4.java:8)
- [java] at Exception_kng.Expt_4.g(Expt_4.java:18)
- [java] at Exception_kng.Expt_4.h(Expt_4.java:34)
- [java] at Enter_1.main(Enter_1.java:31)
- [java] h() is done!!
這種情況是1中編程的低級錯誤, 在項目中是不允許出現.
避免方法也十分簡單, 在catch子句集的最后增加1個catch(Exception e)就ok, 因為Exception是所有異常的超類, 只要有異常拋出, 則肯定會捕捉到.
4.3.2 在catch子句內有return子句.
下面例子:
- try{
- f();
- ff();
- }
- catch(ArithException e){
- g();
- return j();
- }
- catch(IOException e){
- gg();
- return j();
- }
- catch(AuthorizedException e){
- ggg();
- return j();
- }
- finally{
- h();
- }
- k();
假如在f()函數拋出了IOExcepion 異常被捕捉到.
那么執行路線就是
f() -> gg() -> j() -> h() -> 上一級function
也就說, 這種情況下finally里的子句會在return回上一級function前執行. 而后面的k()就被放棄了.
4.3.3 finally作用小結.
可以看出, finally里的語句, 無論如何都會被執行.
至有兩種情況除外, 一是斷電, 二是exit函數.
在項目中, 我們一般在finally編寫一些釋放資源的動作, 例如初始化公共變量. 關閉connections, 關閉文件等.
4.4 try catch finally里一些要注意的問題.
4.4.1 無論如何最多只有1個catch被執行
這個上面提到過了, 一旦捕捉到1個異常, 就不會嘗試捕捉其他異常.
如果try里面的一段代碼可能拋出3種異常A B C,
首先看它先拋出哪個異常, 如果先拋出A, 如果捕捉到A, 那么就執行catch(A)里的代碼. 然后finally.. B和C就沒有機會再拋出了.
如果捕捉不到A, 就執行finally{}里的語句后中斷當前函數, 拋給上一級函數...(應該避免)
4.4.2 有可能所有catch都沒有被執行
兩種情況, 1就是沒有異常拋出, 另一種就是拋出了異常但是沒有捕捉不到(應該避免)
4.4.3 先捕捉子類異常, 再捕捉父類異常, 否則編譯失敗
加入try 里面嘗試捕捉兩個異常, 1個是A, 1個是B, 但是A是B的父類.
這種情況下, 應該把catch(B)寫在catch(A)前面.
原因也很簡單, 加入把catch(A)寫在前面, 因為多態的存在, 即使拋出了B異常, 也會被catch(A)捕捉, 后面的catch(B)就沒有意義了.
也就是說如果捕捉Exception這個異常基類, 應該放在最后的catch里, 項目中也強烈建議這么做, 可以避免上述4.3.1的情況出現.
4.4.4 catch與catch之間不能有任何代碼.
這個沒什么好說的. 語法規則
4.4.5 finally里不能訪問catch里捕捉的異常對象e
每1個異常對象只能由catch它的catch子句里訪問.
4.4.6 try里面的定義變量不能在try外面使用.
跟if類似, 不多說了.
4.4.7 try catch finally可以嵌套使用.
這個也不難理解..
五, throw 和throws的機制和用法.
下面開始詳講異常另一種處理方法throw 和 throws了.
注意的是, 這兩種用法都沒有真正的處理異常, 真正處理的異常方法只有try catch, 這兩種方法只是交給上一級方法處理.
就如一個組織里 , 有1個大佬, 1個黨主, 1個小弟.
大佬叫黨主干活, 堂主叫小弟干活, 然后小弟碰上麻煩了, 但是小弟不會處理這個麻煩, 只能中斷工作拋給黨主處理, 然后堂主發現這個麻煩只有大佬能處理, 然后拋給大佬處理..
道理是相通的..
5.1 throw 的語法與作用
throws的語法很簡單.
語法:
throw new XException();
其中xException必須是Exception的派生類.
這里注意throw 出的是1個異常對象, 所以new不能省略
作用就是手動令程序拋出1個異常對象.
5.2 throw 1個 RuntimeException及其派生類
我們看回上面3.2 的例子:
- public int f(int a, int b){
- if (0 == b){
- throw new ArithmeticException("Shit !!! / by zero!");
- }
- return a/b;
- }
5.2.1 throw會中斷當前函數, 當前函數執行失敗(不完整)
當這個函數的if 判斷了b=0時, 就利用throws手動拋出了1個異常. 這個異常會中斷這個函數. 也就是說f()執行不完整, 是沒有返回值的.
5.2.2, 接下來哪個調用這個函數就會在調用這個函數的語句上收到異常.
- public void g(){
- int i;
- h();
- i = f(); //recevie excepton
- k();
- }
例如上沒的g()函數, 在調用f() 會收到1個異常.
這時g()函數有三種選擇.
1. 不做任何處理
這時, g()收到f()里拋出的異常就會打斷g()執行, 也就是說g()里面的k(); 被放棄了, 然后程序會繼續把這個函數拋給調用g()函數.
然后一級一級尋求處理, 如果都不處理, 則拋給jvm處理. jvm會中斷程序, 輸出異常信息. 這個上沒提到過了.
2. 使用try catch處理
如果catch成功, 則g()函數能完整執行, 而且這個異常不會繼續向上拋.
如果catch失敗(盡量避免), 則跟情況1相同.
5.3 throw 1個 非RuntimeException派生類的異常
將上面的例子改一下:
- public int f(int a, int b){
- if (0 == b){
- throw new IOException("Shit !!! / by zero!");
- }
- return a/b;
- }
例如, 我不想拋出ArithmeticException, 我想拋出IOExcetpion.
注意 這里, IOException雖然邏輯上是錯誤的(完全不是IO的問題嘛), 但是在程序中完全可行, 因為程序猿可以根據需要控制程序指定拋出任何1個異常.
但是這段代碼編譯失敗, 因為IOException 不是 RuntimeException的派生類.
java規定:
5.3.1 如果一個方法里利用throw手動拋出1個非RuntimeException異常, 必須在函數定義聲明里加上throws 后綴
改成這樣就正確了:
- public int f(int a, int b) throws IOException{
- if (0 == b){
- throw new IOException("Shit !!! / by zero!");
- }
- return a/b;
- }
注意在方法定義里加上了throws子句. 告訴調用它的函數我可能拋出這個異常.
5.3.2 調用該方法的方法則必須處理這個異常
例如抄回上面的例子, g()調用f()函數.
- public void g(){
- int i;
- h();
- i = f(); //recevie excepton
- k()
- }
但是編譯失敗.
因為f()利用throws 聲明了會拋出1個非runtimeExcetpion. 這時g()必須做出處理.
處理方法有兩種:
1. try catch自己處理:
- public void g(){
- int i = 0;
- h();
- try{
- i = f(); //recevie excepton
- }
- catch(IOException e){
- }
- k();
- }
需要注意的是, catch里面要么寫上throws對應的異常(這里是 IOException), 要么寫上這個異常的超類, 否則還是編譯失敗.
2.g()利用throws 往上一級方法拋
.
- public void g() throws IOException{
- int i = 0;
- h();
- i = f(); //recevie excepton
- k();
- }
這是調用g()的函數也要考慮上面的這兩種處理方法了...
但是最終上級的方法(main 方法)還是不處理的話, 就編譯失敗, 上面說過了, 非runtimeException無法拋給jvm處理.
雖然這兩種處理方法都能通過編譯, 但是運行效果是完全不同的.
第一種, g()能完整執行.
第二種, g()被中斷, 也就是g()里面的k(); 執行失敗.
5.4 throws 的語法.
throws稍微比throw難理解點:
語法是:
public void f() throws Exception1, Exception2...{
}
也就是講, thorws可以加上多個異常, 注意這里拋出的不是對象, 不能加上new.
而且不是告訴別人這個函數有可能拋出這么多個異常. 而是告訴別人, 有可能拋出這些異常的其中一種.
5.5 throws 的作用.
如果為f()函數加上throws后續, 則告訴調用f()的方法, f()函數有可能拋出這些異常的一種.
如果f()throws 了1個或若干個非RuntimeException, 則調用f()的函數必須處理這些非RuntimeException, 如上面的g()函數一樣.
如果f() throws的都是RuntimeException, 則調用f()的函數可以不處理, 也能通過編譯, 但是實際上還是強烈建議處理它們.
實際上, 如果1個方法f() throws A,B
那么它有可能不拋出任何異常.(程序運行狀態良好)
也有能拋出C異常(應該避免, 最好在throws上加上C)
5.6 什么時候應該用throws
5.6.1 一個函數體里面手動throw了1個RumtimeException, 則這個函數的定義必須加上throws子句
這個是強制, 告訴別人這個函數內有炸彈.
5.6.2 一個函數內有可能由系統拋出異常.
這個是非強制的, 但是如果你知道一個函數內的代碼有可能拋出異常, 最好還是寫上throws 后綴
無論這個異常是否runtimeExcepion.
5.7 一般情況下,調用1個帶有throws方法時怎么辦
個人建議, 如果你調用1個函數throws A, B, C
那么你就在當前函數寫上
try
catch(A)
catch(B)
catch(C)
catch(Exception)
這樣能處理能保證你的函數能完整執行, 不會被收到的異常中斷.
當然如果你允許你的函數可以被中斷, 那么就可以在當前函數定義加上throws A, B 繼續拋給上一級的函數.
5.8 重寫方法時, throws的范圍不能大於超類的對應方法.
例如你在一個派生類重寫一個方法f(), 在超類里的f() throws A, B 你重寫方法時就不throws出 A,,B,C 或者throws A和B的超類.
原因也是由於多態的存在.
因為1個超類的引用可以指向1個派生類的對象並調用不同的方法. 如果派生類throws的范圍加大
那么利用多態寫的代碼的try catch就不再適用.
六, throw和throws一些主要區別.
面試問得多,單獨拉出來寫了:
6.1 throw 寫在函數體內, throws寫在函數定義語句中.
應付面試官.
6.2 throw 是拋出1個異常對象, throws是有能拋出異常的種類
所以throw后面的一般加上new 和exception名字().
而throws后面不能加上new的
6.3 一個方法最多只能throw1個異常, 但是可以throws多個種類異常
因為一旦一個函數throw出1個異常, 這個函數就會被中斷執行, 后面的代碼被放棄, 如果你嘗試在函數內寫兩個throw, 編譯失敗.
而throws 是告訴別人這個函數有可能拋出這幾種異常的一種. 但是最多只會拋出一種.
6.4 如果在一個函數體內throw 1個非runtimeException, 那么必須在函數定義上加上throws后綴. 但反過來就不是必須的.
原因上面講過了.
七, 自定義異常.
我們可以自定義異常, 只需要編寫1個類, 繼承1個異常類就ok
例子:
- package Exception_kng;
- class User_Exception1 extends ArithmeticException{
- public User_Exception1(String Exception_name){
- super(Exception_name);
- }
- public void printStackTrace(){ //overwrite
- super.printStackTrace();
- System.out.printf("hey man, i am an user_defined excetpion\n");
- }
- }
- class Exp6{
- public int f(int a, int b){
- if (0 == b){
- throw new User_Exception1("Shit !!! / by zero!"); //use User_defined exception
- }
- return a/b;
- }
- }
- public class Expt_6{
- public static void g() {
- Exp6 ex = new Exp6();
- int i = 22;
- try{
- i = ex.f(8,0); //throw excetpion
- }catch(User_Exception1 e){
- e.printStackTrace();
- }
- System.out.printf("i is %d\n", i);
- System.out.printf("g() is done!!\n");
- }
- }
上面的類User_Exception1 就是1個自定義異常, 並重寫了printStackTrace()方法.
八,java異常的優缺點.
8.1 c語言是如何處理程序錯誤的.
我們要理解異常的優缺點, 首先看看沒有異常的C語言是如何處理錯誤的.
下面是個例子:
- //openfile
- if (fileOpen() > 0){
- //check the length of the file
- if (gotLengthOfTheFile() > 0){
- //check the memory
- if (gotEnoughMemory() > 0){
- //load file to memory
- if (loadFileToMem() > 0){
- readFile();
- }else{
- errorCode = -5;
- }
- }else{
- errorCode = -5;
- }
- }else{
- errorCode = -5;
- }
- }else{
- errorCode = -5;
- }
- //handle error
- case errorCode....
- //release Source
- releaseSource();
可以見到c語言處理錯誤有這些特點
1. 大部分精力都在錯誤處理.
2. 需要把各種可能出現的錯誤全部考慮到, 才能保證程序的穩定性.
3. 程序可讀性差, 錯誤處理代碼混雜在其他代碼中.
4. 出錯返回信息少, 一旦出錯難以調試.
5. 一旦出現了未考慮到的錯誤, 資源釋放代碼無法執行.
8.2 java異常機制下是如何編寫上述代碼的.
- try{
- fileOpen();
- gotLengthOfTheFile();
- gotEnoughMemory();
- loadFileToMem();
- readFile();
- }
- catch(fileOpenFail) { handle1()}
- catch(gotLengthOfTheFileFail) { handle2()}
- catch(gotEnoughMemoryFail) { handle3()}
- catch(loadFileToMemFail) { handle4()}
- catch(readFileFail) { handle4()}
- catch(Exception e) { handle5()} //catch unexpected error
- finally{
- releasSource();
- }
8.3 java異常機制的優點:
由上面的代碼可以看出部分優點:
1. 業務代碼和錯誤處理代碼分離.
2. 強制程序猿考慮程序的穩定性.
3. 有利於代碼調試(異常信息)
4. 即使任何異常產生, 能保證占用的釋放(finally)
8.4 java異常機制的缺點:
1. 異常嵌套難免影響代碼可讀性
2. 並不能令程序邏輯更加清晰.
3. 異常並不能解決所有問題