天天看點

Java常見重構技巧,去除!=null判空的5種思路,第4和第5種知道的開發者不多Java常見重構技巧 - 去除不必要的!=

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

項目中會存在大量判空代碼,多麼醜陋繁冗!如何避免這種情況?我們是否濫用了判空呢?@pdai
  • 常見重構技巧 - 去除不必要的!=
    • 場景一:null無意義之正常判斷空
    • 場景二:null無意義之使用斷言Assert
    • 場景三:寫util類是否都需要逐級判斷空
    • 場景四:讓null變的有意義
    • 場景五:Java8中使用Optional

場景一: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種思路,第4和第5種知道的開發者不多Java常見重構技巧 - 去除不必要的!=

我們來改造一下

類定義如下,這樣定義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.精簡

因為無論什麼情況,都不會傳回空對象,是以通過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 全棧知識體系 400+文章帶你構築Java開發體系