Stream 流式編程,讓代碼變優雅

一、引言

流式編程的概念和作用

Java 流 (Stream) 是一連串的元素序列,可以進行各種操作以實現數據的轉換和處理。流式編程的概念基於函數式編程的思想,旨在簡化代碼,提高可讀性和可維護性。

Java Stream 的主要作用有以下幾個方面:

  1. 簡化集合操作:使用傳統的 for 循環或迭代器來處理集合數據可能會導致冗長而複雜的代碼。而使用流式編程,能夠用更直觀、更簡潔的方式對集合進行過濾、映射、排序、聚合等操作,使代碼變得更加清晰易懂。

  2. 延遲計算:流式操作允許你在處理數據之前定義一系列的操作步驟,但只在需要結果時纔會實際執行。這種延遲計算的特性意味着可以根據需要動態調整數據處理的操作流程,提升效率。

  3. 並行處理:Java Stream 提供了並行流的支持,可以將數據分成多個塊進行並行處理,從而充分利用多核處理器的性能優勢,提高代碼的執行速度。

  4. 函數式編程風格:流式編程鼓勵使用函數式編程的思想,通過傳遞函數作爲參數或使用 Lambda 表達式來實現代碼的簡化和靈活性。這種函數式的編程模式有助於減少副作用,並使代碼更易測試和調試。

爲什麼使用流式編程可以提高代碼可讀性和簡潔性

  1. 聲明式編程風格:流式編程採用了一種聲明式的編程風格,你只需描述你想要對數據執行的操作,而不需要顯式地編寫迭代和控制流語句。這使得代碼更加直觀和易於理解,因爲你可以更專注地表達你的意圖,而無需關注如何實現。

  2. 鏈式調用:流式編程使用方法鏈式調用的方式,將多個操作鏈接在一起。每個方法都返回一個新的流對象,這樣你可以像 “流水線” 一樣在代碼中順序地寫下各種操作,使代碼邏輯清晰明瞭。這種鏈式調用的方式使得代碼看起來更加流暢,減少了中間變量和臨時集合的使用。

  3. 操作的組合:流式編程提供了一系列的操作方法,如過濾、映射、排序、聚合等,這些方法可以按照需要進行組合使用。你可以根據具體的業務需求將這些操作串聯起來,形成一個複雜的處理流程,而不需要編寫大量的循環和條件語句。這種組合操作的方式使得代碼更加模塊化和可維護。

  4. 減少中間狀態:傳統的迭代方式通常需要引入中間變量來保存中間結果,這樣會增加代碼的複雜度和維護成本。而流式編程將多個操作鏈接在一起,通過流對象本身來傳遞數據,避免了中間狀態的引入。這種方式使得代碼更加簡潔,減少了臨時變量的使用。

  5. 減少循環和條件:流式編程可以替代傳統的循環和條件語句的使用。例如,可以使用 filter() 方法進行元素的篩選,使用 map() 方法進行元素的轉換,使用 reduce() 方法進行聚合操作等。這些方法可以用一行代碼完成相應的操作,避免了繁瑣的循環和條件邏輯,使得代碼更加簡潔明瞭。

二、Stream 基礎知識

什麼是 Stream

Stream(流)是 Java 8 引入的一個新的抽象概念,它代表着一種處理數據的序列。簡單來說,Stream 是一系列元素的集合,這些元素可以是集合、數組、I/O 資源或者其他數據源。

Stream API 提供了豐富的操作方法,可以對 Stream 中的元素進行各種轉換、過濾、映射、聚合等操作,從而實現對數據的處理和操作。Stream API 的設計目標是提供一種高效、可擴展和易於使用的方式來處理大量的數據。

Stream 具有以下幾個關鍵特點:

  1. 數據源:Stream 可以基於不同類型的數據源創建,如集合、數組、I/O 資源等。你可以通過調用集合或數組的 stream() 方法來創建一個流。

  2. 數據處理:Stream 提供了豐富的操作方法,可以對流中的元素進行處理。這些操作可以按需求組合起來,形成一個流水線式的操作流程。常見的操作包括過濾(filter)、映射(map)、排序(sorted)、聚合(reduce)等。

  3. 惰性求值:Stream 的操作是惰性求值的,也就是說在定義操作流程時,不會立即執行實際計算。只有當終止操作(如收集結果或遍歷元素)被調用時,纔會觸發實際的計算過程。

  4. 不可變性:Stream 是不可變的,它不會修改原始數據源,也不會產生中間狀態或副作用。每個操作都會返回一個新的流對象,以保證數據的不可變性。

  5. 並行處理:Stream 支持並行處理,可以通過 parallel() 方法將流轉換爲並行流,利用多核處理器的優勢來提高處理速度。在某些情況下,使用並行流可以極大地提高程序的性能。

通過使用 Stream,我們可以使用簡潔、函數式的方式處理數據。相比傳統的循環和條件語句,Stream 提供了更高層次的抽象,使代碼更具可讀性、簡潔性和可維護性。它是一種強大的工具,可以幫助我們更有效地處理和操作集合數據。

