Java 缺失的特性:擴充套件方法
*作者:周密(之葉)*
## 什麼是擴充套件方法
擴充套件方法,就是能夠向現有型別直接“新增”方法,而無需建立新的派生型別、重新編譯或以其他方式修改現有型別。呼叫擴充套件方法的時候,與呼叫在型別中實際定義的方法相比沒有明顯的差異。
## 為什麼需要擴充套件方法
考慮要實現這樣的功能:從 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/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- .Net3.5新特性-擴充套件方法套件
- C#新特性:匿名類和擴充套件方法C#套件
- Java 中模擬 C# 的擴充套件方法JavaC#套件
- Json擴充套件方法JSON套件
- LINQ擴充套件方法套件
- 五、談擴充套件方法的理解套件
- centos安裝php缺失fileinfo.so擴充套件解決CentOSPHP套件
- 再學Blazor——擴充套件方法Blazor套件
- C#.NET擴充套件方法C#套件
- android view 擴充套件方法AndroidView套件
- kotlin 擴充套件(擴充套件函式和擴充套件屬性)Kotlin套件函式
- C# 擴充套件方法 借籤於 Objective-C 擴充套件類.C#套件Object
- Flutter——Dart Extension擴充套件方法的使用FlutterDart套件
- tonyenc加密擴充套件使用方法加密套件
- JavaScript String 物件擴充套件方法JavaScript物件套件
- CheckBoxList擴充套件方法程式碼套件
- javax.mail Java Extension(擴充套件)JavaAI套件
- Java-IoUtil擴充套件工具類Java套件
- java資料型別擴充套件Java資料型別套件
- WCF擴充套件:行為擴充套件Behavior Extension套件
- redis的PHP擴充套件包安裝方法RedisPHP套件
- C++11語言擴充套件:常規特性C++套件
- ?用Chrome擴充套件管理器, 管理你的擴充套件Chrome套件
- linux下線上擴大擴充套件分割槽的方法Linux套件
- ncurses其他特性:curs_set(),離開curses模式,ACS_擴充套件字符集,擴充套件庫模式套件
- 【Kotlin】擴充套件屬性、擴充套件函式Kotlin套件函式
- es6 陣列擴充套件方法陣列套件
- Sanic 擴充套件套件
- ORACLE 擴充套件Oracle套件
- 擴充套件工具套件
- 擴充套件歐幾里得套件
- DOM擴充套件套件
- 擴充套件ACL套件
- Lua擴充套件套件
- 照片擴充套件套件
- 擴充套件篇套件
- disable or 擴充套件套件
- 擴充套件表套件