java lambda 深入淺出

戳藍字「TopCoder」關注我們哦!

JDK8中包含了許多內建的Java中常用到函數接口,比如Comparator或者Runnable接口,這些接口都增加了@FunctionalInterface註解以便能用在lambda上。

nametypedescription

ConsumerConsumer< T >

PredicatePredicate< T >

FunctionFunction< T, R >

SupplierSupplier< T >

UnaryOperatorUnaryOperator

BinaryOperatorBinaryOperator

標註爲@FunctionalInterface的接口是函數式接口,該接口只有一個自定義方法。注意,只要接口只要包含一個抽象方法,編譯器就默認該接口爲函數式接口。

Collection中的新方法

List.forEach() 該方法的簽名爲void forEach(Consumer<? super E> action),作用是對容器中的每個元素執行action指定的動作,其中Consumer是個函數接口,裏面只有一個待實現方法void accept(T t)。注意,這裏的Consumer不重要,只需要知道它是一個函數式接口即可,一般使用不會看見Consumer的身影。

list.forEach(item -> System.out.println(item));

List.removeIf() 該方法簽名爲boolean removeIf(Predicate<? super E> filter),作用是刪除容器中所有滿足filter指定條件的元素,其中Predicate是一個函數接口,裏面只有一個待實現方法boolean test(T t),同樣的這個方法的名字根本不重要,因爲用的時候不需要書寫這個名字。

// list中元素類型Stringlist.removeIf(item -> item.length() < 2);

List.replaceAll() 該方法簽名爲void replaceAll(UnaryOperator operator),作用是對每個元素執行operator指定的操作,並用操作結果來替換原來的元素。

// list中元素類型Stringlist.replaceAll(item -> item.toUpperCase());

List.sort() 該方法定義在List接口中,方法簽名爲void sort(Comparator<? super E> c),該方法根據c指定的比較規則對容器元素進行排序。Comparator接口我們並不陌生,其中有一個方法int compare(T o1, T o2)需要實現,顯然該接口是個函數接口。

// List.sort()方法結合Lambda表達式ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));list.sort((str1, str2) -> str1.length()-str2.length());

Map.forEach() 該方法簽名爲void forEach(BiConsumer<? super K,? super V> action),作用是對Map中的每個映射執行action指定的操作,其中BiConsumer是一個函數接口,裏面有一個待實現方法void accept(T t, U u)。

map.forEach((key, value) -> System.out.println(key + ": " + value));

Stream API

認識了幾個Java8 Collection新增的幾個方法,在瞭解下Stream API,你會發現它在集合數據處理方面的強大作用。常見的Stream接口繼承關係圖如下:

Stream是數據源的一種視圖,這裏的數據源可以是數組、集合類型等。得到一個stream一般不會手動創建,而是調用對應的工具方法:

調用Collection.stream()或者Collection.parallelStream()方法調用Arrays.stream(T[] array)方法

Stream的特性

無存儲。stream不是一種數據結構,它只是某種數據源的一個視圖。本質上stream只是存儲數據源中元素引用的一種數據結構,注意stream中對元素的更新動作會反映到其數據源上的。(有點類似於MySQL中的視圖概念)爲函數式編程而生。對stream的任何修改都不會修改背後的數據源,比如對stream執行過濾操作並不會刪除被過濾的元素,而是會產生一個不包含被過濾元素的新stream。惰式執行。stream上的操作並不會立即執行,只有等到用戶真正需要結果的時候纔會執行。可消費性。stream只能被“消費”一次,一旦遍歷過就會失效,就像容器的迭代器那樣,想要再次遍歷必須重新生成。

對Stream的操作分爲2種,中間操作與結束操作,二者的區別是,前者是惰性執行,調用中間操作只會生成一個標記了該操作的新的stream而已;後者會把所有中間操作積攢的操作以pipeline的方式執行,這樣可以減少迭代次數。計算完成之後stream就會失效。

操作類型接口方法
中間操作oncat() distinct() filter() flatMap() limit() map() peek() skip() sorted() parallel() sequential() unordered()
結束操作allMatch() anyMatch() collect() count() findAny() findFirst() forEach() forEachOrdered() max() min() noneMatch() reduce() toArray()