Stream 的特性和優勢

  1. 簡化的編程模型:Stream 提供了一種更簡潔、更聲明式的編程模型,使代碼更易於理解和維護。通過使用 Stream API,我們可以用更少的代碼實現複雜的數據操作,將關注點從如何實現轉移到了更關注我們想要做什麼。

  2. 函數式編程風格:Stream 是基於函數式編程思想設計的,它鼓勵使用不可變的數據和純函數的方式進行操作。這種風格避免了副作用,使代碼更加模塊化、可測試和可維護。此外,Stream 還支持 Lambda 表達式,使得代碼更加簡潔和靈活。

  3. 惰性求值:Stream 的操作是惰性求值的,也就是說在定義操作流程時並不會立即執行計算。只有當終止操作被調用時,纔會觸發實際的計算過程。這種特性可以避免對整個數據集進行不必要的計算,提高了效率。

  4. 並行處理能力:Stream 支持並行處理,在某些情況下可以通過 parallel() 方法將流轉換爲並行流,利用多核處理器的優勢來提高處理速度。並行流能夠自動將數據劃分爲多個子任務,並在多個線程上同時執行,提高了處理大量數據的效率。

  5. 優化的性能:Stream API 內部使用了優化技術,如延遲執行、短路操作等,以提高計算性能。Stream 操作是通過內部迭代器實現的,可以更好地利用硬件資源,並適應數據規模的變化。

  6. 支持豐富的操作方法:Stream API 提供了許多豐富的操作方法,如過濾、映射、排序、聚合等。這些方法可以按需求組合起來形成一個操作流程。在組合多個操作時,Stream 提供了鏈式調用的方式,使代碼更加簡潔和可讀性更強。

  7. 可以操作各種數據源:Stream 不僅可以操作集合類數據,還可以操作其他數據源,如數組、I/O 資源甚至無限序列。這使得我們可以使用相同的編程模型來處理各種類型的數據。

如何創建 Stream 對象

  1. 從集合創建:我們可以通過調用集合的 stream() 方法來創建一個 Stream 對象。例如:

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    Stream<Integer> stream = numbers.stream();
  2. 從數組創建:Java 8 引入了 Arrays 類的 stream() 方法,我們可以使用它來創建一個 Stream 對象。例如:

    String[] names = {"Alice""Bob""Carol"};
    Stream<String> stream = Arrays.stream(names);
  3. 通過 Stream.of() 創建:我們可以使用 Stream.of() 方法直接將一組元素轉換爲 Stream 對象。例如:

    Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
  4. 通過 Stream.builder() 創建:如果我們不確定要添加多少個元素到 Stream 中,可以使用 Stream.builder() 創建一個 Stream.Builder 對象,並使用其 add() 方法來逐個添加元素,最後調用 build() 方法生成 Stream 對象。例如:

    Stream.Builder<String> builder = Stream.builder();
    builder.add("Apple");
    builder.add("Banana");
    builder.add("Cherry");
    Stream<String> stream = builder.build();
  5. 從 I/O 資源創建:Java 8 引入了一些新的 I/O 類(如 BufferedReaderFiles 等),它們提供了很多方法來讀取文件、網絡流等數據。這些方法通常返回一個 Stream 對象,可以直接使用。例如:

    Path path = Paths.get("data.txt");
    try (Stream<String> stream = Files.lines(path)) {
        // 使用 stream 處理數據
    } catch (IOException e) {
        e.printStackTrace();
    }
  6. 通過生成器創建:除了從現有的數據源創建 Stream,我們還可以使用生成器來生成元素。Java 8 中提供了 Stream.generate() 方法和 Stream.iterate() 方法來創建無限 Stream。例如:

    Stream<Integer> stream = Stream.generate(() -> 0); // 創建一個無限流,每個元素都是 0
    Stream<Integer> stream = Stream.iterate(0, n -> n + 1); // 創建一個無限流,從 0 開始遞增

需要注意的是,Stream 對象是一種一次性使用的對象,它只能被消費一次。一旦對 Stream 執行了終止操作(如收集結果、遍歷元素),Stream 就會被關閉,後續無法再使用。因此,在使用 Stream 時,需要根據需要重新創建新的 Stream 對象。

