前言
在上篇文章中,我們提到了java.util.stream包,今天我們就來詳細的研究一下這個包。
整體框架
分析stream包,我們先從整體架構入手,然後再深入到細節。我們先來看看API文檔:
從上圖中可以看見stream包中的接口比較多,類和枚舉比較少。我們先來看接口:
DoubleStream,IntStream,LongStream,Stream都繼承於BaseStream接口。並且它們都有各自的Builder接口:DoubleStream.Builder,IntStream.Builder,LongStream.Builder,Stream.Builder。剩下就只有Collector接口,Collectors,StreamSupport類,Collector,Characteristics枚舉。
Stream接口
Stream接口是一個泛型接口,而DoubleStream,IntStream,LongStream只不過是對double,int,long的包裝而已,所以我們弄懂Stream,其他的接口也都大同小異。
1.forEach
void forEach(Consumer<? super T> action)
forEach接收一個Consumer接口,該接口我們之前講Function包時已經提過了。它只接收不參數,沒有返回值。然後在 Stream 的每一個元素上執行該表達式。
範例:
Stream<String> stream = Stream.of("I", "love", "you");
stream.forEach(System.out::println);
System.out.println方法我們都很熟悉了,它接收一個參數,並且在控制檯打印出來。這正好符合Consumer接口,所以這裏輸出的結果是 :
I
love
you
2.peek
Stream<T> peek(Consumer<? super T> action)
peek方法也是接收一個Consumer功能型接口,它與forEach的區別就是它會返回Stream接口,也就是說forEach是一個Terminal操作,而peek是一個Intermediate操作,forEach完了以後Stream就消費完了,不能繼續再使用,而peek還可以繼續使用。
範例:
Stream<String> stream = Stream.of("I", "love", "you");
stream.peek(System.out::println).forEach(System.out::println);
代碼很簡單,但是大家可以先思考一下,輸出的結果是什麼?
輸出結果:
I
I
love
love
you
you
怎麼樣?跟你想的是一樣的嗎?有人可能會問,爲什麼輸出結果不是以下這種呢?
I
love
you
I
love
you
明明peek方法在前面。這是因爲我們前面提到過的懶加載,peek是一個Intermediate操作,它並不會馬上執行,當forEach的時候纔會把peek和forEach一起執行,來提高效率,所以等於是每個stream元素執行兩次打印操作,再執行下一個元素。
3.filter
Stream<T> filter(Predicate<? super T> predicate)
filter方法接收一個斷言型的接口,斷言型接口接收一個參數,返回一個Boolean類型。filter方法根據某個條件對stream元素進行過濾,通過過濾的元素將生成一個新的stream。
範例:
Stream<Integer> stream = Stream.of(1, 2, 3,4,5,6);
stream.filter((n)->n>2).forEach(System.out::println);
以上代碼通過filter方法把大於2的元素過濾出來,然後輸出。
4.map
<R> Stream<R> map(Function<? super T,? extends R> mapper)
map方法接收一個功能型接口,功能型接口接收一個參數,返回一個值。map方法的用途是將舊數據轉換後變爲新數據,是一種1:1的映射,每個輸入元素按照規則轉換成另一個元素。該方法是Intermediate操作。
Stream<String> stream = Stream.of("a","b","c","d");
stream.map(String::toUpperCase).forEach(System.out::println);
以上代碼通過map方法,把a,b,c,d全部轉變成大寫,然後輸出。
5.flatMap
<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
flatMap從結構上來看跟map差不多,主要是可以用來將stream層級扁平化。
Stream<List<Integer>> inputStream = Stream.of(
Arrays.asList(1),
Arrays.asList(2, 3),
Arrays.asList(4, 5, 6)
);
inputStream.flatMap((n)->n.stream()).forEach(System.out::println);
我們可以看見,inputStream由3個list組成,在經過flatMap以後,list就沒有了,以前list中的元素全部放在了一起。相關的方法還有:flatMapToInt,flatMapToLong,flatMapToDouble,只不過他們返回的分別是IntStream,LongStrea和DoubleStream。
6.findFirst:返回stream的第一個元素的Optional或爲空。這是一個Terminal操作,也是一個短路操作。
7.count:返回此流元素的數量。
8.sorted:將此流中的元素根據自然順序排序,sorted方法還有一個重載方法,可以傳入一個Comparator,這樣就可以根據Comparator來排序。
9.min/max:Stream接口中的這兩個方法接收一個Comparator參數,通過Comparator返回此流最小或者最大的元素。IntStream,DoubleStream.LongStream則不需要傳入Comparator。
10.limit:該方法接收一個long型參數,表示一共返回幾個元素。
11.skip:接收一個long類型的參數,表示跳過幾個元素。
12.distinct:消除重複元素後返回一個新Stream。
13.allMatch:Stream中的所有元素滿足傳入的斷言型接口,就返回true。
14.anyMatch:Stream中的只要有一個元素滿足傳入的斷言型接口,就返回true。
15.noneMatch:Stream中沒有元素滿足傳入的斷言型接口,就返回true。
16.generate:接收一個Supplier接口,返回一個Stream,通過實現supplier接口,可以自己來控制流的生成。
17.iterate:
static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
iterate接收兩個參數,第一個是泛型,seed可以理解爲種子值或者起始值。UnaryOperator是一個接口:
@FunctionalInterface
public interface UnaryOperator<T> extends Function<T,T>
該接口繼承了Function接口,那麼也必須實現Function接口中的apply方法。除此之外該接口還有一個靜態方法---identity,該方法始終返回其輸入參數。
iterate方法的作用是將種子值成爲stream的第一個元素,f(seed)爲第二個元素,f(f(seed))爲第三個元素,說遞歸你應該比較容易明白。
範例:
Stream.iterate(3, n->n+3).limit(10).forEach(System.out::println);
輸出:
3
6
9
12
15
以上範例中,3即爲種子值,然後f(3)等於6,f(f(3))得9。需要注意的是,iterate方法和generate方法返回的都是無限stream,需要用limite來限制stream的長度。
18.reduce:
reduce提供了三種重載方法。
1. Optional<T> reduce(BinaryOperator<T> accumulator)
2. T reduce(T identity, BinaryOperator<T> accumulator)
3. <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
第一個方法返回一個Optional對象,接收一個BinaryOperator。我們先來看BinaryOperator是什麼?
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T>
可以看到BinaryOperator是一個函數型接口,繼承了BiFunction,並且傳入參數和返回值都是相同類型。我們接着看BiFunction的定義:
@FunctionalInterface
public interface BiFunction<T, U, R>{
R apply(T t, U u);
}
BiFunction接口中有一個apply方法,有兩個參數,一個返回值。到這裏我們大概知道reduce方法傳入的參數大概怎麼用了,在來看返回值Optional的定義:
public final class Optional<T> extends Object
Optional是一個普通的對象,裏面的方法大家可以自己去看API,這裏就不詳細說了。到這裏你可能會說,寫了這麼多,你也沒說reduce到底有什麼作用啊?我們通過名字去猜測一下,reduce有減少,歸納之意。那我們是否可以理解爲,把Stream提供的多個元素歸納成一個對象?
範例:
Integer sum=Stream.of(1,2,3,4).reduce(Integer::sum).get();
System.out.println(sum);
輸出:
10
我們通過reduce方法,把1-4累加起來得到結果10.你肯定會問爲什麼傳的參數是Integer::sum?我們在以前的文章裏面提到了方法引用::,在這裏就是引用了Integer類的sum方法:
static int sum(int a, int b)
這個方法是不是就跟BiFunction中定義的apply一樣呢?接收兩個參數和一個返回值。
我們接着看reduce的第二個重載方法,在這個重載方法中多了一個參數T,這就是起始值,然後返回值由Optional變成了T。
範例:
Integer sum=Stream.of(1,2,3,4).reduce(1,Integer::sum);
System.out.println(sum);
輸出:
11
在此範例中,我們添加了起始值1,使得最後輸出結果多加了1。如果你覺得還不明白,那麼再來看一個例子。
範例:
String sum=Stream.of("a","b","c","d").reduce("1",String::concat);
System.out.println(sum);
輸出:
1abcd
這會應該明白了。
關於reduce的第三個重載方法,主要是用於parallelStream的,reduce操作是併發進行的,爲了避免競爭,每個reduce線程都會有獨立的result,combiner參數的作用就是在於合併每個線程的result得到最終的結果。由於第三個方法不是特別常用,我就只說一下方法不給出範例了。
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
這個方法起初一看,頭都大了,這都是什麼鬼?又是U,又是T,又是BiFunction,又是BinaryOperator。BinaryOperator不是繼承與BiFunction的麼?爲什麼不兩個都使用BiFunction呢?
那我們就來解析一下這個方法,首先該方法的返回值是由第一個參數決定的。也就是說第一個參數是什麼類型,該方法就返回什麼類型。這點明確了很重要。
我們接着看第二個參數-BiFunction,爲了理解深刻,我們再次拿出該接口的定義:
@FunctionalInterface
public interface BiFunction<T, U, R>{
R apply(T t, U u);
}
該接口接收兩個參數,這兩個參數的類型可以不一致。並且返回一個值,值的類型也可以不一致。
接着我們看reduce方法裏面定義的
BiFunction<U,? super T,U> accumulator
我們來對應一下,該接口接收兩個參數,其中第一個爲U,第二個爲T的子類,返回類型爲U。這下就明白多了,也就是說接收兩個不同類型的參數,但是返回值類型跟第一個參數一致,而第一個參數的類型也就是reduce方法的第一個參數類型U。
在看reduce第三個參數-BinaryOperator
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T>
該接口繼承了BiFunction,但是最重要的是,繼承的BiFunction的兩個接收參數和返回值都是同一個類型T。所以簡單來說BinaryOperator接收兩個參數,返回一個值都是同一類型。
到這裏我們應該明白了爲什麼reduce第二個參數是BiFunction,第三個參數是BinaryOperator了吧?
因爲第二個參數的作用是accumulator,所以接收的兩個參數類型可以不一樣。而前面說了在parallelStream的情況下,combiner的作用是合併每個線程的結果,而每個線程返回的結果都應該是同一個類型,所以在這裏用BinaryOperator而不是BiFunction。
不得不說這種設計真的是太精妙了。
19.collect:
collect方法跟reduce方法功能很類似,都是聚合方法。不同的是,reduce方法在操作每一個元素時總創建一個新值,而collect方法只是修改現存的值,而不是創建一個新值。
方法定義:
1.<R,A> R collect(Collector<? super T,A,R> collector)
2.<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)
這兩個方法都是泛型方法,我們先看第一個。第一個方法接收一個Collector接口作爲參數。如果我們要自己實現它會很麻煩,好在java.util.stream包中給我們提供了一個叫Collectors的類。這個方法我就不在這裏介紹了,大家可以自己去看API,通過Collectors這個類我們可以很容易得到一個Collector對象,這個類中提供了很多統計的操作和創建集合的操作。
範例:
Stream<String> stream = Stream.of("a", "b", "c", "d");
List<String> list =stream.collect(Collectors.toList());
for (String string : list) {
System.out.println(string);
}
輸出:
a
b
c
d
在這裏我們將一個stream流轉換爲了一個List對象。
collect方法的第二種形式跟我們前面說的reduce的很像。接收3個參數,第一個參數是Supplier接口,這個接口我們以前說過。
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
BiConsumer接口跟Consumer接口類似,不同的是Consumer接口只接收一個參數而BiConsumer接口接收兩個參數。collect的第二個參數和第三個參數都是BiConsumer接口,但是參數類型卻不一樣。BiConsumer<R,? super T> 第一個參數跟collect返回值一樣,也跟第一個參數一樣。第二個參數類型跟stream的類型一樣。BiConsumer<R,R>則兩個參數類型是相同的。
範例:
System.out.println(Arrays.asList("1","2","3","4").parallelStream().collect(
StringBuilder::new,
new BiConsumer<StringBuilder,String>(){
@Override
public void accept(StringBuilder t, String u) {
System.out.println("accumulator operate current thread:"+Thread.currentThread().getId()+" t:"+t+" u:"+u);
t.append(u);
System.out.println("accumulator operate current thread:"+Thread.currentThread().getId()+" result t:"+t+" u:"+u);
}
}
, new BiConsumer<StringBuilder,StringBuilder>(){
@Override
public void accept(StringBuilder t, StringBuilder u) {
System.out.println("combiner operate current thread:"+Thread.currentThread().getId()+" t:"+t+" u:"+u);
t.append(u);
System.out.println("combiner operate current thread:"+Thread.currentThread().getId()+" result t:"+t+" u:"+u);
}
}));
輸出:
accumulator operate current thread:1 t: u:3
accumulator operate current thread:11 t: u:4
accumulator operate current thread:10 t: u:2
accumulator operate current thread:12 t: u:1
accumulator operate current thread:12 result t:1 u:1
accumulator operate current thread:10 result t:2 u:2
accumulator operate current thread:11 result t:4 u:4
accumulator operate current thread:1 result t:3 u:3
combiner operate current thread:1 t:3 u:4
combiner operate current thread:10 t:1 u:2
combiner operate current thread:1 result t:34 u:4
combiner operate current thread:10 result t:12 u:2
combiner operate current thread:10 t:12 u:34
combiner operate current thread:10 result t:1234 u:34
1234
爲了方便大家理解,我並沒有使用lambda表達式。我們首先創建了一個並行的stream,每個stream元素的類型爲String,接着我們調用了collect方法,collect方法第一個參數是創建一個StringBuilder對象,在第二個參數中,我們打印了當前的線程id,和t,u的值方便調試。執行的操作也只是把String加入到StringBuilder中,第三個參數則把兩個StringBuilder合併。從輸出結果中我們可以看見,在執行accumulator操作的時候t的值是空的,並且是4個線程同時進行了accumulator操作,每個線程都把String加入到了StringBuilder中,而在執行combiner操作的時候,就由4個線程變成了2個,然後進行合併操作。最終結果爲1234。由於是多線程的,所以每次輸出的順序是不一樣的。以上輸出只能作爲參考。
到目前爲止,Stream接口中的大部分方法我們都講過了。至於那些IntStream,LongStream都大同小異,大家可以自己去看看,我就不做詳細介紹了。
作者:娃娃要從孩子抓起
鏈接:http://www.jianshu.com/p/b1b7e334ff79
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。