新聞中心
學(xué)習(xí)如何使用 Java 8 中的流 API 和函數(shù)式編程結(jié)構(gòu)。
創(chuàng)新互聯(lián)長(zhǎng)期為超過(guò)千家客戶提供的網(wǎng)站建設(shè)服務(wù),團(tuán)隊(duì)從業(yè)經(jīng)驗(yàn)10年,關(guān)注不同地域、不同群體,并針對(duì)不同對(duì)象提供差異化的產(chǎn)品和服務(wù);打造開放共贏平臺(tái),與合作伙伴共同營(yíng)造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為奎屯企業(yè)提供專業(yè)的做網(wǎng)站、成都網(wǎng)站建設(shè),奎屯網(wǎng)站改版等技術(shù)服務(wù)。擁有十多年豐富建站經(jīng)驗(yàn)和眾多成功案例,為您定制開發(fā)。
當(dāng) Java SE 8(又名核心 Java 8)在 2014 年被推出時(shí),它引入了一些更改,從根本上影響了用它進(jìn)行的編程。這些更改中有兩個(gè)緊密相連的部分:流 API 和函數(shù)式編程構(gòu)造。本文使用代碼示例,從基礎(chǔ)到高級(jí)特性,介紹每個(gè)部分并說(shuō)明它們之間的相互作用。
基礎(chǔ)特性
流 API 是在數(shù)據(jù)序列中迭代元素的簡(jiǎn)潔而高級(jí)的方法。包 java.util.stream 和 java.util.function 包含了用于流 API 和相關(guān)函數(shù)式編程構(gòu)造的新庫(kù)。當(dāng)然,代碼示例勝過(guò)千言萬(wàn)語(yǔ)。
下面的代碼段用大約 2,000 個(gè)隨機(jī)整數(shù)值填充了一個(gè) List:
Random rand = new Random2();Listlist = new ArrayList (); // 空 list for (int i = 0; i < 2048; i++) list.add(rand.nextInt()); // 填充它
另外用一個(gè) for 循環(huán)可用于遍歷填充列表,以將偶數(shù)值收集到另一個(gè)列表中。
流 API 提供了一種更簡(jiǎn)潔的方法來(lái)執(zhí)行此操作:
Listevens = list .stream() // 流化 list.filter(n -> (n & 0x1) == 0) // 過(guò)濾出奇數(shù)值.collect(Collectors.toList()); // 收集偶數(shù)值
這個(gè)例子有三個(gè)來(lái)自流 API 的函數(shù):
stream函數(shù)可以將集合轉(zhuǎn)換為流,而流是一個(gè)每次可訪問(wèn)一個(gè)值的傳送帶。流化是惰性的(因此也是高效的),因?yàn)橹凳歉鶕?jù)需要產(chǎn)生的,而不是一次性產(chǎn)生的。filter函數(shù)確定哪些流的值(如果有的話)通過(guò)了處理管道中的下一個(gè)階段,即collect階段。filter函數(shù)是 高階的higher-order,因?yàn)樗膮?shù)是一個(gè)函數(shù) —— 在這個(gè)例子中是一個(gè) lambda 表達(dá)式,它是一個(gè)未命名的函數(shù),并且是 Java 新的函數(shù)式編程結(jié)構(gòu)的核心。
lambda 語(yǔ)法與傳統(tǒng)的 Java 完全不同:
n -> (n & 0x1) == 0
箭頭(一個(gè)減號(hào)后面緊跟著一個(gè)大于號(hào))將左邊的參數(shù)列表與右邊的函數(shù)體分隔開。參數(shù) n 雖未明確類型,但也可以明確。在任何情況下,編譯器都會(huì)發(fā)現(xiàn) n 是個(gè) Integer。如果有多個(gè)參數(shù),這些參數(shù)將被括在括號(hào)中,并用逗號(hào)分隔。
在本例中,函數(shù)體檢查一個(gè)整數(shù)的最低位(最右)是否為零,這用來(lái)表示偶數(shù)。過(guò)濾器應(yīng)返回一個(gè)布爾值。盡管可以,但該函數(shù)的主體中沒(méi)有顯式的 return。如果主體沒(méi)有顯式的 return,則主體的最后一個(gè)表達(dá)式即是返回值。在這個(gè)例子中,主體按照 lambda 編程的思想編寫,由一個(gè)簡(jiǎn)單的布爾表達(dá)式 (n & 0x1) == 0 組成。
collect函數(shù)將偶數(shù)值收集到引用為evens的列表中。如下例所示,collect函數(shù)是線程安全的,因此,即使在多個(gè)線程之間共享了過(guò)濾操作,該函數(shù)也可以正常工作。
方便的功能和輕松實(shí)現(xiàn)多線程
在生產(chǎn)環(huán)境中,數(shù)據(jù)流的源可能是文件或網(wǎng)絡(luò)連接。為了學(xué)習(xí)流 API, Java 提供了諸如 IntStream 這樣的類型,它可以用各種類型的元素生成流。這里有一個(gè) IntStream 的例子:
IntStream // 整型流.range(1, 2048) // 生成此范圍內(nèi)的整型流.parallel() // 為多個(gè)線程分區(qū)數(shù)據(jù).filter(i -> ((i & 0x1) > 0)) // 奇偶校驗(yàn) - 只允許奇數(shù)通過(guò).forEach(System.out::println); // 打印每個(gè)值
IntStream 類型包括一個(gè) range 函數(shù),該函數(shù)在指定的范圍內(nèi)生成一個(gè)整數(shù)值流,在本例中,以 1 為增量,從 1 遞增到 2048。parallel 函數(shù)自動(dòng)劃分該工作到多個(gè)線程中,在各個(gè)線程中進(jìn)行過(guò)濾和打印。(線程數(shù)通常與主機(jī)系統(tǒng)上的 CPU 數(shù)量匹配。)函數(shù) forEach 參數(shù)是一個(gè)方法引用,在本例中是對(duì)封裝在 System.out 中的 println 方法的引用,方法輸出類型為 PrintStream。方法和構(gòu)造器引用的語(yǔ)法將在稍后討論。
由于具有多線程,因此整數(shù)值整體上以任意順序打印,但在給定線程中是按順序打印的。例如,如果線程 T1 打印 409 和 411,那么 T1 將按照順序 409-411 打印,但是其它某個(gè)線程可能會(huì)預(yù)先打印 2045。parallel 調(diào)用后面的線程是并發(fā)執(zhí)行的,因此它們的輸出順序是不確定的。
map/reduce 模式
map/reduce 模式在處理大型數(shù)據(jù)集方面變得很流行。一個(gè) map/reduce 宏操作由兩個(gè)微操作構(gòu)成。首先,將數(shù)據(jù)分散(映射mapped)到各個(gè)工作程序中,然后將單獨(dú)的結(jié)果收集在一起 —— 也可能收集統(tǒng)計(jì)起來(lái)成為一個(gè)值,即歸約reduction。歸約可以采用不同的形式,如以下示例所示。
下面 Number 類的實(shí)例用 EVEN 或 ODD 表示有奇偶校驗(yàn)的整數(shù)值:
public class Number {enum Parity { EVEN, ODD }private int value;public Number(int n) { setValue(n); }public void setValue(int value) { this.value = value; }public int getValue() { return this.value; }public Parity getParity() {return ((value & 0x1) == 0) ? Parity.EVEN : Parity.ODD;}public void dump() {System.out.format("Value: %2d (parity: %s)\n", getValue(),(getParity() == Parity.ODD ? "odd" : "even"));}}
下面的代碼演示了用 Number 流進(jìn)行 map/reduce 的情形,從而表明流 API 不僅可以處理 int 和 float 等基本類型,還可以處理程序員自定義的類類型。
在下面的代碼段中,使用了 parallelStream 而不是 stream 函數(shù)對(duì)隨機(jī)整數(shù)值列表進(jìn)行流化處理。與前面介紹的 parallel 函數(shù)一樣,parallelStream 變體也可以自動(dòng)執(zhí)行多線程。
final int howMany = 200;Random r = new Random();Number[] nums = new Number[howMany];for (int i = 0; i < howMany; i++) nums[i] = new Number(r.nextInt(100));ListlistOfNums = Arrays.asList(nums); // 將數(shù)組轉(zhuǎn)化為 list Integer sum4All = listOfNums.parallelStream() // 自動(dòng)執(zhí)行多線程.mapToInt(Number::getValue) // 使用方法引用,而不是 lambda.sum(); // 將流值計(jì)算出和值System.out.println("The sum of the randomly generated values is: " + sum4All);
高階的 mapToInt 函數(shù)可以接受一個(gè) lambda 作為參數(shù),但在本例中,它接受一個(gè)方法引用,即 Number::getValue。getValue 方法不需要參數(shù),它返回給定的 Number 實(shí)例的 int 值。語(yǔ)法并不復(fù)雜:類名 Number 后跟一個(gè)雙冒號(hào)和方法名?;叵胍幌孪惹暗睦?System.out::println,它在 System 類中的 static 屬性 out 后面有一個(gè)雙冒號(hào)。
方法引用 Number::getValue 可以用下面的 lambda 表達(dá)式替換。參數(shù) n 是流中的 Number 實(shí)例中的之一:
mapToInt(n -> n.getValue())
通常,lambda 表達(dá)式和方法引用是可互換的:如果像 mapToInt 這樣的高階函數(shù)可以采用一種形式作為參數(shù),那么這個(gè)函數(shù)也可以采用另一種形式。這兩個(gè)函數(shù)式編程結(jié)構(gòu)具有相同的目的 —— 對(duì)作為參數(shù)傳入的數(shù)據(jù)執(zhí)行一些自定義操作。在兩者之間進(jìn)行選擇通常是為了方便。例如,lambda 可以在沒(méi)有封裝類的情況下編寫,而方法則不能。我的習(xí)慣是使用 lambda,除非已經(jīng)有了適當(dāng)?shù)姆庋b方法。
當(dāng)前示例末尾的 sum 函數(shù)通過(guò)結(jié)合來(lái)自 parallelStream 線程的部分和,以線程安全的方式進(jìn)行歸約。但是,程序員有責(zé)任確保在 parallelStream 調(diào)用引發(fā)的多線程過(guò)程中,程序員自己的函數(shù)調(diào)用(在本例中為 getValue)是線程安全的。
最后一點(diǎn)值得強(qiáng)調(diào)。lambda 語(yǔ)法鼓勵(lì)編寫純函數(shù)pure function,即函數(shù)的返回值僅取決于傳入的參數(shù)(如果有);純函數(shù)沒(méi)有副作用,例如更新一個(gè)類中的 static 字段。因此,純函數(shù)是線程安全的,并且如果傳遞給高階函數(shù)的函數(shù)參數(shù)(例如 filter 和 map )是純函數(shù),則流 API 效果最佳。
對(duì)于更細(xì)粒度的控制,有另一個(gè)流 API 函數(shù),名為 reduce,可用于對(duì) Number 流中的值求和:
Integer sum4AllHarder = listOfNums.parallelStream() // 多線程.map(Number::getValue) // 每個(gè) Number 的值.reduce(0, (sofar, next) -> sofar + next); // 求和
此版本的 reduce 函數(shù)帶有兩個(gè)參數(shù),第二個(gè)參數(shù)是一個(gè)函數(shù):
- 第一個(gè)參數(shù)(在這種情況下為零)是特征值,該值用作求和操作的初始值,并且在求和過(guò)程中流結(jié)束時(shí)用作默認(rèn)值。
- 第二個(gè)參數(shù)是累加器,在本例中,這個(gè) lambda 表達(dá)式有兩個(gè)參數(shù):第一個(gè)參數(shù)(
sofar)是正在運(yùn)行的和,第二個(gè)參數(shù)(next)是來(lái)自流的下一個(gè)值。運(yùn)行的和以及下一個(gè)值相加,然后更新累加器。請(qǐng)記住,由于開始時(shí)調(diào)用了parallelStream,因此map和reduce函數(shù)現(xiàn)在都在多線程上下文中執(zhí)行。
在到目前為止的示例中,流值被收集,然后被規(guī)約,但是,通常情況下,流 API 中的 Collectors 可以累積值,而不需要將它們規(guī)約到單個(gè)值。正如下一個(gè)代碼段所示,收集活動(dòng)可以生成任意豐富的數(shù)據(jù)結(jié)構(gòu)。該示例使用與前面示例相同的 listOfNums:
Map> numMap = listOfNums .parallelStream().collect(Collectors.groupingBy(Number::getParity));Listevens = numMap.get(Number.Parity.EVEN); Listodds = numMap.get(Number.Parity.ODD);
第一行中的 numMap 指的是一個(gè) Map,它的鍵是一個(gè) Number 奇偶校驗(yàn)位(ODD 或 EVEN),其值是一個(gè)具有指定奇偶校驗(yàn)位值的 Number 實(shí)例的 List。同樣,通過(guò) parallelStream 調(diào)用進(jìn)行多線程處理,然后 collect 調(diào)用(以線程安全的方式)將部分結(jié)果組裝到 numMap 引用的 Map 中。然后,在 numMap 上調(diào)用 get 方法兩次,一次獲取 evens,第二次獲取 odds。
實(shí)用函數(shù) dumpList 再次使用來(lái)自流 API 的高階 forEach 函數(shù):
private void dumpList(String msg, Listlist) { System.out.println("\n" + msg);list.stream().forEach(n -> n.dump()); // 或者使用 forEach(Number::dump)}
這是示例運(yùn)行中程序輸出的一部分:
The sum of the randomly generated values is: 3322The sum again, using a different method: 3322Evens:Value: 72 (parity: even)Value: 54 (parity: even)...Value: 92 (parity: even)Odds:Value: 35 (parity: odd)Value: 37 (parity: odd)...Value: 41 (parity: odd)
用于代碼簡(jiǎn)化的函數(shù)式結(jié)構(gòu)
函數(shù)式結(jié)構(gòu)(如方法引用和 lambda 表達(dá)式)非常適合在流 API 中使用。這些構(gòu)造代表了 Java 中對(duì)高階函數(shù)的主要簡(jiǎn)化。即使在糟糕的過(guò)去,Java 也通過(guò) Method 和 Constructor 類型在技術(shù)上支持高階函數(shù),這些類型的實(shí)例可以作為參數(shù)傳遞給其它函數(shù)。由于其復(fù)雜性,這些類型在生產(chǎn)級(jí) Java 中很少使用。例如,調(diào)用 Method 需要對(duì)象引用(如果方法是非靜態(tài)的)或至少一個(gè)類標(biāo)識(shí)符(如果方法是靜態(tài)的)。然后,被調(diào)用的 Method 的參數(shù)作為對(duì)象實(shí)例傳遞給它,如果沒(méi)有發(fā)生多態(tài)(那會(huì)出現(xiàn)另一種復(fù)雜性?。?,則可能需要顯式向下轉(zhuǎn)換。相比之下,lambda 和方法引用很容易作為參數(shù)傳遞給其它函數(shù)。
但是,新的函數(shù)式結(jié)構(gòu)在流 API 之外具有其它用途。考慮一個(gè) Java GUI 程序,該程序帶有一個(gè)供用戶按下的按鈕,例如,按下以獲取當(dāng)前時(shí)間。按鈕按下的事件處理程序可能編寫如下:
JButton updateCurrentTime = new JButton("Update current time");updateCurrentTime.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {currentTime.setText(new Date().toString());}});
這個(gè)簡(jiǎn)短的代碼段很難解釋。關(guān)注第二行,其中方法 addActionListener 的參數(shù)開始如下:
new ActionListener() {
這似乎是錯(cuò)誤的,因?yàn)?ActionListener 是一個(gè)抽象接口,而抽象類型不能通過(guò)調(diào)用 new 實(shí)例化。但是,事實(shí)證明,還有其它一些實(shí)例被實(shí)例化了:一個(gè)實(shí)現(xiàn)此接口的未命名內(nèi)部類。如果上面的代碼封裝在名為 OldJava 的類中,則該未命名的內(nèi)部類將被編譯為 OldJava$1.class。actionPerformed 方法在這個(gè)未命名的內(nèi)部類中被重寫。
現(xiàn)在考慮使用新的函數(shù)式結(jié)構(gòu)進(jìn)行這個(gè)令人耳目一新的更改:
updateCurrentTime.addActionListener(e -> currentTime.setText(new Date().toString()));
lambda 表達(dá)式中的參數(shù) e 是一個(gè) ActionEvent 實(shí)例,而 lambda 的主體是對(duì)按鈕上的 setText 的簡(jiǎn)單調(diào)用。
函數(shù)式接口和函數(shù)組合
到目前為止,使用的 lambda 已經(jīng)寫好了。但是,為了方便起見,我們可以像引用封裝方法一樣引用 lambda 表達(dá)式。以下一系列簡(jiǎn)短示例說(shuō)明了這一點(diǎn)。
考慮以下接口定義:
@FunctionalInterface // 可選,通常省略interface BinaryIntOp {abstract int compute(int arg1, int arg2); // abstract 聲明可以被刪除}
注釋 @FunctionalInterface 適用于聲明唯一抽象方法的任何接口;在本例中,這個(gè)抽象接口是 compute。一些標(biāo)準(zhǔn)接口,(例如具有唯一聲明方法 run 的 Runnable 接口)同樣符合這個(gè)要求。在此示例中,compute 是已聲明的方法。該接口可用作引用聲明中的目標(biāo)類型:
BinaryIntOp div = (arg1, arg2) -> arg1 / arg2;div.compute(12, 3); // 4
包 java.util.function 提供各種函數(shù)式接口。以下是一些示例。
下面的代碼段介紹了參數(shù)化的 Predicate 函數(shù)式接口。在此示例中,帶有參數(shù) String 的 Predicate 類型可以引用具有 String 參數(shù)的 lambda 表達(dá)式或諸如 isEmpty 之類的 String 方法。通常情況下,Predicate 是一個(gè)返回布爾值的函數(shù)。
Predicatepred = String::isEmpty; // String 方法的 predicate 聲明 String[] strings = {"one", "two", "", "three", "four"};Arrays.asList(strings).stream().filter(pred) // 過(guò)濾掉非空字符串.forEach(System.out::println); // 只打印空字符串
在字符串長(zhǎng)度為零的情況下,isEmpty Predicate 判定結(jié)果為 true。 因此,只有空字符串才能進(jìn)入管道的 forEach 階段。
下一段代碼將演示如何將簡(jiǎn)單的 lambda 或方法引用組合成更豐富的 lambda 或方法引用。考慮這一系列對(duì) IntUnaryOperator 類型的引用的賦值,它接受一個(gè)整型參數(shù)并返回一個(gè)整型值:
IntUnaryOperator doubled = n -> n * 2;IntUnaryOperator tripled = n -> n * 3;IntUnaryOperator squared = n -> n * n;
IntUnaryOperator 是一個(gè) FunctionalInterface,其唯一聲明的方法為 applyAsInt?,F(xiàn)在可以單獨(dú)使用或以各種組合形式使用這三個(gè)引用 doubled、tripled 和 squared:
int arg = 5;doubled.applyAsInt(arg); // 10tripled.applyAsInt(arg); // 15squared.applyAsInt(arg); // 25
以下是一些函數(shù)組合的樣例:
int arg = 5;doubled.compose(squared).applyAsInt(arg); // 5 求 2 次方后乘 2:50tripled.compose(doubled).applyAsInt(arg); // 5 乘 2 后再乘 3:30doubled.andThen(squared).applyAsInt(arg); // 5 乘 2 后求 2 次方:100squared.andThen(tripled).applyAsInt(arg); // 5 求 2 次方后乘 3:75
函數(shù)組合可以直接使用 lambda 表達(dá)式實(shí)現(xiàn),但是引用使代碼更簡(jiǎn)潔。
構(gòu)造器引用
構(gòu)造器引用是另一種函數(shù)式編程構(gòu)造,而這些引用在比 lambda 和方法引用更微妙的上下文中非常有用。再一次重申,代碼示例似乎是最好的解釋方式。
考慮這個(gè) POJO 類:
public class BedRocker { // 基巖的居民private String name;public BedRocker(String name) { this.name = name; }public String getName() { return this.name; }public void dump() { System.out.println(getName()); }}
該類只有一個(gè)構(gòu)造函數(shù),它需要一個(gè) String 參數(shù)。給定一個(gè)名字?jǐn)?shù)組,目標(biāo)是生成一個(gè) BedRocker 元素?cái)?shù)組,每個(gè)名字代表一個(gè)元素。下面是使用了函數(shù)式結(jié)構(gòu)的代碼段:
String[] names = {"Fred", "Wilma", "Peebles", "Dino", "Baby Puss"};Streambedrockers = Arrays.asList(names).stream().map(BedRocker::new); BedRocker[] arrayBR = bedrockers.toArray(BedRocker[]::new);Arrays.asList(arrayBR).stream().forEach(BedRocker::dump);
在較高的層次上,這個(gè)代碼段將名字轉(zhuǎn)換為 BedRocker 數(shù)組元素。具體來(lái)說(shuō),代碼如下所示。Stream 接口(在包 java.util.stream 中)可以被參數(shù)化,而在本例中,生成了一個(gè)名為 bedrockers 的 BedRocker 流。
Arrays.asList 實(shí)用程序再次用于流化一個(gè)數(shù)組 names,然后將流的每一項(xiàng)傳遞給 map 函數(shù),該函數(shù)的參數(shù)現(xiàn)在是構(gòu)造器引用 BedRocker::new。這個(gè)構(gòu)造器引用通過(guò)在每次調(diào)用時(shí)生成和初始化一個(gè) BedRocker 實(shí)例來(lái)充當(dāng)一個(gè)對(duì)象工廠。在第二行執(zhí)行之后,名為 bedrockers 的流由五項(xiàng) BedRocker 組成。
這個(gè)例子可以通過(guò)關(guān)注高階 map 函數(shù)來(lái)進(jìn)一步闡明。在通常情況下,一個(gè)映射將一個(gè)類型的值(例如,一個(gè) int)轉(zhuǎn)換為另一個(gè)相同類型的值(例如,一個(gè)整數(shù)的后繼):
map(n -> n + 1) // 將 n 映射到其后繼
然而,在 BedRocker 這個(gè)例子中,轉(zhuǎn)換更加戲劇化,因?yàn)橐粋€(gè)類型的值(代表一個(gè)名字的 String)被映射到一個(gè)不同類型的值,在這個(gè)例子中,就是一個(gè) BedRocker 實(shí)例,這個(gè)字符串就是它的名字。轉(zhuǎn)換是通過(guò)一個(gè)構(gòu)造器調(diào)用來(lái)完成的,它是由構(gòu)造器引用來(lái)實(shí)現(xiàn)的:
map(BedRocker::new) // 將 String 映射到 BedRocker
傳遞給構(gòu)造器的值是 names 數(shù)組中的其中一項(xiàng)。
此代碼示例的第二行還演示了一個(gè)你目前已經(jīng)非常熟悉的轉(zhuǎn)換:先將數(shù)組先轉(zhuǎn)換成 List,然后再轉(zhuǎn)換成 Stream:
Streambedrockers = Arrays.asList(names).stream().map(BedRocker::new);
第三行則是另一種方式 —— 流 bedrockers 通過(guò)使用數(shù)組構(gòu)造器引用 BedRocker[]::new 調(diào)用 toArray 方法:
BedRocker[ ] arrayBR = bedrockers.toArray(BedRocker[]::new);
該構(gòu)造器引用不會(huì)創(chuàng)建單個(gè) BedRocker 實(shí)例,而是創(chuàng)建這些實(shí)例的整個(gè)數(shù)組:該構(gòu)造器引用現(xiàn)在為 BedRocker[]:new,而不是 BedRocker::new。為了進(jìn)行確認(rèn),將 arrayBR 轉(zhuǎn)換為 List,再次對(duì)其進(jìn)行流式處理,以便可以使用 forEach 來(lái)打印 BedRocker 的名字。
FredWilmaPeeblesDinoBaby Puss
該示例對(duì)數(shù)據(jù)結(jié)構(gòu)的微妙轉(zhuǎn)換僅用幾行代碼即可完成,從而突出了可以將 lambda,方法引用或構(gòu)造器引用作為參數(shù)的各種高階函數(shù)的功能。
柯里化Currying
柯里化函數(shù)是指減少函數(shù)執(zhí)行任何工作所需的顯式參數(shù)的數(shù)量(通常減少到一個(gè))。(該術(shù)語(yǔ)是為了紀(jì)念邏輯學(xué)家 Haskell Curry。)一般來(lái)說(shuō),函數(shù)的參數(shù)越少,調(diào)用起來(lái)就越容易,也更健壯。(回想一下一些需要半打左右參數(shù)的噩夢(mèng)般的函數(shù)?。┮虼耍瑧?yīng)將柯里化視為簡(jiǎn)化函數(shù)調(diào)用的一種嘗試。java.util.function 包中的接口類型適合于柯里化,如以下示例所示。
引用的 IntBinaryOperator 接口類型是為函數(shù)接受兩個(gè)整型參數(shù),并返回一個(gè)整型值:
IntBinaryOperator mult2 = (n1, n2) -> n1 * n2;mult2.applyAsInt(10, 20); // 200mult2.applyAsInt(10, 30); // 300
引用 mult2 強(qiáng)調(diào)了需要兩個(gè)顯式參數(shù),在本例中是 10 和 20。
前面介紹的 IntUnaryOperator 比 IntBinaryOperator 簡(jiǎn)單,因?yàn)榍罢咧恍枰粋€(gè)參數(shù),而后者則需要兩個(gè)參數(shù)。兩者均返回整數(shù)值。因此,目標(biāo)是將名為 mult2 的兩個(gè)參數(shù) IntBinraryOperator 柯里化成一個(gè)單一的 IntUnaryOperator 版本 curriedMult2。
考慮 IntFunction 類型。此類型的函數(shù)采用整型參數(shù),并返回類型為 R 的結(jié)果,該結(jié)果可以是另一個(gè)函數(shù) —— 更準(zhǔn)確地說(shuō),是 IntBinaryOperator。讓一個(gè) lambda 返回另一個(gè) lambda 很簡(jiǎn)單:
arg1 -> (arg2 -> arg1 * arg2) // 括號(hào)可以省略
完整的 lambda 以 arg1 開頭,而該 lambda 的主體以及返回的值是另一個(gè)以 arg2 開頭的 lambda。返回的 lambda 僅接受一個(gè)參數(shù)(arg2),但返回了兩個(gè)數(shù)字的乘積(arg1 和 arg2)。下面的概述,再加上代碼,應(yīng)該可以更好地進(jìn)行說(shuō)明。
以下是如何柯里化 mult2 的概述:
- 類型為
IntFunction的 lambda 被寫入并調(diào)用,其整型值為 10。返回的IntUnaryOperator緩存了值 10,因此變成了已柯里化版本的mult2,在本例中為curriedMult2。 - 然后使用單個(gè)顯式參數(shù)(例如,20)調(diào)用
curriedMult2函數(shù),該參數(shù)與緩存的參數(shù)(在本例中為 10)相乘以生成返回的乘積。。
這是代碼的詳細(xì)信息:
// 創(chuàng)建一個(gè)接受一個(gè)參數(shù) n1 并返回一個(gè)單參數(shù) n2 -> n1 * n2 的函數(shù),該函數(shù)返回一個(gè)(n1 * n2 乘積的)整型數(shù)。IntFunctioncurriedMult2Maker = n1 -> (n2 -> n1 * n2);
調(diào)用 curriedMult2Maker 生成所需的 IntUnaryOperator 函數(shù):
// 使用 curriedMult2Maker 獲取已柯里化版本的 mult2。// 參數(shù) 10 是上面的 lambda 的 n1。IntUnaryOperator curriedMult2 = curriedMult2Maker2.apply(10);
值 10 現(xiàn)在緩存在 curriedMult2 函數(shù)中,以便 curriedMult2 調(diào)用中的顯式整型參數(shù)乘以 10:
curriedMult2.applyAsInt(20); // 200 = 10 * 20curriedMult2.applyAsInt(80); // 800 = 10 * 80
緩存的值可以隨意更改:
curriedMult2 = curriedMult2Maker.apply(50); // 緩存 50curriedMult2.applyAsInt(101); // 5050 = 101 * 50
當(dāng)然,可以通過(guò)這種方式創(chuàng)建多個(gè)已柯里化版本的 mult2,每個(gè)版本都有一個(gè) IntUnaryOperator。
柯里化充分利用了 lambda 的強(qiáng)大功能:可以很容易地編寫 lambda 表達(dá)式來(lái)返回需要的任何類型的值,包括另一個(gè) lambda。
總結(jié)
Java 仍然是基于類的面向?qū)ο蟮木幊陶Z(yǔ)言。但是,借助流 API 及其支持的函數(shù)式構(gòu)造,Java 向函數(shù)式語(yǔ)言(例如 Lisp)邁出了決定性的(同時(shí)也是受歡迎的)一步。結(jié)果是 Java 更適合處理現(xiàn)代編程中常見的海量數(shù)據(jù)流。在函數(shù)式方向上的這一步還使以在前面的代碼示例中突出顯示的管道的方式編寫清晰簡(jiǎn)潔的 Java 代碼更加容易:
dataStream.parallelStream() // 多線程以提高效率.filter(...) // 階段 1.map(...) // 階段 2.filter(...) // 階段 3....collect(...); // 或者,也可以進(jìn)行歸約:階段 N
自動(dòng)多線程,以 parallel 和 parallelStream 調(diào)用為例,建立在 Java 的 fork/join 框架上,該框架支持 任務(wù)竊取task stealing 以提高效率。假設(shè) parallelStream 調(diào)用后面的線程池由八個(gè)線程組成,并且 dataStream 被八種方式分區(qū)。某個(gè)線程(例如,T1)可能比另一個(gè)線程(例如,T7)工作更快,這意味著應(yīng)該將 T7 的某些任務(wù)移到 T1 的工作隊(duì)列中。這會(huì)在運(yùn)行時(shí)自動(dòng)發(fā)生。
在這個(gè)簡(jiǎn)單的多線程世界中,程序員的主要職責(zé)是編寫線程安全函數(shù),這些函數(shù)作為參數(shù)傳遞給在流 API 中占主導(dǎo)地位的高階函數(shù)。尤其是 lambda 鼓勵(lì)編寫純函數(shù)(因此是線程安全的)函數(shù)。
名稱欄目:Java中的數(shù)據(jù)流和函數(shù)式編程
轉(zhuǎn)載源于:http://www.5511xx.com/article/djggpcp.html


咨詢
建站咨詢

