《Java8实战》笔记(04):引入流
幾乎每個Java應用程序都會制造和處理集合。
盡管集合對于幾乎任何一個Java應用都是不可或缺的,但集合操作卻遠遠算不上完美。
-
很多業務邏輯都涉及類似于數據庫的操作,比如對幾道菜按照類別進行分組 (比如全素菜肴),或查找出最貴的菜。你自己用迭代器重新實現過這些操作多少遍?大部分數據庫都允許你聲明式地指定這些操作。比如,以下SQL查詢語句就可以選出熱量較低的菜肴名稱:SELECT name FROM dishes WHERE calorie < 400。你看,你不需要實現如何根據菜肴的屬性進行篩選(比如利用迭代器和累加器),你只需要表達你想要什么。這個基本的思路意味著,你用不著擔心怎么去顯式地實現這些查詢語句——都替你辦好了!怎么到了集合這里就不能這樣了呢?
-
要是要處理大量元素又該怎么辦呢?為了提高性能,你需要并行處理,并利用多核架構。但寫并行代碼比用迭代器還要復雜,而且調試起來也夠受的!
流是什么
流是Java API的新成員,它允許你以聲明性方式處理數據集合(通過查詢語句來表達,而不是臨時編寫一個實現)。就現在來說,你可以把它們看成遍歷數據集的高級迭代器。
此外,流還可以透明地并行處理,你無需寫任何多線程代碼了!
StreamBasic
Java8之前:
List<Dish> lowCaloricDishes = new ArrayList<>(); for(Dish d: menu){if(d.getCalories() < 400){lowCaloricDishes.add(d);} }Collections.sort(lowCaloricDishes, new Comparator<Dish>() {public int compare(Dish d1, Dish d2){return Integer.compare(d1.getCalories(), d2.getCalories());} });List<String> lowCaloricDishesName = new ArrayList<>();for(Dish d: lowCaloricDishes){lowCaloricDishesName.add(d.getName()); }Java8之后:
import static java.util.Comparator.comparing; import static java.util.stream.Collectors.toList;List<String> lowCaloricDishesName = menu.stream().filter(d -> d.getCalories() < 400).sorted(comparing(Dish::getCalories)).map(Dish::getName).collect(toList());為了利用多核架構并行執行這段代碼,你只需要把stream()換成parallelStream():
List<String> lowCaloricDishesName = menu.parallelStream().filter(d -> d.getCalories() < 400).sorted(comparing(Dishes::getCalories)).map(Dish::getName).collect(toList());新的方法有幾個顯而易見的好處。
-
代碼是以聲明性方式寫的:說明想要完成什么(篩選熱量低的菜肴)而不是說明如何實現一個操作(利用循環和if條件等控制流語句)。這種方法加上行為參數化讓你可以輕松應對變化的需求:你很容易再創建一個代碼版本,利用Lambda表達式來篩選高卡路里的菜肴,而用不著去復制粘貼代碼。
-
你可以把幾個基礎操作鏈接起來,來表達復雜的數據處理流水線(在filter后面接上sorted、map和collect操作,如下圖),同時保持代碼清晰可讀。filter的結果被傳給了sorted方法,再傳給map方法,最后傳給collect方法。
因為filter、sorted、map和collect等操作是與具體線程模型無關的高層次構件,所以它們的內部實現可以是單線程的,也可能透明地充分利用你的多核架構!在實踐中,這意味著你用不著為了讓某些數據處理任務并行而去操心線程和鎖了,Stream API都替你做好了!
Map<Dish.Type, List<Dish>> dishesByType =menu.stream().collect(groupingBy(Dish::getType));
按照Map里面的類別對菜肴進行分組
{FISH=[prawns, salmon], OTHER=[french fries, rice, season fruit, pizza], MEAT=[pork, beef, chicken]}其他庫:Guava、Apache和lambdaj
為了給Java程序員提供更好的庫操作集合,前人已經做過了很多嘗試。
總結一下,Java 8中的Stream API可以讓你寫出這樣的代碼:
- 聲明性——更簡潔,更易讀
- 可復合——更靈活
- 可并行——性能更好
數據準備Dish
流簡介
流定義——從支持數據處理操作的源生成的元素序列
-
元素序列——就像集合一樣,流也提供了一個接口,可以訪問特定元素類型的一組有序值。因為集合是數據結構,所以它的主要目的是以特定的時間/空間復雜度存儲和訪問元素(如ArrayList 與 LinkedList)。但流的目的在于表達計算,比如你前面見到的filter、 sorted和map。集合講的是數據,流講的是計算。
-
源——流會使用一個提供數據的源,如集合、數組或輸入/輸出資源。 請注意,從有序集合生成流時會保留原有的順序。由列表生成的流,其元素順序與列表一致。
-
數據處理操作——流的數據處理功能支持類似于數據庫的操作,以及函數式編程語言中的常用操作,如filter、 map、 reduce、 find、 match、 sort等。流操作可以順序執行,也可并行執行。
此外,流操作有兩個重要的特點。
- 流水線——很多流操作本身會返回一個流,這樣多個操作就可以鏈接起來,形成一個大的流水線。這讓我們下一章中的一些優化成為可能,如延遲和短路。流水線的操作可以看作對數據源進行數據庫式查詢。
- 內部迭代——與使用迭代器顯式迭代的集合不同,流的迭代操作是在背后進行的。
import static java.util.stream.Collectors.toList;List<String> threeHighCaloricDishNames = menu.stream().filter(d -> d.getCalories() > 300).map(Dish::getName).limit(3).collect(toList());System.out.println(threeHighCaloricDishNames);
流程圖
流與集合
只能遍歷一次
請注意,和迭代器類似,流只能遍歷一次。遍歷完之后,我們就說這個流已經被消費掉了。你可以從原始數據源那里再獲得一個新的流來重新遍歷一遍
List<String> title = Arrays.asList("Java8", "In", "Action"); Stream<String> s = title.stream(); s.forEach(System.out::println); s.forEach(System.out::println);//java.lang.IllegalStateException:流已被操作或關閉外部迭代與內部迭代
外部迭代
List<String> names = new ArrayList<>();for(Dish d: menu){names.add(d.getName()); }//使用迭代器模式 List<String> names = new ArrayList<>(); Iterator<String> iterator = menu.iterator();while(iterator.hasNext()) {Dish d = iterator.next();names.add(d.getName()); }內部迭代
List<String> names = menu.stream().map(Dish::getName).collect(toList());內部迭代時,項目可以透明地并行處理,或者用更優化的順序進行處理。
要是用Java過去的那種外部迭代方法,這些優化都是很困難的。這似乎有點兒雞蛋里挑骨頭,但這差不多就是Java 8引入流的理由了——Streams庫的內部迭代可以自動選擇一種適合你硬件的數據表示和并行實現。
外部迭代一個集合,顯式地取出每個項目再加以處理。
內部迭代時,項目可以透明地并行處理,或者用更優化的順
序進行處理。
流利用了內部迭代:替你把迭代做了。但是,只有你已經預先定義好了能夠隱藏迭代的操作列表,例如filter或map,這個才有用。
流操作
List<String> names = menu.stream().filter(d -> d.getCalories() > 300).map(Dish::getName).limit(3).collect(Collectors.toList())- 中間操作 filter、 map和limit可以連成一條流水線;
- 終端操作 collect觸發流水線執行并關閉它。
中間操作
為了搞清楚流水線中到底發生了什么
List<String> names = menu.stream().filter(d -> {System.out.println("filtering" + d.getName());return d.getCalories() > 300;}).map(d -> {System.out.println("mapping" + d.getName());return d.getName();}).limit(3).collect(toList()); System.out.println(names);此代碼執行時將打印:
filtering pork mapping pork filtering beef mapping beef filtering chicken mapping chicken [pork, beef, chicken]會發現,有好幾種優化利用了流的延遲性質。
盡管很多菜的熱量都高于300卡路里,但只選出了前三個!這是因為limit操作和一種稱為短路的技巧。
盡管filter和map是兩個獨立的操作,但它們合并到同一次遍歷中了。
終端操作
終端操作會從流的流水線生成結果。其結果是任何不是流的值,比如List、Integer,甚至void。
menu.stream().forEach(System.out::println);使用流
總而言之,流的使用一般包括三件事:
- 一個數據源(如集合)來執行一個查詢;
- 一個中間操作鏈,形成一條流的流水線;
- 一個終端操作,執行流水線,并能生成結果。
流的流水線背后的理念類似于構建器模式Builder。
中間操作
| map | Stream<R> | Function<T, R> | T -> R |
| filter | Stream<T> | Predicate<T> | T -> boolean |
| limit | Stream<T> | - | - |
| sorted | Stream<T> | Comparator<T> | (T, T) -> int |
| distinct | Stream<T> | - | - |
終端操作
| forEach | 消費流中的每個元素并對其應用 Lambda。這一操作返回 void |
| count | 返回流中元素的個數。這一操作返回 long |
| collect | 把流歸約成一個集合,比如 List、 Map 甚至是 Integer |
小結
- 流是“從支持數據處理操作的源生成的一系列元素”。
- 流利用內部迭代:迭代通過filter、map、sorted等操作被抽象掉了。
- 流操作有兩類:中間操作和終端操作。
- filter和map等中間操作會返回一個流,并可以鏈接在一起。可以用它們來設置一條流
水線,但并不會生成任何結果。 - forEach和count等終端操作會返回一個非流的值,并處理流水線以返回結果。
- 流中的元素是按需計算的。
總結
以上是生活随笔為你收集整理的《Java8实战》笔记(04):引入流的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《Python Cookbook 3rd
- 下一篇: 《Java8实战》笔记(15):面向对象