Understanding Java Stream API: Operations, Characteristics, and Performance Compared to Iterator
This article introduces Java 8's Stream API, explains its intermediate and terminal operations, highlights its functional characteristics, compares its performance with traditional iterator loops through benchmark tests, and offers practical recommendations for using streams and parallel streams in backend development.
Java SE 8 introduced the Stream API (java.util.stream) as a new abstraction for processing sequences of elements, represented by Stream<T> and specialized streams such as IntStream , LongStream , and DoubleStream . Streams replace many collection operations, allowing concise functional-style processing of collections, arrays, and other data sources.
Types of Stream Operations
Intermediate Operations
Operations performed while data flows through the pipeline, returning another stream to enable chaining.
Common intermediate operations include filter , distinct , map , sorted , etc.
Terminal Operations
Operations that trigger the processing of the pipeline and produce a result, such as a collection, array, or scalar value.
Examples include collect , reduce , forEach , max , min , etc.
Characteristics of Streams
Single-use
A stream can be traversed only once; after the pipeline processes elements, the stream is exhausted and a new stream must be obtained from the source for further operations.
Internal Iteration
Unlike external iteration with an Iterator , streams use internal iteration where the processing logic is declared and the stream handles element traversal, often yielding better performance for large data sets.
Advantages Over Traditional Collections
No storage: Streams do not store elements; they generate values on demand from the source.
Functional style: Operations produce new results without mutating the original data source.
Lazy evaluation: Most operations are evaluated lazily, allowing short‑circuiting and reduced passes over data.
Unbounded streams: Streams can represent infinite sequences, enabling operations like continual filtering until a condition is met.
Concise code: Stream pipelines often replace verbose iterator loops with readable, chainable statements.
Efficiency Comparison Between Stream and Iterator
Benchmark tests were performed on an Ubuntu 16.04 machine (Intel Core i7‑8550U, 16 GB RAM, JDK 1.8.0_151) comparing stream, iterator, and parallel stream implementations across six scenarios: mapping, filtering, sorting, reduction, string joining, and mixed operations. The test code and environment settings are shown below.
System:Ubuntu 16.04 xenial
CPU:Intel Core i7-8550U
RAM:16GB
JDK version:1.8.0_151
JVM:HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode)
-Jms1024m
-Xmx6144m
-XX:MaxMetaspaceSize=512m
-XX:ReservedCodeCacheSize=1024m
-XX:+UseConcMarkSweepGC
-XX:SoftRefLRUPolicyMSPerMB=1001. Mapping Test
Increment each integer in a List<Integer> by one and collect the results.
// stream
List
result = list.stream()
.mapToInt(x -> x)
.map(x -> ++x)
.boxed()
.collect(Collectors.toCollection(ArrayList::new));
// iterator
List
result = new ArrayList<>();
for (Integer e : list) {
result.add(++e);
}
// parallel stream
List
result = list.parallelStream()
.mapToInt(x -> x)
.map(x -> ++x)
.boxed()
.collect(Collectors.toCollection(ArrayList::new));2. Filtering Test
Select elements greater than 200 from the list.
// stream
List
result = list.stream()
.mapToInt(x -> x)
.filter(x -> x > 200)
.boxed()
.collect(Collectors.toCollection(ArrayList::new));
// iterator
List
result = new ArrayList<>(list.size());
for (Integer e : list) {
if (e > 200) {
result.add(e);
}
}
// parallel stream
List
result = list.parallelStream()
.mapToInt(x -> x)
.filter(x -> x > 200)
.boxed()
.collect(Collectors.toCollection(ArrayList::new));3. Sorting Test
Sort the list using natural order.
// stream
List
result = list.stream()
.mapToInt(x -> x)
.sorted()
.boxed()
.collect(Collectors.toCollection(ArrayList::new));
// iterator
List
result = new ArrayList<>(list);
Collections.sort(result);
// parallel stream
List
result = list.parallelStream()
.mapToInt(x -> x)
.sorted()
.boxed()
.collect(Collectors.toCollection(ArrayList::new));4. Reduction Test
Find the maximum value in the list.
// stream
int max = list.stream()
.mapToInt(x -> x)
.max()
.getAsInt();
// iterator
int max = -1;
for (Integer e : list) {
if (e > max) {
max = e;
}
}
// parallel stream
int max = list.parallelStream()
.mapToInt(x -> x)
.max()
.getAsInt();5. String Joining Test
Join list elements into a comma‑separated string.
// stream
String result = list.stream().map(String::valueOf).collect(Collectors.joining(","));
// iterator
StringBuilder builder = new StringBuilder();
for (Integer e : list) {
builder.append(e).append(",");
}
String result = builder.length() == 0 ? "" : builder.substring(0, builder.length() - 1);
// parallel stream
String result = list.stream().map(String::valueOf).collect(Collectors.joining(","));6. Mixed Operations Test
Filter nulls, increment, filter >200, remove duplicates, and collect.
// stream
List
result = list.stream()
.filter(Objects::nonNull)
.mapToInt(x -> x + 1)
.filter(x -> x > 200)
.distinct()
.boxed()
.collect(Collectors.toCollection(ArrayList::new));
// iterator
HashSet
set = new HashSet<>(list.size());
for (Integer e : list) {
if (e != null && e > 200) {
set.add(e + 1);
}
}
List
result = new ArrayList<>(set);
// parallel stream
List
result = list.parallelStream()
.filter(Objects::nonNull)
.mapToInt(x -> x + 1)
.filter(x -> x > 200)
.distinct()
.boxed()
.collect(Collectors.toCollection(ArrayList::new));The benchmark results show that for small data sizes (≤ 1 000), traditional iterator loops are slightly faster, but the difference is negligible in real‑world business scenarios. For larger data sets (≥ 10 000), streams—especially parallel streams on multi‑core CPUs—outperform iterators. However, parallel streams add overhead and may be slower on single‑core machines.
Recommendations for Using Streams
Use iterator for simple, single‑step loops; prefer streams for multi‑step pipelines to gain readability with minimal performance loss.
Avoid parallel streams on single‑core CPUs; employ them on multi‑core systems with large data volumes.
When a pipeline involves boxed types, convert to primitive streams (e.g., IntStream ) before intermediate operations to reduce boxing/unboxing overhead.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.