Java | Stream 基本概念及創建方法


前言

相信很多人(包括我自己),在很長一段時間內雖然使用了 JDK 1.8 ,卻從來沒有使用過自1.8開始增加的 Stream 這一強大使用的新特性,本文則將先從如何創建 Stream 開始,逐步去學會 Stream 的使用。本文不會涉及對流中數據的操作,而只討論創建流的幾種方法,以及一些基礎概念,關於流的實用操作將會在后續文章中一一介紹。

Stream 與 Collection 的區別

  1. 用途與關注點不同

    Collection 主要關注於對象的存儲方面,通過使用 ListMapSet等等數據結構,讓數據被更好的組織起來,以便於使用。而 Stream 則關注於對象的操作方面,包含reducemapfilter等等實用的操作。

  2. 流是懶搜索(Laziness-seeking)的

    先看一個例子,考慮一下代碼:

    Random random = new Random(29);
    random.ints()
          .filter(v -> v > 5 && v < 31)
          .limit(3)
          .forEach(System.out::println);
    
    // output:
    // 	 21
    // 	 22
    // 	 28
    

    代碼首先創建了一個隨機整數流,然后過濾得到其中在(5, 31)范圍內的數,最終得到其中的3個數並輸出,這里創建的流就是3中所說的無限流,而流在執行的過程中一旦得到一個滿足條件的整數就會加到結果序列中,並且開始進行下一輪的搜索,直到找到3個滿足的整數為止。流只會完成所給任務(找到3個滿足指定范圍的整數並輸出),不會有額外的操作。

  3. 流的大小可以是無限的

    盡管 Collection 的數據量也可以動態擴展改變,但由於計算機內存是有限的,所以其數據量大小始終可以看成只能為有限的大小。但 Stream 則不同,由於流是懶加載的,所以當使用limit類似的短路操作時,就可以利用特性2的原因去接收一個無限流。

  4. 流操作不存在副作用

    和 Collection 中的某些操作,例如remove會刪除集合中的元素不同,流不會修改生成流的原有集合中的數據,例如使用filter時,會產生一個經過元素過濾后的新流,而不會修改原集合中的數據。

  5. 流屬於消耗品(Consumable)

    不同與 Collection 沒有訪問次數與使用的限制,一個流在其生命周期中只能被執行一次,當執行了終端操作(terminal operation,在之后的文章中會具體介紹)后,即使沒有將流關閉,例如上述代碼中的forEach,也無法再次訪問了(類似迭代器),如下代碼所示,想要再操作,必須重新創建一個流。

    IntStream stream = new Random(29).ints();
    stream.filter(v -> v > 5 && v < 31)
          .limit(3)
          .forEach(System.out::println);
    // 當執行了終端操作后再使用,就會出現一下異常提示信息
    // java.lang.IllegalStateException: stream has already been operated upon or closed
    stream.forEach(System.out::println);
    

創建流

