超強的Lambda Stream流操作


原文:https://www.cnblogs.com/niumoo/p/11880172.html

 

在使用 Stream 流操作之前你應該先了解 Lambda 相關知識,如果還不了解,可以參考之前文章:還看不懂同事的代碼?Lambda 表達式、函數接口了解一下 。

1. Stream 流介紹

Stream 不同於其他集合框架,它也不是某種數據結構,也不會保存數據,但是它負責相關計算,使用起來更像一個高級的迭代器。在之前的迭代器中,我們只能先遍歷然后在執行業務操作,而現在只需要指定執行什么操作, Stream 就會隱式的遍歷然后做出想要的操作。另外 Stream 和迭代器一樣的只能單向處理,如同奔騰長江之水一去而不復返。

由於 Stream 流提供了惰性計算並行處理的能力,在使用並行計算方式時數據會被自動分解成多段然后並行處理,最后將結果匯總。所以 Stream 操作可以讓程序運行變得更加高效。

2. Stream 流概念

Stream 流的使用總是按照一定的步驟進行,可以抽象出下面的使用流程。

數據源(source) -> 數據處理/轉換(intermedia) -> 結果處理(terminal )

2.1. 數據源

數據源(source)也就是數據的來源,可以通過多種方式獲得 Stream 數據源,下面列舉幾種常見的獲取方式。

  • Collection.stream(); 從集合獲取流。
  • Collection.parallelStream(); 從集合獲取並行流。
  • Arrays.stream(T array) or Stream.of(); 從數組獲取流。
  • BufferedReader.lines(); 從輸入流中獲取流。
  • IntStream.of() ; 從靜態方法中獲取流。
  • Stream.generate(); 自己生成流

2.2. 數據處理

數據處理/轉換(intermedia)步驟可以有多個操作,這步也被稱為intermedia(中間操作)。在這個步驟中不管怎樣操作,它返回的都是一個新的流對象,原始數據不會發生任何改變,而且這個步驟是惰性計算處理的,也就是說只調用方法並不會開始處理,只有在真正的開始收集結果時,中間操作才會生效,而且如果遍歷沒有完成,想要的結果已經獲取到了(比如獲取第一個值),會停止遍歷,然后返回結果。惰性計算可以顯著提高運行效率。

數據處理演示。

