當前位置: 妍妍網 > 碼農

全面剖析 Stream 流式編程,編程之路亦能盡顯優雅風範

2024-03-07碼農

正文

1 引言

流式編程的概念和作用

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

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

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

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

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

  4. 函數語言程式設計風格: 流式編程鼓勵使用函數語言程式設計的思想,透過傳遞函式作為參數或使用 Lambda 運算式來實作程式碼的簡化和靈活性。這種函式式的編程模式有助於減少副作用,並使程式碼更易測試和偵錯。

為什麽使用流式編程可以提高程式碼可讀性和簡潔性

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

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

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

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

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

2 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(12345);
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(12345);

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 類(如 BufferedReader Files 等),它們提供了很多方法來讀取檔、網路流等數據。這些方法通常返回一個 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(12345);
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(12),
Arrays.asList(34),
Arrays.asList(56)
);
Stream<Integer> flattenedStream = nestedList.stream().flatMap(List::stream); // 扁平化為一個流

4.截斷(Limit): limit() 方法可以限制 Stream 的大小,只保留前 n 個元素。例如:

Stream<Integer> stream = Stream.of(12345);
Stream<Integer> limitedStream = stream.limit(3); // 只保留前 3 個元素

5.跳過(Skip): skip() 方法可以跳過 Stream 中的前 n 個元素,返回剩下的元素組成的新 Stream。例如:

Stream<Integer> stream = Stream.of(12345);
Stream<Integer> skippedStream = stream.skip(2); // 跳過前 2 個元素

6.排序(Sorted): sorted() 方法用於對 Stream 中的元素進行排序,預設是自然順序排序。還可以提供自訂的 Comparator 參數來指定排序規則。例如:

Stream<Integer> stream = Stream.of(52413);
Stream<Integer> sortedStream = stream.sorted(); // 自然順序排序

7.去重(Distinct): distinct() 方法用於去除 Stream 中的重復元素,根據元素的 equals() hashCode() 方法來判斷是否重復。例如:

Stream<Integer> stream = Stream.of(122333);
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() 方法用於將 Stre am 中的元素依次進行二元操 作,得到一個最終的結果。它接受一個初始值和一個 BinaryOperator 函式作為參數。例如:

Stream<Integer> stream = Stream.of(12345);
Optional<Integer> sum = stream.reduce((a, b) -> a + b); // 對所有元素求和

10.統計(Summary Statistics): summaryStatistics() 方法可以從 Stream 中獲取一些常用的統計資訊,如元素個數、最小值、最大值、總和和平均值。例如:

IntStream stream = IntStream.of(12345);
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)等。另外,搜尋公眾號Linux就該這樣學後台回復「猴子」,獲取一份驚喜禮包。

3 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(12345);
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(12345);
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(12345);
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() 方法則沒有意義。

4 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(12345);
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(12345);
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(12345);
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(12345);
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(12345);
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(12345);
long count = numbers.stream()
.count();
System.out.println(count); // 輸出結果: 5

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

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

範例程式碼:

List<Integer> numbers = Arrays.asList(12345);
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(12345);
Optional<Integer> min = numbers.stream()
.min(Integer::compareTo);
min.ifPresent(System.out::println); // 輸出結果: 1

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

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

5 並列流

什麽是並列流

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

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

要將一個順序流轉換為並列流,只需呼叫流的 parallel() 方法即可。範例程式碼如下所示:

List<Integer> numbers = Arrays.asList(12345);
numbers.stream()
.parallel()
.forEach(System.out::println);

在這個範例中,我們建立了一個包含整數的 List,並透過 stream() 方法將其轉換為流。接著呼叫 parallel() 方法將流轉換為並列流,然後使用 forEach 方法遍歷流中的元素並輸出。

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

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

如何使用並列流提高效能

使用並列流可以透過利用多執行緒並列處理數據,從而提高程式的執行效能。下面是一些使用並列流提高效能的常見方法:

  1. 建立並列流: 要建立一個並列流,只需在普通流上呼叫 parallel() 方法。

List<Integer> numbers = Arrays.asList(12345);
Stream<Integer> parallelStream = numbers.parallelStream();

  1. 利用任務並列性: 並列流會將數據分成多個小塊,並在多個執行緒上並列處理這些小塊。這樣可以充分利用多核處理器的優勢。

List<Integer> numbers = Arrays.asList(12345);
numbers.parallelStream()
.map(n -> compute(n)) // 在多個執行緒上並列處理計算
.forEach(System.out::println);

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

  1. 避免共享可變狀態: 在並列流中,多個執行緒會同時運算元據。如果共享可變狀態(如全域變量)可能導致數據競爭和不確定的結果。因此,避免在並列流中使用共享可變狀態,或者采取適當的同步措施來確保執行緒安全。

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

List<Integer> numbers = Arrays.asList(12345);
// 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());

在這個範例中, reduce filter 的操作在並列流中具有良好的效能,而 sorted 操作可能導致效能下降。

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

並列流的適用場景和註意事項

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

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

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

並列流的註意事項包括:

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

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

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

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

6 實踐套用範例

使用 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(12345);
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 classFileStreamExample{
publicstaticvoidmain(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 進行一些操作:

  • 使用 forEach() 方法打印檔的每一行內容。

  • 使用 count() 方法統計檔的行數。

  • 使用 filter() 方法篩選出包含關鍵詞的行,並打印輸出。

  • 使用 map() 方法將檔內容轉換為大寫,並打印輸出。

  • 使用 collect() 方法將檔內容收集到 List 中。

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

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

    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    public classStreamExample{
    publicstaticvoidmain(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 對列表進行操作:

  • 使用 stream() 方法將列表轉換為一個 Stream。

  • 使用 map() 方法將每個名稱轉換為大寫。

  • 使用 filter() 方法篩選出長度大於3的名稱。

  • 使用 collect() 方法將篩選後的結果收集到一個新的列表中。

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