常用的 Stream 操作方法

  1. 過濾(Filter):filter() 方法接受一個 Predicate 函數作爲參數,用於過濾 Stream 中的元素。只有滿足 Predicate 條件的元素會被保留下來。例如:

    Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
    Stream<Integer> filteredStream = stream.filter(n -> n % 2 == 0); // 過濾出偶數
  2. 映射(Map):map() 方法接受一個 Function 函數作爲參數,用於對 Stream 中的元素進行映射轉換。對每個元素應用函數後的結果會構成一個新的 Stream。例如:

    Stream<String> stream = Stream.of("apple""banana""cherry");
    Stream<Integer> mappedStream = stream.map(s -> s.length()); // 映射爲單詞長度
  3. 扁平映射(FlatMap):flatMap() 方法類似於 map() 方法,不同之處在於它可以將每個元素映射爲一個流,並將所有流連接成一個流。這主要用於解決嵌套集合的情況。例如:

    List<List<Integer>> nestedList = Arrays.asList(
        Arrays.asList(1, 2),
        Arrays.asList(3, 4),
        Arrays.asList(5, 6)
    );
    Stream<Integer> flattenedStream = nestedList.stream().flatMap(List::stream); // 扁平化爲一個流
  4. 截斷(Limit):limit() 方法可以限制 Stream 的大小,只保留前 n 個元素。例如:

    Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
    Stream<Integer> limitedStream = stream.limit(3); // 只保留前 3 個元素
  5. 跳過(Skip):skip() 方法可以跳過 Stream 中的前 n 個元素,返回剩下的元素組成的新 Stream。例如:

    Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
    Stream<Integer> skippedStream = stream.skip(2); // 跳過前 2 個元素
  6. 排序(Sorted):sorted() 方法用於對 Stream 中的元素進行排序,默認是自然順序排序。還可以提供自定義的 Comparator 參數來指定排序規則。例如:

    Stream<Integer> stream = Stream.of(5, 2, 4, 1, 3);
    Stream<Integer> sortedStream = stream.sorted(); // 自然順序排序
  7. 去重(Distinct):distinct() 方法用於去除 Stream 中的重複元素,根據元素的 equals()hashCode() 方法來判斷是否重複。例如:

    Stream<Integer> stream = Stream.of(1, 2, 2, 3, 3, 3);
    Stream<Integer> distinctStream = stream.distinct(); // 去重
  8. 彙總(Collect):collect() 方法用於將 Stream 中的元素收集到結果容器中,如 List、Set、Map 等。可以使用預定義的 Collectors 類提供的工廠方法來創建收集器,也可以自定義收集器。例如:

    Stream<String> stream = Stream.of("apple""banana""cherry");
    List<String> collectedList = stream.collect(Collectors.toList()); // 收集爲 List
  9. 歸約(Reduce):reduce() 方法用於將 Stream 中的元素依次進行二元操作,得到一個最終的結果。它接受一個初始值和一個 BinaryOperator 函數作爲參數。例如:

    Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
    Optional<Integer> sum = stream.reduce((a, b) -> a + b); // 對所有元素求和
  10. 統計(Summary Statistics):summaryStatistics() 方法可以從 Stream 中獲取一些常用的統計信息,如元素個數、最小值、最大值、總和和平均值。例如:

```
IntStream stream = IntStream.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = stream.summaryStatistics();
System.out.println("Count: " + stats.getCount());
System.out.println("Min: " + stats.getMin());
System.out.println("Max: " + stats.getMax());
System.out.println("Sum: " + stats.getSum());
System.out.println("Average: " + stats.getAverage());


```

以上只是 Stream API 提供的一部分常用操作方法,還有許多其他操作方法,如匹配(Match)、查找(Find)、遍歷(ForEach)等

三、Stream 的中間操作

過濾操作(filter)

過濾操作(filter)是 Stream API 中的一種常用操作方法,它接受一個 Predicate 函數作爲參數,用於過濾 Stream 中的元素。只有滿足 Predicate 條件的元素會被保留下來,而不滿足條件的元素將被過濾掉。

過濾操作的語法如下:

Stream<T> filter(Predicate<? super T> predicate)

其中,T 表示 Stream 元素的類型,predicate 是一個函數式接口 Predicate 的實例,它的泛型參數和 Stream 元素類型一致。

使用過濾操作可以根據自定義的條件來篩選出符合要求的元素,從而對 Stream 進行精確的數據過濾。

下面是一個示例,演示如何使用過濾操作篩選出一個整數流中的偶數:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> filteredStream = stream.filter(n -> n % 2 == 0);
filteredStream.forEach(System.out::println); // 輸出結果: 2 4

在這個示例中,我們首先創建了一個包含整數的 Stream,並調用 filter() 方法傳入一個 Lambda 表達式 n -> n % 2 == 0,表示要篩選出偶數。然後通過 forEach() 方法遍歷輸出結果。

需要注意的是,過濾操作返回的是一個新的 Stream 實例,原始的 Stream 不會受到改變。這也是 Stream 操作方法的一個重要特點,它們通常返回一個新的 Stream 實例,以便進行鏈式調用和組合多個操作步驟。

在實際應用中,過濾操作可以與其他操作方法結合使用,如映射(map)、排序(sorted)、歸約(reduce)等,以實現更復雜的數據處理和轉換。而過濾操作本身的優點在於,可以高效地對大型數據流進行篩選,從而提高程序的性能和效率。

映射操作(map)

映射操作(map)是 Stream API 中的一種常用操作方法,它接受一個 Function 函數作爲參數,用於對 Stream 中的每個元素進行映射轉換,生成一個新的 Stream。

映射操作的語法如下:

<R> Stream<R> map(Function<? super T, ? extends R> mapper)