@Test public void streamDemo(){ List<String> nameList = Arrays.asList("Darcy", "Chris", "Linda", "Sid", "Kim", "Jack", "Poul", "Peter"); // 1. 篩選出名字長度為4的 // 2. 名字前面拼接 This is // 3. 遍歷輸出 nameList.stream() .filter(name -> name.length() == 4) .map(name -> "This is "+name) .forEach(name -> System.out.println(name)); } // 輸出結果 // This is Jack // This is Poul

數據處理/轉換操作自然不止是上面演示的過濾 filter 和 map映射兩種,另外還有 map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered 等。

2.3. 收集結果

結果處理(terminal )是流處理的最后一步,執行完這一步之后流會被徹底用盡,流也不能繼續操作了。也只有到了這個操作的時候,流的數據處理/轉換等中間過程才會開始計算,也就是上面所說的惰性計算結果處理也必定是流操作的最后一步。

常見的結果處理操作有 forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator 等。

下面演示了簡單的結果處理的例子。

/** * 轉換成為大寫然后收集結果,遍歷輸出 */ @Test public void toUpperCaseDemo() { List<String> nameList = Arrays.asList("Darcy", "Chris", "Linda", "Sid", "Kim", "Jack", "Poul", "Peter"); List<String> upperCaseNameList = nameList.stream() .map(String::toUpperCase) .collect(Collectors.toList()); upperCaseNameList.forEach(name -> System.out.println(name + ",")); } // 輸出結果 // DARCY,CHRIS,LINDA,SID,KIM,JACK,POUL,PETER,

2.4. short-circuiting

有一種 Stream 操作被稱作 short-circuiting ,它是指當 Stream 流無限大但是需要返回的 Stream 流是有限的時候,而又希望它能在有限的時間內計算出結果,那么這個操作就被稱為short-circuiting。例如 findFirst 操作。

3. Stream 流使用

Stream 流在使用時候總是借助於 Lambda 表達式進行操作,Stream 流的操作也有很多種方式,下面列舉的是常用的 11 種操作。

3.1. Stream 流獲取

獲取 Stream 的幾種方式在上面的 Stream 數據源里已經介紹過了,下面是針對上面介紹的幾種獲取 Stream 流的使用示例。

@Test public void createStream() throws FileNotFoundException { List<String> nameList = Arrays.asList("Darcy", "Chris", "Linda", "Sid", "Kim", "Jack", "Poul", "Peter"); String[] nameArr = {"Darcy", "Chris", "Linda", "Sid", "Kim", "Jack", "Poul", "Peter"}; // 集合獲取 Stream 流 Stream<String> nameListStream = nameList.stream(); // 集合獲取並行 Stream 流 Stream<String> nameListStream2 = nameList.parallelStream(); // 數組獲取 Stream 流 Stream<String> nameArrStream = Stream.of(nameArr); // 數組獲取 Stream 流 Stream<String> nameArrStream1 = Arrays.stream(nameArr); // 文件流獲取 Stream 流 BufferedReader bufferedReader = new BufferedReader(new FileReader("README.md")); Stream<String> linesStream = bufferedReader.lines(); // 從靜態方法獲取流操作 IntStream rangeStream = IntStream.range(1, 10); rangeStream.limit(10).forEach(num -> System.out.print(num+",")); System.out.println(); IntStream intStream = IntStream.of(1, 2, 3, 3, 4); intStream.forEach(num -> System.out.print(num+",")); }

3.2. forEach

forEach 是 Strean 流中的一個重要方法,用於遍歷 Stream 流,它支持傳入一個標准的 Lambda 表達式。但是它的遍歷不能通過 return/break 進行終止。同時它也是一個 terminal 操作,執行之后 Stream 流中的數據會被消費掉。

如輸出對象。

List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); numberList.stream().forEach(number -> System.out.println(number+",")); // 輸出結果 // 1,2,3,4,5,6,7,8,9,

3.3. map / flatMap

使用 map 把對象一對一映射成另一種對象或者形式。

/** * 把數字值乘以2 */ @Test public void mapTest() { List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); // 映射成 2倍數字 List<Integer> collect = numberList.stream() .map(number -> number * 2) .collect(Collectors.toList()); collect.forEach(number -> System.out.print(number + ",")); System.out.println(); numberList.stream() .map(number -> "數字 " + number + ",") .forEach(number -> System.out.println(number)); } // 輸出結果 // 2,4,6,8,10,12,14,16,18, // 數字 1,數字 2,數字 3,數字 4,數字 5,數字 6,數字 7,數字 8,數字 9,

上面的 map 可以把數據進行一對一的映射,而有些時候關系可能不止 1對 1那么簡單,可能會有1對多。這時可以使用 flatMap。下面演示使用 flatMap把對象扁平化展開。

/** * flatmap把對象扁平化 */ @Test public void flatMapTest() { Stream<List<Integer>> inputStream = Stream.of( Arrays.asList(1), Arrays.asList(2, 3), Arrays.asList(4, 5, 6) ); List<Integer> collect = inputStream .flatMap((childList) -> childList.stream()) .collect(Collectors.toList()); collect.forEach(number -> System.out.print(number + ",")); } // 輸出結果 // 1,2,3,4,5,6,

3.4. filter

使用 filter 進行數據篩選,挑選出想要的元素,下面的例子演示怎么挑選出偶數數字。

/** * filter 數據篩選 * 篩選出偶數數字 */ @Test public void filterTest() { List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); List<Integer> collect = numberList.stream() .filter(number -> number % 2 == 0) .collect(Collectors.toList()); collect.forEach(number -> System.out.print(number + ",")); }

得到如下結果。

2,4,6,8,

3.5. findFirst

findFirst 可以查找出 Stream 流中的第一個元素,它返回的是一個 Optional 類型,如果還不知道 Optional 類的用處,可以參考之前文章 Jdk14都要出了,還不能使用 Optional優雅的處理空指針? 。

/** * 查找第一個數據 * 返回的是一個 Optional 對象 */ @Test public void findFirstTest(){ List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); Optional<Integer> firstNumber = numberList.stream() .findFirst(); System.out.println(firstNumber.orElse(-1)); } // 輸出結果 // 1

findFirst 方法在查找到需要的數據之后就會返回不再遍歷數據了,也因此 findFirst 方法可以對有無限數據的 Stream 流進行操作,也可以說 findFirst 是一個 short-circuiting 操作。

3.6. collect / toArray

