Java 缺失的特性:擴充套件方法

阿里巴巴雲原生發表於2023-04-03

*作者:周密(之葉)*


## 什麼是擴充套件方法


擴充套件方法,就是能夠向現有型別直接“新增”方法,而無需建立新的派生型別、重新編譯或以其他方式修改現有型別。呼叫擴充套件方法的時候,與呼叫在型別中實際定義的方法相比沒有明顯的差異。


## 為什麼需要擴充套件方法


考慮要實現這樣的功能:從 Redis 取出包含多個商品ID的字串後(每個商品ID使用英文逗號分隔),先對商品ID進行去重(並能夠維持元素的順序),最後再使用英文逗號將各個商品ID進行連線。


```

// "123,456,123,789"

String str = redisService.get(someKey)

```


傳統寫法:


```

String itemIdStrs = String.join(",", new LinkedHashSet<>(Arrays.asList(str.split(","))));

```


使用 Stream 寫法:


```

String itemIdStrs = Arrays.stream(str.split(",")).distinct().collect(Collectors.joining(","));

```


假設在 Java 中能實現擴充套件方法,並且我們為陣列新增了擴充套件方法 toList(將陣列變為 List),為 List 新增了擴充套件方法 toSet(將 List 變為 LinkedHashSet),為 Collection 新增了擴充套件方法 join(將集合中元素的字串形式使用給定的連線符進行連線),那我們將可以這樣寫程式碼:


```

String itemIdStrs = str.split(",").toList().toSet().join(",");

```


相信此刻你已經有了為什麼需要擴充套件方法的答案:


-   可以對現有的類庫,進行**直接**增強,而不是使用工具類

-   相比使用工具類,使用型別本身的方法寫程式碼更流暢更舒適

-   程式碼更容易閱讀,因為是鏈式呼叫,而不是用靜態方法套娃  


## 在 Java 中怎麼實現擴充套件方法


我們先來問問最近大火的 ChatGPT:


![1.png](~tplv-k3u1fbpfcp-zoom-1.image "1.png")


好吧,ChatGPT 認為 Java 裡面的擴充套件方法就是透過工具類提供的靜態方法 :)。所以接下來我將介紹一種全新的黑科技:


Manifold(<)


### 準備條件


Manifold 的原理和 Lombok 是類似的,也是在編譯期間透過註解處理器進行處理。所以要在 IDEA 中正確使用 Manifold,需要安裝 Manifold IDEA 的外掛:


![2.png](~tplv-k3u1fbpfcp-zoom-1.image "2.png")


然後再在專案 pom 的 maven-compiler-plugin 中加入 annotationProcessorPaths:


```

<project xmlns=" xmlns:xsi="

         xsi:schemaLocation="


  ...


    <properties>

        <manifold.version>2022.1.35</manifold.version>

    </properties>


    <dependencies>

        <dependency>

            <groupId>systems.manifold</groupId>

            <artifactId>manifold-ext</artifactId>

            <version>${manifold.version}</version>

        </dependency>


        ...

    </dependencies>


    <!--Add the -Xplugin:Manifold argument for the javac compiler-->

    <build>

        <plugins>

            <plugin>

                <groupId>org.apache.maven.plugins</groupId>

                <artifactId>maven-compiler-plugin</artifactId>

                <version>3.8.1</version>

                <configuration>

                    <source>8</source>

                    <target>8</target>

                    <encoding>UTF-8</encoding>

                    <compilerArgs>

                        <arg>-Xplugin:Manifold no-bootstrap</arg>

                    </compilerArgs>

                    <annotationProcessorPaths>

                        <path>

                            <groupId>systems.manifold</groupId>

                            <artifactId>manifold-ext</artifactId>

                            <version>${manifold.version}</version>

                        </path>

                    </annotationProcessorPaths>

                </configuration>

            </plugin>

        </plugins>

    </build>

</project>

```


如果你的專案中使用了 Lombok,需要把 Lombok 也加入 annotationProcessorPaths:


```

<annotationProcessorPaths>

    <path>

        <groupId>org.projectlombok</groupId>

        <artifactId>lombok</artifactId>

        <version>${lombok.version}</version>

    </path>

    <path>

        <groupId>systems.manifold</groupId>

        <artifactId>manifold-ext</artifactId>

        <version>${manifold.version}</version>

    </path>

</annotationProcessorPaths>

```


### 編寫擴充套件方法