stream方法

forEach() stream的遍歷操作。

filter() 函數原型爲Stream filter(Predicate<? super T> predicate),作用是返回一個只包含滿足predicate條件元素的Stream。 

distinct() 函數原型爲Stream distinct(),作用是返回一個去除重複元素之後的Stream。 

sorted() 排序函數有兩個,一個是用自然順序排序,一個是使用自定義比較器排序,函數原型分別爲Stream sorted()和Stream sorted(Comparator<? super T> comparator)。 

map() 函數原型爲 Stream map(Function<? super T,? extends R> mapper),作用是返回一個對當前所有元素執行執行mapper之後的結果組成的Stream。直觀的說,就是對每個元素按照某種操作進行轉換,轉換前後Stream中元素的個數不會改變,但元素的類型取決於轉換之後的類型。

List<Integer> list = CollectionUtil.newArrayList(1, 2, 3, 4);list.stream().map(item -> String.valueOf(item)).forEach(System.out::println);

reduce 和 collect

reduce的作用是從stream中生成一個值,sum()、max()、min()、count()等都是reduce操作,將他們單獨設爲函數只是因爲常用。

// 找出最長的單詞Stream<String> stream = Stream.of("I", "love", "you", "too");Optional<String> longest = stream.reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2);

collect方法是stream中重要的方法,如果某個功能沒有在Stream接口中找到,則可以通過collect方法實現。

// 將Stream轉換成容器或MapStream<String> stream = Stream.of("I", "love", "you", "too");List<String> list = stream.collect(Collectors.toList());// Set<String> set = stream.collect(Collectors.toSet());// Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length));

諸如String::length的語法形式稱爲方法引用,這種語法用來替代某些特定形式Lambda表達式。如果Lambda表達式的全部內容就是調用一個已有的方法,那麼可以用方法引用來替代Lambda表達式。方法引用可以細分爲四類。引用靜態方法 Integer::sum,引用某個對象的方法 list::add,引用某個類的方法 String::length,引用構造方法 HashMap::new。

Stream Pipelines原理

ArrayList<String> list = CollectionUtil.newArrayList("I", "love", "you");list.stream()        .filter(s -> s.length() > 1)        .map(String::toUpperCase)        .sorted()        .forEach(System.out::println);

上面的代碼和下面的功能一樣,不過下面的代碼便於打斷點調試。

ArrayList<String> list = CollectionUtil.newArrayList("I", "love", "you");list.stream()    .filter(s -> {        return s.length() > 1;    })    .map(s -> {        return s.toUpperCase();    })    .sorted()    .forEach(s -> {        System.out.println(s);    });

首先filter方法瞭解一下:

// ReferencePipeline@Overridepublic final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) {    Objects.requireNonNull(predicate);    return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE,                                 StreamOpFlag.NOT_SIZED) {        // 生成state對應的Sink實現        @Override        Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) {            return new Sink.ChainedReference<P_OUT, P_OUT>(sink) {                @Override                public void begin(long size) {                    downstream.begin(-1);                }
@Override public void accept(P_OUT u) { if (predicate.test(u)) downstream.accept(u); } }; } };}

filter方法返回一個StatelessOp實例,並實現了其opWrapSink方法,可以肯定的是opWrapSink方法在之後某個時間點會被調用,進行Sink實例的創建。從代碼中可以看出,filter方法不會進行真正的filter動作(也就是遍歷列表進行filter操作)。

filter方法中出現了2個新面孔,StatelessOp和Sink,既然是新面孔,那就先認識下。

abstract class AbstractPipeline<E_IN, E_OUT, S extends BaseStream<E_OUT, S>>        extends PipelineHelper<E_OUT> implements BaseStream<E_OUT, S>

 StatelessOp繼承自AbstractPipeline,lambda的流處理可以分爲多個stage,每個stage對應一個AbstractPileline和一個Sink。