Stream 流可以輕松的轉換為其他結構,下面是幾種常見的示例。

 /** * Stream 轉換為其他數據結構 */ @Test public void collectTest() { List<Integer> numberList = Arrays.asList(1, 1, 2, 2, 3, 3, 4, 4, 5); // to array Integer[] toArray = numberList.stream() .toArray(Integer[]::new); // to List List<Integer> integerList = numberList.stream() .collect(Collectors.toList()); // to set Set<Integer> integerSet = numberList.stream() .collect(Collectors.toSet()); System.out.println(integerSet); // to string String toString = numberList.stream() .map(number -> String.valueOf(number)) .collect(Collectors.joining()).toString(); System.out.println(toString); // to string split by , String toStringbJoin = numberList.stream() .map(number -> String.valueOf(number)) .collect(Collectors.joining(",")).toString(); System.out.println(toStringbJoin); } // 輸出結果 // [1, 2, 3, 4, 5] // 112233445 // 1,1,2,2,3,3,4,4,5

3.7. limit / skip

獲取或者扔掉前 n 個元素

/** * 獲取 / 扔掉前 n 個元素 */ @Test public void limitOrSkipTest() { // 生成自己的隨機數流 List<Integer> ageList = Arrays.asList(11, 22, 13, 14, 25, 26); ageList.stream() .limit(3) .forEach(age -> System.out.print(age+",")); System.out.println(); ageList.stream() .skip(3) .forEach(age -> System.out.print(age+",")); } // 輸出結果 // 11,22,13, // 14,25,26,

3.8. Statistics

數學統計功能,求一組數組的最大值、最小值、個數、數據和、平均數等。

/** * 數學計算測試 */ @Test public void mathTest() { List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6); IntSummaryStatistics stats = list.stream().mapToInt(x -> x).summaryStatistics(); System.out.println("最小值:" + stats.getMin()); System.out.println("最大值:" + stats.getMax()); System.out.println("個數:" + stats.getCount()); System.out.println("和:" + stats.getSum()); System.out.println("平均數:" + stats.getAverage()); } // 輸出結果 // 最小值:1 // 最大值:6 // 個數:6 // 和:21 // 平均數:3.5

3.9. groupingBy

分組聚合功能,和數據庫的 Group by 的功能一致。

/** * groupingBy * 按年齡分組 */ @Test public void groupByTest() { List<Integer> ageList = Arrays.asList(11, 22, 13, 14, 25, 26); Map<String, List<Integer>> ageGrouyByMap = ageList.stream() .collect(Collectors.groupingBy(age -> String.valueOf(age / 10))); ageGrouyByMap.forEach((k, v) -> { System.out.println("年齡" + k + "0多歲的有:" + v); }); } // 輸出結果 // 年齡10多歲的有:[11, 13, 14] // 年齡20多歲的有:[22, 25, 26]

3.10. partitioningBy

/** * partitioningBy * 按某個條件分組 * 給一組年齡,分出成年人和未成年人 */ public void partitioningByTest() { List<Integer> ageList = Arrays.asList(11, 22, 13, 14, 25, 26); Map<Boolean, List<Integer>> ageMap = ageList.stream() .collect(Collectors.partitioningBy(age -> age > 18)); System.out.println("未成年人:" + ageMap.get(false)); System.out.println("成年人:" + ageMap.get(true)); } // 輸出結果 // 未成年人:[11, 13, 14] // 成年人:[22, 25, 26]

3.11. 進階 - 自己生成 Stream 流

/** * 生成自己的 Stream 流 */ @Test public void generateTest(){ // 生成自己的隨機數流 Random random = new Random(); Stream<Integer> generateRandom = Stream.generate(random::nextInt); generateRandom.limit(5).forEach(System.out::println); // 生成自己的 UUID 流 Stream<UUID> generate = Stream.generate(UUID::randomUUID); generate.limit(5).forEach(System.out::println); } // 輸出結果 // 793776932 // -2051545609 // -917435897 // 298077102 // -1626306315 // 31277974-841a-4ad0-a809-80ae105228bd // f14918aa-2f94-4774-afcf-fba08250674c // d86ccefe-1cd2-4eb4-bb0c-74858f2a7864 // 4905724b-1df5-48f4-9948-fa9c64c7e1c9 // 3af2a07f-0855-455f-a339-6e890e533ab3

上面的例子中 Stream 流是無限的,但是獲取到的結果是有限的,使用了 Limit 限制獲取的數量,所以這個操作也是 short-circuiting 操作。

4. Stream 流優點

4.1. 簡潔優雅

正確使用並且正確格式化的 Stream 流操作代碼不僅簡潔優雅,更讓人賞心悅目。下面對比下在使用 Stream 流和不使用 Stream 流時相同操作的編碼風格。

/** * 使用流操作和不使用流操作的編碼風格對比 */ @Test public void diffTest() { // 不使用流操作 List<String> names = Arrays.asList("Jack", "Jill", "Nate", "Kara", "Kim", "Jullie", "Paul", "Peter"); // 篩選出長度為4的名字 List<String> subList = new ArrayList<>(); for (String name : names) { if (name.length() == 4) { subList.add(name); } } // 把值用逗號分隔 StringBuilder sbNames = new StringBuilder(); for (int i = 0; i < subList.size() - 1; i++) { sbNames.append(subList.get(i)); sbNames.append(", "); } // 去掉最后一個逗號 if (subList.size() > 1) { sbNames.append(subList.get(subList.size() - 1)); } System.out.println(sbNames); } // 輸出結果 // Jack, Jill, Nate, Kara, Paul

如果是使用 Stream 流操作。

// 使用 Stream 流操作 String nameString = names.stream() .filter(num -> num.length() == 4) .collect(Collectors.joining(", ")); System.out.println(nameString);

4.2. 惰性計算

上面有提到,數據處理/轉換(intermedia) 操作 map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered 等這些操作,在調用方法時並不會立即調用,而是在真正使用的時候才會生效,這樣可以讓操作延遲到真正需要使用的時刻。

下面會舉個例子演示這一點。

 /** * 找出偶數 */ @Test public void lazyTest() { // 生成自己的隨機數流 List<Integer> numberLIst = Arrays.asList(1, 2, 3, 4, 5, 6); // 找出偶數 Stream<Integer> integerStream = numberLIst.stream() .filter(number -> { int temp = number % 2; if (temp == 0 ){ System.out.println(number); } return temp == 0; }); System.out.println("分割線"); List<Integer> collect = integerStream.collect(Collectors.toList()); }

如果沒有 惰性計算,那么很明顯會先輸出偶數,然后輸出 分割線。而實際的效果是。

分割線
2
4
6

可見 惰性計算 把計算延遲到了真正需要的時候。

4.3. 並行計算

獲取 Stream 流時可以使用 parallelStream 方法代替 stream 方法以獲取並行處理流,並行處理可以充分的發揮多核優勢,而且不增加編碼的復雜性。

下面的代碼演示了生成一千萬個隨機數后,把每個隨機數乘以2然后求和時,串行計算和並行計算的耗時差異。

  /** * 並行計算 */ @Test public void main() { // 生成自己的隨機數流,取一千萬個隨機數 Random random = new Random(); Stream<Integer> generateRandom = Stream.generate(random::nextInt); List<Integer> numberList = generateRandom.limit(10000000).collect(Collectors.toList()); // 串行 - 把一千萬個隨機數,每個隨機數 * 2 ,然后求和 long start = System.currentTimeMillis(); int sum = numberList.stream() .map(number -> number * 2) .mapToInt(x -> x) .sum(); long end = System.currentTimeMillis(); System.out.println("串行耗時:"+(end - start)+"ms,和是:"+sum); // 並行 - 把一千萬個隨機數,每個隨機數 * 2 ,然后求和 start = System.currentTimeMillis(); sum = numberList.parallelStream() .map(number -> number * 2) .mapToInt(x -> x) .sum(); end = System.currentTimeMillis(); System.out.println("並行耗時:"+(end - start)+"ms,和是:"+sum); }

得到如下輸出。

串行耗時:1005ms,和是:481385106 並行耗時:47ms,和是:481385106

效果顯而易見,代碼簡潔優雅。

5. Stream 流建議

5.1 保證正確排版

從上面的使用案例中,可以發現使用 Stream 流操作的代碼非常簡潔,而且可讀性更高。但是如果不正確的排版,那么看起來將會很糟糕,比如下面的同樣功能的代碼例子,多幾層操作呢,是不是有些讓人頭大?

// 不排版 String string = names.stream().filter(num -> num.length() == 4).map(name -> name.toUpperCase()).collect(Collectors.joining(",")); // 排版 String string = names.stream() .filter(num -> num.length() == 4) .map(name -> name.toUpperCase()) .collect(Collectors.joining(","));

5.1 保證函數純度

如果想要你的 Stream 流對於每次的相同操作的結果都是相同的話,那么你必須保證 Lambda 表達式的純度,也就是下面亮點。

  • Lambda 中不會更改任何元素。
  • Lambda 中不依賴於任何可能更改的元素。

這兩點對於保證函數的冪等非常重要,不然你程序執行結果可能會變得難以預測,就像下面的例子。

@Test public void simpleTest(){ List<Integer> numbers = Arrays.asList(1, 2, 3); int[] factor = new int[] { 2 }; Stream<Integer> stream = numbers.stream() .map(e -> e * factor[0]); factor[0] = 0; stream.forEach(System.out::println); } // 輸出結果 // 0 // 0 // 0

文中代碼都已經上傳到

https://github.com/niumoo/jdk-feature/blob/master/src/main/java/net/codingme/feature/jdk8/Jdk8Stream.java


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM