Java常見重構技巧 - 去除不必要的!=null判斷空的5種方式,很少有人知道後兩種

pdai 發表於 2020-09-15

常見重構技巧 - 去除不必要的!=

專案中會存在大量判空程式碼,多麼醜陋繁冗!如何避免這種情況?我們是否濫用了判空呢?@pdai

場景一:null無意義之常規判斷空

  • 通常是這樣的
private void xxxMethod(String key){
    if(key!=null&&!"".equals(key)){
        // do something
    }
}
  • 初步的,使用Apache Commons,Guvava, Hutool等StringUtils
private void xxxMethod(String key){
    if(StringUtils.isNotEmpty(key)){
        // do something
    }
}

場景二:null無意義之使用斷言Assert

  • 考慮用Assert斷言
private void xxxMethod(String key){
    Assert.notNull(key);

    // do something
}

場景三:寫util類是否都需要逐級判斷空

逐級判斷空,還是丟擲自定義異常,還是不處理?It Depends...

隨手翻了下,hutool IdcardUtil 顯然是交給呼叫者判斷的。

/**
    * 是否有效身份證號
    *
    * @param idCard 身份證號,支援18位、15位和港澳臺的10位
    * @return 是否有效
    */
public static boolean isValidCard(String idCard) {
    idCard = idCard.trim();// 這裡idCard沒判斷空
    int length = idCard.length();
    switch (length) {
        case 18:// 18位身份證
            return isValidCard18(idCard);
        case 15:// 15位身份證
            return isValidCard15(idCard);
        case 10: {// 10位身份證,港澳臺地區
            String[] cardVal = isValidCard10(idCard);
            return null != cardVal && "true".equals(cardVal[2]);
        }
        default:
            return false;
    }
}
  • 再比如 Apache Common IO中, 並沒判斷空
/**
    * Copy bytes from a <code>byte[]</code> to an <code>OutputStream</code>.
    * @param input the byte array to read from
    * @param output the <code>OutputStream</code> to write to
    * @throws IOException In case of an I/O problem
    */
public static void copy(final byte[] input, final OutputStream output)
        throws IOException {
    output.write(input);
}

場景四:讓null變的有意義

返回一個空物件(而非null物件),比如NO_ACTION是特殊的Action,那麼我們就定義一個ACTION。下面舉個“栗子”,假設有如下程式碼

public interface Action {
  void doSomething();}

public interface Parser {
  Action findAction(String userInput);
}

其中,Parse有一個介面FindAction,這個介面會依據使用者的輸入,找到並執行對應的動作。假如使用者輸入不對,可能就找不到對應的動作(Action),因此findAction就會返回null,接下來action呼叫doSomething方法時,就會出現空指標。

解決這個問題的一個方式,就是使用Null Object pattern(空物件模式)

NullObject模式首次發表在“ 程式設計模式語言 ”系列叢書中。一般的,在面嚮物件語言中,對物件的呼叫前需要使用判空檢查,來判斷這些物件是否為空,因為在空引用上無法呼叫所需方法。

Java常見重構技巧 - 去除不必要的!=null判斷空的5種方式,很少有人知道後兩種

我們來改造一下

類定義如下,這樣定義findAction方法後,確保無論使用者輸入什麼,都不會返回null物件:

public class MyParser implements Parser {
  private static Action NO_ACTION = new Action() {
    public void doSomething() { /* do nothing */ }
  };

  public Action findAction(String userInput) {
    // ...
    if ( /* we can't find any actions */ ) {
      return NO_ACTION;
    }
  }
}

對比下面兩份呼叫例項

1.冗餘: 每獲取一個物件,就判一次空

Parser parser = ParserFactory.getParser();
if (parser == null) {
  // now what?
  // this would be an example of where null isn't (or shouldn't be) a valid response
}
Action action = parser.findAction(someInput);
if (action == null) {
  // do nothing} 
else {
  action.doSomething();
}

2.精簡

ParserFactory.getParser().findAction(someInput).doSomething();

因為無論什麼情況,都不會返回空物件,因此通過findAction拿到action後,可以放心地呼叫action的方法。

順便再提下一個外掛:

.NR Null Object外掛
NR Null Object是一款適用於Android Studio、IntelliJ IDEA、PhpStorm、WebStorm、PyCharm、RubyMine、AppCode、CLion、GoLand、DataGrip等IDEA的Intellij外掛。其可以根據現有物件,便捷快速生成其空物件模式需要的組成成分,其包含功能如下:

  • 分析所選類可宣告為介面的方法;
  • 抽象出公有介面;
  • 建立空物件,自動實現公有介面;
  • 對部分函式進行可為空宣告;
  • 可追加函式進行再次生成;
  • 自動的函式命名規範

場景五:Java8中使用Optional

假設我們有一個像這樣的類層次結構:

class Outer {
    Nested nested;
    Nested getNested() {
        return nested;
    }
}
class Nested {
    Inner inner;
    Inner getInner() {
        return inner;
    }
}
class Inner {
    String foo;
    String getFoo() {
        return foo;
    }
}

解決這種結構的深層巢狀路徑是有點麻煩的。我們必須編寫一堆 null 檢查來確保不會導致一個 NullPointerException:

Outer outer = new Outer();
if (outer != null && outer.nested != null && outer.nested.inner != null) {
    System.out.println(outer.nested.inner.foo);
}

我們可以通過利用 Java 8 的 Optional 型別來擺脫所有這些 null 檢查。map 方法接收一個 Function 型別的 lambda 表示式,並自動將每個 function 的結果包裝成一個 Optional 物件。這使我們能夠在一行中進行多個 map 操作。Null 檢查是在底層自動處理的。

Optional.of(new Outer())
    .map(Outer::getNested)
    .map(Nested::getInner)
    .map(Inner::getFoo)
    .ifPresent(System.out::println);

還有一種實現相同作用的方式就是通過利用一個 supplier 函式來解決巢狀路徑的問題:

Outer obj = new Outer();
resolve(() -> obj.getNested().getInner().getFoo());
    .ifPresent(System.out::println);

呼叫 obj.getNested().getInner().getFoo()) 可能會丟擲一個 NullPointerException 異常。在這種情況下,該異常將會被捕獲,而該方法會返回 Optional.empty()。

public static <T> Optional<T> resolve(Supplier<T> resolver) {
    try {
        T result = resolver.get();
        return Optional.ofNullable(result);
    }
    catch (NullPointerException e) {
        return Optional.empty();
    }
}

請記住,這兩個解決方案可能沒有傳統 null 檢查那麼高的效能。不過在大多數情況下不會有太大問題。

  • 更多Optional,可以看這篇: Java 8 - Optional類
    • Optional類的意義
    • Optional類有哪些常用的方法
    • Optional舉例貫穿所有知識點
    • 多重類巢狀Null值判斷

更多

更多文章請參考 Java 全棧知識體系