Effective Java~45. 谨慎使用Stream
在 Java 8 中添加了 Stream API,以簡化順序或并行執行批量操作的任務。 該 API 提供了兩個關鍵的抽象:流(Stream),表示有限或無限的數據元素序列,以及流管道 (stream pipeline),表示對這些元素的多級計算。 Stream 中的元素可以來自任何地方。 常見的源包括集合,數組,文件,正則表達式模式匹配器,偽隨機數生成器和其他流。流中的數據元素可以是對象引用或基本類型。 支持三種基本類型:int,long 和 double。
流管道由源流(source stream)的零或多個中間操作和一個終結操作組成。每個中間操作都以某種方式轉換流,例如將每個元素映射到該元素的函數或過濾掉所有不滿足某些條件的元素。中間操作都將一個流轉換為另一個流,其元素類型可能與輸入流相同或不同。終結操作對流執行最后一次中間操作產生的最終計算,例如將其元素存儲到集合中、返回某個元素或打印其所有元素。
管道延遲(lazily)計算求值:計算直到終結操作被調用后才開始,而為了完成終結操作而不需要的數據元素永遠不會被計算出來。 這種延遲計算求值的方式使得可以使用無限流。 請注意,沒有終結操作的流管道是靜默無操作的,所以不要忘記包含一個。
????????Stream API 流式的(fluent)::它設計允許所有組成管道的調用被鏈接到一個表達式中。事實上,多個管道可以鏈接在一起形成一個表達式。
????????默認情況下,流管道按順序 (sequentially) 運行。 使管道并行執行就像在管道中的任何流上調用并行方法一樣簡單,但很少這樣做(第 48 個條目)。
????????Stream API 具有足夠的通用性,實際上任何計算都可以使用 Stream 執行,但僅僅因為可以,并不意味著應該這樣做。如果使用得當,流可以使程序更短更清晰;如果使用不當,它們會使程序難以閱讀和維護。對于何時使用流沒有硬性的規則,但是有一些啟發。
????????考慮以下程序,該程序從字典文件中讀取單詞并打印其大小符合用戶指定的最小值的所有變位詞(anagram)組。如果兩個單詞由長度相通,不同順序的相同字母組成,則它們是變位詞。程序從用戶指定的字典文件中讀取每個單詞并將單詞放入 map 對象中。map 對象的鍵是按照字母排序的單詞,因此『staple』的鍵是『aelpst』,『petals』的鍵也是『aelpst』:這兩個單詞就是同位詞,所有的同位詞共享相同的依字母順序排列的形式(或稱之為alphagram)。map 對象的值是包含共享字母順序形式的所有單詞的列表。 處理完字典文件后,每個列表都是一個完整的同位詞組。然后程序遍歷 map 對象的 values() 的視圖并打印每個大小符合閾值的列表:
// Prints all large anagram groups in a dictionary iteratively public class Anagrams {public static void main(String[] args) throws IOException {File dictionary = new File(args[0]);int minGroupSize = Integer.parseInt(args[1]);Map<String, Set<String>> groups = new HashMap<>();try (Scanner s = new Scanner(dictionary)) {while (s.hasNext()) {String word = s.next();groups.computeIfAbsent(alphabetize(word), (unused) -> new TreeSet<>()).add(word);}}for (Set<String> group : groups.values())if (group.size() >= minGroupSize)System.out.println(group.size() + ": " + group);}private static String alphabetize(String s) {char[] a = s.toCharArray();Arrays.sort(a);return new String(a);} }????????現在考慮以下程序,它解決了同樣的問題,但大量過度使用了流。 請注意,整個程序(打開字典文件的代碼除外)包含在單個表達式中。 在單獨的表達式中打開字典文件的唯一原因是允許使用 try-with-resources 語句,該語句確保關閉字典文件:
// Overuse of streams - don't do this! public class Anagrams {public static void main(String[] args) throws IOException {Path dictionary = Paths.get(args[0]);int minGroupSize = Integer.parseInt(args[1]);try (Stream<String> words = Files.lines(dictionary)) {words.collect(groupingBy(word -> word.chars().sorted().collect(StringBuilder::new,(sb, c) -> sb.append((char) c),StringBuilder::append).toString())).values().stream().filter(group -> group.size() >= minGroupSize).map(group -> group.size() + ": " + group).forEach(System.out::println);}} }????????如果你發現這段代碼難以閱讀,不要擔心;你不是一個人。它更短,但是可讀性也更差,尤其是對于那些不擅長使用流的程序員來說。過度使用流使程序難于閱讀和維護。
????????幸運的是,有一個折中的辦法。下面的程序解決了同樣的問題,使用流而不過度使用它們。其結果是一個比原來更短更清晰的程序:
// Tasteful use of streams enhances clarity and conciseness public class Anagrams {public static void main(String[] args) throws IOException {Path dictionary = Paths.get(args[0]);int minGroupSize = Integer.parseInt(args[1]);try (Stream<String> words = Files.lines(dictionary)) {words.collect(groupingBy(word -> alphabetize(word))).values().stream().filter(group -> group.size() >= minGroupSize).forEach(g -> System.out.println(g.size() + ": " + g));}}// alphabetize method is the same as in original version }????????請注意,仔細選擇 lambda 參數名稱。 上面程序中參數 g 應該真正命名為 group,但是生成的代碼行對于本書來說太寬了。 在沒有顯式類型的情況下,仔細命名 lambda 參數對于流管道的可讀性至關重要。
另請注意,單詞字母化是在單獨的 alphabetize 方法中完成的。 這通過提供操作名稱并將實現細節保留在主程序之外來增強可讀性。 使用輔助方法對于流管道中的可讀性比在迭代代碼中更為重要,因為管道缺少顯式類型信息和命名臨時變量。
流可以很容易地做一些事情:
· 統一轉換元素序列
· 過濾元素序列
· 使用單個操作組合元素序列 (例如添加、連接或計算最小值)
· 將元素序列累積到一個集合中,可能通過一些公共屬性將它們分組
· 在元素序列中搜索滿足某些條件的元素
????????總之,有些任務最好使用流來完成,有些任務最好使用迭代來完成。將這兩種方法結合起來,可以最好地完成許多任務。對于選擇使用哪種方法進行任務,沒有硬性規定,但是有一些有用的啟發式方法。在許多情況下,使用哪種方法將是清楚的;在某些情況下,則不會很清楚。如果不確定一個任務是通過流還是迭代更好地完成,那么嘗試這兩種方法,看看哪一種效果更好。
總結
以上是生活随笔為你收集整理的Effective Java~45. 谨慎使用Stream的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring Data JPA 从入门到
- 下一篇: 掌控谈话~谈价格的秘诀