Java 中的流與循環
一、簡介
在本教程中,我們將深入探討 Java Streams 與 For-Loops 的比較。這些工具在每個 Java 開發人員的數據處理中發揮著至關重要的作用。儘管它們在很多方面都有所不同,但正如我們將在本文的其餘部分中看到的那樣,它們具有非常相似的用例,並且可以輕鬆地多次互換。
Java 8 中引入的流提供了函數式和聲明性方法,而 for 循環則提供了傳統的命令式方法。在本文結束時,我們可以為我們的編程任務做出最合適的決定。
2. 性能
當談到比較特定編程問題的解決方案時,我們經常不得不談論性能。此外,本案也不例外。由於流和 for 循環都用於處理大量數據,因此性能對於選擇正確的解決方案非常重要。
讓我們通過一個全面的基準測試示例來了解 for 循環和流之間的性能差異。我們將使用 for 循環和流來比較涉及過濾、映射和求和的複雜操作的執行時間。為此,我們將使用 Java Microbenchmarking Harness (JMH),這是一個專門為對 Java 代碼進行基準測試而設計的工具。
2.1.入門
我們首先定義依賴關係:
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.37</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.37</version>
</dependency>
我們始終可以在 Maven Central 上找到最新版本的JMH Core和JMH Annotation Processor 。
2.2.設定基準
在我們的基準測試中,我們將創建一個場景,其中包含範圍從 0 到 999,999 的整數列表。我們想要過濾掉偶數,對它們求平方,然後計算它們的和。除此之外,為了確保公平性,我們首先使用傳統的 for 循環來實現此過程:
@State(Scope.Thread)
public static class MyState {
List<Integer> numbers;
@Setup(Level.Trial)
public void setUp() {
numbers = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
numbers.add(i);
}
}
}
這個State
類將傳遞到我們的基準測試。此外, Setup
將在它們每個之前運行。
2.3.使用 For 循環進行基準測試
我們的 for 循環實現涉及迭代數字列表、檢查均勻性、對它們進行平方並將總和累加到變量中:
@Benchmark
public int forLoopBenchmark(MyState state) {
int sum = 0;
for (int number : state.numbers) {
if (number % 2 == 0) {
sum = sum + (number * number);
}
}
return sum;
}
2.4.使用流進行基準測試
接下來,我們將使用 Java 流實現相同的複雜操作。此外,我們將首先過濾偶數,將它們映射到它們的平方,並最終計算總和:
@Benchmark
public int streamBenchMark(MyState state) {
return state.numbers.stream()
.filter(number -> number % 2 == 0)
.map(number -> number * number)
.reduce(0, Integer::sum);
}
我們使用終結操作reduce()
來計算數字的總和。此外,我們可以通過多種方式計算總和。
2.5.運行基準測試
使用我們的基準測試方法後,我們將使用 JMH 運行基準測試。我們將多次執行基準測試,以確保結果準確並測量平均執行時間。為此,我們將向我們的類添加以下註釋:
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
通過這些補充,我們確保結果更加準確,在三次預熱後運行基準測試五次併計算所有五次迭代的平均值。現在,我們可以運行 main 方法來查看結果:
public static void main(String[] args) throws RunnerException {
Options options = new OptionsBuilder()
.include(PerformanceBenchmark.class.getSimpleName())
.build();
new Runner(options).run();
}
2.6。分析結果
一旦我們運行基準測試,JMH 將為我們提供 for 循環和流實現的平均執行時間:
Benchmark Mode Cnt Score Error Units
PerformanceBenchmark.forLoopBenchmark avgt 5 3386660.051 ± 1375112.505 ns/op
PerformanceBenchmark.streamBenchMark avgt 5 12231480.518 ± 1609933.324 ns/op
我們可以看到,在我們的示例中,從性能角度來看,for 循環的性能比流好得多。儘管在此示例中流的性能比 for 循環差,但這在某些情況下可能會發生變化,尤其是對於並行流。
3. 語法和可讀性
作為程序員,我們代碼的可讀性起著重要的作用。正因為如此,當我們嘗試為問題選擇最佳解決方案時,這一方面就變得很重要。
首先,讓我們深入了解流的語法和可讀性。流促進了更簡潔和更具表現力的編碼風格。這在過濾和映射數據時很明顯:
List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape");
long count = fruits.stream()
.filter(fruit -> fruit.length() > 5)
.count();
流代碼讀起來就像一個流暢的操作序列,過濾條件和計數操作在單個流暢的鏈中清晰地表達。此外,由於其聲明性,流通常會生成更易於閱讀的代碼。代碼更關注需要做什麼而不是如何做。
相反,讓我們探討一下 for 循環的語法和可讀性。 for 循環提供了更傳統和命令式的編碼風格:
List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape");
long count = 0;
for (String fruit : fruits) {
if (fruit.length() > 5) {
count++;
}
}
這裡,代碼涉及顯式迭代和條件語句。雖然大多數開發人員都很好地理解這種方法,但它有時會導致代碼更加冗長,從而可能難以閱讀,尤其是對於復雜的操作。
4. 並行性和並發性
在比較 Java 中的流和 for 循環時,並行性和並發性是需要考慮的重要方面。在利用多核處理器和管理並發操作時,這兩種方法提供了不同的功能和挑戰。
流旨在使並行處理更容易實現。 Java 8 引入了並行流的概念,它自動利用多核處理器來加速數據處理。我們可以輕鬆地重寫上一點的基準來同時計算總和:
@Benchmark
public int parallelStreamBenchMark(MyState state) {
return state.numbers.parallelStream()
.filter(number -> number % 2 == 0)
.map(number -> number * number)
.reduce(0, Integer::sum);
}
並行化進程唯一需要的是將stream()
替換為parallelStream()
方法。另一方面,重寫 for 循環來並行計算數字之和則更加複雜:
@Benchmark
public int concurrentForLoopBenchmark(MyState state) throws InterruptedException, ExecutionException {
int numThreads = Runtime.getRuntime().availableProcessors();
ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
List<Callable<Integer>> tasks = new ArrayList<>();
int chunkSize = state.numbers.size() / numThreads;
for (int i = 0; i < numThreads; i++) {
final int start = i * chunkSize;
final int end = (i == numThreads - 1) ? state.numbers.size() : (i + 1) * chunkSize;
tasks.add(() -> {
int sum = 0;
for (int j = start; j < end; j++) {
int number = state.numbers.get(j);
if (number % 2 == 0) {
sum = sum + (number * number);
}
}
return sum;
});
}
int totalSum = 0;
for (Future<Integer> result : executorService.invokeAll(tasks)) {
totalSum += result.get();
}
executorService.shutdown();
return totalSum;
}
我們可以使用 Java 的並發實用程序,例如ExecutorService
來並發執行任務。我們將列表分成塊並使用線程池同時處理它們。在決定流和 for 循環之間的並行性和並發性時,我們應該考慮任務的複雜性。流提供了一種更直接的方法來支持可以輕鬆並行化的任務的並行處理。另一方面,具有手動並發控制的for循環適用於需要自定義線程管理和協調的更複雜的場景。
5. 可變性
現在,讓我們探討可變性方面以及它在流和 for 循環之間的區別。了解它們如何處理可變數據對於做出明智的選擇至關重要。
首先也是最重要的,我們需要認識到流本質上促進了不變性。在流的上下文中,集合中的元素不會直接修改。相反,流上的操作會創建新的流或集合作為中間結果:
List<String> fruits = new ArrayList<>(Arrays.asList("apple", "banana", "orange"));
List<String> upperCaseFruits = fruits.stream()
.map(fruit -> fruit.toUpperCase())
.collect(Collectors.toList());
在這個流操作中,原始列表保持不變。 map()
操作生成一個新的流,其中每個水果都轉換為大寫,而collect()
操作將這些轉換後的元素收集到一個新列表中。
相比之下,for 循環可以直接對可變數據結構進行操作:
List<String> fruits = new ArrayList<>(Arrays.asList("apple", "banana", "orange"));
for (int i = 0; i < fruits.size(); i++) {
fruits.set(i, fruits.get(i).toUpperCase());
}
在這個 for 循環中,我們直接修改原始列表,將每個元素替換為其大寫對應項。當我們需要就地修改現有數據時,這可能是有利的,但也需要小心處理以避免意外後果。
六,結論
流和循環都有各自的優點和缺點。流提供了一種更具功能性和聲明性的方法,增強了代碼的可讀性,並且通常會帶來簡潔而優雅的解決方案。另一方面,循環提供了熟悉且顯式的控制結構,使其適用於精確執行順序或可變性控制至關重要的場景。
本文的完整源代碼和所有代碼片段都在 GitHub 上。