記住這 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 會著重提醒你:此處有 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,
- word.equals("haha")
- word.equals(defaultVal)
對於第一個,通常有兩種處理辦法:
- word!=null&&word.equals("haha")
- "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; }
但是如果你的方法調用者確實有要處理包裝類的需求,那麼,調用的人就得小心了,他們同樣會遇到自動拆箱問題:
這種情況下你的方法就得負責判空了。你也可以這樣寫:
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。
總結
- 意識:使用 obj.doSomething() 時記得判斷 obj != null。意識的養成需要一個漫長的過程,我們可以通過工具來幫忙,IDEA 就是一個非常出色的工具。
- 判斷對象是否相等時,使用 Objects.equals(a, b) ,當然 Objects 工具類還為我們提供了 toString 和 requireNonNull 這樣的好幫手
- 自動拆箱的陷阱。當使用包裝類與原始類型做比對時,要特別注意空指針問題。
- 檢查字符串是否為空時,使用 commons-lang3 包提供的 StringUtils.isEmpty/isBlank() 。另外, 使用 StringUtils.lowerCase/upperCase() 進行字符串轉換大小寫轉換,也可以避免空指針
- 使用 commons-collections 包的 CollectionUtils.isEmpty() 檢查集合是否為空
- 返回集合的接口若需要返回空,則返回空集而不是 null。但是每次都 new 出新的集合,會影響性能和不必要的對象創建,使用 Collections.emptyList(); 可以返回全局共享的不可變空集合
- Optional 是 Java8 推出的解決 NPE 的利器,當它跟 Lambda 表達式結合時會非常強大。
原文網址:https://kknews.cc/code/mlzenlz.html
留言
張貼留言