JDK 中,String 的 split 方法,使用的是字串作為引數,即 String[] split(String)。我們現在來為 String 新增一個擴充套件方法 String[] split(char):按給定的字元進行分割。


基於 Manifold,編寫擴充套件方法:


```

package com.alibaba.zhiye.extensions.java.lang.String;


import manifold.ext.rt.api.Extension;

import manifold.ext.rt.api.This;

import org.apache.commons.lang3.StringUtils;


/**

 * String 的擴充套件方法

 */

@Extension

public final class StringExt {


    public static String[] split(@This String str, char separator) {

        return StringUtils.split(str, separator);

    }

}

```


可以發現本質上還是工具類的靜態方法,但是有一些要求:


1.  工具類需要使用 Manifold 的 @Extension 註解  


1.  靜態方法中,目標型別的引數,需要使用 @This 註解  


1.  工具類所在的包名,需要以 **extensions.目標型別全限定類名** 結尾 


—— 用過 C# 的同學應該會會心一笑,這就是模仿的 C# 的擴充套件方法。


關於第 3 點,之所以有這個要求,是因為 Manifold 希望能快速找到專案中的擴充套件方法,避免對專案中所有的類進行註解掃描,提升處理的效率。


具備了擴充套件方法的能力,現在我們就可以這樣呼叫了:


Amazing!而且你可以發現,System.out.println(numStrs.toString()) 列印的居然是陣列物件的字串形式 —— 而不是陣列物件的地址。檢視反編譯後的 App.class,發現是將擴充套件方法的呼叫,替換為靜態方法呼叫:


![4.png](~tplv-k3u1fbpfcp-zoom-1.image "4.png")


而陣列的 toString 方法,使用的是 Manifold 為陣列定義的擴充套件方法 ManArrayExt.toString(@This Object array):


![5.png](~tplv-k3u1fbpfcp-zoom-1.image "5.png")


[Ljava.lang.String;@511d50c0 什麼的,Goodbye,再也不見~


因為是在編譯期將擴充套件方法的呼叫替換為靜態方法呼叫,所以使用 Manifold 的擴充套件方法,即使呼叫方法的物件是 null 也沒有問題,因為處理後的程式碼是把 null 作為引數傳遞到對應的靜態方法。比如我們對 Collection 進行擴充套件:


```

package com.alibaba.zhiye.extensions.java.util.Collection;


import manifold.ext.rt.api.Extension;

import manifold.ext.rt.api.This;


import java.util.Collection;


/**

 * Collection 的擴充套件方法

 */

@Extension

public final class CollectionExt {


    public static boolean isNullOrEmpty(@This Collection<?> coll) {

        return coll == null || coll.isEmpty();

    }

}

```


然後呼叫的時候:


```

List<String> list = getSomeNullableList();


// list 如果為 null 會進入 if 塊,而不會觸發空指標異常

if (list.isNullOrEmpty()) {

  // TODO

}

```


java.lang.NullPointerException,Goodbye,再也不見~


### 陣列擴充套件方法


JDK 中,陣列並沒有一個具體的對應型別,那為陣列定義的擴充套件類,要放到什麼包中呢?看下 ManArrayExt 的原始碼,發現 Manifold 專門提供了一個類 manifold.rt.api.Array,用來表示陣列。比如 ManArrayExt 中為陣列提供的 toList 的方法:


![6.png](~tplv-k3u1fbpfcp-zoom-1.image "6.png")


我們看到 List<@Self(true) Object> 這樣的寫法:@Self 是用來表示被註解的值應該是什麼型別,如果是 @Self,即 @Self(false),表示被註解的值和 @This 註解的值是同一個型別;@Self(true) 則表示是陣列中元素的型別。


對於物件陣列,我們可以看到 toList 方法返回的就是對應的 List<T>(T 為陣列元素的型別):


![7.png](~tplv-k3u1fbpfcp-zoom-1.image "7.png")


但如果是原始型別陣列,IDEA 指示的返回值是:


![8.png](~tplv-k3u1fbpfcp-zoom-1.image "8.png")


但是我用的是 Java 啊,擦除法泛型怎麼可能擁有 List<char> 這麼偉大的功能 —— 所以你只能用原生型別來接收這個返回值 :)


![9.png](~tplv-k3u1fbpfcp-zoom-1.image "9.png")


—— 許個願,希望 Project Valhalla 早日 GA。


我們經常在各個專案中看到,大家先把某個物件包裝成 Optional,然後進行 filter、map 等。透過 @Self 的型別對映,你可以這樣為 Object 加入一個非常實用的辦法:


