摘要:本文主要介紹了中的閉包與局部套用功能,由國內管理平臺編譯呈現。譬如,認為給帶來了閉包特性就是其中之一。但是首先,我們將考慮如何利用閉包進行實現。很顯然,閉包打破了這一準則。這就是局部調用,它總是比閉包更為穩妥。
【編者按】本文作者為專注于自然語言處理多年的 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{ 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(); } }); }
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 Streamcalculate(Stream 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 Listcalculate(List list, Integer a) { return list.map(new Function >>() { @Override public Function > apply(final Integer x) { return new Function >() { @Override public Function apply(final Integer y) { return new Function () { @Override public Integer apply(Integer t) { return x + y * t; } }; } }; } }.apply(b).apply(a)); }
以上代碼完全可以實現所需功能,但是要想說服開發者,讓他們用這種方式編寫代碼,恐怕非常困難!還好,Java 8的lambda句法提供了以下實現方式:
private Streamcalculate(Stream stream, Integer a) { return stream.map(((Function >>) x -> y -> t -> x + y * t).apply(b).apply(a)); }
怎么樣?或者,是不是可以寫得更簡單一點:
private Streamcalculate(Stream 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>> {} private Stream calculate(Stream stream, Integer a) { return stream.map(((F3) x -> y -> z -> x + y * z).apply(b).apply(a)); }
現在,我們來為函數命名,并在必要時重用它:
private Streamcalculate(Stream stream, Integer a) { F3 calculation = x -> y -> z -> x + y * z; return stream.map(calculation.apply(b).apply(a)); }
我們還可以聲明計算函數為一個輔助類的靜態成員,使用靜態導入來進一步簡化代碼:
public class Functions { static Function>> calculation = x -> y -> z -> x + y * z; } ... import static Functions.calculation; private Stream calculate(Stream stream, Integer a) { return stream.map(calculation.apply(b).apply(a)); }
可惜,Java 8 鼓勵的是使用閉包。不然,我會介紹更多能讓局部套用的使用更為簡便的功能性語法糖。比如,在Scala中,以上例子就可以這樣改寫:
stream.map(calculation(b)(a))
雖然在Java中我們沒法這樣寫。可是,通過下面的靜態方法,我們可以達到相似的效果:
static Function>> calculation = x -> y -> z -> x + y * z; static Function calculation(Integer x, Integer y) { return calculation.apply(x).apply(y); }
現在,我們可以寫:
private Streamcalculate(Stream stream, Integer a) { return stream.map(calculation(b, a)); }
請注意,calculation(b, a)不是帶有兩個參數的函數。它只是一個方法,在將兩個參數逐一地局部調用至一個帶有三個參數的函數之后,它會返回一個帶有一個參數的函數,該函數便可傳遞給映射函數。
現在,calculation方法便可以多帶帶測試了。
自動局部調用在之前的例子中,我們已經親手實踐過局部調用了。然而,我們大可以編寫程序來自動化調用過程。我們可以編寫這樣一個方法:它會接收帶有兩個參數的函數,并返回該函數的局部調用版本。寫起來非常簡單:
public Function> curry(final BiFunction f) { return (A a) -> (B b) -> f.apply(a, b); }
有必要的話,我們還可以寫一個方法來顛倒這一過程。這個過程可以接受A的Function函數作為參數,返回一個可返回C的B的Function函數,最終返回一個返回C的A,B的BiFunction函數。
public BiFunction uncurry(Function> 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
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/65857.html
摘要:真正留給我們要實現的僅僅是返回另外一部分用于局部應用的一元函數罷了。總結各用一句話做個小結吧局部應用是一種轉換技巧,通過預先傳入一個或多個參數來把多元函數轉變為更少一些元的函數甚或是一元函數。 局部應用(Partial Application,也譯作偏應用或部分應用)和局部 套用( Currying, 也譯作柯里化),是函數式編程范式中很常用的技巧。 本文著重于闡述它們的...
摘要:要理解閉包,首先必須理解特殊的變量作用域。使用閉包有一個優點,也是它的缺點就是可以把局部變量駐留在內存中,可以避免使用全局變量。 js閉包 閉包是指有權訪問另一個函數作用域中的變量的函數,創建閉包的常見的方式,就是在一個函數內部創建另一個函數,通過另一個函數訪問這個函數的局部變量。要理解閉包,首先必須理解Javascript特殊的變量作用域。變量的作用域無非就是兩種:全局變量和局部變量...
摘要:由于各種原因,我們需要在函數的外部調用函數內部定義的局部變量。閉包的主要用處是把函數內部的變量一直保存在內存中可以省略該局部變量一直保存在內存中該函數被賦予給全局變量,所以一直存在,該函數的外層函數因此也一直存在舉例 由于各種原因,我們需要在函數的外部調用函數內部定義的局部變量。 閉包實際上就是函數內部的函數,通過在函數內部再定義一個函數,內部函數返回函數的局部變量,函數再返回內部函數...
摘要:到底什么是閉包這個問題在面試是時候經常都會被問,很多小白一聽就懵逼了,不知道如何回答好。上面這么說閉包是一種特殊的對象。閉包的注意事項通常,函數的作用域及其所有變量都會在函數執行結束后被銷毀。從而使用閉包模塊化代碼,減少全局變量的污染。 閉包,有人說它是一種設計理念,有人說所有的函數都是閉包。到底什么是閉包?這個問題在面試是時候經常都會被問,很多小白一聽就懵逼了,不知道如何回答好。這個...
閱讀 995·2023-04-25 19:35
閱讀 2633·2021-11-22 09:34
閱讀 3679·2021-10-09 09:44
閱讀 1713·2021-09-22 15:25
閱讀 2932·2019-08-29 14:00
閱讀 3372·2019-08-29 11:01
閱讀 2596·2019-08-26 13:26
閱讀 1735·2019-08-23 18:08