淺談Kotlin的Checked Exception機制

本文同步發表於個人微信公衆號,掃一掃文章底部的二維碼或在微信搜索 郭霖 便可關注,每一個工做日都有文章更新。

如今使用Kotlin的Android開發者已經愈來愈多了。java

這門語言從一開始的無人問津,到後來成爲Android開發的一級語言,再到後來Google官宣的Kotlin First。Kotlin正在被愈來愈多的開發者接受和承認。程序員

許多學習Kotlin的開發者以前都是學習過Java的,而且自己Kotlin就是一款基於JVM語言,所以不可避免地須要常常和Java進行比較。編程

Kotlin的諸多特性,在熟悉Java的開發者看來,有些人很喜歡,有些人不喜歡。但即便是不喜歡的那些人,一旦用熟了Kotlin進行程序開發以後,也難逃真香定律。數組

今天我想跟你們聊一聊的話題,是Kotlin在早期的時候爭議比較大的一個特性:Checked Exception機制。安全

因爲Kotlin取消了Checked Exception,這在不少Java開發者看來是徹底不可接受的,可能也是許多Java支持者拒絕使用Kotlin的緣由。但目前Kotlin已經被Google轉正兩年多了,開發了成千上萬的Android應用。你會發現,即便沒有Checked Exception,Kotlin編寫出的程序也並無出現比Java更多的問題,所以編程語言中對於Checked Exception的必要性可能並無許多人想象中的那麼高。微信

固然,本篇文章中我並不能給出一個結論來證實誰對誰錯,更多的是跟你們談一談我本身的觀點和我的心得,另外引用一些大佬的權威觀點。網絡

另外,這個問題永遠是沒有正確答案的,由於世界上沒有最好的編程語言(PHP除外)。每一個編程語言選擇不一樣的處理方式都有着本身的一套理論和邏輯,因此與其去爭論Java中的Checked Exception機制是否是多餘的,不如去論證Kotlin中沒有Checked Exception機制爲何是合理的。app

那麼,咱們首先從什麼是Checked Exception開始提及。

編程語言

什麼是Checked Exception?

Checked Exception,簡稱CE。它是編程語言爲了保證程序可以更好的處理和捕獲異常而引入的一種機制。學習

具體而言,就是當一個方法調用了另一個可能會拋出異常的接口時,要麼將這個異常進行捕獲,要麼將這個異常拋出,交給上一層進行捕獲。

熟悉Java語言的朋友對這一機制必定不會陌生,由於咱們幾乎天天都在這個機制的影響下編寫程序。

觀察以下代碼:

