自救必看三大準則

記住這 7 條,從此告別 NullPointerException NPE 低級錯誤

 

無處不在的 NPE

有開發經驗的人都知道 Java 中的空指針異常 NullPointerException(NPE),當我們試圖使用一個值為 null 的對象引用時,就會拋出這個異常。

public class NpeDemo{
 public static void main(String[] args){
   NpeDemo npeDemo = null;
   npeDemo.go();
 }
}

當執行上面的程序的時候,就會報NPE:

Exception in thread "main" java.lang.NullPointerException
        at NpeTest.main(NpeTest.java:4)

像上面這樣的簡單方法,我們根據堆棧信息,很快就能找到問題所在。但是實際開發中,空指針可謂無處不在,一不小心就會踩坑,而且有些 NPE 是潛伏在程序當中的,只有遇到某些特殊情況時才會出現,很難測出來

那麼,我們要如何儘量避免 NPE 的發生呢?總結起來,有以下 7 條,值得收藏!


一、意識:調用方法時記得判空


這其實是句正確的廢話!寫 Java 的人都知道使用對象時有可能會出現空指針,所以 「最好都要記得」 判空,但關鍵是,如果都能記得的話,NPE 就不會那麼難纏了。而且有些情況下,NPE 隱藏得很深,往往是在真正出問題的時候才會被發現。

提高判空意識,養成判空的直覺和習慣,肯定是最理想的解決方式。但是理想很性感,現實很骨感,人是不可靠的,因此我們可以藉助工具來監督我們。

IDEA,就是一個非常不錯的幫手,上面那段代碼在 IDEA 的視角下,是這樣的:

IDEA的空指針警告

像這種低級的錯誤,IDEA 會著重提醒你:此處有 NPE 出沒,請格外小心!養成看警告的習慣比養成判空的習慣更容易些。


二、 equals 方法


我們經常需要判斷兩個對象是否相等(equals),此時,我們可能會這麼做:

public String maybeNull(String word, String defaultVal) {
         // don't do this!
 if (word.equals("haha")) {
     return defaultVal;
   } else if (word.equals(defaultVal)) {
     return "Same";
   } else {
     return "OK";
   }
 }

這個方法裡,有兩個地方容易出現 NPE,

  1. word.equals("haha")
  2. word.equals(defaultVal)

對於第一個,通常有兩種處理辦法:

  1. word!=null&&word.equals("haha")
  2. "haha".equals(word)

第二種顯然更受歡迎,然而對於兩個都是變量的 word.equals(defaultVal) 比對,第二種方法就不適用了,需要這樣判斷:

if ("haha".equals(word)) {
   return defaultVal;
 } else if (word != null && word.equals(defaultVal)) {
   return "Same";
 }

想像一下,如果你的方法裡有一堆這樣的判斷,會不會覺得煩,而且很容易走神遺漏幾個判空

所以我推薦使用 Objects.equals(a,b) 方法,這樣無論兩個對象是否有 null 值的存在,都不必擔心:

if (Objects.equals("Null", word)) {
  return defaultVal;
} else if (Objects.equals(word, defaultVal)) {
  return "Same";
}

三、自動拆箱的陷阱

有一個不容易發現但是非常容易出現 NPE 的場景:

public boolean isZero(Integer num) {
  return num == 0;
}

這個方法,可能一眼很難發現其中的問題,而且正常使用的話也不會出問題。直到有一天,你把 null 值傳給了這個方法,悲劇就發生了

Exception in thread "main" java.lang.NullPointerException
        at com.dadiyang.Computer.isZero(Computer.java:18)
        at com.dadiyang.Computer.main(Computer.java:9)

這就是自動拆箱機制導致的異常,在做 == 比較的時候,包裝類 Integer 對象會自動拆箱為 int,當這個 Integer 對象是 null 值時,拆箱就會拋出 NPE。

這種場景下,最好的方法當然是儘量將方法參數改為 int:

