Java8中最大的兩個亮點,一個是Lambda表達式,另外一個就是Stream。新特性的加入,必定是爲了某種需求,那麼Stream是什麼,它能幫助咱們作什麼?首先看下面這個例子:java
有這樣一份數據,一組考卷List,每一個Paper有三個屬性分別是學生名字studentName、課程名稱className和分數score。如今咱們須要從中找出語文不及格(分數低於60)的學生名字,而且按分數從高到低排序。在不使用Java8新特性以前,相信大部分人都是像下面這樣寫的:程序員
public static List<String> getFailedPaperStudentNamesByJava7(List<Paper> papers) { // 篩選出不及格的考卷 List<Paper> failedPapers = new ArrayList<>(); for (Paper paper : papers) { if (paper.getClassName().equals("語文") && paper.getScore() < 60) { failedPapers.add(paper); } } // 按分數從高到低排序 Collections.sort(failedPapers, new Comparator<Paper>() { @Override public int compare(Paper o1, Paper o2) { return o2.getScore() - o1.getScore(); } }); // 記下不及格的學生名字 List<String> failedPaperStudentNames = new ArrayList<>(); for (Paper failedPaper : failedPapers) { failedPaperStudentNames.add(failedPaper.getStudentName()); } return failedPaperStudentNames; }
下面是用Java8的Lambda表達式+Stream改寫的版本:數組
public static List<String> getFailedPaperStudentNamesByJava8(List<Paper> papers) { return papers.stream() .filter(p -> p.getClassName().equals("語文") && p.getScore() < 60) .sorted((p1, p2) -> p2.getScore() - p1.getScore()) .map(Paper::getStudentName) .collect(Collectors.toList()); }
可直觀的看出,代碼量少了,只用了一行代碼把全部操做連接起來了。咱們再細看下,從這方法的名字上去理解,首先經過stream從List得到Stream,而後可使用流式操做處理數據,先是filter篩選出語文課且不及格的考卷,接着sorted對分數排序,再是map得到每一個Paper中的學生名字,最後collect把全部的名字收集成一個List。可看出,從語義上的理解也更爲直觀了,在篩選語文課不及格的試卷時,咱們不是使用命令式寫法(遍歷,而後判斷,再放到一個新的List裏),而是相似SQL中的where條件,經過聲明式寫法直接給出數據須要符合的條件,便能獲得須要的數據。數據結構
咱們說了好久的「Stream」,到底什麼是「Stream」,筆者從Stream API這個角度談本身的理解:app
Stream是Java提供的一個接口,該接口容許以聲明式的寫法處理數據,能夠把操做連接起來,造成數據處理流水線,還能將數據處理任務並行化。
聲明式和連接操做,前面的例子已經能看出。那什麼是並行化,例如統計學生名字,咱們能夠將該任務劃分紅多個,交給多個CPU分別計算,最後再彙總結果,這一系列複雜操做,可交給Stream完成,以下:dom
public static List<String> getFailedPaperStudentNamesByJava8(List<Paper> papers) { return papers.parallelStream() .filter(p -> p.getClassName().equals("語文") && p.getScore() < 60) .sorted((p1, p2) -> p2.getScore() - p1.getScore()) .map(Paper::getStudentName) .collect(Collectors.toList()); }
只是將stream()方法改成parallelStream()方法就能將該任務並行化,是否是十分簡單。ide
經過以上介紹,想必已對Stream有了初步認識,下面開始系統學習Stream。函數
首先咱們仍是要弄清楚流和集合在概念上的區別。集合,例如List、Set、Tree等,是一種存儲數據的數據結構。關於數據,是已經存在了的,咱們只是經過一種數據結構將數據組織起來,便於某種方式讀取或保持某種結構。流不一樣於集合的地方在於數據並不是在使用前所有得到,而是在使用過程當中按需得到。例如文件流,咱們能夠經過readline將文件一行一行的讀取。還有視頻流,咱們能夠邊看邊下載,不用等將全部數據下載完畢才能觀看。性能
因此雖然咱們都能從集合、流中獲取數據,但數據產生的時間是有區別的,集合的數據是預先產生的,而流則是根據須要實時產生的。二者的特性也致使用途上的差別,集合側重存儲,流側重計算。所以咱們常聽到的流式計算的叫法。學習
下面說下流和集合在使用上的區別。集合,能夠隨時取用,但流在建立後只能被使用一次,若重複消費,則會報錯。
List<String> list = Arrays.asList("A", "B", "C"); Stream<String> stream = list.stream(); stream.forEach(System.out::print); stream.forEach(System.out::print); // ABC // java.lang.IllegalStateException: stream has already been operated upon or closed
另外,集合在遍歷時,就像咱們前面描述的例子,只能經過程序員編寫 for-each 這種顯示的代碼去迭代,這被稱做外部迭代。而流在遍歷時,例如map會對流中的每一個元素進行處理,因此咱們不須要寫具體的迭代代碼,而是交由Java內部完成,這被稱做內部迭代。內部迭代的好處在於,它是一個黑盒。你須要的是迭代,那Java只要完成你的目標就好了。而至於如何迭代,則交由Java來完成,這就爲優化提供了可能,優化的方向有兩點,一是更優化的順序來處理,二是將操做並行化,例如咱們在學生的示例中,只是將stream改爲parallelStream(),後續其餘的map操做能夠根據判斷是並行流從而將任務並行化,而咱們不用修改map操做。
下面咱們來學習如何使用流。
在對流進行操做以前,咱們首先須要得到一個Stream對象,建立流有如下幾種方式。
2.1 集合
Collection的默認方法stream(),能夠由集合類建立流。
List<String> list = Arrays.asList("A", "B", "C"); Stream<String> stream = list.stream();
2.2 值
Stream的靜態方法of(T... values),經過顯示值建立流,可接受任意數量的參數。
Stream<String> stream = Stream.of("A","B","C");
2.3 數組
Arrays的靜態方法stream(T[] array)從數組建立流,接受一個數組參數。
String[] ss = new String[]{"A", "B", "C"}; Stream<String> stream = Arrays.stream(ss); 2.4 文件 NIO中有較多靜態方法建立流,例如Files的靜態方法lines(Path path)從返回指定文件中的各行構成的字符串流。 try (Stream<String> stream = Files.lines(Paths.get("data.txt"))) { stream.forEach(System.out::print); } catch (IOException e) { }
2.5 函數
Stream的靜態iterate和generate可根據函數計算建立無限流。首先看下iterate,一般用於依次生成一系列值,其聲明以下:
Stream<T> iterate(final T seed, final UnaryOperator<T> f)
seed爲初始值,UnaryOperator<T>是Function<T,R>的子類,區別在於規定輸入、輸出都是T類型,下面看個示例:
Stream.iterate(0, n -> n + 1) .forEach(System.out::println);
該示例會根據初始值,而後經過函數計算依次獲得下一個值,0、一、2......你能夠試着運行下,發現根本停不下來,這就是剛剛說到的無限流。咱們能夠經過limit(n)來對無限流作限制。
Stream.iterate(0, n -> n + 1) .limit(10) .forEach(System.out::println);
接着是generate,不一樣於iterate依次根據上次計算的結果生成, 而是經過一個Supplier<T>實例提供新的值,其聲明以下:
Stream<T> generate(Supplier<T> s)
例如,咱們生成10個隨機數:
Stream.generate(Math::random) .limit(10) .forEach(System.out::println);
2.6 數值流
你可能已經注意到上述介紹的Stream<T>使用了泛型,因此能夠適用於任意引用類型,而對於原始類型則只能使用其包裝類,例如:
Stream.of(1, 2, 3) .forEach(n -> { System.out.println(n.getClass()); // Integer int x = n * 2; // 須要拆箱 });
Stream在處理原始類型上會因爲裝箱拆箱形成較大的性能損耗,因此Java8提供了三種特殊的流接口IntStream、DoubleStream、LongStream,將流中的元素特化爲int、double和long。
IntStream.of(1, 2, 3) .forEach(n -> { int x = n * 2; // n爲int });
除了使用of建立流,還能夠將普通流轉成數值流,mapToInt、mapToDouble和mapToLong。
Stream.of(1, 2, 3) .mapToInt(Integer::intValue) .forEach(n -> { int x = n * 2; // n爲int });
數值流也能轉成普通流,boxed裝箱。
IntStream.of(1, 2, 3) .boxed() .forEach(n -> { int x = n * 2; // n爲Integer });
流建立好了,下面學習對流進行操做。流的操做分爲兩種:
public static List<String> getFailedPaperStudentNamesByJava8(List<Paper> papers) { return papers.parallelStream() .filter(p -> p.getClassName().equals("語文") // Stream<Paper> && p.getScore() < 60) .sorted((p1, p2) -> p2.getScore() - p1.getScore()) // Stream<Paper> .map(Paper::getStudentName) // Stream<String> .collect(Collectors.toList()); // List<String> }
這裏的filter、sorted、map就是中間操做,collect爲終端操做。終端操做用於執行流水線是什麼意思?意思是若是沒有終端操做,將不會執行前面連接的中間操做。例如:
List<String> list = Arrays.asList("A", "B", "C"); list.stream() .map(s -> { System.out.print(s); return s; }); // 無輸出
無終端操做的狀況下,中間操做map裏的代碼塊將不會執行,不會有輸出。而咱們在此基礎上加上一個終端操做forEach,便能觸發流水線的執行。
List<String> list = Arrays.asList("A", "B", "C"); list.stream() .map(s -> { System.out.print(s); return s; }) .forEach(s -> {}); // ABC
下面來看下經常使用的流操做。
3.1 篩選
(1)filter
Stream<T> filter(Predicate<? super T> predicate)
過濾
List<String> list = Arrays.asList("AA", "AB", "BC"); list.stream() .filter(s -> s.startsWith("A")) .forEach(System.out::println); // AA // AB
(2)distinct
Stream<T> distinct()
去重
3.2 切片
(1)limit
Stream<T> limit(long maxSize);
截斷
List<String> list = Arrays.asList("A", "B", "C"); list.stream() .limit(2) .forEach(System.out::print); // AB
(2)skip
Stream<T> skip(long n)
跳過元素
List<String> list = Arrays.asList("A", "B", "C"); list.stream() .skip(2) .forEach(System.out::print); // C
3.3 映射
(1)map
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
對每一個元素應用函數
List<Paper> papers = Arrays.asList( new Paper("小明", "語文", 40), new Paper("小紅", "語文", 80), new Paper("小藍", "語文", 50) ); papers.stream() .map(Paper::getStudentName) .forEach(System.out::println); // 小明 // 小紅 // 小藍
(2)flatMap
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
流的扁平化,什麼意思,先看下面這個例子:
List<String> list = Arrays.asList("ABC", "DEF", "GHI"); list.stream() .map(s -> s.split("")) // Stream<String[]> .forEach(System.out::println); // [Ljava.lang.String;@2f4d3709 // [Ljava.lang.String;@4e50df2e // [Ljava.lang.String;@1d81eb93
咱們想將字符串「ABC」,「DEF」,「GHI」三個字符中的每一個字符組合成一個流而後打印出來,可是上述的寫法,經過split函數拆分了String[]數組,流中的元素也被映射成了數組,例如String[]{"A", "B", "C"},因此forEach打印獲得的結果是數組地址。
咱們如何才能把數組中的元素組合在一塊兒,獲得"A", "B", "C", "D"...的一個流呢。這就須要扁平化的處理。flatMap接受一個函數(T -> Stream),把流中每一個元素映射爲一個流,而後再把全部的流組合成一個最終的流。例如這裏的元素是String[],那咱們就把數組映射成流Arrays::stream,這樣就能把每一個數組裏的元素鏈接在一塊兒了。
List<String> list = Arrays.asList("ABC", "DEF", "GHI"); list.stream() .map(s -> s.split("")) // Stream<String[]> .flatMap(Arrays::stream) // Steam<String> .forEach(System.out::println);
3.4 匹配
(1)anyMatch
boolean anyMatch(Predicate<? super T> predicate)
至少匹配一個元素
List<String> list = Arrays.asList("A", "B", "C"); list.stream() .map(s -> { System.out.print(s); return s; }) .anyMatch(s -> s.startsWith("B")); // true // AB
當遇到B時,匹配到了,便會直接返回,不會再迭代後續元素,這是一種短路操做。
(2)allMatch
boolean allMatch<Predicate<? super T> predicate>
匹配全部元素
List<String> list = Arrays.asList("A", "B", "C"); list.stream() .map(s -> { System.out.println(s); return s; }) .allMatch(s -> s.startsWith("B")); // false // A
(3)nonMatch
boolean nonMatch(Predicate<? super T> predicate)
全部元素不匹配
List<String> list = Arrays.asList("A", "B", "C"); list.stream() .map(s -> { System.out.println(s); return s; }) .noneMatch(s -> s.startsWith("B")); // false // A
3.5 查找
(1)findAny
Optional<T> findAny()
返回當前流中的任意元素,用Optional封裝元素,迫使顯示檢查元素是否存在。
(2)findFirst
Optional<T> findFirst()
返回當前流中的第一個元素
對比:
二者都是返回一個元素,若是不關心返回的元素是哪一個,優先使用findAny,由於這樣在並行上的限制更少,可優化的空間更大。
3.6 歸約
reduce
// 有初始值 T reduce(T identity, BinaryOperator<T> accumulator) // 無初始值 Optional<T> reduce(BinaryOperator<T> accumulator)
經過接收一個BinaryOperator(T, T) -> T,將兩個元素結合產生一個新值。reduce將一直執行該操做直到最後流中只剩一個元素返回。
int[] nums = new int[]{1, 2, 3, 4, 5}; int sum = IntStream.of(nums).reduce(0, Integer::sum); // 15 int max = IntStream.of(nums).reduce(Integer::max).orElse(-1); // 5 int min = IntStream.of(nums).reduce(Integer::min).orElse(-1); // 1
3.7 數值流的特殊操做
在2.6節中咱們說過針對原始類型有特殊的原始類型流,因爲都是數值,因此也設計了些針對數值的方法。
int[] nums = new int[]{1, 2, 3, 4, 5}; int sum = IntStream.of(nums).sum(); // 15,等同reduce(0, Integer::sum) int max = IntStream.of(nums).max().orElse(-1); // 5,等同reduce(Integer::max).orElse(-1) int min = IntStream.of(nums).min().orElse(-1); // 1,等同reduce(Integer::min).orElse(-1) double avg = IntStream.of(nums).average().orElse(-1); //3.0
3.8 收集
collect在前面的示例中已經見過了,能夠將流中的元素進行彙總。
<R, A> R collect(Collector<? super T, A, R> collector);
接收一個Collector收集器。在Collectors中已經內置了一些經常使用的收集器。
下面看示例:
List<Paper> papers = Arrays.asList( new Paper("小明", "語文", 40), new Paper("小明", "數學", 80), new Paper("小紅", "語文", 80), new Paper("小紅", "數學", 80), new Paper("小藍", "語文", 50), new Paper("小藍", "數學", 60) ); // 全部語文卷子 List<Paper> chinesePapers = papers.stream() .filter(p -> p.getClassName().equals("語文")) .collect(toList()); // 全部學科 Set<String> classNames = papers.stream() .map(Paper::getClassName) .collect(toSet()); // 最高分的卷子,最低分改爲minBy就行 Paper maxScorePaper = papers.stream() .collect(maxBy((p1, p2) -> p1.getScore() - p2.getScore())).get(); // 總分數 int sumScore = papers.stream() .collect(summingInt(Paper::getScore)); // 平均分 double avgScore = papers.stream() .collect(averagingInt(Paper::getScore)); // 統計數、總和、最大值、最小值和平均值 IntSummaryStatistics summaryStatistics = papers.stream() .collect(summarizingInt(Paper::getScore)); long count = summaryStatistics.getCount(); long sum = summaryStatistics.getSum(); int max = summaryStatistics.getMax(); int min = summaryStatistics.getMin(); double avg = summaryStatistics.getAverage(); // 學生名字鏈接在一塊兒 String studentNameStr = papers.stream() .map(Paper::getStudentName) .distinct() .collect(joining(",")); // 按學科將卷子分組 Map<String, List<Paper>> groupPapers = papers.stream() .collect(groupingBy(Paper::getClassName));
以上介紹的是經常使用的Collector,咱們還能夠根據須要自定義Collector,本文就不敘述了。
本文列舉了在平常開發中較爲經常使用的流操做,可是還有未涉及之處,感興趣的讀者能夠直接看Stream的API。本文也沒有講述並行流,雖然用法簡單,可是可否真正提升效率,仍是要看具體狀況,還缺少經驗就不敘述了。先掌握基本的流式操做吧。