其中,T 表示原始 Stream 的元素類型,R 表示映射後的 Stream 的元素類型,mapper 是一個函數式接口 Function 的實例,它的泛型參數分別是原始 Stream 元素的類型和映射後的元素類型。

使用映射操作可以對 Stream 中的元素逐個進行處理或轉換,從而獲得一個新的 Stream。這個過程通常涉及對每個元素應用傳入的函數,根據函數的返回值來構建新的元素。

下面是一個示例,演示如何使用映射操作將一個字符串流中的每個字符串轉換爲其長度:

Stream<String> stream = Stream.of("apple""banana""cherry");
Stream<Integer> mappedStream = stream.map(s -> s.length());
mappedStream.forEach(System.out::println); // 輸出結果: 5 6 6

在這個示例中,我們首先創建了一個包含字符串的 Stream,並調用 map() 方法傳入一個 Lambda 表達式 s -> s.length(),表示要將每個字符串轉換爲其長度。然後通過 forEach() 方法遍歷輸出結果。

需要注意的是,映射操作返回的是一個新的 Stream 實例,原始的 Stream 不會受到改變。這也是 Stream 操作方法的一個重要特點,它們通常返回一個新的 Stream 實例,以便進行鏈式調用和組合多個操作步驟。

在實際應用中,映射操作可以與其他操作方法結合使用,如過濾(filter)、排序(sorted)、歸約(reduce)等,以實現更復雜的數據處理和轉換。而映射操作本身的優點在於,可以通過簡單的函數變換實現對原始數據的轉換,減少了繁瑣的循環操作,提高了代碼的可讀性和維護性。

需要注意的是,映射操作可能引發空指針異常(NullPointerException),因此在執行映射操作時,應確保原始 Stream 中不包含空值,並根據具體情況進行空值處理。

排序操作(sorted)

排序操作(sorted)是 Stream API 中的一種常用操作方法,它用於對 Stream 中的元素進行排序。排序操作可以按照自然順序或者使用自定義的比較器進行排序。

排序操作的語法如下:

Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)

第一種語法形式中,sorted() 方法會根據元素的自然順序進行排序。如果元素實現了 Comparable 接口並且具備自然順序,那麼可以直接調用該方法進行排序。

第二種語法形式中,sorted(Comparator<? super T> comparator) 方法接受一個比較器(Comparator)作爲參數,用於指定元素的排序規則。通過自定義比較器,可以對非 Comparable 類型的對象進行排序。

下面是一個示例,演示如何使用排序操作對一個字符串流進行排序:

Stream<String> stream = Stream.of("banana""apple""cherry");
Stream<String> sortedStream = stream.sorted();
sortedStream.forEach(System.out::println); // 輸出結果: apple banana cherry

在這個示例中,我們首先創建了一個包含字符串的 Stream,並直接調用 sorted() 方法進行排序。然後通過 forEach() 方法遍歷輸出結果。

需要注意的是,排序操作返回的是一個新的 Stream 實例,原始的 Stream 不會受到改變。這也是 Stream 操作方法的一個重要特點,它們通常返回一個新的 Stream 實例,以便進行鏈式調用和組合多個操作步驟。

在實際應用中,排序操作可以與其他操作方法結合使用,如過濾(filter)、映射(map)、歸約(reduce)等,以實現更復雜的數據處理和轉換。排序操作本身的優點在於,可以將數據按照特定的順序排列,便於查找、比較和分析。

需要注意的是,排序操作可能會影響程序的性能,特別是對於大型數據流或者複雜的排序規則。因此,在實際應用中,需要根據具體情況進行權衡和優化,選擇合適的算法和數據結構來提高排序的效率。

截斷操作(limit 和 skip)

截斷操作(limit 和 skip)是 Stream API 中常用的操作方法,用於在處理流的過程中對元素進行截斷。

  1. limit(n):保留流中的前 n 個元素,返回一個包含最多 n 個元素的新流。如果流中元素少於 n 個,則返回原始流。

  2. skip(n):跳過流中的前 n 個元素,返回一個包含剩餘元素的新流。如果流中元素少於 n 個,則返回一個空流。

下面分別詳細介紹這兩個方法的使用。

limit(n) 方法示例:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> limitedStream = stream.limit(3);
limitedStream.forEach(System.out::println); // 輸出結果: 1 2 3

在這個示例中,我們創建了一個包含整數的 Stream,並調用 limit(3) 方法來保留前三個元素。然後使用 forEach() 方法遍歷輸出結果。

skip(n) 方法示例:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> skippedStream = stream.skip(2);
skippedStream.forEach(System.out::println); // 輸出結果: 3 4 5

在這個示例中,我們創建了一個包含整數的 Stream,並調用 skip(2) 方法來跳過前兩個元素。然後使用 forEach() 方法遍歷輸出結果。

需要注意的是,截斷操作返回的是一個新的 Stream 實例,原始的 Stream 不會受到改變。這也是 Stream 操作方法的一個重要特點,它們通常返回一個新的 Stream 實例,以便進行鏈式調用和組合多個操作步驟。

