Java 8怎麼了:區域性套用vs閉包
【編者按】本文作者為專注於自然語言處理多年的 Pierre-Yves Saumont,Pierre-Yves 著有30多本主講 Java 軟體開發的書籍,自2008開始供職於 Alcatel-Lucent 公司,擔任軟體研發工程師。
本文主要介紹了 Java 8 中的閉包與區域性套用功能,由國內 ITOM 管理平臺 OneAPM 編譯呈現。
關於Java 8,存在著許多錯誤觀念。譬如,認為Java 8給Java帶來了閉包特性就是其中之一。這個想法是錯的,因為閉包特性從Java誕生之初就已經存在了。然而閉包是有缺陷的。儘管Java 8似乎傾向於函數語言程式設計,我們仍應盡力避免使用Java閉包。但是,Java 8並沒有在此方面提供過多幫助。
我們知道,引數求值時間是使用方法和使用函式時的一個重大區別。在Java中,我們可以寫一個帶引數且有返回值的方法。但是,這可以被稱作函式嗎?當然不能。方法只可以通過呼叫進行操縱,這表示它的引數會在該方法執行前取值。這是Java中引數按值傳遞的結果。
函式則與之不同。操作函式時我們可以不計算引數,且對引數何時取值有絕對的控制權。而且,如果一個函式有多個引數,它們可以不同時取值。這一點通過區域性套用就可以做到。但是首先,我們將考慮如何利用閉包進行實現。
閉包舉例
對函式而言,閉包能夠在封裝的上下文中獲取內容。在函數語言程式設計中,一個函式的結果應當僅由其引數決定。很顯然,閉包打破了這一準則。
請看Java 5/6/7中的示例:
private Integer b = 2;
List list = Arrays.asList(1, 2, 3, 4, 5);
System.out.println(calculate(list.stream(), 3).collect(toList()));
private Stream calculate(Stream stream, Integer a) {
return stream.map(new Function() {
@Override
public Integer apply(Integer t) {
return t * a + b;
}
});
}
public interface Function<T, U> {
U apply(T t);
}
以上程式碼將產生如下結果:
[5, 8, 11, 14, 17]
所得結果是函式 f(x) = x * 3 + 2 對於列 [1, 2, 3, 4, 5]
的對映。到這一步都沒什麼問題。但是3和2可以用其他值替換嗎?換句話說,它難道不是函式f(x, a, b) = x * a + b 對於該列的對映嗎?
是,也不是。不是的原因在於a和b都被隱性定義了final
關鍵詞,因此它們在函式取值時作為常數參與計算。但是當然,它們的值也會有變動。它們的final
屬性(在Java 8中隱性定義,在之前版本中則顯性定義)只是編譯器優化編譯過程的一種方式。編譯器並不在乎任何潛在的變動值。它只在乎引用有沒有發生變動,也就是說,它想要確保Integer
整數物件a
和b
的引用不發生變化,但並不在意它們的取值。這個特性在以下程式碼中可以看出:
private Integer b = 2;
private Integer getB() {
return this.b;
}
List list = Arrays.asList(1, 2, 3, 4, 5);
System.out.println(calculator.calculate(list.stream(), new Int(3)).collect(toList()));
private Streamcalculate00(Streamstream, final Int a) {
return stream.map(new Function() {
@Override
public Integer apply(Integer t) {
return t * a.value + getB();
}
});
}
-
static private class Int {
public int value;
public Int(int value) {
this.value = value;
}
}
在這裡,我們使用了可變物件a
(屬於Int
類,而不是不可變的Integer
類),以及一個方法來獲取b
。現在,我們來模擬一個有三個變數的函式,但是仍舊使用僅有一個變數的函式,同時使用閉包來代替其他兩個變數。很顯然,這是非函式性的,因為它打破了僅依賴於函式引數的準則。
結果之一是,儘管有需要,我們也不能在別的地方重用這個函式,因為它依賴於上下文而不僅僅依賴於引數。我們要複製這些程式碼才能實現重用。另一個結果是,由於它需要上下文才能執行,我們也不能單獨進行函式測試。
那麼,我們應該使用帶有三個引數的函式嗎?我們可能會認為,這不可能實現。因為具體的實現過程與三個引數何時取值相關。它們都在不同的地方取值。如果我們剛才使用的是帶有三個引數的函式,它們就必須同時取值。而對映方法只會對映帶一個引數的函式到流,不可能對映帶有三個引數的函式。因此,其餘兩個引數在函式繫結時(也即傳遞給對映時)必須已經取值。解決方法是先對其餘兩個引數取值。
我們也可以用閉包來實現這一功能,但是所得程式碼是不可測試的,且可能存在重疊。
使用Java 8 的句法(lambdas)也無法改變這一狀況:
private Integer b = 2;
private Stream<Integer> calculate(Stream<Integer> stream, Integer a) {
return stream.map(t -> t * a + b);
}
我們需要的是一種在不同時間獲取三個引數的方法——Currying(區域性套用,也稱柯里化函式,儘管它其實是Moses Shönfinkel發明的)。
使用區域性閉包
區域性閉包就是逐一對函式引數取值,每一步都生成少一個引數的新函式。舉例來看,如果我們有如下函式:
f(x, y, z) = x * y + z
我們可以同時取引數值為2,4,5,得到以下方程:
f(3, 4, 5) = 3 * 4 + 5 = 17
我們也可以只取一個引數為3,得到以下方程:
f(3, y, z) = g(y, z) = 3 * y + z
現在,我們得到了只有兩個引數的新函式g。再對該函式進行區域性套用,將4賦值給y:
g(4, z) = h(z) = 3 * 4 + z
給引數賦值的順序對計算結果並無影響。此處,我們並不是在區域性相加,(如果是區域性相加,我們還得考慮運算子優先順序。)而是在進行對函式的區域性應用。
那麼,我們如何在Java中實現這種方法呢?以下是在Java5/6/7中的應用:
private static List<Integer> calculate(List<Integer> list, Integer a) {
return list.map(new Function<Integer, Function<Integer, Function<Integer, Integer>>>() {
@Override
public Function<Integer, Function<Integer, Integer>> apply(final Integer x) {
return new Function<Integer, Function<Integer, Integer>>() {
@Override
public Function<Integer, Integer> apply(final Integer y) {
return new Function<Integer, Integer>() {
@Override
public Integer apply(Integer t) {
return x + y * t;
}
};
}
};
}
}.apply(b).apply(a));
}
以上程式碼完全可以實現所需功能,但是要想說服開發者,讓他們用這種方式編寫程式碼,恐怕非常困難!還好,Java 8的lambda句法提供了以下實現方式:
private Stream<Integer> calculate(Stream<Integer> stream, Integer a) {
return stream.map(((Function<Integer, Function<Integer, Function<Integer, Integer>>>)
x -> y -> t -> x + y * t).apply(b).apply(a));
}
怎麼樣?或者,是不是可以寫得更簡單一點:
private Stream<Integer> calculate(Stream<Integer> stream, Integer a) {
return stream.map((x -> y -> t -> x + y * t).apply(b).apply(a));
}
完全可以,但是Java 8不能自行判斷引數型別,因此我們必須使用manifest型別來幫助確認(manifest在Java規範中的意思是explicit)。為了讓程式碼看起來更整潔,我們可以使用一些小技巧:
interface F3 extends Function<Integer, Function<Integer, Function<Integer, Integer>>> {}
private Stream<Integer> calculate(Stream<Integer> stream, Integer a) {
return stream.map(((F3) x -> y -> z -> x + y * z).apply(b).apply(a));
}
現在,我們來為函式命名,並在必要時重用它:
private Stream<Integer> calculate(Stream<Integer> stream, Integer a) {
F3 calculation = x -> y -> z -> x + y * z;
return stream.map(calculation.apply(b).apply(a));
}
我們還可以宣告計算函式為一個輔助類的靜態成員,使用靜態匯入來進一步簡化程式碼:
public class Functions {
static Function<Integer, Function<Integer, Function<Integer, Integer>>> calculation =
x -> y -> z -> x + y * z;
}
...
import static Functions.calculation;
private Stream<Integer> calculate(Stream<Integer> stream, Integer a) {
return stream.map(calculation.apply(b).apply(a));
}
可惜,Java 8 鼓勵的是使用閉包。不然,我會介紹更多能讓區域性套用的使用更為簡便的功能性語法糖。比如,在Scala中,以上例子就可以這樣改寫:
stream.map(calculation(b)(a))
雖然在Java中我們沒法這樣寫。可是,通過下面的靜態方法,我們可以達到相似的效果:
static Function<Integer, Function<Integer, Function<Integer, Integer>>> calculation
= x -> y -> z -> x + y * z;
static Function<Integer, Integer> calculation(Integer x, Integer y) {
return calculation.apply(x).apply(y);
}
現在,我們可以寫:
private Stream<Integer> calculate(Stream<Integer> stream, Integer a) {
return stream.map(calculation(b, a));
}
請注意,calculation(b, a)
不是帶有兩個引數的函式。它只是一個方法,在將兩個引數逐一地區域性呼叫至一個帶有三個引數的函式之後,它會返回一個帶有一個引數的函式,該函式便可傳遞給對映函式。
現在,calculation
方法便可以單獨測試了。
自動區域性呼叫
在之前的例子中,我們已經親手實踐過區域性呼叫了。然而,我們大可以編寫程式來自動化呼叫過程。我們可以編寫這樣一個方法:它會接收帶有兩個引數的函式,並返回該函式的區域性呼叫版本。寫起來非常簡單:
public <A, B, C> Function<A, Function<B, C>> curry(final BiFunction<A, B, C> f) {
return (A a) -> (B b) -> f.apply(a, b);
}
有必要的話,我們還可以寫一個方法來顛倒這一過程。這個過程可以接受A的Function函式作為引數,返回一個可返回C的B的Function函式,最終返回一個返回C的A,B的BiFunction函式。
public <A, B, C> BiFunction<A, B, C> uncurry(Function<A, Function<B, C>> f) {
return (A a, B b) -> f.apply(a).apply(b);
}
區域性呼叫的其他應用
區域性呼叫的應用方式還有很多。最重要的應用是模擬多引數函式。在Java 8提供了單引數函式(java.util.functions.Function
)以及雙引數函式(java.util.functions.BiFunction
)。但並未提供存在於其他語言中的三引數、四引數、五引數甚至更多引數的函式。其實,有沒有這些函式並不重要。它們只是在特定情況下,需要同時對所有引數取值時應用的語法糖。實際上,這也是BiFunctin
在Java 8中存在的原因:函式的常見使用方法就是模擬二元運算子,(請注意:在Java 8中有BinaryOperator
介面,但它只用於兩個引數以及返回值都屬於同一型別的特殊情況。我們將在下一篇文章中討論這一點。)
區域性呼叫在函式的各個引數需要在不同地方取值時是非常好用的。通過區域性呼叫,我們可以在某一元件中對一個引數取值,然後將計算結果傳遞到另一元件對其他引數取值,如此反覆,直到所有引數值都被取到。
小結
Java 8並不是一種函式式語言(可能永遠也不會是)。但是,我們仍可以在Java(甚至是Java 8之前的版本)中使用函式式正規化。這樣做的確會略有代價。但這種代價在Java 8中已經大幅減少了。儘管如此,想要寫函式型程式碼的開發者還是得動動腦筋才能掌握這種正規化。使用區域性呼叫就是智力成果之一。
請記住:
(A, B, C) -> D
總是可以由如下方式替代:
A -> B -> C -> D
即便Java 8無法判斷該表達方式的型別,你只要自行指定其型別就可以了。這就是區域性呼叫,它總是比閉包更為穩妥。
OneAPM 能為您提供端到端的 Java 應用效能解決方案,我們支援所有常見的 Java 框架及應用伺服器,助您快速發現系統瓶頸,定位異常根本原因。分鐘級部署,即刻體驗,Java 監控從來沒有如此簡單。想閱讀更多技術文章,請訪問 OneAPM 官方技術部落格。 本文轉自 OneAPM 官方部落格 編譯自:https://dzone.com/articles/whats-wrong-java-8-currying-vs
相關文章
- Python閉包區域性變數問題Python變數
- 區域性類實現C++的閉包薦C++
- 圖片區域性識別怎麼操作
- 區域性代理ip與全域性代理ip怎麼用?
- Java™ 教程(區域性類)Java
- 公司區域網丟包率高怎麼辦?-VeCloudCloud
- 04.區域性安裝npm包NPM
- JS作用域與閉包JS
- JS閉包作用域解析JS
- Javascript-this/作用域/閉包JavaScript
- 美圖AI區域性重繪技術大揭秘!想怎麼改,就怎麼改!美圖區域性重繪讓你隨心所欲AI
- 區域分佈圖怎麼做,怎麼做區域網格分佈圖
- Java區域性變數與全域性變數Java變數
- java 全域性變數和區域性變數Java變數
- Java 8怎麼了之二:函式和原語Java函式
- 怎麼製作區域分佈圖?區域網格分佈圖怎麼做?
- JavaScript之作用域和閉包JavaScript
- 圖解作用域及閉包圖解
- JavaScript從作用域到閉包JavaScript
- 原型、原型鏈、作用域、作用域鏈、閉包原型
- windows8怎麼關閉家庭組Windows
- 瀏覽器是怎麼看閉包的。瀏覽器
- Java之區域性匿名內部類物件Java物件
- 【JS基礎】作用域和閉包JS
- 淺談JS作用域、this及閉包JS
- javascript 基礎(作用域和閉包)JavaScript
- Javascript深入之作用域與閉包JavaScript
- 變數的作用域--js閉包變數JS
- JS作用域與閉包--例項JS
- 當初,我怎麼會頭腦發熱選了Python!Java VS Python怎麼選?PythonJava
- 當關閉了eclipse的toolbar 又關閉了menu...怎麼辦!Eclipse
- 【區域性特徵】ASIFT特徵
- JavaScript物件導向~ 作用域和閉包JavaScript物件
- Go知識盲區--閉包Go
- 海外HTTP代理中全域性代理和區域性代理是什麼?有什麼區別?HTTP
- 【集合論】關係閉包 ( 關係閉包求法 | 關係圖求閉包 | 關係矩陣求閉包 | 閉包運算與關係性質 | 閉包複合運算 )矩陣
- 深入學習js之——閉包#8JS
- 你的模型真的陷入區域性最優點了嗎?模型