1、概述
在計算機科學(xué)中,函數(shù)式編程是一種編程范式,一種構(gòu)建計算機程序結(jié)構(gòu)和元素的方式,類似于比較熟悉的面向?qū)ο缶幊?。將計算視為?shù)學(xué)函數(shù)的處理并盡量避免改變狀態(tài)和可變數(shù)據(jù)。它是一種聲明性編程范例,這意味著使用表達(dá)式或聲明而不是語句來完成編程。在功能代碼中,函數(shù)的輸出值僅取決于傳遞給函數(shù)的參數(shù),因此用參數(shù)x的相同值調(diào)用函數(shù)f兩次會產(chǎn)生相同的結(jié)果f(x)。這與依賴于本地或全局狀態(tài)的過程形成對比,當(dāng)使用相同的參數(shù)但使用不同的程序狀態(tài)時,有可能在不同的時間產(chǎn)生不同的結(jié)果。不依賴于函數(shù)輸入的狀態(tài)變化,就可以使得更容易理解和預(yù)測程序的行為,這是開發(fā)函數(shù)式編程的關(guān)鍵動機之一。
和Java沒有任何關(guān)系的語言JavaScript就是一個函數(shù)編程語言。Java最初被設(shè)計出來是作為一門面向?qū)ο缶幊陶Z言來設(shè)計的。但是隨著時代的變化,異步處理在編程中變得越來越頻繁。在現(xiàn)代編程語言中,任何可并行操作希望是一個簡單的動詞,開發(fā)者不僅希望能處理并發(fā)問題,并且必須能夠使用這些動詞而無需任何復(fù)雜的實現(xiàn)。對象都可以根據(jù)并發(fā)動詞進行演變。所以在Java8中帶有用于異步任務(wù)的動詞,例如CompletableFuture和用于并行處理的CoolJoinPool等結(jié)構(gòu)。異步編程需要隔離的編程單元,使用隔離的編程單元,可以保證結(jié)果,這種隔離的編程單元稱為“純函數(shù)”。如果在程序中使用純函數(shù),那么可以理解為在進行函數(shù)式編程。而異步處理的這種需求,正好非常適合用函數(shù)編程來解決,這也促使來Java 需要支持函數(shù)式編程。Java8 正好做了相關(guān)的支持。在Android開發(fā)中經(jīng)常使用的Rx系列也采取了函數(shù)式編程的理念。
2、相關(guān)前置概念討論
2.1 純函數(shù)
純函數(shù)不依賴于任何其他對象實例或類定義。在Java中,如果對象實例是實例函數(shù),則該函數(shù)必須存在于對象實例中,如果它是靜態(tài)的,則必須存在于類定義上。所以說,嚴(yán)格來講Java里面其實是沒有純函數(shù)的。同時純函數(shù)的輸出可以是一個函數(shù)。
2.2 函數(shù)程序外觀
Java代碼下面處理一個CSV文件數(shù)據(jù)。每行通過轉(zhuǎn)換數(shù)據(jù)或搜索,過濾,排序數(shù)據(jù)來輸出新的結(jié)果集。
try(Stream<String> stream = Files.lines(Paths.get(fileName)) ){
stream
.limit(10)
.map(t->t.split(","))
.flatMap(Arrays::stream)
.collect(Collectors.groupingBy(Function.identity(),Collectors.counting()))
.entrySet().stream()
.filter(t -> t.getValue()>10)
.forEach(t->System.out.println(t.getKey()+" "+t.getValue()));
}catch(IOException e ){}
上面的代碼過濾了一些大于10的數(shù)據(jù)。里面的語法用到了很多Java里面的新語法,之后會進行討論,可以認(rèn)為大于10的數(shù)據(jù)會被某些東西過濾掉。這里沒有如何過濾數(shù)據(jù)的操作,只是按照需求一步步的進行操作。在傳統(tǒng)的程序中,可能需要遍歷列表,將每個項目與某些項目進行比較,如果正確,則將其放入目標(biāo)列表。但這看到這樣的東西嗎,因為這里不使用命令式編程,而是使用聲明性編程。此外,在此代碼中看不到任何for循環(huán)。因為函數(shù)式編程不使用迭代和迭代器,而使用遞歸。
3、對象不可變
相同引用可以指向的不同對象是面向?qū)ο缶幊痰幕灸芰χ?。但在函?shù)式編程中,對象必須是不可變的,即使有權(quán)訪問對象,也必須無法根據(jù)需要對其進行修改。因為可變性是多線程環(huán)境中出現(xiàn)錯誤的主要原因。任何線程都可能意外地更改對象的狀態(tài),從而導(dǎo)致程序出錯。為了防止這種情況,使用synchronized關(guān)鍵字,Barrier類,互斥體,信號量,鎖等。但所有這些實現(xiàn)都很麻煩,甚至可能會導(dǎo)致死鎖,線程匱乏等問題。因此,如果為了同步而導(dǎo)致這些問題并且解決起來如此費勁,為什么不能直接消除其可變性,來一勞永逸。這就是在函數(shù)式編程中所做的。
在下面的代碼中,創(chuàng)建了一個健身房會員對象,會員開始日期與會員數(shù)據(jù)一起存儲。這里,printEndDate()和printStartDate()方法都具有訪問和修改startDate值的能力。它在不考慮任何其他方法的情況下能修改原始日期對象。
public class GymMember {
String name;
Integer id;
Date startDate;
public GymMember(String name, Integer id, Date date) {
super();
this.name = name;
this.id = id;
this.startDate = date;
}
public static void main(String[] args) {
GymMember object = new GymMember("talha", 1, new Date());
object.printEndDate();
object.printStartDate();
}
private void printEndDate() {
Calendar calendar = Calendar.getInstance();
calendar.setTime(startDate);
calendar.add(Calendar.MONTH,1);
startDate = calendar.getTime();
System.out.println(startDate);
}
private void printStartDate() {
System.out.println(startDate);
}
}
為了防止上面的隨意修改,可以將startDate屬性設(shè)置為final
final Date startDate;
當(dāng)這時候在在其中一個方法中更改startDate時,編譯器將報錯。但這樣設(shè)置并不能完全將startDate設(shè)置為不可變。final僅阻止修改引用,使之無法更改此引用引用的地址。但仍然可以更改引用指向的對象的屬性。仍然可以更改startDate的屬性值,即使它被final修飾。所以在并行環(huán)境中使用它仍然可能不安全。為此Java8 中對于Data類提供了一個不可變類LocalDate。同時為了防止GymMember類通過子類注入的方式改變其屬性,比如如下:
public class VIPGymMember extends GymMember {
public VIPGymMember(String name, Integer id, LocalDate date) {
super(name, id, date);
id = id + 1000000;
}
}
需要將類設(shè)置為final可以阻止這類事情的發(fā)生,下面是一個最終版的:
public final class GymMember {
final String name;
final Integer id;
final LocalDate startDate;
private void printEndDate() {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MONTH,1);
System.out.println(startDate);
}
public GymMember(String name, Integer id, LocalDate date) {
super();
this.name = name;
this.id = id;
this.startDate = date;
}
private void printStartDate() {
System.out.println(startDate);
}
}
如上面的Data,Java8 中提供了許多不可變的對象,例如JDK架構(gòu)師為每個集合提供不可變版本??梢允褂眠@些不可變集合類型并使用Collections實用程序類轉(zhuǎn)換可變集合,如下所示:
public static void main(String[] args) {
final Map<Integer, String> myMap = new HashMap<>();
Map myUnmodifiableMap = Collections.unmodifiableMap(myMap);
myMap.put(2, "talha");
addElement(myUnmodifiableMap); // 會報錯
}
public static void addElement(Map m) {
m.put(3, "john");
}
如上所示,使用Collections的 unmodifiableMap方法使Map不可變??梢栽俅问褂肕ap引用此方法的結(jié)果。但是此集合初始化后,永遠(yuǎn)不允許修改。
綜上所述,主要討論了使用final關(guān)鍵字使自定義對象不可變。final關(guān)鍵字用于;
① 防止對象的地址更改
② 防止對象實例的屬性的地址更改
③ 防止類被子類繼承
同時還討論了JDK集合和Date API是可變的。但是在在函數(shù)式編程中這是不可接受的。于是在Java8中出現(xiàn)了LocalDate類來替換Date類并將可變Map,List,Set接口用不可變類替換??梢酝ㄟ^使用java.util.Collections類的unmodifiableMap(),unmodifiableCollection()和unModifiableSet()等方法來獲取不可變集合。
4、引用函數(shù)
函數(shù)是函數(shù)式編程中的一等公民。這句話可以說是函數(shù)式編程中最關(guān)鍵的一句話。但比較遺憾的是在Java中函數(shù)并不是一等公民,類或者說對象才是一等公民。Java中的函數(shù)是不能脫離類或者對象而存在的。但是在Java8中通過一些巧妙的方法將函數(shù)變成了偽一等公民。而在比較新的一門語言Kotlin中函數(shù)就正式成為了一等公民。
函數(shù)不能作為參數(shù)傳遞給Java 8之前的其他函數(shù)。在Java8函數(shù)可以作為一種偽獨立對象調(diào)用,它們可以作為偽參數(shù)傳遞給其他函數(shù)。它其實是一個只有一個接口函數(shù)的接口類。在Java8中增加了一些語法糖,使它看上去像一個函數(shù)對象,這會在下面進行討論。
同時在Java8中可以用“ ::” 來表示引用此函數(shù),可以通過此表示法訪問類或?qū)嵗臉?gòu)造函數(shù)和方法。同時可以用Supplier和Function接口來引用對應(yīng)的構(gòu)造函數(shù)和方法,以上面的GymMember類為例。
public GymMember() {
this.name = "gpj";
this.id = 0;
startDate = LocalDate.MIN;
}
public Date getNextDay(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DAY_OF_YEAR, 1);
Date dateNew = calendar.getTime();
return dateNew;
}
public static void main(String[] args) {
Supplier<GymMember> memberCreator = GymMember::new;
GymMember mGymMember = memberCreator.get();
Function<Date, Date> nextDayCalculator = mGymMember::getNextDay;
nextDayCalculator.apply(new Date());
}
在上面的main函數(shù)中第一行用了GymMember::new來獲取GymMember的構(gòu)造函數(shù),左側(cè)是一個特殊的SAM(單一抽象方法),下面將討論,并用于引用一個沒有輸入但返回一個對象的函數(shù)。將其引用賦值給memberCreator引用。第二行通過這個引用來構(gòu)造對象。第三行獲取了GymMember的getNextDay 賦值給Function類型nextDayCalculator。這里的Function 第一個泛型是函數(shù)輸入類型,第二個是輸出參數(shù)類型。Function這個類型只有一個輸入,這邊對于這個設(shè)計可以給的一個解釋是 純函數(shù)的性質(zhì)和形式定義來講,必須有且只有一個輸入?yún)?shù)。第四行使用apply方法來獨立調(diào)用一個函數(shù)并獨立地提供輸入?yún)?shù),可以理解為此時該函數(shù)是剝離于對象的。
4.1 單一抽象方法接口(SAM)
前面已經(jīng)多次提到了這個概念,在Java中實際上是不存在脫離于對象的函數(shù)的。于是為了滿足函數(shù)式編程,設(shè)計了單一抽象方法接口來進行模擬。其實在Java8之前就有許多單一抽象方法接口,比如說Runnable,Callable,Comparator和ActionListener等。他們的相似之處在于,當(dāng)實現(xiàn)其中任何一個時,只需處理此接口的一種方法。其他方法對開發(fā)者來說并不重要。在實現(xiàn)此特定方法(此方法默認(rèn)情況下未實現(xiàn),即abstract)之后,將傳遞實現(xiàn)這些接口的類的新實例。
在Java 8中,幾個SAM接口和SAM接口的新表示(Lambda表達(dá)式)被添加到JDK中。使用lambda表達(dá)式(后面會討論),不再需要創(chuàng)建一個類來包裝所需的方法并調(diào)用它。因為從開發(fā)者層面講可以引用方法并獨立調(diào)用它。所有這些SAM接口都可以在Stream API中使用。以下是Java8中新的SAM接口:
| 接口 | 輸入 | 輸出 | 目標(biāo) |
|---|---|---|---|
| Function | 一個任意類型 | 一個任意類型 | 實現(xiàn)或改變一個邏輯功能 |
| BiFunction | 兩個任意類型 | 一個任意類型 | 實現(xiàn)或改變一個邏輯功能 |
| Predicate | 一個或多個任意類型 | boolean | 測試值是否符合邏輯 |
| Consumer | 一個或多個任意類型 | 無 | 使用數(shù)據(jù)并產(chǎn)出一些副效果 |
| Supplier | 無 | 一個任意類型 | 創(chuàng)建所需類型的對象 |
下面會分別對這些類進行討論。
4.2 Function
這個函數(shù)用于基于給定輸入和邏輯創(chuàng)建輸出對象,并可能與其他函數(shù)鏈接??梢灾貙懫鋋pply()方法。也是需要實施的唯一方法。
| 方法 | 功能 |
|---|---|
| R apply(T t) | 調(diào)用該方法,其傳入?yún)?shù)類型是T,返回值類型是R |
| andThen(Function after) | 先調(diào)用其自己的apply()方法,再調(diào)用傳入的Function的apply()方法 |
| compose(Function before) | 先調(diào)用其傳入的Function的apply()方法,再調(diào)用自己的apply()方法 |
| identity() | 靜態(tài)函數(shù),返回一個輸入輸出類型一樣的Function |
4.3 Predicate
測試數(shù)據(jù)是否符合某些值或邏輯。其test()是需要實現(xiàn)的單一抽象接口。已經(jīng)實現(xiàn)了其他方法。
| 方法 | 功能 |
|---|---|
| boolean test(T t) | 測試值是否符合邏輯 |
| Predicate and(Predicate other) | 和傳入的Predicate進行與操作作為一個合的Predicate返回 |
| Predicate or(Predicate other) | 和傳入的Predicate進行或操作作為一個合的Predicate返回 |
| Predicate<T> negate() | 將自身取反作為一個合的Predicate返回 |
4.4 Consumer
獲取一個對象,使用但不返回。它可能有副效果,例如更改對象的值或?qū)⒛承┹敵鰧懭肽程帯?/p>
| 方法 | 功能 |
|---|---|
| void accept(T t) | 使用實例t,必要時修改它而不返回輸出 |
| void andThen(Consumer other) | 形成消費鏈,調(diào)用完自己的accept()后 調(diào)用傳入的Consumer 的accept() |
4.5 Supplier
通過構(gòu)造函數(shù)或其他方式(構(gòu)建器,工廠等)提供給定類型的實例。
| 方法 | 功能 |
|---|---|
| T get() | 提供一個實例 |
5、Lambda表達(dá)式
在上一節(jié)中,討論了Functional Interface。由于這些實際上都是接口類,所以實現(xiàn)起來需要實現(xiàn)多行代碼,這個函數(shù)式編程里面的單行編程理念所違背。于是在Java8中也同時推出了Lambda表達(dá)式用來簡化需要編寫的代碼。以上一節(jié)的Function為例,原始的實現(xiàn)方法如下:
Function<String, Integer> findWordCount = new Function<String,Integer>(){
@Overide
public Integer apply(String t){
return t.split(" ").length;
}
}
使用Lambda表達(dá)式實現(xiàn)上述接口類
Function<String, Integer> findWordCount = (String t)->{ return t.split(" ").length; };
上面的Lambda表達(dá)式刪除了匿名類外殼,只留下了剩余的部分。Lambda表達(dá)式一般由如下部分組成:
① 在括號內(nèi)輸入?yún)?shù)
② - > lambda簽名
③ 返回單個結(jié)果的方法體
如果輸入列表只有一個項目,則可以刪除括號
Function<String, Integer> findWordCount = t ->{ return t.split(" ").length; };
下面是一些常見的lambda表達(dá)式語法:
| 表達(dá)式 | 例子 |
|---|---|
| (parameters)->expression | (List<String> list )->list.isEmpty() |
| (parameters)->{return ... ;} | (int a, int b )->{return a+b;} |
| (parameters)->statements | (int a, int b )->a+b |
lambda表達(dá)式的右側(cè)是方法體。如果更喜歡使用return關(guān)鍵字,則必須遵循方法體語法并使用分號和花括號。但是如果喜歡語法糖,可以刪除花括號和return關(guān)鍵字,如第三個表達(dá)式所示,但這種情況下必須是方法體里面只有一句代碼。
上面的例子在復(fù)制號右側(cè)編寫了一個方法體。JDK會自動檢查方法的輸入和輸出參數(shù),并使用適當(dāng)?shù)腇unctional Interface類型包裝此方法。比如如下lambda表達(dá)式:
t - > t%2 == 0
這個lambda表達(dá)式是符合上一節(jié)所說的Predicate接口的,所以可以用Predicate來引用該表達(dá)式。
Predicate<Integer> predicate = t -> t % 2 == 0;
boolean isEven = predicate.test(5);
6、Stream API
在前面第二節(jié)舉的關(guān)于函數(shù)式編程的例子里面用的就是Stream API,這個API也是在Java8新出的。這是一個非常經(jīng)典的用于函數(shù)式編程的思想來進行處理的API,主要用來處理集合結(jié)構(gòu)。這個API可以理解為將數(shù)據(jù)倒入到一個管道中,在整個管道中一些過濾器過濾掉不正確的項目,一些處理器轉(zhuǎn)換項目,一些根據(jù)某些邏輯對它們進行分組,一些數(shù)據(jù)在整個管道中流式傳輸時計算一個值。
類似上圖所示在流開始處,獲取一些塑料球作為數(shù)據(jù)源,過濾紅色球,將其融化并將其轉(zhuǎn)換為隨機三角形。之后過濾器過濾掉小三角形。最后計算出總周長。
使用常規(guī)Collection執(zhí)行此操作意味著多次迭代,多次函數(shù)調(diào)用。使用常規(guī)Collection,無法并行化該過程。并且只能處理有限數(shù)量的對象。如果數(shù)據(jù)源是無限流式傳輸?shù)?,則在Java8之前就無法很好的進行處理。而采用Stream API就能很好的解決這些問題,其優(yōu)點在于:
① 可以處理來自源的無限數(shù)量的對象
② 具有函數(shù)式編程風(fēng)格
③ 能使用前面所說的Functional Interface
在Stream中,可以依次連接幾個處理器。最后一個用于產(chǎn)生結(jié)果。此結(jié)果可能是示例中的標(biāo)量值,也可能是全新的集合,例如List或Map。最后一個操作稱為終端操作,因為它終止了流進程。其他處理器稱為中間操作。終端操作后,無法再次使用該流,也無法再使用Stream的其他中間操作。
以下是中間操作:
| Stream 操作 | 作用 | 輸入 |
|---|---|---|
| filter | 根據(jù)給定的Predicate過濾對象 | Predicate |
| map | 根據(jù)給定對象對其進行相應(yīng)的轉(zhuǎn)換 | Function |
| limit | 限制集合個數(shù) | int |
| sorted | 給對象進行排序 | Comparator |
| distinct | 根據(jù)對象的equals()過濾掉重復(fù)的對象 | |
| flatMap | 將多個流合并成一個流 | Function,Stream |
以下是終端操作
| Stream 操作 | 作用 | 輸入 |
|---|---|---|
| forEach | 遍歷每個對象,并執(zhí)行傳入的Consumer的操作 | Consumer |
| count | 輸出對象個數(shù) | |
| collect | 將最終過濾的數(shù)據(jù)輸出 |