Java8--lambda表達式與函數式編程

前言

2014年Java8發佈,引入了很多的新特性,其中最具有代表性的就是Lambda表達式、方法引用、函數式接口、Stream API等新特性,而這幾個新特性往往都是互相配合使用的,使得編碼更加的簡潔。

1、新特性簡介

1.1、Lambda表達式:

Lambda允許把函數作爲一個方法的參數,將函數作爲參數傳遞到方法中,Lambda表達式語法格式爲:

(parameters) -> expression 或 (parameters) ->{ statements; }

如下幾個簡單例子

1.不需要參數,返回值5 :()->5

2.接收一個參數,不返回值 :(x)->System.out.println(x);

3.接收一個參數,返回值參數的2倍數字:x->2*x;

4.接收兩個參數,返回兩個參數的和:(x,y)-> x+y;

lambda表達式中只能引用外部final類型修飾的變量,或者在lambda表達式後不會再修改的非final類型變量,換句話說lambda表達式中引用的變量在外部就不可改了

1.2、函數式接口

函數式接口(Functional Interface)就是一個有且僅有一個抽象方法,但是可以有多個非抽象方法的接口,本質上還是一個接口,只是增加了一個限制而已。

爲了將函數式接口和普通接口區分開,通常會在接口定義上添加@FunctionalInterface註解用於標識當前接口爲函數式接口,比如常見的Runnable接口就是一個函數式接口,代碼如下:

@FunctionalInterface
public interface Runnable {
 
    public abstract void run();
}

 

另外爲了簡化編碼,如果接口只定義了一個抽象方法,那麼及時沒有添加@FuncationInterface註解,編譯時該接口也會被標記爲上函數式接口;而如果一個接口定義了多個抽象方法,那麼及時添加了@FuncationInterface註解也不會被當作是函數式接口,並且編譯就無法通過。

1.3、方法引用

方法引用提供了非常有用的語法,可以直接引用已有Java類或對象(實例)的方法或構造器。與lambda聯合使用,方法引用可以使語言的構造更緊湊簡潔,減少冗餘代碼。

方法引用通過方法的名字來指向一個方法,方法引用可以使語言的構造更緊湊簡潔,減少冗餘代碼,方法引用使用一對冒號 :: ,案例如下:

1 list.forEach(System.out::println);

 

通過方法引用的方式直接採用println方法名就可以執行System.out的println方法

1.4、Stream API(java.util.stream)

採用流的方式處理集合數據,把真正的函數式編程風格引入到Java中。可以說Lambda、函數式編程、方法引用只是代碼語法上進行了優化,而Stream則是在Lambda、函數式編程、方法引用的基礎之上優化了集合的操作,採用流式處理使得集合只需要經過一個遍歷就可以執行大量的操作,不僅簡化了集合操作而提供了操作的性能。

比如有一個User類,屬性包括userName(姓名)、score(分數)、gener(性別),有一個List存儲了大量的User對象,那麼如果想要從List中找到男生中分數最高的三個人的姓名,此時需要如何實現?

大概需要如下幾個步驟:

1、篩選出所有男生;2、將男生按分數進行排序;3、獲取前三名男生信息;4、獲取男生的姓名

實現代碼如下:

List<String> manList = new ArrayList<>();
       Collections.sort(list);
       for (User user : list){
            if(user.getGender()==0){
                manList.add(user.getName());
            }
        }
      manList = manList.subList(0,3);
      System.out.println(JSON.toJSONString(manList));

 

而如果採用Stream的API那麼需要一行代碼就可以實現,實現代碼如下:

1 list.stream().sorted().filter(i->i.getGender()==0).map(i->i.getName()).limit(3).forEach((u)->System.out.println(u));

 

這一行代碼中就包括了排序、過濾、屬性提取、數量提取、遍歷打印等一系列操作,大大簡化了代碼編寫,並且全程只需要執行一個遍歷操作。

 

2、Stream的實現原理

 Stream最簡單的實現方式莫過於每次函數調用都執行一次遍歷處理,然後將結果在傳遞給下一個函數,以上述爲例,調用sorted()時先將所有數據進行排序,存儲到臨時集合list1中,然後調用filter函數時將排好序的list1傳入進行遍歷過濾處理生成臨時集合list2,

調用map函數時再傳入list2處理之後生成list3,依次類推直到執行最後一個函數。這種方式簡單粗暴,雖然實現比較簡單但是有兩個缺點,一個缺點是需要執行多次遍歷操作,每執行一個函數就需要對所有數據進行遍歷一次;還有一個缺點是每個函數執行後都需要創建

一個臨時集合存儲處理的結果,就需要創建多個臨時集合用於存儲臨時數據。

Stream本質是一個流水線,流水線是特點就是數據只需要遍歷一次,每條數據流水線頭部流下尾部並進行計算得出結果。

Stream的操作主要分成中間操作和結束操作,中間操作只會對操作進行記錄,不會實際處理數據。而結束操作會觸發實際的計算操作,是一種惰性計算的方式,這是Stream高效的原因之一。

中間操作又分爲有狀態操作StatefulOp和無狀態操作StatelessOp兩種,有狀態操作指處理元素時必須拿到所有元素纔可以處理,比如sorted()、limit()、distinct()等操作;無狀態操作指處理元素時不受之前的元素影響,比如filter()、map()等操作;

結束操作又分爲短路操作和非短路操作兩種,短路操作指達到某種條件之後就可以結束流水線,不需要處理完全部數據,比如findFirst()、findAny()等操作; 非短路操作必須處理完全部數據才能得到結果,比如collect()、max()、count()、foreach()等操作

 