public void readFromFile(File file) {
    FileInputStream in = null;
    BufferedReader reader = null;
    StringBuilder content = new StringBuilder();
    try {
        in = new FileInputStream(file);
        reader = new BufferedReader(new InputStreamReader(in));
        String line = "";
        while ((line = reader.readLine()) != null) {
            content.append(line);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (reader != null) {
            try {
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

這段代碼每位Java程序員應該都很是熟悉,這是一段Java文件流操做的代碼。

咱們在進行文件流操做時有各類各樣潛在的異常可能會發生,所以這些異常必須被捕獲或者拋出,不然程序將沒法編譯經過,這就是Java的Checked Exception機制。

有了Checked Exception,就能夠保證咱們的程序不會存在一些隱藏很深的潛在異常,否則的話,這些異常會像定時炸彈同樣,隨時可能會引爆咱們的程序。

由此看來,Checked Exception是一種很是有必要的機制。

爲何Kotlin中沒有CE?

Kotlin中是沒有Checked Exception機制的,這意味着咱們使用Kotlin進行文件流操做時,即便不捕獲或者拋出異常,也能夠正常編譯經過。

熟悉Java的開發者們是否是以爲這樣嚴重沒有安全感?

那麼咱們就來嘗試分析和思考一下,爲何Kotlin中沒有Checked Exception。

我在學習Kotlin時,發現這門語言在不少設計方面都參考了一些業內的最佳編程實踐。

舉個例子,《Effective Java》這本書中有提到過,若是一個類並不是是專門爲繼承而設計的,那麼咱們就應該將它聲明成final,使其不可被繼承。

而在Kotlin當中,一個類默認就是不可被繼承的,除非咱們主動將它聲明成open。

相似的例子還有不少不少。

所以,Kotlin取消Checked Exception也確定不是隨隨便便拍腦瓜決定的,而是有不少的理論依據爲其支持。

好比說,《Thinking in Java》的做者 Bruce Eckel就曾經公開表示,Java語言中的Checked Exception是一個錯誤的決定,Java應該移除它。C#之父Anders Hejlsberg也認同這個觀點,所以C#中是沒有Checked Exception的。

那麼咱們大多數Java開發者都認爲很是有必要的Checked Exception機制到底存在什麼問題呢?

這些大佬們例舉了不少方面的緣由,可是我我的認爲最主要的緣由其實就是一個:麻煩。

Checked Exception機制雖然提高了編程語言的安全性,可是有時卻讓咱們在書寫代碼時至關抓狂。

因爲Checked Exception機制的存在,對於一些可能發生潛在異常的代碼,咱們必需要對其進行處理才行。處理方式只有兩種:要麼使用try catch代碼塊將異常捕獲住,要麼使用throws關鍵字將異常拋出。

以剛纔的文件流操做舉例,咱們使用了兩次try catch代碼塊來進行潛在的異常捕獲,但其實更多隻是爲了能讓編譯器滿意:

public void readFromFile(File file) {
    BufferedReader reader = null;
    try {
        ...
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (reader != null) {
            try {
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

這段代碼在Java當中是最標準和規範的寫法,然而你會發現,咱們幾乎沒有人能在catch中寫出什麼有意義的邏輯處理,一般都只是打印一下異常信息,告知流發生異常了。那麼流發生異常應該怎麼辦呢?沒人知道應該怎麼辦,理論上流應該老是能正常工做的。

思考一下,是否是你在close文件流時所加的try catch都只是爲了可以讓編譯經過而已?你有在close的異常捕獲中進行過什麼有意義的邏輯處理嗎?

而Checked Exception機制的存在強制要求咱們對這些未捕獲的異常進行處理,即便咱們明確不想對它進行處理都不能夠。

這種機制的設計思路自己是好的,可是卻也間接造就了不少填鴨式的代碼,只是爲了知足編譯器去編程,致使編寫了不少無心義的try catch語句,讓項目代碼看來得變得更加臃腫。

那麼若是咱們選擇不對異常進行捕獲,而是將異常向上拋出呢?事實證實,這可能也並非什麼特別好的主意。

絕大多數Java程序員應該都使用過反射的API,編寫反射代碼時有一點特別討厭,就是它的API會拋出一大堆的異常:

Object reflect(Object object, String className, String methodName, Object[] parameters, Class<?>[] parameterTypes)
        throws SecurityException, IllegalArgumentException, 
        IllegalAccessException, InvocationTargetException, 
        NoSuchMethodException, ClassNotFoundException {
    Class<?> objectClass = Class.forName(className);
    Method method = objectClass.getMethod(methodName, parameterTypes);
    return method.invoke(object, parameters);
}

這裏我只是編寫了一段最簡單的反射代碼,居然有6個異常要等着我去處理。其中每一個異常表明什麼意思我也沒能徹底搞明白,與其我本身去寫一大堆的try catch代碼,還不如直接將全部異常都拋出到上一層得了,這樣代碼看起來還能清爽一點。

你是這麼想的,上一層的人也是這麼想的,更過度的是,他可能還會在你拋出異常的基礎之上,再增長一點其餘的異常繼續往上拋出。

根據我查閱到的資料,有些項目通過這樣的層層累加以後,調用一個接口甚至須要捕獲80多個異常。想必調用這個接口的人內心必定在罵娘吧。你以爲在這種狀況下,他還能耐心地對每一種異常類型都細心進行處理嗎?絕對不可能,大機率可能他只會catch一個頂層的Exception,把全部異常都囊括進去,從而完全地讓Checked Exception機制失去意義。又或者,他可能會在當前異常拋出鏈上再加一把火,爲拋出100個異常作出貢獻。。。

最終咱們能夠看出,Java的Checked Exception機制,自己的設計初衷確實是好的,並且是先進的,可是卻對程序員有着較高的編碼規範要求。每一層方法的設計者都應該能清楚地辨別哪些異常是應該本身內部捕獲的,哪些異常是應該向上拋出的,從而讓整個方法調用棧的異常鏈都在一個合理和可控的範圍內。

然而比較遺憾的現實是,絕大多數的程序員其實都是作不到這一點的,濫用和惰性使用CE機制的狀況普遍存在,徹底達不到Java自己設計這個機制所預期的效果,這也是Kotlin取消Checked Exception的緣由。

沒有CE不會出現問題嗎?

許多Java程序員會比較擔憂這一點,Kotlin取消了Checked Exception機制,這樣不會致使個人程序變得很危險嗎?每當我調用一個方法時,都徹底不知道這個方法可能會拋出什麼異常。

首先這個問題在開頭已經給出了答案,通過兩年多的實踐發現,即便沒有Checked Exception,Kotlin開發出的程序也並無比Java開發的程序出現更多的異常。偏偏相反,Kotlin程序反卻是減小了不少異常,由於Kotlin增長了編譯期處理空指針異常的功能(空指針在各種語言的崩潰率排行榜中都一直排在第一位)。

那麼至於爲何取消Checked Exception並不會成爲致使程序出現更多異常的緣由,我想分紅如下幾個點討論。

第一,Kotlin並無阻止你去捕獲潛在的異常,只是不強制要求你去捕獲而已。

經驗豐富的程序員在編寫程序時,哪些地方最有可能發生異常其實大可能是心中有數的。好比我正在編寫網絡請求代碼,因爲網絡存在不穩定性,請求失敗是極有可能發生的事情,因此即便沒有Checked Exception,大多數程序員也都知道應該在這裏加上一個try catch,防止由於網絡請求失敗致使程序崩潰。

另外,當你不肯定調用一個方法會不會有潛在的異常拋出時,你永遠能夠經過打開這個方法,觀察它的拋出聲明來進行肯定。無論你有沒有這個類的源碼均可以看到它的每一個方法拋出了哪些異常:

public class FileInputStream extends InputStream {

    public FileInputStream(File file) throws FileNotFoundException {
        throw new RuntimeException("Stub!");
    }

    public int read(byte[] b, int off, int len) throws IOException {
        throw new RuntimeException("Stub!");
    }

    public void close() throws IOException {
        throw new RuntimeException("Stub!");
    }
    ...
}

而後當你以爲須要對這個異常進行捕獲時,再對它進行捕獲便可,至關於你仍然能夠按照以前在Java中捕獲異常的方式去編寫Kotlin代碼,只是沒有了強制的要求,你能夠自由選擇要不要進行捕獲和拋出。

第二,絕大多數的方法其實都是沒有拋出異常的。

這是一個事實,否則你絕對不會愛上Checked Exception機制,而是會每天咒罵它。

試想一下,假如你編寫的每一行代碼,調用的每個方法,都必需要對它try catch捕獲一下才行,你是否是想摔鍵盤的心都有了?

我說的這種狀況在Java中真的有一個很是典型的例子,就是Thread.sleep()方法。因爲Thread.sleep()方法會拋出一個InterruptedException,因此每次咱們調用這個方法時,都必需要用try catch捕獲一下:

public class Main {
    
    public void test() {
        // do something before
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // do something after
    }
    
}

這也是我極其不喜歡這個方法的緣由,用起來就是一個字:煩。

事實上,可能絕大多數Java程序員甚至都不知道爲何要捕獲這個異常,只知道編譯器提醒我必須捕獲。

之因此咱們在調用Thread.sleep()方法時須要捕獲InterruptedException,是由於若是在當前線程睡眠的過程當中,咱們在另一個線程對中這個睡眠中的線程進行中斷(調用thrad.interrupt()方法),那麼sleep()方法會結束休眠,並拋出一個InterruptedException。這種操做是很是少見的,可是因爲Checked Exception的存在,咱們每一個人都須要爲這一個少見的操做買單:即每次調用Thread.sleep()方法時,都要寫一段長長的try catch代碼。

而到了Kotlin當中,你會再也不討厭使用Thread.sleep()方法,由於沒有了Checked Exception,代碼也變得清爽了:

class Main {

    fun test() {
        // do something before
        Thread.sleep(1000)
        // do something after
    }

}

第三,擁有Checked Exception的Java也並非那麼安全。

有些人認爲,Java中擁有Checked Exception機制,調用的每一個方法你都會感到放心,由於知道它會拋出什麼異常。而沒有Checked Exception的話,調用任何方法內心都感受沒底。

那麼這種說法有道理嗎?顯然這不是真的。否則,你的Java程序應該永遠都不會崩潰纔對。

事實上,Java將全部的異常類型分紅了兩類:受檢查異常和不受檢查異常。只有受檢查異常纔會受到Checked Exception機制的約束,不受檢查異常是不會強制要求你對異常進行捕獲或拋出的。

好比說,像NullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException這些都是不受檢查的異常,因此你調用的方法中即便存在空指針、數組越界等異常風險,Checked Exception機制也並不會要求你進行捕獲或拋出。

因而可知,即便Java擁有Checked Exception機制,也並不能向你保證你調用的每一個方法都是安全的,並且我認爲空指針和數組越界等異常要遠比InterruptedException之類的異常更加常見,但Java並無對此進行保護。

至於Java是如何劃分哪些異常屬於受檢查異常,哪些屬於不受檢查異常,這個我也不太清楚。Java的設計團隊必定有本身的一套理論依據,只不過這套理論依據看上去並無被其餘語言的設計者所承認。

所以,你大概能夠理解成,Kotlin就是把異常類型進一步進行了簡化,將全部異常都歸爲了避免受檢查異常,僅此而已。

結論

因此,最終的結論是什麼呢?

很遺憾,沒有結論。正如任何事物都有其多樣性同樣,關於Checked Exception這個問題上面,也沒有一個統一的定論。

Java擁有Checked Exception機制並非錯誤的,Kotlin中取消Checked Exception機制也不是錯誤的。我想這大概就是你閱讀完本文以後可以得出的結論吧。

可是,但願你自此日後,在使用Kotlin編程程序時,不要再爲有沒有Checked Exception的問題所糾結了。

若是想要學習Kotlin和最新的Android知識,能夠參考個人新書 《第一行代碼 第3版》點擊此處查看詳情


關注個人技術公衆號,每一個工做日都有優質技術文章推送。

微信掃一掃下方二維碼便可關注:

image

相關文章
相關標籤/搜索