
Stream英文直譯為“流”,其實這里也是對文件流、集合流等各種流的操作。(雖然我們最常用在Collection集合中進(jìn)行操作,但它的應(yīng)用可是很廣泛的)它是java8中最亮眼的特性之一,能極大的方便我們對于數(shù)據(jù)的操作,讓程序員們能寫出非常高效率又干凈的代碼。下面我們先看一個例子,從例子中進(jìn)行初步認(rèn)識,然后再帶大家慢慢的來揭露它的神秘面紗。
Stream基礎(chǔ)
先來感受一下Stream的神奇吧~
public class StreamDemo {
/**
* 這里有一個很簡單的需求:
* 有一組用戶數(shù)據(jù)(List<User>),
* 我們要從這組數(shù)據(jù)中篩選出年齡在18-22,并且性別為男的所有用戶
*/
// java8以下的方法
public static List<User> filterUser(List<User> users){
Iterator<User> it = users.iterator();
List<User> us = new ArrayList<>();
while (it.hasNext()){
User u = it.next();
if(u.getAge()>=18 && u.getAge()<=22 && u.getSex().contentEquals("男")){
us.add(new User(u.getName(),u.getAge(),u.getSex()));
}
}
return us;
}
// 使用stream方式
public static List<User> filterByStream(List<User> users){
List<User> us = new ArrayList<>();
Stream<User> stream = users.stream();
stream.filter(user -> user.getAge()>=18 && user.getAge()<=22 && user.getSex().contentEquals("男"))
.forEach(user -> us.add(new User(user.getName(),user.getAge(),user.getSex())));
stream.close(); // 記得關(guān)流,否則將耗盡你的內(nèi)存,造成內(nèi)存泄漏程序崩潰。
return us;
}
public static void main(String[] args) {
List<User> data = getData();
// java8前的用法:
StreamDemo.filterUser(data).forEach((User u)-> System.out.println(u.getName())); // 輸出張三 肖二
// Stream的用法:
StreamDemo.filterByStream(data).forEach((User u)-> System.out.println(u.getName()));// 輸出張三 肖二
}
public static List<User> getData(){
List<User> users = new ArrayList<>();
users.add(new User("張三",22,"男"));
users.add(new User("李四",18,"女"));
users.add(new User("王五",28,"男"));
users.add(new User("肖二",20,"男"));
return users;
}
}
由上可以看出使用Stream是非常簡便的。那么它有什么書寫規(guī)則可以遵循呢?
其實書寫Stream分為三步走。第一步,先創(chuàng)建一個Stream()對象,然后執(zhí)行一些中間的聚合操作(如上面例子中的filter),最后是執(zhí)行最終的操作(如上面例子中的forEach)。其中中間方法是可以有多個的。其實它就是一個元素流管道,先經(jīng)過中間方法的處理,最后由最終操作(terminal operation)得到前面處理的結(jié)果。
下面我們來分步認(rèn)識一下
1.創(chuàng)建Stream(源)
其實,創(chuàng)建Stream是有許多種方式的,畢竟也有各種各樣的流嘛。下面列出了九種創(chuàng)建方式,請客官慢慢品一下吧。
- 空的Stream
Stream<String> streamEmpty = Stream.empty;
創(chuàng)建一個空Stream通常被用來避免空指針或者零元素對象的Stream返回為null的現(xiàn)象。
- 集合Steram
Collections<String> co = Arrays.asList("a", "b", "c");
Stream<String> streamCollection = co.stream();
可以創(chuàng)建Collection家族下面的List、Set、Queue。
- 數(shù)組Stream
Stream<String> streamArray = Stream.of("a", "b", "c");
上面是一種簡便方式,當(dāng)然也可以先創(chuàng)建一個數(shù)組(arr),再給數(shù)組創(chuàng)建Stream(Arrays.stream(arr))。
- 通過構(gòu)建器來創(chuàng)建
Stream<String> streamBuilder = Stream.<String>builder().add("a").add("b").add("c").build();
當(dāng)builder被用于指定參數(shù)類型時,應(yīng)被額外標(biāo)識在聲明右側(cè),否則方法build()將創(chuàng)建一個Stream(Object)實例:
- 通過生成器來生成一個Stream對象
Stream<String> streamGenerated = Stream.generate( () -> "element").limit(10); // limit也是一個terminal
方法generator()接受一個供應(yīng)器Supplier<T>用于元素生成。Supplier函數(shù)式接口在第二篇中有講到,不接收參數(shù)返回一個T類型結(jié)果,有點像工廠直接給你創(chuàng)建一個對象一樣。這里代碼的意思是一直創(chuàng)建String字符串“element”,然后用limit來限制只有10個。否則會一直創(chuàng)建到j(luò)vm內(nèi)存達(dá)到頂值。
- 通過迭代器
Stream<Integer> streamItreated = Stream.iterate(20, n -> n + 2).limit(10);
這里也是會永遠(yuǎn)迭代直到達(dá)到j(luò)vm內(nèi)存的頂值,所以也要使用limit來限制一下。生成流中的元素為:20、22、24、...。
- 基元流
怎么理解這個基佬流呢?說錯了,是基元流。其實它就是對于八大基礎(chǔ)數(shù)據(jù)類型的流,不過這里只提供了三個:int、long、double。分別為IntStream、LongStream、DoubleStream。
IntStream intStream = IntStream.range(1, 10);
LongStream longStream = LongStream.rangeClosed(1, 10);
方法range(int start, int end)創(chuàng)建了一個有序流。它使后面的值每個增加1,但卻不包括最后一個參數(shù)。所以上面的range方法最終會創(chuàng)建一個1到9的IntStream。方法rangeClosed(int start, int end)與range()大致相同,但它卻包含了最后一個值。所以最終會創(chuàng)建一個1到10的LongStream
這兩個方法用于生成三大基本數(shù)據(jù)類型的stream。
另外,以前我們用來創(chuàng)建隨機(jī)數(shù)的類Random也擴(kuò)展了用于生成基元流
Random random = new Random();
DoubleStream doubleStream = random.doubles(2);
- 字符串流
字符串流主要得益于char。char和int的私密關(guān)系我想你是知道的...
IntStream streamOfChars = "abc".chars();
- 文件流
Path path = Paths.get("C:\\file.txt");
Stream<String> streamString = Files.lines(path);
Stream<String> streamCharset = Files.lines(path, Charset.forName("utf-8"));
Java NIO類文件允許通過方法lines()生成文本文件的Stream<String>。文本的每一行都會變成stream的一個元素
2.中間(Intermediate)方法
我這里第二步的中間方法講的是 - 惰性求值。
什么是惰性求值呢?
答: 惰性求值就是像上面例子中filter的這類函數(shù),它最終不會產(chǎn)生出一個結(jié)果,而只是對數(shù)據(jù)進(jìn)行進(jìn)一步的操作。相反,還有一個叫做及早求值,如上面例子中的forEach,最終會從Stream中產(chǎn)生一個新的值。
++及早求值 我在這里把它歸到第三步中,也就是最后一步求出值的操作中(當(dāng)然,別忘了關(guān)流)++
那像這樣惰性求值的Intermediate有哪些呢?
答:主要有這些:map (mapToInt, mapToLong, mapToDouble)、flatMap 、 filter、 distinct、 sorted、 peek、 skip、 parallel、 sequential、 unordered。
這些惰性求值的函數(shù)具體都有什么用呢?
答: 這里賣個關(guān)子,請滑到下面的Stream繼續(xù)探索,我會在那里根據(jù)源碼來講解。
3.最終(terminal)方法完成對數(shù)據(jù)的處理
最終方法基本就是那些
及早求值的函數(shù)了:
不過它也分為兩大類,一類是Terminal(終結(jié)符、末尾),還有一類是short-circuiting(短路)
- Terminal:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、iterator
- short-circuiting:anyMatch、 allMatch、 noneMatch、 findFirst、 findAny
下面會通過源碼來學(xué)習(xí)幾個中間方法和最終方法
注意:
- JAVA8 是不允許重復(fù)使用stream的。如果對于同一個stream有相同的引用,則會造成
IllegalStateException異常 - Stream的執(zhí)行順序是垂直調(diào)用,如例:
stream.filter(user -> user.getAge()>=18 && user.getAge()<=22 && user.getSex().contentEquals("男"))
.skip(1)
.forEach(user -> us.add(new User(user.getName(),user.getAge(),user.getSex())));
// 中間方法skip的作用是返回一個跳過第幾元素(上面為跳過第一元素)的流。所以上面的方法執(zhí)行會得到第二個元素開始的集合(如果值夠的話)
上面的例子會執(zhí)行filter方法,當(dāng)filter方法滿足條件執(zhí)行完成后就會再去執(zhí)行skip,最后到達(dá)最終方法forEach。這就是Stream的垂直調(diào)用。
Stream繼續(xù)探索(源碼解讀)
既然是探索,那肯定得帶著疑問走啊?,F(xiàn)在的問題就是我已經(jīng)明白了怎么去創(chuàng)建一個Stream了,但是還不懂中間方法和最終方法的作用和用法。所以下面的操作就是一起來跟著源碼來操作一番吧~(為了篇幅著想,這里只舉了幾個例子)
中間方法的幾個例子
1. map
map在Stream中算是一個非常常用的方法了。
- 源碼:
// 接收一個Function功能型函數(shù)式接口的參數(shù)(第二篇中有講到即接收一個T類型的參數(shù),返回一個R類型的結(jié)果)
// 從下面定義的泛型可以看出參數(shù)函數(shù)式接口的類型應(yīng)該與返回值的類型關(guān)系應(yīng)該是繼承或是一致的
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
- 用法:
/**
* 如下例子:從集合中的每個數(shù)值都拿出來乘二后再組成一個新的集合
*/
List<Integer> data = Arrays.asList(1,2,4,8,16);
List<Integer> res = data.stream().map((param)->param*2).collect(Collectors.toList());
System.out.println(res); // [2, 4, 8, 16, 32]
- 作用: 可以來映射每個元素到對應(yīng)的結(jié)果
2. flatMap
看完上面一個map,這里又來了一個flatMap。flat? 平的? 平面? A?
- 源碼
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
- 用法
List<String> hello = Arrays.asList("hello","hi","你好","||");
List<String> straightMan = Arrays.asList("隨便","你忙","多喝熱水","||"); //直男三連
List<String> goodBoy = Arrays.asList("你是好人","you are good guy","very good");
List<List<String>> data = new ArrayList<>();
data.add(hello);
data.add(straightMan);
data.add(goodBoy);
List<String> res = data.stream().flatMap(flat->flat.stream()).collect(Collectors.toList());
System.out.println(res);// 輸出[hello, hi, 你好, ||, 隨便, 你忙, 多喝熱水, ||, 你是好人, you are good guy, very good]
// 不多說,這個例子希望你是看得懂的^_^..
- 作用: 和map類似,不同的是其每個元素轉(zhuǎn)換得到的是Stream對象,它其實是對流進(jìn)行一個扁平化的操作。
3. peek
剛剛看完平的,現(xiàn)在又來一個偷看(peek)?Stream可真好玩啊~
- 源碼
// 這里又用到了一個Consumer消費(fèi)型函數(shù)式接口(接收一個T類型的參數(shù),不返回結(jié)果),果然是看在眼里記在心里。
Stream<T> peek(Consumer<? super T> action);
- 用法
List<String> heart = Arrays.asList("I","Love","U");
Stream stream = heart.stream().peek(System.out::print); // 這里會輸出 ILoveU
Object say = stream.collect(Collectors.toList());
stream.close();
System.out.println(say); // 這里會輸出 ILoveU[I, Love, U]
- 作用:從上面例子可以看出peek會返回Stream接口,它是一個Intermediate,所以它返回的Stream可以供后續(xù)繼續(xù)使用。
最終方法的幾個例子
1. reduce
- 源碼
//BinaryOperator接口繼承于BiFunction函數(shù)式方法,在第一篇最后部分有講到,接收兩參數(shù)返回一結(jié)果
//BinaryOperator<T>接口用于執(zhí)行l(wèi)ambda表達(dá)式并返回一個T類型的返回值
T reduce(T identity, BinaryOperator<T> accumulator);
- 用法
int reducedTwoParams =
IntStream.range(1, 3).reduce(10, (a, b) -> a + b);
System.out.println("reducedTwoParams ="+reducedTwoParams); // reducedTwoParams = 13
- 作用:reduce主要被用來計算值,結(jié)果也是一個值,如上reducedTwoParams = 10+1+2+3
2. findAny
- 源碼
// Optional也是java8的一個新特性,在下一章會開始探索。主要被用來防止空指針異常的
Optional<T> findAny();
- 用法
/**
* 源碼里面關(guān)于它的注釋:
* Returns an {@link Optional} describing some element of the stream,
* or an empty {@code Optional} if the stream is empty.
*/
// 簡要:如果stream是空的情況下就返回一個empty,否則按照條件來返回相應(yīng)的值
Stream<String> stream = Stream.of("a", "o", "e","i","w").filter(element -> element.contains("e"));
Optional<String> anyElement = stream.findAny();
System.out.println(anyElement); // Optional[e]
// 如上,如果把filter操作改成filter(element -> element.contains("q"));,則會返回Optional.empty
- 作用:可以用來查找相關(guān)數(shù)值.
關(guān)于Stream的知識我上面講的這些還遠(yuǎn)遠(yuǎn)不達(dá)標(biāo),所以探索下去的心請希望能一直在跳動著。