Java Java8的函數(shù)式編程

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

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)了其他方法。

Predicate可視化
方法 功能
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>

Consumer 可視化
方法 功能
void accept(T t) 使用實例t,必要時修改它而不返回輸出
void andThen(Consumer other) 形成消費鏈,調(diào)用完自己的accept()后 調(diào)用傳入的Consumer 的accept()

4.5 Supplier

通過構(gòu)造函數(shù)或其他方式(構(gòu)建器,工廠等)提供給定類型的實例。

Supplier 可視化
方法 功能
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ù)在整個管道中流式傳輸時計算一個值。

Stream 模擬

類似上圖所示在流開始處,獲取一些塑料球作為數(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ù)輸出
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容