Java Stream vs Iterator: Performance Benchmarks and Best Practices

This article explains Java 8 Stream fundamentals, compares intermediate and terminal operations, outlines stream characteristics and advantages over collections, and presents detailed benchmark results comparing Stream, parallel Stream, and traditional iterator across various data‑processing tasks.

Programmer DD
Programmer DD
Programmer DD
Java Stream vs Iterator: Performance Benchmarks and Best Practices

Stream is a key abstraction added in Java SE 8, defined in java.util.stream, representing object streams ( Stream<T>) and specialized streams such as IntStream, LongStream, and DoubleStream.

Java 8 introduced Stream to replace some Collection operations; each stream represents a sequence of values and provides common aggregation operations, allowing convenient processing of collections, arrays, and other data structures.

Stream operation types

Intermediate operations

All operations applied after the data source is placed on the pipeline are called intermediate operations.

Intermediate operations return a stream, allowing chaining.

Examples include filter, distinct, map, sorted, etc.

Terminal operations

After all intermediate operations, a terminal operation is needed to retrieve data from the pipeline.

Terminal operations can produce a result or convert it to a collection, array, String, etc.

Characteristics of streams

Can be traversed only once

Once elements have passed through the pipeline, they cannot be operated on again; a new stream must be created from the source.

Uses internal iteration

Unlike external iteration with Iterator, stream processing is performed internally, which is more efficient for large data sets.

Advantages of streams over collections

No storage: streams do not store values; elements come from the source.

Functional style: operations produce results without modifying the source.

Lazy evaluation: many operations are lazy, enabling short‑circuiting.

Unbounded: streams can represent infinite sequences.

Concise code: stream pipelines are often more readable than traditional loops.

Stream vs Iterator efficiency comparison

Conclusion: traditional iterator (for‑loop) outperforms Stream for small data sizes; parallel streams excel for large data on multi‑core CPUs.

Test environment

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)
JVM Settings:
    -Xms1024m
    -Xmx6144m
    -XX:MaxMetaspaceSize=512m
    -XX:ReservedCodeCacheSize=1024m
    -XX:+UseConcMarkSweepGC
    -XX:SoftRefLRUPolicyMSPerMB=100

1. Mapping test

Increment each element of a List<Integer> by 1 and collect into a new list. Benchmarked with Stream, iterator, and parallel Stream.

//stream
List<Integer> result = list.stream()
    .mapToInt(x -> x)
    .map(x -> ++x)
    .boxed()
    .collect(Collectors.toCollection(ArrayList::new));

//iterator
List<Integer> result = new ArrayList<>();
for (Integer e : list) {
    result.add(++e);
}

//parallel stream
List<Integer> result = list.parallelStream()
    .mapToInt(x -> x)
    .map(x -> ++x)
    .boxed()
    .collect(Collectors.toCollection(ArrayList::new));

2. Filtering test

Filter elements greater than 200 from a List<Integer> and collect.

//stream
List<Integer> result = list.stream()
    .mapToInt(x -> x)
    .filter(x -> x > 200)
    .boxed()
    .collect(Collectors.toCollection(ArrayList::new));

//iterator
List<Integer> result = new ArrayList<>(list.size());
for (Integer e : list) {
    if (e > 200) {
        result.add(e);
    }
}

//parallel stream
List<Integer> result = list.parallelStream()
    .mapToInt(x -> x)
    .filter(x -> x > 200)
    .boxed()
    .collect(Collectors.toCollection(ArrayList::new));

3. Natural sorting test

Sort a List<Integer> using Stream and iterator (Collections.sort).

//stream
List<Integer> result = list.stream()
    .mapToInt(x -> x)
    .sorted()
    .boxed()
    .collect(Collectors.toCollection(ArrayList::new));

//iterator
List<Integer> result = new ArrayList<>(list);
Collections.sort(result);

//parallel stream
List<Integer> result = list.parallelStream()
    .mapToInt(x -> x)
    .sorted()
    .boxed()
    .collect(Collectors.toCollection(ArrayList::new));

4. Reduction test

Find the maximum value in a List<Integer>.

//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<Integer> 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, remove duplicates, map, filter, and collect.

//stream
List<Integer> result = list.stream()
    .filter(Objects::nonNull)
    .mapToInt(x -> x + 1)
    .filter(x -> x > 200)
    .distinct()
    .boxed()
    .collect(Collectors.toCollection(ArrayList::new));

//iterator
HashSet<Integer> set = new HashSet<>(list.size());
for (Integer e : list) {
    if (e != null && e > 200) {
        set.add(e + 1);
    }
}
List<Integer> result = new ArrayList<>(set);

//parallel stream
List<Integer> result = list.parallelStream()
    .filter(Objects::nonNull)
    .mapToInt(x -> x + 1)
    .filter(x -> x > 200)
    .distinct()
    .boxed()
    .collect(Collectors.toCollection(ArrayList::new));

Experimental results summary

For small data (size ≤ 1000), iterator is faster, but the difference is sub‑millisecond and streams provide cleaner code.

For large data (size > 10000), streams, especially parallel streams on multi‑core CPUs, outperform iterator.

Parallel streams depend heavily on CPU core availability; without multiple cores, they may be slower due to ForkJoinPool overhead.

Recommendations for using streams

Use iterator for simple loops; use streams for multi‑step pipelines to gain readability with minimal performance loss.

Avoid parallel streams on single‑core CPUs; prefer them on multi‑core machines with large data sets.

When streams involve boxed types, convert to primitive streams before intermediate operations to reduce boxing overhead.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaBenchmarkIteratorStream APIParallel Stream
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.