截斷操作在處理大型數據流或需要對數據進行切分和分頁顯示的場景中非常有用。通過限制或跳過指定數量的元素,可以控制數據的大小和範圍,提高程序的性能並減少不必要的計算。

需要注意的是,在使用截斷操作時需要注意流的有界性。如果流是無界的(例如 Stream.generate()),那麼使用 limit() 方法可能導致程序陷入無限循環,而使用 skip() 方法則沒有意義。

四、Stream 的終端操作

forEach 和 peek

forEach 和 peek 都是 Stream API 中用於遍歷流中元素的操作方法,它們在處理流的過程中提供了不同的功能和使用場景。

  1. forEach:forEach 是一個終端操作方法,它接受一個 Consumer 函數作爲參數,對流中的每個元素執行該函數。它沒有返回值,因此無法將操作結果傳遞給後續操作。forEach 會遍歷整個流,對每個元素執行相同的操作。

示例代碼:

List<String> names = Arrays.asList("Alice""Bob""Charlie");
names.stream()
     .forEach(System.out::println);

這個示例中,我們創建了一個包含字符串的 List,並通過 stream() 方法將其轉換爲流。然後使用 forEach 方法遍歷輸出每個元素的值。

  1. peek:peek 是一箇中間操作方法,它接受一個 Consumer 函數作爲參數,對流中的每個元素執行該函數。與 forEach 不同的是,peek 方法會返回一個新的流,該流中的元素和原始流中的元素相同。

示例代碼:

List<String> names = Arrays.asList("Alice""Bob""Charlie");
List<String> upperCaseNames = names.stream()
                                   .map(String::toUpperCase)
                                   .peek(System.out::println)
                                   .collect(Collectors.toList());

在這個示例中,我們首先將 List 轉換爲流,並通過 map 方法將每個元素轉換爲大寫字母。然後使用 peek 方法在轉換之前輸出每個元素的值。最後通過 collect 方法將元素收集到一個新的 List 中。

需要注意的是,無論是 forEach 還是 peek,它們都是用於在流的處理過程中執行操作。區別在於 forEach 是終端操作,不返回任何結果,而 peek 是中間操作,可以和其他操作方法進行組合和鏈式調用。

根據使用場景和需求,選擇使用 forEach 或 peek 來遍歷流中的元素。如果只是需要遍歷輸出元素,不需要操作結果,則使用 forEach。如果需要在遍歷過程中執行一些其他操作,並將元素傳遞給後續操作,則使用 peek。

聚合操作(reduce 和 collect)

reduce 和 collect 都是 Stream API 中用於聚合操作的方法,它們可以將流中的元素進行彙總、計算和收集。

  1. reduce:reduce 是一個終端操作方法,它接受一個 BinaryOperator 函數作爲參數,對流中的元素逐個進行合併操作,最終得到一個結果。該方法會將流中的第一個元素作爲初始值,然後將初始值與下一個元素傳遞給 BinaryOperator 函數進行計算,得到的結果再與下一個元素進行計算,以此類推,直到遍歷完所有元素。

示例代碼:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> sum = numbers.stream()
                               .reduce((a, b) -> a + b);
sum.ifPresent(System.out::println); // 輸出結果: 15

在這個示例中,我們創建了一個包含整數的 List,並通過 stream() 方法將其轉換爲流。然後使用 reduce 方法對流中的元素進行求和操作,將每個元素依次相加,得到結果 15。

  1. collect:collect 是一個終端操作方法,它接受一個 Collector 接口的實現作爲參數,對流中的元素進行收集和彙總的操作。Collector 接口定義了一系列用於聚合操作的方法,例如收集元素到 List、Set、Map 等容器中,或進行字符串連接、分組、計數等操作。

示例代碼:

List<String> names = Arrays.asList("Alice""Bob""Charlie");
String joinedNames = names.stream()
                          .collect(Collectors.joining(", "));
System.out.println(joinedNames); // 輸出結果: Alice, Bob, Charlie

在這個示例中,我們創建了一個包含字符串的 List,並通過 stream() 方法將其轉換爲流。然後使用 collect 方法將流中的元素連接成一個字符串,每個元素之間使用逗號和空格分隔。

需要注意的是,reduce 和 collect 都是終端操作,它們都會觸發流的遍歷和處理。不同的是,reduce 方法用於對流中的元素進行累積計算,得到一個最終結果;而 collect 方法用於對流中的元素進行收集和彙總,得到一個容器或其他自定義的結果。

在選擇使用 reduce 還是 collect 時,可以根據具體需求和操作類型來決定。如果需要對流中的元素進行某種計算和合並操作,得到一個結果,則使用 reduce。如果需要將流中的元素收集到一個容器中,進行彙總、分組、計數等操作,則使用 collect。

匹配操作(allMatch、anyMatch 和 noneMatch)

在 Stream API 中,allMatch、anyMatch 和 noneMatch 是用於進行匹配操作的方法,它們可以用來檢查流中的元素是否滿足特定的條件。

  1. allMatch:allMatch 方法用於判斷流中的所有元素是否都滿足給定的條件。當流中的所有元素都滿足條件時,返回 true;如果存在一個元素不滿足條件,則返回 false。

