還看不懂同事的代碼?超強的 Stream 流操做姿式還不學習一下

Java 8 新特性系列文章索引。java

  1. Jdk14都要出了,還不能使用 Optional優雅的處理空指針?
  2. Jdk14 都要出了,Jdk8 的時間處理姿式還不瞭解一下?
  3. 還看不懂同事的代碼?Lambda 表達式、函數接口瞭解一下

若是以爲文章不錯,能夠關注個人公衆號:未讀代碼(weidudaima)。一個堅持原創、保證質量的公衆號。git

前言

咱們都知道 Lambda 和 Stream 是 Java 8 的兩大亮點功能,在前面的文章裏已經介紹過 Lambda 相關知識,此次介紹下 Java 8 的 Stream 流操做。它徹底不一樣於 java.io 包的 Input/Output Stream ,也不是大數據實時處理的 Stream 流。這個 Stream 流操做是 Java 8 對集合操做功能的加強,專一於對集合的各類高效、便利、優雅的聚合操做。藉助於 Lambda 表達式,顯著的提升編程效率可讀性。且 Stream 提供了並行計算模式,能夠簡潔的編寫出並行代碼,能充分發揮現在計算機的多核處理優點。github

在使用 Stream 流操做以前你應該先了解 Lambda 相關知識,若是還不瞭解,能夠參考以前文章:還看不懂同事的代碼?Lambda 表達式、函數接口瞭解一下面試

1. Stream 流介紹

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

因爲 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(中間操做)。在這個步驟中無論怎樣操做,它返回的都是一個新的流對象,原始數據不會發生任何改變,並且這個步驟是惰性計算處理的,也就是說只調用方法並不會開始處理,只有在真正的開始收集結果時,中間操做纔會生效,並且若是遍歷沒有完成,想要的結果已經獲取到了(好比獲取第一個值),會中止遍歷,而後返回結果。惰性計算能夠顯著提升運行效率。app

數據處理演示。

@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
複製代碼

數據處理/轉換操做天然不止是上面演示的過濾 filtermap映射兩種,另外還有 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&emsp;操做。

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.2 保證函數純度

若是想要你的 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

<完>

我的網站:www.codingme.net
若是你喜歡這篇文章,能夠關注公衆號,一塊兒成長。
關注公衆號回覆資源能夠沒有套路的獲取全網最火的的 Java 核心知識整理&面試資料。

公衆號
相關文章
相關標籤/搜索