public boolean isZero(int num) {
  return num == 0;
}

但是如果你的方法調用者確實有要處理包裝類的需求,那麼,調用的人就得小心了,他們同樣會遇到自動拆箱問題:

自動拆箱導致的NPE

這種情況下你的方法就得負責判空了。你也可以這樣寫:

public boolean isZero(Integer num) {
  return Objects.equals(0, num);
}
  • 注意,只有你明確知道你的方法就是需要處理包裝類的時候,才使用包裝類做參數

Objects 工具類為我們提供了幾個相當不錯的靜態方法,使用起來可以大大降低 NPE 出現的機率。除了 Objects.equals 方法之外,

Objects.toString(Object o, String nullDefault)
Objects.requireNonNull(T obj, Supplier<String> messageSupplier)

也是非常好用的方法!

四、判斷字符串是否為空

實踐中,對字符串進行判空是非常高頻的操作。我們不僅要判斷一個字符串是否為 null,而且還要知道它是否為空串,甚至還需要判斷它是否為空白字符串。

新人容易犯的錯誤就是上來就直接判斷:

if(str.trim().isEmpty())

NPE 就此產生!

這種場景下,一定要記得 str 有可能為空,所以正確的姿勢應該是:

if(str==null||str.trim().isEmpty())

然而,還是想像一下,無處不在的這種散發著臭味的無聊判空,寫著寫著就會打起瞌睡來,然後遺漏掉一兩個。還是那句話,人是不可靠的!

其實 commons-lang3 為我們提供了一些非常好用的方法,最常用的,就是 StringUtils.isEmpty/isBlank(str) 這兩個用於判斷字符串是否為空的方法了。無需擔心 str 為空,儘管使用就可以了:

if(StringUtils.isBlank(str))

另一個類似的方法就是字符串的大小寫轉換了:

if(word.equals(str.toLowerCase()))

如果你經常這麼寫的話,肯定天天被 NPE 纏身。這樣的表達式,稍微思考一下,你可能會知道進行判空:

if (word != null && word.equals(str != null ? str.toLowerCase() : null))

OK,如果你這麼寫,至少不會報空指針了,然而,一個坑也就挖好了!可能幾個月後的某一天,一個讓你死活查不出來的 Bug,讓你通宵一整晚!就因為你沒有考慮到 word 和 str 都為 null 值的情況。所以正確的寫法是:

if (word == null && str == null) {
        return true;
} else {
        String lowerStr = str == null ? null : str.toLowerCase();
        return word != null && word.equals(lowerStr);
}

好吧,這樣無聊的代碼,我實在編不下去了~~

於是小手一揮,我寫下:

if (Objects.equals(word, StringUtils.lowerCase(str))){
        ...
}

五、檢查集合是否為空

跟字符串判空一樣,集合判空也是很常見的語句:

if(!list.isEmpty()){}

說了那麼多,你還是這麼寫,NPE 估計愛死你了~

if(list!=null&&!list.isEmpty()){}

能不能更優雅些呢?Of course!

commons-lang3 的好哥們 commons-collections 為我們提供了對集合的一系列工具方法,其中就有判空的方法 CollectionUtils.isEmpty(list),因此,更優雅的寫法是:

if(CollectionUtils.isNotEmpty(list)){}

有 isEmpty 當然就有 isNotEmpty 咯~

六、寫方法時儘量不要返回 null 值

我們經常會定義一個返回一個集合的方法,像這樣:

public List<String> asList(String[] arr){
        if (arr == null) {
                return null;
        }
        return Arrays.asList(arr);
}

這樣的方法定義,當然是沒問題。但是如果調用你的方法的人是個大馬哈,就會比較抓狂了。咱們做開發,還是要有點用戶思維比較好。為了儘量避免給用戶(其實是我們自己)帶來不必要的麻煩,寫方法時儘量不要返回 null 值,特別是當返回值是集合時,可以通過返回空集合來避免空指針。當然啦,總是 new 出一個空集合來,也是很影響性能的,因此我們可以使用: Collections.emptyMap/emptySet/emptyList();來返回一個全局共享的不可變的空集合:

public List<String> asList(String[] arr){
        if (arr == null) {
                return Collections.emptyList();
        }
        return Arrays.asList(arr);
}

注意咯,Collections.emptyList() 返回的是一個不可變的空集合,不要妄想往裡添加元素,否則會報錯的!絕大多數情況下,我們不可能直接往方法返回的集合里添加元素的,所以放心使用就好。

作為調用者,我們需要採取 「不信任」 的原則,就算這個方法聲稱不會返回 null 值,我們在處理返回值時仍要考慮 null 值。鬼知道哪天會不會一個不小心有人把這個方法給改了,突然返回一個 null 值呢~

七、Optional

Optional 是 Java8 帶來的新特性,它還算挺不錯的,但是相較於其他語言,如 Grovvy 或 C# 對空指針的處理,這個 Optional 我感覺真是弱爆了。Grovvy 和 C# 中,有一個 .? 操作符,你只要 str?.toString() 就可以達到

str==null?null:str.toString()

相同的效果。但是 Java 則不行。

不過,畢竟 Optional 是 Java8 推出的解決 NPE 的利器,我們還是有必要學習一下的。特別是當 Optional 結合 Lambda 表達式的場景下會非常強大。具體可以參考這篇文章:http://weishu.me/2015/12/08/use-optional-avoid-nullpointexception/

假設我們有這樣的代碼:

if ("3.0".equals(soundcard.getUSB().getVersion())) {
        System.out.println("ok");
}

顯然,這裡 soundcard 和 soundcard.getUSB() 都有可能為空,所以要進行判空:

if(soundcard != null){
 USB usb = soundcard.getUSB();
 if(usb != null && "3.0".equals(usb.getVersion()){
 System.out.println("ok");
 }
}

使用 Optional 和 Lambda 表達式,可以免去這些判空(使用lambda表達式要注意換行和縮進):

Optional.ofNullable(soundCard)
                .map(SoundCard::getUsb)
 .filter(usb -> "3.0".equals(usb.getVersion()))
 .ifPresent((usb) -> System.out.println("OK"));

這樣,當 soundCard 為空,或者 soundCard.getUsb() 為空時, 什麼事情都不會發生。只有當它們都不為空,而且 usb.getVersion() 為 3.0 時,才會列印 OK。

總結

  1. 意識:使用 obj.doSomething() 時記得判斷 obj != null。意識的養成需要一個漫長的過程,我們可以通過工具來幫忙,IDEA 就是一個非常出色的工具。
  2. 判斷對象是否相等時,使用 Objects.equals(a, b) ,當然 Objects 工具類還為我們提供了 toString 和 requireNonNull 這樣的好幫手
  3. 自動拆箱的陷阱。當使用包裝類與原始類型做比對時,要特別注意空指針問題。
  4. 檢查字符串是否為空時,使用 commons-lang3 包提供的 StringUtils.isEmpty/isBlank() 。另外, 使用 StringUtils.lowerCase/upperCase() 進行字符串轉換大小寫轉換,也可以避免空指針
  5. 使用 commons-collections 包的 CollectionUtils.isEmpty() 檢查集合是否為空
  6. 返回集合的接口若需要返回空,則返回空集而不是 null。但是每次都 new 出新的集合,會影響性能和不必要的對象創建,使用 Collections.emptyList(); 可以返回全局共享的不可變空集合
  7. Optional 是 Java8 推出的解決 NPE 的利器,當它跟 Lambda 表達式結合時會非常強大


原文網址:https://kknews.cc/code/mlzenlz.html

留言

這個網誌中的熱門文章

IIS - ASP.NET 網站基本優化設定

Node.js 部署至 IIS 站台

遇見 Parameters 參數上限之大量資料寫入方法