java8 stream學習筆記

pipeline

爲了更好地對集合進行並行操作,java8中加入了stream API。以前對集合的操作策略由客戶端提供,有了stream API後,對集合的操作集成到了集合內部,客戶端只需要按需調用即可。stream API支持函數式編程,它對集合的操作一般分爲三個階段:

(1)source:即集合的開始狀態。

(2)intermedia operations :0個或者多箇中間階段,比如Stream.filter,Stream.map等,中間階段通常都是lazy,比如filter操作並不會馬上開始過濾,而是返回一個新的stream對象。直到遇到terminal operation時纔會真正地執行。

(3)terminal operation:一個終結操作,比如foreach,IntStream.sum。

以代碼爲例分析stream的執行過程:

public static void main(String[] args) {
	List<String> words = asList("aboutjava", "accessibility", "addressing", "addshortcut", "about", "all", "become","bacteriumparatyphosum");
	// 以a開頭的字符串的最大長度
	int result = words.stream().filter(s -> s.startsWith("a")).mapToInt(s -> s.length()).max().getAsInt();
	System.out.println(result);
}
以上代碼是計算字符串列表中以a開頭的字符串的最大長度,一共包括兩個intermedia operations:filter和mapToInt,一個terminal operation:max。

如下圖所示,每一個操作都會創建一個Stream Pipeline,每個Pipeline包含一個Sink(也就是具體操作),max實際上是由reduce操作來實現的,Stream Pipeline通過upstream字段形成一個鏈表。


當調用max()方法時,PipelineHelper.wrapAndCopyInto()方法會被調用,這個方法的實現如下:

final <P_IN, S extends Sink<E_OUT>> S wrapAndCopyInto(S sink, Spliterator<P_IN> spliterator) {
	copyInto(wrapSink(Objects.requireNonNull(sink)), spliterator);
	return sink;
}
主要做了兩件事:

1、將所有的Sink(Ops)進行合併,合併的代碼如下:

final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
	Objects.requireNonNull(sink);

	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;
}
這些Sink最終會通過downstream形成一個鏈表。

舉個栗子,要從豆腐中過濾出豆漿,通常的做法是準備好幾張過濾網(每張過濾網的功能不一樣,有的是過濾豆腐渣的,有的可能是過濾沙,有的過濾其他更小的雜質),將豆腐倒入第一個網中,用桶接住,將桶中的豆腐再倒入第二張網中。。。依次類推,最後得到豆漿。

這裏合併Sink的過程,就是把所有過濾網疊在一起,看起來像一張網但兼有以上幾張網的功能,以後只需要過濾一次就可以得到豆漿了。

2、遍歷集合,代碼如下:

if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
	wrappedSink.begin(spliterator.getExactSizeIfKnown());
	spliterator.forEachRemaining(wrappedSink);
	wrappedSink.end();
}
else {
	copyIntoWithCancel(wrappedSink, spliterator);
}
wrappedSink.begin會依次調用所有Sink的begin方法,相當於疊好網,放到指定位置。

spliterator.forEachRemaining會依賴調用Sink的accept方法,相當於把豆腐倒在網上進行過濾。

這裏有對Short_circuit類型的操作進行特殊處理,比如findFirst只需要找一個滿足要求的,當找到一個後,就不再找了。好比桶中裝滿一杯豆漿後,就不倒豆腐了。

wrappedSink.end表明已處理完,直接出結果。


狀態

在Stream的這些操作中,有的是無狀態的,有的是有狀態的,有狀態的操作包括:distinct、sorted、limit、skip,比如distince依賴之前的結果,否則不知道當前結果是否唯一,這種情況下線程之前會有競爭,從而影響併發的性能,因此並行情況下要謹慎使用有狀態的操作。

short-circuiting

有時候需要在遍歷中途停止操作,比如查找第一個滿足條件的元素或者limit操作。在Stream中short-circuiting操作有:anyMatch、allMatch、noneMatch、findFirst、findAny、limit,這些操作在Sink中都有一個變量來判斷是否短路,比如limit用的是m,match用的是stop,find用的是hasValue。Sink中的cancellationRequested方法就是用來針對short-circuiting操作的,一旦發現是短路操作,就會調用AbstractPipeline的copyIntoWithCancel方法:
final <P_IN> void copyIntoWithCancel(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
	@SuppressWarnings({"rawtypes","unchecked"})
	AbstractPipeline p = AbstractPipeline.this;
	while (p.depth > 0) {
		p = p.previousStage;
	}
	wrappedSink.begin(spliterator.getExactSizeIfKnown());
	p.forEachWithCancel(spliterator, wrappedSink);
	wrappedSink.end();
}
其中forEachWithCancel方法會跳過後面所有的Sink:
final void forEachWithCancel(Spliterator<Integer> spliterator, Sink<Integer> sink) {
	Spliterator.OfInt spl = adapt(spliterator);
	IntConsumer adaptedSink = adapt(sink);
	do { } while (!sink.cancellationRequested() && spl.tryAdvance(adaptedSink));
}
 

順序

在Stream中存在兩種順序:
(1)encounter order:如果集合本身有序(比如:list),返回的結果就按集合的順序;
(2)thread order :在並行情況下,集合被分成幾部分分別在不同的線程中執行,有可能處於後面的元素先處理,這種線程順序也稱爲時間順序。
以下代碼中forEach打印的結果是亂序的,forEachOrdered是有順序的。
public static void main(String[] args) {
	List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
	list.stream().parallel().forEach(x -> System.out.print(x));
	list.stream().parallel().forEachOrdered(x -> System.out.print(x));
}
除了使用forEachOrdered保證順序外,Collectors.toList()也可以保證順序,二都最終都是通過ForEachOrderedTask類來實現的,具體可以參看ForEachOp.ForEachOrderedTask類中的代碼。

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