定義
java中的異常提供了一種識別及響應(yīng)錯誤情況的一致性機制,若有效地處理異常能使程序更加的健壯,且更易于調(diào)試仲吏。異常之所以是一種強大的調(diào)試手段,在于其回答了三個問題:什么出了錯暂题?在哪出的錯?為什么出錯究珊?
異常的類型回答了“什么”被拋出(什么出了錯)薪者,異常的堆棧信息回答了“在哪”拋出(在哪出的錯),異常的函數(shù)調(diào)用信息回答了“為什么”會拋出(為什么出錯)剿涮,如果你的異常沒有回答以上全部問題言津,那么可能你沒有很好地使用它們。
異常分類
異常整體上可以分為以下3種類型:
-
運行時異常:是
RuntimeException
類及其子類標識的異常取试,程序中可以選擇捕獲處理悬槽,也可以不處理。這些異常一般是由程序邏輯錯誤引起的瞬浓,我們需要從程序邏輯的角度盡可能避免這類異常的發(fā)生初婆。(運行時異常的特點是Java編譯器不會檢查它) - 檢查性異常:正確的程序在運行中,很容易出現(xiàn)的猿棉、情理可容的異常狀況磅叛。檢查性異常雖然是異常狀況,但在一定程度上它的發(fā)生是可以預(yù)計的萨赁,而且一旦發(fā)生這種異常狀況弊琴,需要采取某種方式進行處理(捕獲處理或者繼續(xù)拋出)。(這些異常在編譯時不能被簡單地忽略杖爽,必須捕獲處理或繼續(xù)拋出)
-
錯誤:
Error
類型及子類是程序所無法處理的錯誤访雪,表示運行的應(yīng)用程序中的較嚴重問題。大多數(shù)錯誤與代碼編寫者執(zhí)行的操作無關(guān)掂林,而是表示代碼運行時JVM(Java 虛擬機)出現(xiàn)的問題臣缀。如:當JVM不再有繼續(xù)執(zhí)行操作所需的內(nèi)存資源時將出現(xiàn)OutOfMemoryError
等。
具體使用
略泻帮。(本篇文章不打算介紹基本使用)
異常處理三原則
使用以下三個原則可以幫助我們在調(diào)試過程中最大限度地使用好異常機制精置。
具體明確
當我們需要拋出一個異常時,應(yīng)盡可能的使用能描述具體問題的異常子類锣杂,而不是new
一個通用的基類(如錯誤寫法:throw new Exception("test exception");
)脂倦。其目的是為了在捕獲異常的時候,我們能根據(jù)異常的類型一眼就能分辨什么出了錯元莫,另外更重要的是捕獲異常處理時赖阻,能將異常類型對應(yīng)到不同的catch塊,以便針對不同的異常做出不同的處理踱蠢。
比如火欧,異常IOException
更加特化的異常FileNotFoundException
棋电、EOFException
和ObjectStreamException
,這些都是IOException
的子類苇侵,每一種都描述了一類特定的I/O
錯誤:分別是文件不存在赶盔,異常文件結(jié)尾和錯誤的序列化對象流。提早拋出
提早拋出異常的意思是榆浓,在可預(yù)見的異常前通過程序代碼檢查可能的異常(特別是運行時異常)于未,要么通過if條件過濾即將拋出的異常使整個函數(shù)調(diào)用立即返回,要么構(gòu)造一個更加特例的異常提前拋出陡鹃。其目的是為了能更清晰的定義一些可能預(yù)知的異常烘浦、避免不必要的對象構(gòu)造或資源占用,比如文件或網(wǎng)絡(luò)連接萍鲸,還能避開了資源操作所帶來的清理動作谎倔。
比如:
testException.readValueFromFile(null); //測試傳入一個為null的文件名
public int readValueFromFile(String filename) {
int size = 0;
InputStream in = null;
try {
in = new FileInputStream(filename); //FileInputStream會拋出異常
} catch (Exception e) {
} finally {
in.close();
}
return 0;
}
將會拋出一下異常:
java.lang.NullPointerException: Attempt to invoke virtual method 'char[] java.lang.String.toCharArray()' on a null object reference
at java.io.File.fixSlashes(File.java:183)
at java.io.File.<init>(File.java:130)
at java.io.FileInputStream.<init>(FileInputStream.java:103)
at com.android.test.demo.exception.TestException.readValueFromFile(TestException.java:22)
at com.android.test.demo.MainActivity.testException(MainActivity.java:56)
at com.android.test.demo.MainActivity.onCreate(MainActivity.java:41)
從異常堆棧信息看:是FileInputStream
的構(gòu)造函數(shù)中拋出了NullPointerException
異常,而JDK API一般不會出錯的猿推,很可能是我們的調(diào)用邏輯有問題片习,但異常堆棧卻不能清晰的看出到底是什么為空導(dǎo)致的空指針,假設(shè)我們回退堆棧和檢查程序最終還是會發(fā)現(xiàn)是filename
參數(shù)傳了空導(dǎo)致的蹬叭。
??如果我們在實例化FileInputStream
之前做一次參數(shù)檢查藕咏,若為空時提早拋出IllegalArgumentException
,如下所示:
if (filename == null){
throw new IllegalArgumentException("filename is null");
}
則會拋出這樣的堆棧信息:
java.lang.IllegalArgumentException: filename is null
at com.android.test.demo.exception.TestException.readValueFromFile(TestException.java:23)
at com.android.test.demo.MainActivity.testException(MainActivity.java:56)
at com.android.test.demo.MainActivity.onCreate(MainActivity.java:41)
此時堆棧信息不會深入到FileInputStream
中去了(一般情況不會是JDK的API出現(xiàn)異常)秽五,而是明確的告訴了我們?nèi)齻€問題:出了什么錯(提供了非法參數(shù)值)坦喘,為什么出錯(文件名不能為空值)盲再,以及哪里出的錯(readValueFromFile()函數(shù))瓣铣。
-
延遲捕獲
延遲捕獲的意思是答朋,當函數(shù)出現(xiàn)異常且在當前函數(shù)無法做出有效處理時,應(yīng)當將其拋給函數(shù)的調(diào)用者去處理棠笑,以便調(diào)用者有機會通過不同的參數(shù)從異常中恢復(fù)出來。比如础钠,在調(diào)用者第一次傳入錯誤的參數(shù)導(dǎo)致異常后翻具,調(diào)用者仍有機會(在catch{}
中)再一次傳入某個默認參數(shù)來使該函數(shù)調(diào)用成功工禾。
??大多數(shù)人可能都會犯的一個錯是槽畔,在程序有能力處理異常之前就捕獲了它。Java編譯器要求檢查出的異常必須被捕獲或拋出間接助長了這種行為拾给,大家自然而然的做法就是立即將代碼用try
塊包裝起來额衙,并使用catch
捕獲異常,以免編譯器報錯。如果當前函數(shù)有能力處理異常還好,不然只能僅僅打印一下異常堆棧信息耸黑,無法通過異常機制對函數(shù)的執(zhí)行提供有效的幫助大刊。
finally{}語句塊的執(zhí)行情況
一般的描述是:try{}
里有一個return
語句,那么緊跟在這個try
后的finally{}
里的code會不會被執(zhí)行三椿,什么時候被執(zhí)行缺菌,在return
前還是后。會不會影響返回值搜锰?
先給結(jié)論:finally
塊的語句在try
或catch
中的return
語句執(zhí)行之后返回之前執(zhí)行伴郁,且finally
里的修改語句可能影響也可能不影響try
或catch
中return
已經(jīng)確定的返回值(由返回值的傳遞類型決定:傳值還是傳地址),若finally
里也有return
語句則覆蓋try
或catch
中的return
語句直接返回蛋叼。
測試1:
//測試調(diào)用
final int value = testException.testFinally1();
Log.d(TAG, "testFinally1 return size: " + value);
public int testFinally1() {
int x = 1;
try {
++x;
Log.d(TAG, "try{} x: " + x);
return returnSize(x);
} finally { //finally塊在retrun語句執(zhí)行執(zhí)行之后焊傅,返回之前執(zhí)行
++x;
Log.d(TAG, "finally{} x: " + x);
}
}
private int returnSize(int size) {
Log.d(TAG, "enter returnSize()");
return size;
}
執(zhí)行結(jié)果:
TestException( 1468): try{} x: 2
TestException( 1468): enter returnSize()
TestException( 1468): finally{} x: 3
TestException( 1468): testFinally1 return size: 2
可以看到函數(shù)returnSize
在finally{}
塊之前執(zhí)行了(finally{}
在return
語句執(zhí)行之后,返回之前執(zhí)行)狈涮,且finally{}
的++x
沒有生效(return
的時候是復(fù)制了一份變量然后返回狐胎,所以之后finally
操作的變量如果是基本類型的話不會影響返回值)
測試2:
//測試調(diào)用
Map<String, String> map = testException.testFinally2();
Log.d(TAG, "testFinally2 map: " + (map != null ? map.get("key") : "null"));
public Map<String, String> testFinally2() {
Map<String, String> result = new HashMap<>();
result.put("key", "start");
try {
result.put("key", "try");
return result;
} catch (Exception e) {
result.put("key", "catch");
} finally {
result.put("key", "finally"); //此處生效
result = null; //此處不生效
}
return result;
}
執(zhí)行結(jié)果:
TestException( 1468): testFinally2 map: finally
從測試結(jié)果我們可以看到,finally{}
的code生效了歌馍,這是因為返回值為引用類型握巢,雖然在return
的時候是復(fù)制了一份變量然后返回,但該變量指向的是同一個對象松却,因此這里的操作會反映到返回結(jié)果中暴浦;result = null;
這句代碼不生效的原因同測試1溅话。
另外,若finally
里也有return
語句時歌焦,則覆蓋try
或catch
中的return
語句直接返回飞几。----原因也可以從上面兩個測試例子中得出結(jié)論。
自定義異常
在開發(fā)過程中独撇,自我認知范圍內(nèi)需要使用自定義異常的情況總結(jié)為以下兩種:
- 自定義異常繼承自某個相關(guān)異常屑墨,拋出自定義異常時,信息可以根據(jù)情況自定義券勺,從而使得異承髟浚可以隱藏底層的異常中灿里,使得信息更安全关炼、也更加直觀。因為自定義異诚坏酰可以拋出我們自己想要拋出的信息儒拂,也可以通過拋出的信息區(qū)分異常發(fā)生的位置、根據(jù)異常名我們就可以知道哪里有異常色鸳,根據(jù)異常提示信息進行程序修改社痛。比如空指針異常NullPointException,我們可以拋出信息為“xxx為空”定位異常位置命雀,而不用輸出堆棧信息蒜哀。
測試:
//自定義異常類
public class TestGHException extends RuntimeException {
public TestGHException(Throwable cause) {
super(cause);
}
public TestGHException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public TestGHException(String message) {
super(message);
}
public TestGHException(String message, Throwable cause) {
super(message, cause);
}
}
測試1(不使用自定義異常):
public void testGHException() {
String value = "test";
value = null;
final String sub = value.substring(0, 1);
}
輸出異常信息:
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String java.lang.String.substring(int, int)' on a null object reference
at com.android.test.demo.exception.TestException.testGHException(TestException.java:112)
at com.android.test.demo.MainActivity.testException(MainActivity.java:64)
at com.android.test.demo.MainActivity.onCreate(MainActivity.java:40)
at android.app.Activity.performCreate(Activity.java:6362)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1122)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2656)
測試2(使用自定義異常):
public void testGHException() {
try {
String value = "test";
value = null;
final String sub = value.substring(0, 1);
} catch (NullPointerException e) {
throw new TestGHException("string value == null", e);
}
}
輸出異常信息:
Caused by: com.android.test.demo.exception.TestGHException: string value == null
at com.android.test.demo.exception.TestException.testGHException(TestException.java:105)
at com.android.test.demo.MainActivity.testException(MainActivity.java:64)
at com.android.test.demo.MainActivity.onCreate(MainActivity.java:40)
at android.app.Activity.performCreate(Activity.java:6362)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1122)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2656)
結(jié)論:通過比較上面兩個測試例子可以看出,自定義異常使得異常堆棧信息更加直觀吏砂,一樣就能看到出現(xiàn)問題的地方撵儿。
- 可以實現(xiàn)異常在受檢異常和運行時異常之間互相轉(zhuǎn)換。有時候引用的某些API拋出了運行時異常(由于不需要編譯器檢查狐血,卻又有可能異常)淀歇,為了具體業(yè)務(wù)的需要,可以在該API上再包一層將這個運行時異常轉(zhuǎn)換成受檢異常匈织,以便提醒調(diào)用者注意該異常浪默,或者反之,簡化繁瑣的受檢異常缀匕,典型的用例是jOOR中對反射接口的受檢異常(ClassNotFoundException纳决、NoSuchMethodException、NoSuchFieldException等)統(tǒng)一轉(zhuǎn)化成ReflectException定義的運行時異常乡小。
jOOR代碼:
//ReflectException自定義異常
public class ReflectException extends RuntimeException {
}
//函數(shù)執(zhí)行接口岳链,
public Reflect call(String name, Object... args) throws ReflectException {
Class<?>[] types = types(args);
// Try invoking the "canonical" method, i.e. the one with exact
// matching argument types
try {
Method method = exactMethod(name, types);
return on(method, object, args);
}
// If there is no exact match, try to find a method that has a "similar"
// signature if primitive argument types are converted to their wrappers
catch (NoSuchMethodException e) {
try {
Method method = similarMethod(name, types);
return on(method, object, args);
} catch (NoSuchMethodException e1) {
throw new ReflectException(e1); //此處將受檢異常轉(zhuǎn)換成了運行時異常
}
}
}
//調(diào)用時 ,省去了檢查操作
public static boolean isSplitMode(Context context) {
return Reflect.on("meizu.splitmode.FlymeSplitModeManager")
.call("getInstance", context)
.call("isSplitMode").get();
}
常見影響app的崩潰率指標的異常分析
準備在單獨章節(jié)中持續(xù)更新劲件。
總結(jié)
有經(jīng)驗的的開發(fā)人員都知道掸哑,調(diào)試程序的最大難點不在于修復(fù)bug约急,而在于從海量的代碼中找出bug的藏身之處。因此苗分,我們要做的就是在寫代碼過程中盡可能的暴露bug的可能出處厌蔽。
在《Effective Java》中對異常的使用給出了以下指導(dǎo)原則:
- 不要將異常處理用于正常的控制流(設(shè)計良好的API不應(yīng)該強迫它的調(diào)用者為了正常的控制流而使用異常)
- 對可以恢復(fù)的情況使用受檢異常,對編程錯誤使用運行時異常
- 避免不必要的使用受檢異常(可以通過一些狀態(tài)檢測手段來避免異常的發(fā)生)
- 優(yōu)先使用標準的異常
- 每個方法拋出的異常都要有文檔
- 保持異常的原子性
- 不要在catch中忽略掉捕獲到的異常*