Core Java

Java Stream vs. Flux.fromIterable()

In Java, both Stream and Flux.fromIterable() allow us to process sequences of data in a functional style. However, they are different in design and behaviour, especially regarding synchronous vs. asynchronous execution, consumption rules, and reactive programming support. This article will explore some distinctions between Java Stream and Flux.fromIterable()

1. Synchronous vs. Asynchronous Processing

A key difference between the two is that Flux operates asynchronously and allows multiple subscribers to re-consume the data stream, whereas Stream is synchronous and can be consumed only once, making it more limited in reactive or multi-consumer scenarios.

Let’s start by looking at simple usage examples for each.

2. Java Stream – Synchronous

Java Stream, introduced in Java 8, is designed for processing collections of data in a functional and declarative style. It operates in a synchronous and blocking manner, meaning each element in the stream is processed one after the other, and the thread executing the stream waits for each operation to complete. This makes Java Stream a natural fit for CPU-bound, in-memory transformations where responsiveness and concurrency are not primary concerns.

Java Stream Example

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class StreamVsFluxDemo {
 
    public static void main(String[] args) {
        List<Product> products = List.of(
                new Product("Laptop", 999.99),
                new Product("Smartphone", 699.49),
                new Product("Monitor", 199.99)
        );
 
        Stream<Product> productStream = products.stream();
 
        // First consumption
        productStream.forEach(p -> System.out.println("Stream = " + p));
 
        // Second consumption - this will cause an exception
        productStream.forEach(p -> System.out.println("Stream again = " + p));
    }
}
 
class Product {
 
    String name;
    double price;
 
    Product(String name, double price) {
        this.name = name;
        this.price = price;
    }
 
    @Override
    public String toString() {
        return name + " - $" + price;
    }
}

Output:

Result of the Java Stream example in the Java Stream vs Flux.fromIterable comparison

Streams in Java can only be consumed once. After a terminal operation like forEach() is executed, the stream is considered closed, and any further attempt to reuse it will result in an IllegalStateException. This makes Java Stream suitable for straightforward, one-pass data processing.

3. Flux.fromIterable() – Reactive and Asynchronous

Flux is a key component in Project Reactor, which powers reactive programming in frameworks like Spring WebFlux. Unlike Java Stream, Flux.fromIterable() is asynchronous, non-blocking, and supports backpressure. It is designed to handle dynamic, high-throughput, event-driven data processing where data can be emitted over time and consumed by multiple subscribers.

Flux Example

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class StreamVsFluxExample2 {
 
    public static void main(String[] args) {
        List<Product> products = List.of(
                new Product("Laptop", 999.99),
                new Product("Smartphone", 699.49),
                new Product("Monitor", 199.99)
        );
 
        Flux<Product> productFlux = Flux.fromIterable(products);
 
        // First subscription
        productFlux.subscribe(p -> System.out.println("Flux 1 = " + p));
 
        // Second subscription
        productFlux.subscribe(p -> System.out.println("Flux 2 = " + p));
    }
}
 
class Product {
 
    String name;
    double price;
 
    Product(String name, double price) {
        this.name = name;
        this.price = price;
    }
 
    @Override
    public String toString() {
        return name + " - $" + price;
    }
}

Output:

1
2
3
4
5
6
Flux 1 = Laptop - $999.99
Flux 1 = Smartphone - $699.49
Flux 1 = Monitor - $199.99
Flux 2 = Laptop - $999.99
Flux 2 = Smartphone - $699.49
Flux 2 = Monitor - $199.99

Flux can be resubscribed multiple times without error. This characteristic is useful in reactive systems where multiple consumers or downstream processors might be interested in the same data stream. Moreover, being non-blocking, Flux operates asynchronously and allows composition, filtering, and transformation with reactive operators like map(), flatMap(), zip(), etc.

4. Error Handling – Java Stream vs Flux.fromIterable()

One critical aspect of any data processing pipeline is how it handles errors. While both Java Stream and Flux can encounter runtime exceptions, their mechanisms for handling these errors differ significantly in terms of flexibility, declarative syntax, and control.

4.1 Error Handling in Java Stream

In Java Stream, error handling is done using traditional try-catch blocks within lambda expressions or surrounding the entire stream pipeline.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
public class StreamErrorExample {
 
    public static void main(String[] args) {
        List<Product> products = List.of(
            new Product("Laptop", 999.99),
            new Product("Smartphone", -1),  // Invalid price
            new Product("Monitor", 199.99)
        );
 
        products.stream()
            .forEach(p -> {
                try {
                    if (p.price < 0) {
                        throw new IllegalArgumentException("Invalid price for product: " + p.name);
                    }
                    System.out.println("Stream = " + p);
                } catch (Exception e) {
                    System.out.println("Error in Stream: " + e.getMessage());
                }
            });
    }
}

