如何在 Java 中將巢狀循環轉換為流
1. 簡介
Java 8 中引入的Streams
API 徹底改變了開發人員使用 Java 處理資料的方式。它允許聲明性、簡潔和有效率地處理資料流,從而更容易對集合執行複雜的操作。
在本教程中,我們將探討將for
迴圈轉換為流背後的思考過程,重點介紹關鍵概念並提供實際範例。我們將從一個簡單的迭代開始,然後根據條件進行過濾,最後研究模擬跳出循環的短路操作。
2. Java Streams
的基礎知識
Java 中的Stream
是支援功能操作的元素序列,從集合、陣列或檔案等來源延遲處理。與集合不同, Streams
不儲存資料而是促進資料處理。
流操作要么是中間操作,要么是終端操作。中間運算(例如filter()
、 map()
和sorted()
傳回一個新的Stream
並進行惰性求值。諸如forEach()
、 collect()
和count()
之類的終端操作會產生結果或副作用,從而觸發執行。
一個關鍵的中間操作flatMap()
將每個元素轉換為Stream
並展平巢狀結構,使其對於處理巢狀集合非常有用。
3. 轉換簡單迭代並列印
讓我們先轉換一個基本的巢狀循環,該循環從兩個清單中產生所有可能的元素對。對於命令式方法,我們將遍歷list1
,然後遍歷list2
,並收集所有可能的對:
public static List<int[]> getAllPairsImperative(List<Integer> list1, List<Integer> list2) {
List<int[]> pairs = new ArrayList<>();
for (Integer num1 : list1) {
for (Integer num2 : list2) {
pairs.add(new int[] { num1, num2 });
}
}
return pairs;
}
基於Stream
的方法更加簡潔:
public static List<int[]> getAllPairsStream(List<Integer> list1, List<Integer> list2) {
return list1.stream()
.flatMap(num1 -> list2.stream().map(num2 -> new int[] { num1, num2 }))
.collect(Collectors.toList());
}
我們先透過呼叫list1.stream()
從list1
建立一個stream
。對於list1
中的每個元素,我們從list2
建立一個tream
,形成一對[num1, num2]
。最後,使用forEach(),
我們收集每個生成的pair
。這兩種實作都給出了相同的輸出:
List<Integer> list1 = Arrays.asList(1, 2, 3);
List<Integer> list2 = Arrays.asList(4, 5, 6);
List<int[]> imperativeResult = NestedLoopsToStreamsConverter.getAllPairsImperative(list1, list2);
List<int[]> streamResult = NestedLoopsToStreamsConverter.getAllPairsStream(list1, list2);
assertEquals(imperativeResult.size(), streamResult.size());
for (int i = 0; i < imperativeResult.size(); i++) {
assertArrayEquals(imperativeResult.get(i), streamResult.get(i));
}
4. 為轉換新增條件
現在,讓我們修改方法並根據條件過濾對。我們不會保存所有的對,而只保存總和大於7
的對。對於經典方法,我們需要在內部迴圈中加入一個額外的if
語句:
public static List<int[]> getFilteredPairsImperative(List<Integer> list1, List<Integer> list2) {
List<int[]> pairs = new ArrayList<>();
for (Integer num1 : list1) {
for (Integer num2 : list2) {
if (num1 + num2 > 7) {
pairs.add(new int[]{num1, num2});
}
}
}
return pairs;
}
讓我們實作Stream
等效項:
public static List<int[]> getFilteredPairsStream(List<Integer> list1, List<Integer> list2) {
return list1.stream()
.flatMap(num1 -> list2.stream().map(num2 -> new int[]{num1, num2}))
.filter(pair -> pair[0] + pair[1] > 7)
.collect(Collectors.toList());
}
在這裡,我們遵循與前面的範例相同的初始步驟。但是,在列印結果之前,我們對對的流應用filter()
,以僅保留總和大於7
的對。然後,我們collect
每個過濾後的pair
。在這種方法中,我們有效地將迭代和過濾分離為不同的操作,從而提高了程式碼的可讀性和可維護性。
我們將使用與前一個用例相同的兩個列表來測試我們的兩種方法是否給出相同的輸出:
List<int[]> imperativeResult = NestedLoopsToStreamsConverter.getFilteredPairsImperative(list1, list2);
List<int[]> streamResult = NestedLoopsToStreamsConverter.getFilteredPairsStream(list1, list2);
assertEquals(imperativeResult.size(), streamResult.size());
for (int i = 0; i < imperativeResult.size(); i++) {
assertArrayEquals(imperativeResult.get(i), streamResult.get(i));
}
5. 引入短路
在某些情況下,一旦找到第一個有效pair
,我們就需要停止處理。傳統上,我們會在循環內使用break
:
public static Optional<int[]> getFirstMatchingPairImperative(List<Integer> list1, List<Integer> list2) {
for (Integer num1 : list1) {
for (Integer num2 : list2) {
if (num1 + num2 > 7) {
return Optional.of(new int[] { num1, num2 });
}
}
}
return Optional.empty();
}
基於Stream-
的方法如下:
public static Optional<int[]> getFirstMatchingPairStream(List<Integer> list1, List<Integer> list2) {
return list1.stream()
.flatMap(num1 -> list2.stream().map(num2 -> new int[] { num1, num2 }))
.filter(pair -> pair[0] + pair[1] > 7)
.findFirst();
}
在這個版本中,我們在前面的範例基礎上,使用findFirst()
引入了短路行為。在這裡,我們使用findFirst()
僅檢索第一個匹配的pair
,一旦找到匹配項就停止執行。
使用findFirst(),
結果預設包裝在Optional
中,如果存在匹配的pair
,則傳回。透過這種方法,我們不再需要手動break
語句,而是提供了一種更實用、更易讀的方式來處理提前終止。
我們期望每種方法都能傳回相同的結果:
Optional<int[]> imperativeResult = NestedLoopsToStreamsConverter.getFirstMatchingPairImperative(list1, list2);
Optional<int[]> streamResult = NestedLoopsToStreamsConverter.getFirstMatchingPairStream(list1, list2);
assertEquals(imperativeResult.isPresent(), streamResult.isPresent());
imperativeResult.ifPresent(pair -> assertArrayEquals(pair, streamResult.get()));
6. 結論
當我們需要一種更具聲明性、可讀性和高效的方式來處理資料時,我們使用Streams
來取代巢狀循環。 Streams
簡化了複雜的轉換,提高了可維護性,並在需要時實現並行執行。
但是,對於可讀性可能受到影響的簡單循環、流開銷很大的效能關鍵程式碼或由於惰性求值而導致偵錯困難的情況,我們避免使用它們。
透過將嵌套循環轉換為Streams
,我們可以編寫更乾淨、更具表現力的程式碼,但我們必須考慮上下文並平衡清晰度和效能以選擇最佳方法。
本文的完整原始碼可以在 GitHub 上找到。