在前不久發的「Java 中模擬 C# 的擴充套件方法」一文中給 Java 模擬了擴充套件方法。但畢竟沒有語法支援,使用起來還是有諸多不便,尤其是需要不斷交錯使用不同類中實現的“擴充套件方法”時,切換物件非常繁瑣。前文也提到,之所以想到研究“擴充套件方法”其實只是為了“鏈式呼叫”。
那麼,為什麼不直接從原始需求出發,只解決鏈式呼叫的問題,而不去考慮更寬泛的擴充套件方法呢?前文研究過通過 Builder 模式來實現鏈式呼叫,這種方式需要自己定義擴充套件方法類(也就是 Builder),仍然比較繁瑣。
Chain 雛形
鏈式呼叫的主要特點就是使用了某個物件之後,可以繼續使用該物件 …… 一直使用下去。你看,這裡就兩件事:一是提供一個物件;二是使用這個物件 —— 這不就是 Supplier 和 Consumer 嗎?Java 在 java.util.function
包中正好提供了同名的兩個函式式介面。這樣一來,我們可以定義一個 Chain
類,從一個 Supplier 開始,不斷的“消費”它,這樣一個簡單的 Chain
雛形就出來了:
public class Chain<T> {
private final T value;
public Chain(Supplier<? extends T> supplier) {
this.value = supplier.get();
}
public Chain<T> consume(Consumer<? super T> consumer) {
consumer.accept(this.value);
return this;
}
}
現在,假如我們有一個 Person
類,它有一些行為方法:
class Person {
public void talk() { }
public void walk(String target) { }
public void eat() { }
public void sleep() { }
}
還是前文中那個業務場景:談妥了,出去,吃飯,回來,睡覺。非鏈試呼叫是這樣的:
public static void main(String[] args) {
var person = new Person();
person.talk();
person.walk("飯店");
person.eat();
person.walk("家");
person.sleep();
}
如果用 Chain
串起來就是:
public static void main(String[] args) {
new Chain<>(Person::new).consume(Person::talk)
.consume(p -> p.walk("飯店"))
.consume(Person::eat)
.consume(p -> p.walk("家"))
.consume(Person::sleep);
}
上面已經完成了 Chain 封裝,還是蠻簡單的,只不過有兩個小問題:
consume()
字太多,寫起來麻煩。如果改名的話,do
很合適,可惜是關鍵字 …… 不如改成act
好了;- 連結呼叫往往是用在表示式中,返回有個返回值,所以得加個
Chain::getValue()
完善 Chain
實際上,鏈式呼叫過程中,也不一定就只是“消費”,有可能還需要“轉換”,用程式設計師的話來說,就是 map()
—— 將當前物件作為引數傳入,計算完成之後得到另一個物件。可能大家在 java stream 中用到 map()
比較多,不過這裡的場景更像 Optional::map
。
來看看 Optional::map
的原始碼:
public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent()) {
return empty();
} else {
return Optional.ofNullable(mapper.apply(value));
}
}
可以看出來這個 map()
的邏輯很簡單,就是把 Function
執行的的結果再封裝成一個 Optional
物件。我們在 Chain
中也可以這麼幹:
public <U> Chain<U> map(Function<? super T, ? extends U> mapper) {
return new Chain<>(() -> mapper.apply(value));
}
寫到這裡發現,使用 Supplier
的思路雖然沒錯,但要直接從“值”構造 Chain
物件還挺不容易的 —— 當然可以加一個建構函式的過載來解決這個問題,但我想像 Optional
那樣寫兩個靜態方法來實現,同時隱藏建構函式。修改後完整的 Chain
如下:
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
public class Chain<T> {
private final T value;
public static <T> Chain<T> of(T value) {
return new Chain<>(value);
}
public static <T> Chain<T> from(Supplier<T> supplier) {
return new Chain<>(supplier.get());
}
private Chain(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public Chain<T> act(Consumer<? super T> consumer) {
consumer.accept(this.value);
return this;
}
public <U> Chain<U> map(Function<? super T, ? extends U> mapper) {
return Chain.of(mapper.apply(value));
}
}
繼續改造 Chain
map()
總是會返回一個新的 Chain
物件。如果某次處理中存在很多個 map 步驟,那就會產生很多個 Chain
物件。能不能在一個 Chain
物件中解決呢?
定義 Chain
的時候用到了泛型,而泛型型別在編譯後就會被擦除掉,和我們直接把 value
定義成 Object
沒多大區別。既然如此,map()
的時候,直接把 value
給換掉,而不是產生新的 Chain
物件是否可行呢?—— 確實可行。但是一方面需要繼續使用泛型來約束 consumer
和 mapper
,另一方面,需要在內部進行強制的型別轉換,還得保證這個轉換不會有問題。
理論上來說,Chain
處理的是鏈式呼叫,一環扣一環,而每一環的結果都儲存在 value
中用於下一環的開始。因此在泛型約束下,不管怎麼變化都是不會出問題的。理論可行,不如實踐一下:
// 因為存在大量的型別轉換(邏輯確認可行),需要忽略掉相關警告
@SuppressWarnings("unchecked")
public class Chain<T> {
// 把 value 宣告為 Object 型別,以便引用各種型別的值
// 同時去掉 final 修飾,使之可變
private Object value;
public static <T> Chain<T> of(T value) {
return new Chain<>(value);
}
public static <T> Chain<T> from(Supplier<T> supplier) {
return new Chain<>(supplier.get());
}
private Chain(T value) {
this.value = value;
}
public T getValue() {
// 使用到 value 的地方都需要把 value 轉換為 Chain<> 的泛型引數型別,下同
return (T) value;
}
public Chain<T> act(Consumer<? super T> consumer) {
consumer.accept((T) this.value);
return this;
}
public <U> Chain<U> map(Function<? super T, ? extends U> mapper) {
// mapper 的計算結果無所謂是什麼型別都可以給 Object 型別的 value 賦值
this.value = mapper.apply((T) value);
// 返回的 Chain 雖然還是自己(就這個物件),但是泛型引數得換成 U 了
// 換了型別之後,後序的操作才會基於 U 型別來進行
return (Chain<U>) this;
}
}
最後那句型別轉換 (Chain<U>) this
很是靈性,Java 中可以這麼幹(因為有型別擦除),C# 中無論如何都做不到!
再寫段程式碼來試驗一下:
public static void main(String[] args) {
// 注意:String 的操作會產生新的 String 物件,所以要用 map
Chain.of(" Hello World ")
.map(String::trim)
.map(String::toLowerCase)
// ↓ 把 String 拆分成 String[],這裡轉換了不相容型別
.map(s ->s.split("\s+"))
// ↓ 消費這個 String[],依次列印出來
.act(ss -> Arrays.stream(ss).forEach(System.out::println));
}
輸出結果正如預期:
hello
world
結語
為了解決鏈式呼叫的問題,我們在上一篇文章中研究了擴充套件方法,研究得有點“過度”。這次迴歸本源,就處理鏈式呼叫。
研究過程中如果有想法,不妨一試。如果發現 JDK 中有類似的處理方式,不防去看看原始碼 —— 畢竟 OpenJDK 是開源的!
還有一點,Java 泛型的型別擦除特性有時候確實會帶來不便,但也有些時候是真的方便!