示例代碼:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean allEven = numbers.stream()
                         .allMatch(n -> n % 2 == 0);
System.out.println(allEven); // 輸出結果: false

在這個示例中,我們創建了一個包含整數的 List,並通過 stream() 方法將其轉換爲流。然後使用 allMatch 方法判斷流中的元素是否都是偶數。由於列表中存在奇數,所以返回 false。

  1. anyMatch:anyMatch 方法用於判斷流中是否存在至少一個元素滿足給定的條件。當流中至少有一個元素滿足條件時,返回 true;如果沒有元素滿足條件,則返回 false。

示例代碼:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean hasEven = numbers.stream()
                         .anyMatch(n -> n % 2 == 0);
System.out.println(hasEven); // 輸出結果: true

在這個示例中,我們創建了一個包含整數的 List,並通過 stream() 方法將其轉換爲流。然後使用 anyMatch 方法判斷流中是否存在偶數。由於列表中存在偶數,所以返回 true。

  1. noneMatch:noneMatch 方法用於判斷流中的所有元素是否都不滿足給定的條件。當流中沒有元素滿足條件時,返回 true;如果存在一個元素滿足條件,則返回 false。

示例代碼:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean noneNegative = numbers.stream()
                             .noneMatch(n -> n < 0);
System.out.println(noneNegative); // 輸出結果: true

在這個示例中,我們創建了一個包含整數的 List,並通過 stream() 方法將其轉換爲流。然後使用 noneMatch 方法判斷流中的元素是否都是非負數。由於列表中的元素都是非負數,所以返回 true。

需要注意的是,allMatch、anyMatch 和 noneMatch 都是終端操作,它們會遍歷流中的元素直到滿足條件或處理完所有元素。在性能上,allMatch 和 noneMatch 在第一個不匹配的元素處可以立即返回結果,而 anyMatch 在找到第一個匹配的元素時就可以返回結果。

查找操作(findFirst 和 findAny)

在 Stream API 中,findFirst 和 findAny 是用於查找操作的方法,它們可以用來從流中獲取滿足特定條件的元素。

  1. findFirst:findFirst 方法用於返回流中的第一個元素。它返回一個 Optional 對象,如果流爲空,則返回一個空的 Optional;如果流非空,則返回流中的第一個元素的 Optional。

示例代碼:

List<String> names = Arrays.asList("Alice""Bob""Charlie");
Optional<String> first = names.stream()
                              .findFirst();
first.ifPresent(System.out::println); // 輸出結果: Alice

在這個示例中,我們創建了一個包含字符串的 List,並通過 stream() 方法將其轉換爲流。然後使用 findFirst 方法獲取流中的第一個元素,並使用 ifPresent 方法判斷 Optional 是否包含值,並進行相應的處理。

  1. findAny:findAny 方法用於返回流中的任意一個元素。它返回一個 Optional 對象,如果流爲空,則返回一個空的 Optional;如果流非空,則返回流中的任意一個元素的 Optional。在順序流中,通常會返回第一個元素;而在並行流中,由於多線程的處理,可能返回不同的元素。

示例代碼:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> any = numbers.stream()
                               .filter(n -> n % 2 == 0)
                               .findAny();
any.ifPresent(System.out::println); // 輸出結果: 2 或 4(取決於並行處理的結果)

在這個示例中,我們創建了一個包含整數的 List,並通過 stream() 方法將其轉換爲流。然後使用 filter 方法篩選出偶數,再使用 findAny 方法獲取任意一個偶數,最後使用 ifPresent 方法判斷 Optional 是否包含值,並進行相應的處理。

需要注意的是,findAny 在並行流中會更有優勢,因爲在多線程處理時,可以返回最先找到的元素,提高效率。而在順序流中,findAny 的性能與 findFirst 相當。

統計操作(count、max 和 min)

在 Stream API 中,count、max 和 min 是用於統計操作的方法,它們可以用來獲取流中元素的數量、最大值和最小值。

  1. count:count 方法用於返回流中元素的數量。它返回一個 long 類型的值,表示流中的元素個數。

示例代碼:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
long count = numbers.stream()
                    .count();
System.out.println(count); // 輸出結果: 5

在這個示例中,我們創建了一個包含整數的 List,並通過 stream() 方法將其轉換爲流。然後使用 count 方法獲取流中元素的數量,並將結果輸出。

  1. max:max 方法用於返回流中的最大值。它返回一個 Optional 對象,如果流爲空,則返回一個空的 Optional;如果流非空,則返回流中的最大值的 Optional。

示例代碼:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> max = numbers.stream()
                               .max(Integer::compareTo);
max.ifPresent(System.out::println); // 輸出結果: 5