流可以通過很多種方式被創建,下面進行一一介紹:

  1. Collection 家族創建的方式

    對於實現了Collection 接口的類,都可以通過stream()parallelStream()創建對應流,如下代碼所示:

    List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
    // 創建一個普通的流
    Stream<Integer> stream = list.stream();
    // 創建一個並行流
    Stream<Integer> parallelStream = list.parallelStream();
    
  2. 數組家族創建的方式

    對於數組類型的元素,都可以使用Arrays類的stream()創建對應的流,如果想獲得並行流則需要使用parallel()方法,如下所示:

    IntStream stream = Arrays.stream(new int[]{1, 2, 3});
    // 生成流對應的並行流
    IntStream parallelStream = stream.parallel();
    
  3. Stream家族的工廠方法

    通過工廠方法來創建流的方式比較多,可以通過emptyofconcatgenerate、``iteraterangerangeClosed以及builder`等方法創建流,下面就通過代碼樣例來一一介紹:

    // 產生一個不包含任何元素的流
    Stream<Object> stream1 = Stream.empty();
    
    // 由給定元素所生成的流
    Stream<Integer> stream2 = Stream.of(1, 2, 3);
    
    // 合並兩個流產生一個新的流
    Stream<Object> stream3 = Stream.concat(stream1, stream2);
    
    // 創建一個<無限流>,流中的數據是通過調用所傳函數產生的
    Stream<Double> stream4 = Stream.generate(Math::random);
    
    // 創建一個<無限流>,流中的數據由第一個參數、將
    // 第一個參數作為函數參數調用產生的值以及不斷將
    // 函數調用得到的值作為參數繼續調用所組成,
    // 例如下面會生成1,2,3....的整數流
    Stream<Integer> stream5 = Stream.iterate(1, v -> v + 1);
    
    // 創建范圍為[1, 5)組成的整數流
    IntStream stream6 = IntStream.range(1, 5);
    
    // 創建范圍為[1, 5]組成的整數流
    IntStream stream7 = IntStream.rangeClosed(1, 5);
    
    // 通過流的建造者模式創建流
    Stream.Builder<Integer> builder = Stream.builder();
    for (int i = 0; i < 10; i++) {
        // add 與 accept 方法均可將元素添加到流中
        // 區別是 add 無返回值, accept 會返回當前 builder 的 this 對象
        // 底層 add 方法也是調用了 accept 然后返回 this
        // 因此對於 add 方法可以進行鏈式調用
        builder.add(i);
        builder.accept(i);
    }
    Stream<Integer> stream8 = builder.build();
    
  4. IO/NIO家族中的方法

    除了兩種獲取lines生成的流外,其它幾種方式都很少使用,這一部分了解即可。

    try {
        String dir = System.getProperty("user.dir");
        // 以下兩種方法均是獲取文件中行數據組成的流
        Stream<String> stream1 = new BufferedReader(new FileReader(dir + "\\demo.txt")).lines();
        Stream<String> stream2 = Files.lines(Paths.get(dir + "\\demo.txt"));
        // 獲取指定路徑下所有文件/文件夾的路徑組成的流
        Stream<Path> stream3 = Files.list(Paths.get("d:\\temp"));
        // 獲取指定路徑下以及指定最深文件層級內(在這里為2)且滿足函數條件的所有文件/文件夾的路徑組成的流
        Stream<Path> stream4 = Files.find(
            Paths.get("d:\\temp"), 1, (path, basicFileAttributes) -> path.isAbsolute());
        // 獲取指定路徑下以及指定最深文件層級內(在這里為2)所有文件/文件夾的路徑組成的流
        Stream<Path> stream5 = Files.walk(Paths.get("d:\\temp"), 2);
    } catch (IOException e) {
        e.printStackTrace();
    }
    
  5. Random 獲取流的方式

    由於直接使用 Random 類生成隨機數無限流,均為基本數據類型組成的流,因此通常還需要使用boxed方法進行裝箱(以前凡是生成的為IntStreamDoubleStreamLongStream均同此),以便可以使用更加豐富的特性。

    Random random = new Random();
    // 以下三種方式得到的均是隨機數組成的<無限流>
    IntStream stream1 = random.ints();
    DoubleStream stream2 = random.doubles();
    LongStream stream3 = random.longs();
    Stream<Integer> boxedStream = stream1.boxed();
    

    下面就先舉一個具體的實用的例子,在之后的文章中會詳細介紹一些實用操作,這里可以先做了解:

    // 對數組元素進行倒序排序
    // 如果不進行裝箱(boxed)處理,則只能使用默認的升序排序方法
    // 通過裝箱,則可以通過自定義比較器,實現更加多樣的排序
    int[] arr = {1, 5, 4, 6, 3, 9, 4, 5, 6, 4};
    int[] reverseArr = Arrays.stream(arr)
            .boxed()
            .sorted(Comparator.reverseOrder())
            .mapToInt(Integer::valueOf)
            .toArray();
    // output: [9, 6, 6, 5, 5, 4, 4, 4, 3, 1]
    System.out.println(Arrays.toString(reverseArr));
    
  6. 其它可以生成流的類

    除了以上介紹的幾個主要可以生成流的類之外,還有一些其它不太常見的可以流的類,下面是部分代碼展示:

    String s = "1,2,3,4,5,6,7";// 由分割后的字符串組成的流// 在這里就是"1", "2", "3", "4", "5", "6", "7"組成的流Stream<String> stream1 = Pattern.compile(",").splitAsStream(s);BitSet bitSet = new BitSet();for (int i = 0; i < 10; i++) {    if (i % 2 == 0) {        bitSet.set(i);    }}// 由 bitset 中被設置為 true 的位下標所組成的流// 在這里就是0, 2, 4, 6, 8IntStream stream2 = bitSet.stream();try {    String dir = System.getProperty("user.dir");    JarFile jarFile = new JarFile(dir + "\\demo.jar");    // 由指定 jar 包中所有文件及文件夾的 JarEntry 對象所組形成的流    Stream<JarEntry> stream3 = jarFile.stream();} catch (IOException e) {    e.printStackTrace();}
    

此外還可以通過 StreamSupport工具類進行產生和操作流,由於本文包括之后的文章主要是為了入門和先簡單上手,所以這里不做詳細討論,感興趣的可以自己進行查閱資料。

總結

本文簡單介紹了 Stream 這個自1.8開始引入的新特性,然后簡單介紹了一些基本概念和流的創建方式,在接下來的文章中還會介紹流的一些實用操作,希望能和大家一起學會使用 Stream 這個實用的特性,當然本文也難免有錯誤之處,希望得到各位的指正。

參考資料


免責聲明!

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



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