Stream流水線中每一個操作都包含了數據源+中間操作+回調函數,可以抽象成一個完整的流程Stage,Stream流水線就是由多個Stage組成,每個Stage分別持有前一個Stage和後一個Stage的引用形成了一個雙向鏈表。

Stream的抽象實現類是AbstractPipeline,AbstractPipeline就可以看作是一個Stage,而第一個Stage就是Head,可以通過Collection.stream()方法獲取Head對象,Head也是Stream的實現類,不包含操作,只是流水線的開頭。

從Head開始每執行一箇中間操作都會產生一個新的Stream,Stream對象以雙向鏈表構成,形成完整的流水線,所以這個雙向鏈表的Stream就完整的記錄了源數據和需要執行的所有操作。

 

通過Stream雙向鏈表可以記錄所有的操作,接下來還需要將各個Stream疊加起來,也就是前面的函數執行完了如何去執行下一個函數,每一個Stage都只知道本身的操作是什麼,並不知道下一個Stage的具體操作是什麼,所以需要有一個串聯機制來讓前一個操作後能夠

調用到下一個操作。此時就用到了Sink接口。

Sink接口定義如下:

 1 interface Sink<T> extends Consumer<T> {
 2         /**
 3          * 開始遍歷元素之前調用該方法,通知Sink做好準備
 4          */
 5         default void begin(long size) {}
 6 
 7         /**
 8          * 所有元素遍歷完成之後調用,通知Sink沒有更多的元素了
 9          */
10         default void end() {}
11 
12         /**
13          * 是否可以結束操作,可以讓短路操作儘早結束
14          */
15         default boolean cancellationRequested() {
16             return false;
17         }
18 
19         /**
20          * 遍歷元素時調用,接受一個待處理元素,並對元素進行處理。
21          * Stage把自己包含的操作和回調方法封裝到該方法裏,前一個Stage只需要調用當前Stage.accept(T t)方法就行了
22          */
23         default void accept(int value) {
24             throw new IllegalStateException("called wrong accept method");
25         }
26 
27     }

 

Stage將自己的操作封裝到Sink中,前一個Stage只需要調用後一個Stage對應的Sink的accept方法即可。而對於有狀態操作begin和end方法也必須實現,比如sorted操作,begin方法需要創建保存結果的容器,end方法負責對容器的數據進行排序。

對於短路操作cancellationRequested()是必須實現的,一旦cancellationRequested返回true就表示操作已經結束了。所以Stream的核心就是如何實現Sink接口。

Stream流水線整體流程就是從Head開始依次調用下一個Stage對於的Sink的begin、end、accept、cancellationRequested方法,而在accept方法中如果還有下一個Stage,那麼還需要在accept方法中繼續調用下一個Stage的accept方法。

如排序的Sink實現類源碼如下:

 1 private static final class RefSortingSink<T> extends AbstractRefSortingSink<T> {
 2         private ArrayList<T> list;
 3 
 4         RefSortingSink(Sink<? super T> sink, Comparator<? super T> comparator) {
 5             super(sink, comparator);
 6         }
 7 
 8         @Override
 9         public void begin(long size) {
10             if (size >= Nodes.MAX_ARRAY_SIZE)
11                 throw new IllegalArgumentException(Nodes.BAD_SIZE);
12             /** 初始化List */
13             list = (size >= 0) ? new ArrayList<T>((int) size) : new ArrayList<T>();
14         }
15 
16         @Override
17         public void end() {
18             /** list排序 */
19             list.sort(comparator);
20             /** 調用下一個Stream的begin方法 */
21             downstream.begin(list.size());
22             if (!cancellationWasRequested) {
23                 /** 如果沒有短路,就遍歷調用下一個Stream的accept方法 */
24                 list.forEach(downstream::accept);
25             }
26             else {
27                 for (T t : list) {
28                     if (downstream.cancellationRequested()) break;
29                     downstream.accept(t);
30                 }
31             }
32             /** 調用下一個Stream的end方法 */
33             downstream.end();
34             list = null;
35         }
36 
37         @Override
38         public void accept(T t) {
39             /** 向list中添加數據*/
40             list.add(t);
41         }
42     }

1、首先begin()方法告訴Sink參與排序的元素個數,方便確定中間結果容器的的大小;

2、之後通過accept()方法將元素添加到中間結果當中,最終執行時調用者會不斷調用該方法,直到遍歷所有元素;

3、最後end()方法告訴Sink所有元素遍歷完畢,啓動排序步驟,排序完成後將結果傳遞給下游的Sink;

4、如果下游的Sink是短路操作,將結果傳遞給下游時不斷詢問下游cancellationRequested()是否可以結束處理。

 

Sink將多個Stream的操作進行了串聯,接下來就需要執行整個流水線的操作,而執行操作是調用結束操作是觸發的,結束操作的Sink只需要處理數據即可,不需要再向下傳遞,而結束操作執行時會就會觸發整個流水線的執行。

還有一個問題是結束操作的Sink如何一層一層找到最上層的Sink,此時就用到了AbstractPipeline的onWrapSink方法,該方法的作用是將當前的Stage操作和將結果傳遞給下游的Stage進行封裝成一個新的Sink,相當於將當前操作和下游的Sink合併成新的Sink,

那麼最終就可以得到一個包含了所有操作的Sink,而從結束操作開始調用onWrapSink方法,相當於執行結束操作的Sink方法,就相當於執行了流水線上所有Sink的處理邏輯。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章