戳藍字「TopCoder」關注我們哦!
JDK8中包含了許多內建的Java中常用到函數接口,比如Comparator或者Runnable接口,這些接口都增加了@FunctionalInterface註解以便能用在lambda上。
name | type | description |
Consumer | Consumer< T > | |
Predicate | Predicate< T > | |
Function | Function< T, R > | |
Supplier | Supplier< T > | |
UnaryOperator | UnaryOperator | |
BinaryOperator | BinaryOperator |
標註爲@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中元素類型String
list.removeIf(item -> item.length() < 2);
List.replaceAll() 該方法簽名爲void replaceAll(UnaryOperator operator),作用是對每個元素執行operator指定的操作,並用操作結果來替換原來的元素。
// list中元素類型String
list.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轉換成容器或Map
Stream<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
@Override
public 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
@Override
public void forEach(Consumer<? super P_OUT> action) {
evaluate(ForEachOps.makeRef(action, false));
}
// ... ->
// AbstractPipeline
@Override
final <P_IN, S extends Sink<E_OUT>> S wrapAndCopyInto(S sink, Spliterator<P_IN> spliterator) {
copyInto(wrapSink(Objects.requireNonNull(sink)), spliterator);
return sink;
}
@Override
final <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;
}
@Override
final <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】閱讀更多精彩好文。