Output:

1
2
3
Stream = Laptop - $999.99
Error in Stream: Invalid price for product: Smartphone
Stream = Monitor - $199.99

Java Stream does not have built-in support for error propagation or recovery. We must manually catch and handle exceptions inline, which can clutter the business logic and reduce code readability.

4.2 Error Handling in Flux.fromIterable()

Flux provides a more elegant and declarative way to handle errors using operators such as onErrorResume, onErrorReturn, and doOnError. These allow us to reactively manage errors without breaking the pipeline.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
public class FluxErrorExample {
 
    public static void main(String[] args) {
        List<Product> products = List.of(
                new Product("Laptop", 999.99),
                new Product("Smartphone", -1), // Invalid price
                new Product("Monitor", 199.99)
        );
 
        Flux.fromIterable(products)
                .map(p -> {
                    if (p.price < 0) {
                        throw new IllegalArgumentException("Invalid price for product: " + p.name);
                    }
                    return p;
                })
                .onErrorContinue((error, obj) -> {
                    System.out.println("Error in Flux: " + error.getMessage());
                })
                .subscribe(p -> System.out.println("Flux = " + p));
    }
}

Output:

1
2
3
Flux = Laptop - $999.99
Error in Flux: Invalid price for product: Smartphone
Flux = Monitor - $199.99

In this example, onErrorContinue allows the pipeline to gracefully handle the error and skip the problematic item, without halting the entire stream. This makes Flux more reliable and flexible, especially in systems that need to keep running smoothly even when something goes wrong.

4. Similarities Between Java Stream and Flux.fromIterable()

At a high level, both Java Stream and Flux allow us to process a sequence of elements using declarative operations like map, filter, and collect.

Let’s consider a use case of converting a list of integers to their squares and filtering out even results.

Using Java Stream

01
02
03
04
05
06
07
08
09
10
11
12
13
public class StreamExample {
 
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
 
        List<Integer> result = numbers.stream()
                .map(n -> n * n)
                .filter(n -> n % 2 != 0)
                .collect(Collectors.toList());
 
        System.out.println("Stream Result: " + result);
    }
}

Using Flux.fromIterable()

01
02
03
04
05
06
07
08
09
10
11
12
public class FluxExample {
 
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
 
        Flux.fromIterable(numbers)
                .map(n -> n * n)
                .filter(n -> n % 2 != 0)
                .collectList()
                .subscribe(result -> System.out.println("Flux Result: " + result));
    }
}

Output for Both:

1
[1, 9, 25]

In both examples, the list is processed using map and filter, and the results are gathered back into a new list. This shared syntax is often the first thing we notice when comparing the two approaches.

5. Key Differences Summary

Below is a summary of the key differences between the two approaches.

FeatureJava StreamFlux.fromIterable()
Execution ModelSynchronousAsynchronous
BlockingYesNo (non-blocking)
Backpressure SupportNoYes
ConsumptionOne-time useMulti-subscriber capable
Use CaseCPU-bound, in-memory processingReactive, IO-bound, event-driven
Error HandlingLimited via try-catchRich error handling (onErrorResume, etc.)

5.1 When Should You Use Each?

Choosing between Java Stream and Flux.fromIterable() depends on the nature of your application and the behaviour you expect from your data processing pipeline. Both serve distinct purposes and shine in different contexts.

Use Java Stream When

  • You are working with a finite dataset in memory and need simple, sequential operations.
  • Your use case is synchronous, such as data transformation or aggregation in a single-threaded context.
  • You only need to consume the data once, and performance or scalability under concurrent load isn’t a concern.
  • Your application doesn’t require reactive features such as backpressure, non-blocking execution, or asynchronous composition.

Use Flux.fromIterable() When

  • You need asynchronous or non-blocking data processing, such as in reactive web applications or event-driven systems.
  • The data source is potentially infinite or slow-producing, like web sockets, file streams, or message queues.
  • You want to support multiple subscribers or reactively compose multiple operations across different threads.
  • You require built-in error recovery, retries, or fallback mechanisms as part of your data pipeline.

6. Conclusion

In this article, we compared Java Stream vs Flux.fromIterable(), highlighting their core differences in execution model, consumption behaviour, and error handling. While Java Stream provides a simple and synchronous way to process collections, it is limited to one-time use and lacks built-in support for reactive programming patterns. On the other hand, Flux.fromIterable() offers an asynchronous alternative that supports multiple subscribers, non-blocking execution, and advanced error recovery strategies.

7. Download the Source Code

This article explored the key differences in Java Stream vs. Flux.fromIterable().

Download
You can download the full source code of this example here: java stream vs flux fromiterable

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest


This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button