Stream流水線組織結構示意圖如下:

 圖中通過Collection.stream()方法得到Head也就是stage0,緊接着調用一系列的中間操作,不斷產生新的Stream。這些Stream對象以雙向鏈表的形式組織在一起,構成整個流水線,由於每個Stage都記錄了前一個Stage和本次的操作以及回調函數,依靠這種結構就能建立起對數據源的所有操作。這就是Stream記錄操作的方式。

Stream上的所有操作分爲兩類:中間操作和結束操作,中間操作只是一種標記,只有結束操作纔會觸發實際計算。中間操作又可以分爲無狀態的(Stateless)和有狀態的(Stateful),無狀態中間操作是指元素的處理不受前面元素的影響,而有狀態的中間操作必須等到所有元素處理之後才知道最終結果,比如排序是有狀態操作,在讀取所有元素之前並不能確定排序結果。

有了AbstractPileline,就可以把整個stream上的多個處理操作(filter/map/...)串起來,但是這隻解決了多個處理操作記錄的問題,還需要一種將所有操作疊加到一起的方案。你可能會覺得這很簡單,只需要從流水線的head開始依次執行每一步的操作(包括回調函數)就行了。這聽起來似乎是可行的,但是你忽略了前面的Stage並不知道後面Stage到底執行了哪種操作,以及回調函數是哪種形式。換句話說,只有當前Stage本身才知道該如何執行自己包含的動作。這就需要有某種協議來協調相鄰Stage之間的調用關係。這就需要Sink接口了,Sink包含的方法如下:

方法名作用
void begin(long size)開始遍歷元素之前調用該方法,通知Sink做好準備。
void end()所有元素遍歷完成之後調用,通知Sink沒有更多的元素了。
boolean cancellationRequested()是否可以結束操作,可以讓短路操作儘早結束。
void accept(T t)遍歷元素時調用,接受一個待處理元素,並對元素進行處理。Stage把自己包含的操作和回調方法封裝到該方法裏,前一個Stage只需要調用當前Stage.accept(T t)方法就行了。

有了上面的協議,相鄰Stage之間調用就很方便了,每個Stage都會將自己的操作封裝到一個Sink裏,前一個Stage只需調用後一個Stage的accept()方法即可,並不需要知道其內部是如何處理的。當然對於有狀態的操作,Sink的begin()和end()方法也是必須實現的。比如Stream.sorted()是一個有狀態的中間操作,其對應的Sink.begin()方法可能創建一個存放結果的容器,而accept()方法負責將元素添加到該容器,最後end()負責對容器進行排序。Sink的四個接口方法常常相互協作,共同完成計算任務。實際上Stream API內部實現的的本質,就是如何重載Sink的這四個接口方法。

回到最開始地方的代碼示例,map/sorted方法流程大致和filter類似,這些操作都是中間操作。重點關注下forEach方法:

// ReferencePipeline@Overridepublic void forEach(Consumer<? super P_OUT> action) {    evaluate(ForEachOps.makeRef(action, false));}
// ... ->

// AbstractPipeline@Overridefinal <P_IN, S extends Sink<E_OUT>> S wrapAndCopyInto(S sink, Spliterator<P_IN> spliterator) { copyInto(wrapSink(Objects.requireNonNull(sink)), spliterator); return sink;}@Overridefinal <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) { // 各個pipeline的opWrapSink方法回調 for ( @SuppressWarnings("rawtypes") AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) { sink = p.opWrapSink(p.previousStage.combinedFlags, sink); } return (Sink<P_IN>) sink;}@Overridefinal <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) { if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) { // sink各個方法的回調 wrappedSink.begin(spliterator.getExactSizeIfKnown()); spliterator.forEachRemaining(wrappedSink); wrappedSink.end(); } else { copyIntoWithCancel(wrappedSink, spliterator); }}

forEach()流程中會觸發各個Sink的操作,也就是執行各個lambda表達式裏的邏輯了。到這裏整個lambda流程也就完成了。

參考資料:

1、https://github.com/CarpenterLee/JavaLambdaInternals

 推薦閱讀 


歡迎小夥伴關注【TopCoder】閱讀更多精彩好文。

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