再見了,空指針異常(NullPointerException)!看看有哪些好的實踐能夠避開它

1. 快速介紹

空指針異常,只有你們寫過業務系統,必定對它不陌生。它是一個運行時錯誤,通常而言常見邏輯不嚴謹、懶散的代碼風格致使。它的緣由理解起來很簡單,可是要避免它卻不是一件容易的事。下面我記錄了一些我認爲比較好的實踐,這些實踐幫助我避免空指針異常的同時,也間接地提高了個人代碼質量和工做效率。這些內容一部分來自我在 Kindle 上讀過的書籍,一部分來自 StackOverflow 的建議和我本身實踐的感覺,這裏記錄下來,但願能夠給你們帶來一些啓發。數據庫

2. 問題定義

假設一個這樣的業務系統:一個任務 Task 中維持最近一次的執行記錄 Execution,執行記錄 Execution 維持了這一次的執行結果 Result。下面是接口定義:編程

public interface Task {
    Execution getExecution();
}

public interface Execution {
    Result getResult();
}

public interface Result {
}
複製代碼

如今從數據庫中查詢得到了 Task,須要得到這一次的執行結果,若是存在結果,則作下一步操做,不然跳過。咱們經常這樣來寫:函數

Task task = queryTaskFromDB();
if (task != null) {
    if (task.getExecution() != null) {
        if (task.getExecution().getResult() != null) {
            doSomethingOnResult(task.getExecution().getResult());
        }
    }
}
複製代碼

爲了邏輯上的嚴密,咱們不得不對每個 CURD 中 R 得到的內容進行判空處理,這樣的麪條代碼顯得很是囉嗦。優化

實際的開發中,可以如此細緻嚴密地對每個獲取的類進行判空處理卻不是一件容易的事情。不少時候業務代碼中的定義並無清楚的說明 Task::getExecution 到底可否能夠返回爲空:若是爲空時返回 null 仍是返回 NoSuchElementException 呢?這經常由於具體的實現的不一樣而發生變化。這也是 Java 中不友好的一面,由於這種設計致使難以徹底貫徹面向接口編程。this

3. 一些常見的最佳實踐

3.1 建議#0 嘗試使用 Optional 改造你的接口

以 2.1 中的簡單的業務系統爲例。咱們嘗試使用 Optional 來改造 TaskExecution 的接口定義:spa

public interface Task {
    Optional<Execution> getExecutionNullable();
}

public interface Execution {
    Optional<Result> getResultNullable();
}

public interface Result {
}
複製代碼

Optional 表示這個方法返回的結果可能存在,也可能爲空,若是存在則裏面是一個給定類型的對象。設計

完成接口的改造後,若是要從數據庫中查詢 Task,並拿到它的執行結果要怎麼作呢?看下面👇的代碼指針

Optional<Task> task = queryTaskFromDBNullable();
task
    .flatMap(Task::getExecutionNullable)
    .flatMap(Execution::getResultNullable)
    .ifPresent(this::doSomethingOnResult);
複製代碼

這裏面處理的關鍵是 flatMap,flatMap 方法結果根據Optional 的內容的不一樣而變化:code

  • 若是 Optional 有值,則取出值賦值給匿名函數
  • 若是 Optional 沒有值,返回一個 Optional.empty()

經過上面的作法,即可以免繁瑣的「麪條代碼」了。:)對象

3.2 其餘的一些開發建議

咱們的業務系統當然能夠經過改造接口來完成對 null pointer check 的規避,可是若是是遺留系統呢,若是是第三方庫呢?這個時候須要從代碼風格入手。

3.2.1 建議#1 習慣用 String.valueOf 代替 toString

Object obj = null;
obj.toString(); // Ops,出錯了
複製代碼

toString() 在開發中是很是經常使用的方法,可是若是 obj 爲空時會出現空指針異常,這時更好的實踐是經過靜態工廠方法 valueOf 來保證 toString 老是成功的:

Object obj = null;
String.valueOf(obj); //若是爲null 返回 "null"
複製代碼

這裏的關鍵技巧在於一部分方法的邏輯在一些靜態工廠方法中也存在,可是使用靜態工廠方法能夠保證這一次的操做徹底函數式,而不用擔憂方法所在的對象不存在。

3.2.2 建議#2 對於初始化後再也不變化的變量增長 final

String prompt = null;
int i = 50;
if (i < 50) {
    prompt = "too small";
} else if (i > 50) {
    prompt = "too large";
}
prompt.toString(); // Ops, 出錯了
複製代碼

若是增長 final 會強制你初始化

final String prompt;
int i = 50;
if (i < 50) {
    prompt = "too small";
} else if (i > 50) {
    prompt = "too large";
} else {
    prompt = "you get it";
}
prompt.toString();
複製代碼

3.2.3 建議#3 equals 比較時值在前,變量在後

String var = null;
if (var.equals("test")) {
    doSomething(); // Ops, 出錯了
}
複製代碼

換成下面的寫法就能夠保證老是正確。

String var = null;
if ("test".equals(var)) {
    doSomething(); 
}
複製代碼

這樣還有一個好處在於 == 誤寫成 = 的錯誤。不過愈來愈多的人開始使用 Intellij,這麼寫的好處已經不存在了。

4. 總結

以上我介紹了一些本身的實踐來規避或者優化空指針檢查。但願你們可以有所收穫。其中一些關於 Optional 還有涉及到 flatmap、Monad 的部分可能一部分讀者不太熟悉,後續會在更新一些文章講講我本身的一些實踐和理解。

相關文章
相關標籤/搜索