在這個示例中,我們創建了一個包含整數的 List,並通過 stream() 方法將其轉換爲流。然後使用 max 方法獲取流中的最大值,並使用 ifPresent 方法判斷 Optional 是否包含值,並進行相應的處理。

  1. min:min 方法用於返回流中的最小值。它返回一個 Optional 對象,如果流爲空,則返回一個空的 Optional;如果流非空,則返回流中的最小值的 Optional。

示例代碼:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> min = numbers.stream()
                               .min(Integer::compareTo);
min.ifPresent(System.out::println); // 輸出結果: 1

在這個示例中,我們創建了一個包含整數的 List,並通過 stream() 方法將其轉換爲流。然後使用 min 方法獲取流中的最小值,並使用 ifPresent 方法判斷 Optional 是否包含值,並進行相應的處理。

這些統計操作方法提供了一種便捷的方式來對流中的元素進行數量、最大值和最小值的計算。通過返回 Optional 對象,可以避免空指針異常。

五、並行流

什麼是並行流

並行流是 Java 8 Stream API 中的一個特性。它可以將一個流的操作在多個線程上並行執行,以提高處理大量數據時的性能。

在傳統的順序流中,所有的操作都是在單個線程上按照順序執行的。而並行流則會將流的元素分成多個小塊,並在多個線程上並行處理這些小塊,最後將結果合併起來。這樣可以充分利用多核處理器的優勢,加快數據處理的速度。

要將一個順序流轉換爲並行流,只需調用流的 parallel() 方法即可。示例代碼如下所示:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
       .parallel()
       .forEach(System.out::println);

在這個示例中,我們創建了一個包含整數的 List,並通過 stream() 方法將其轉換爲流。接着調用 parallel() 方法將流轉換爲並行流,然後使用 forEach 方法遍歷流中的元素並輸出。

需要注意的是,並行流的使用並不總是適合所有情況。並行流的優勢主要體現在數據量較大、處理時間較長的場景下。對於小規模數據和簡單的操作,順序流可能更加高效。在選擇使用並行流時,需要根據具體情況進行評估和測試,以確保獲得最佳的性能。

此外,還需要注意並行流在某些情況下可能引入線程安全的問題。如果多個線程同時訪問共享的可變狀態,可能會導致數據競爭和不確定的結果。因此,在處理並行流時,應當避免共享可變狀態,或採用適當的同步措施來確保線程安全。

如何使用並行流提高性能

使用並行流可以通過利用多線程並行處理數據,從而提高程序的執行性能。下面是一些使用並行流提高性能的常見方法:

  1. 創建並行流:要創建一個並行流,只需在普通流上調用 parallel() 方法。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    Stream<Integer> parallelStream = numbers.parallelStream();
  2. 利用任務並行性:並行流會將數據分成多個小塊,並在多個線程上並行處理這些小塊。這樣可以充分利用多核處理器的優勢。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    numbers.parallelStream()
           .map(n -> compute(n)) // 在多個線程上並行處理計算
           .forEach(System.out::println);

    在這個示例中,使用 map 方法對流中的每個元素進行計算。由於並行流的特性,計算操作會在多個線程上並行執行,提高了計算的效率。

  3. 避免共享可變狀態:在並行流中,多個線程會同時操作數據。如果共享可變狀態(如全局變量)可能導致數據競爭和不確定的結果。因此,避免在並行流中使用共享可變狀態,或者採取適當的同步措施來確保線程安全。

  4. 使用合適的操作:一些操作在並行流中的性能表現更好,而另一些操作則可能導致性能下降。一般來說,在並行流中使用基於聚合的操作(如 reducecollect)和無狀態轉換操作(如 mapfilter)的性能較好,而有狀態轉換操作(如 sorted)可能會導致性能下降。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
        
    // good performance
    int sum = numbers.parallelStream()
                     .reduce(0, Integer::sum);
        
    // good performance
    List<Integer> evenNumbers = numbers.parallelStream()
                                       .filter(n -> n % 2 == 0)
                                       .collect(Collectors.toList());
        
    // potential performance degradation
    List<Integer> sortedNumbers = numbers.parallelStream()
                                         .sorted()
                                         .collect(Collectors.toList());

    在這個示例中,reducefilter 的操作在並行流中具有良好的性能,而 sorted 操作可能導致性能下降。

除了上述方法,還應根據具體情況進行評估和測試,並行流是否能夠提高性能。有時候,並行流的開銷(如線程的創建和銷燬、數據切割和合並等)可能超過了其帶來的性能提升。因此,在選擇使用並行流時,應該根據數據量和操作複雜度等因素進行綜合考慮,以確保獲得最佳的性能提升。

並行流的適用場景和注意事項

  1. 大規模數據集:當需要處理大規模數據集時,使用並行流可以充分利用多核處理器的優勢,提高程序的執行效率。並行流將數據切分成多個小塊,並在多個線程上並行處理這些小塊,從而縮短了處理時間。

  2. 複雜的計算操作:對於複雜的計算操作,使用並行流可以加速計算過程。由於並行流能夠將計算操作分配到多個線程上並行執行,因此可以有效地利用多核處理器的計算能力,提高計算的速度。

  3. 無狀態轉換操作:並行流在執行無狀態轉換操作(如 mapfilter)時表現較好。這類操作不依賴於其他元素的狀態,每個元素的處理是相互獨立的,可以很容易地進行並行處理。