```

package com.alibaba.zhiye.extensions.java.lang.Object;


import manifold.ext.rt.api.Extension;

import manifold.ext.rt.api.Self;

import manifold.ext.rt.api.This;


import java.util.Optional;


/**

 * Object 的擴充套件方法

 */

@Extension

public final class ObjectExt {


    public static Optional<@Self Object> asOpt(@This Object obj) {

        return Optional.ofNullable(obj);

    }

}

```


那麼任何物件,都將擁有 asOpt() 方法。


相比於之前的需要包裝一下的不自然:


```

Optional.ofNullable(someObj).filter(someFilter).map(someMapper).orElseGet(someSupplier);

```


你現在可以自然而然的使用 Optional:


```

someObj.asOpt().filter(someFilter).map(someMapper).orElseGet(someSupplier);

```

    

當然,**Object 是所有的類的父類,這樣做是否合適,還是需要謹慎的思考一下。**


### 擴充套件靜態方法


我們都知道 Java9 給集合新增了工廠方法:


```

List<String> list = List.of("a", "b", "c");

Set<String> set = Set.of("a", "b", "c");

Map<String, Integer> map = Map.of("a", 1, "b", 2, "c", 3);

```


是不是很眼饞?因為如果用的不是 Java9 及以上版本(Java8:直接報我身份證就行),你就得用 Guava 之類的庫 —— 然而 ImmutableList.of 用起來終究是比不上 List.of 這樣的正統來的自然。


沒關係,Manifold 說:“無所謂,我會出手”。基於 Manifold 擴充套件靜態方法,就是在擴充套件類的靜態方法上,也加上 @Extension:


```

package com.alibaba.aladdin.app.extensions.java.util.List;


import manifold.ext.rt.api.Extension;

import manifold.ext.rt.api.This;


import java.util.Arrays;

import java.util.Collections;

import java.util.List;


/**

 * List 擴充套件方法

 */

@Extension

public final class ListExt {


    /**

     * 返回只包含一個元素的不可變 List

     */

    @Extension

    public static <E> List<E> of(E element) {

        return Collections.singletonList(element);

    }


    /**

     * 返回包含多個元素的不可變 List

     */

    @Extension

    @SafeVarargs

    public static <E> List<E> of(E... elements) {

        return Collections.unmodifiableList(Arrays.asList(elements));

    }

}

```


然後你就可以欺騙自己已經用上了 Java8 之後的版本 —— 你發任你發,我用 Java8。


BTW,因為 Object 是所有類的父類,如果你給 Object 新增靜態擴充套件方法,那麼意味著你可以在任何地方直接訪問到這個靜態方法,而不需要 import —— 恭喜你,解鎖了 “頂 級函式”。


### 建議


#### 關於 Manifold


我從 2019 年開始關注 Manifold,那時候 Manifold IDEA 外掛還是收費的,所以當時只是做了簡單的嘗試。最近再看,IDEA 外掛已經完全免費,所以迫不及待地想要物盡其用。目前我已經在一個專案中使用了 Manifold 來實現擴充套件方法的功能 —— 當事人表示非常上癮,已經離不開了。如果你有使用上的建議和疑問,歡迎和我一起討論。


#### 謹慎新增擴充套件方法


如果決定在專案中使用 Manifold 實現擴充套件方法,那麼我們**一定要做到 “管住自己的手”** 。


首先,就是上文說的,給 Object 或者其他在專案中使用非常廣泛的類新增擴充套件方法,一定要非常的慎重,最好是要和專案組的同學一起討論,讓大家一起決定,否則很容易讓人迷惑。


另外,如果要給某個類新增擴充套件方法,一定要先認真思考一個問題:“這個方法的邏輯是不是在這個類的職責範圍內,是否有摻雜業務自定義邏輯”。例如下面這個方法(判斷給定的字串是不是一個合法的引數):


```

public static boolean isValidParam(String str) {

    return StringUtils.isNotBlank(str) && !"null".equalsIgnoreCase(str);

}

```


很明顯,isValidParam 不是 String 這個類的職責範圍,應該把 isValidParam 繼續放在 XxxBizUtils 裡面。當然,如果你把方法名改成 isNotBlankAndNotEqualsIgnoreCaseNullLiteral,那是可以的 :) —— 不過勸你別這麼做,容易被打。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69953029/viewspace-2943454/,如需轉載,請註明出處,否則將追究法律責任。

相關文章