並行流的注意事項包括:

  1. 線程安全問題:並行流的操作是在多個線程上並行執行的,因此需要注意線程安全問題。如果多個線程同時訪問共享的可變狀態,可能會導致數據競爭和不確定的結果。在處理並行流時,應避免共享可變狀態,或者採用適當的同步措施來確保線程安全。

  2. 性能評估和測試:並行流的性能提升並不總是明顯的。在選擇使用並行流時,應根據具體情況進行評估和測試,以確保獲得最佳的性能提升。有時,並行流的開銷(如線程的創建和銷燬、數據切割和合並等)可能超過了其帶來的性能提升。

  3. 併發操作限制:某些操作在並行流中的性能表現可能較差,或者可能導致結果出現錯誤。例如,在並行流中使用有狀態轉換操作(如 sorted)可能導致性能下降或結果出現錯誤。在使用並行流時,應注意避免這類操作,或者在需要時採取適當的處理措施。

  4. 內存消耗:並行流需要將數據分成多個小塊進行並行處理,這可能導致額外的內存消耗。在處理大規模數據集時,應確保系統有足夠的內存來支持並行流的執行,以避免內存溢出等問題。

六、實踐應用示例

使用 Stream 處理集合數據

  1. 篩選出長度大於等於 5 的字符串,並打印輸出:
List<String> list = Arrays.asList("apple""banana""orange""grapefruit""kiwi");
list.stream()
    .filter(s -> s.length() >= 5)
    .forEach(System.out::println);

輸出結果:

banana
orange
grapefruit
  1. 將集合中的每個字符串轉換爲大寫,並收集到新的列表中:
List<String> list = Arrays.asList("apple""banana""orange""grapefruit""kiwi");
List<String> resultList = list.stream()
                              .map(String::toUpperCase)
                              .collect(Collectors.toList());
System.out.println(resultList);

輸出結果:

[APPLE, BANANA, ORANGE, GRAPEFRUIT, KIWI]
  1. 統計集合中以字母 "a" 開頭的字符串的數量:
List<String> list = Arrays.asList("apple""banana""orange""grapefruit""kiwi");
long count = list.stream()
                 .filter(s -> s.startsWith("a"))
                 .count();
System.out.println(count);

輸出結果:

1
  1. 使用並行流來提高處理速度,篩選出長度小於等於 5 的字符串,並打印輸出:
List<String> list = Arrays.asList("apple""banana""orange""grapefruit""kiwi");
list.parallelStream()
    .filter(s -> s.length() <= 5)
    .forEach(System.out::println);

輸出結果:

apple
kiwi
  1. 使用 Stream 對集合中的整數求和:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
                 .mapToInt(Integer::intValue)
                 .sum();
System.out.println(sum);

輸出結果:

15

以上示例展示瞭如何使用 Stream 對集合數據進行篩選、轉換、統計等操作。通過鏈式調用 Stream 的中間操作和終端操作。

使用 Stream 進行文件操作

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;

public class FileStreamExample {
    public static void main(String[] args) {
        String fileName = "file.txt";

        // 讀取文件內容並創建 Stream
        try (Stream<String> stream = Files.lines(Paths.get(fileName))) {
            // 打印文件的每一行內容
            stream.forEach(System.out::println);

            // 統計文件的行數
            long count = stream.count();
            System.out.println("總行數:" + count);

            // 篩選包含關鍵詞的行並打印輸出
            stream.filter(line -> line.contains("keyword"))
                .forEach(System.out::println);
            
            // 將文件內容轉換爲大寫並打印輸出
            stream.map(String::toUpperCase)
                .forEach(System.out::println);

            // 將文件內容收集到 List 中
            List<String> lines = stream.collect(Collectors.toList());
            System.out.println(lines);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上面的代碼中,首先指定了要讀取的文件名 file.txt。然後使用 Files.lines() 方法讀取文件的每一行內容,並創建一個 Stream 對象。接下來,我們對 Stream 進行一些操作:

請根據實際需求修改代碼中的文件名、操作內容和結果處理方式。需要注意的是,在使用完 Stream 後,應及時關閉文件資源,可以使用 try-with-resources 語句塊來自動關閉文件。另外,請處理可能出現的 IOException 異常。

使用 Stream 實現數據轉換和篩選

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Amy""Bob""Charlie""David""Eva");

        // 轉換爲大寫並篩選出長度大於3的名稱
        List<String> result = names.stream()
                                   .map(String::toUpperCase)
                                   .filter(name -> name.length() > 3)
                                   .collect(Collectors.toList());

        // 打印結果
        result.forEach(System.out::println);
    }
}

在上述代碼中,我們首先創建了一個包含一些名字的列表。然後使用 Stream 對列表進行操作:

最後,我們使用 forEach() 方法打印結果列表中的每個名稱。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/oY-46ep6MiOYWLUaV9c3xQ