Index

Reactive Streams Introduction

Java Streams

19.1 Overview of Reactive Programming

Reactive programming is a paradigm focused on asynchronous data flow, non-blocking execution, and responsive systems that can handle data streams dynamically and efficiently. Unlike traditional programming models that are often synchronous and blocking, reactive programming embraces events and streams of data that can be processed as they arrive, enabling highly scalable and resilient applications.

Core Principles of Reactive Programming

The Reactive Streams Specification

To standardize reactive programming in Java, the Reactive Streams specification defines a minimal API to handle asynchronous stream processing with backpressure. This specification is the foundation for many reactive libraries and is included as the java.util.concurrent.Flow API in Java 9+.

Key Components and Terminology

Conceptual Flow: Reactive vs. Synchronous

Synchronous Model Reactive Streams Model
Caller requests data and waits (blocking). Data flows asynchronously without blocking.
Data is pulled on demand or pushed without control. Backpressure allows subscriber to control demand.
Sequential, step-by-step execution. Event-driven, asynchronous processing pipeline.

Visualizing Reactive Streams Flow

Publisher  --->  Subscription  --->  Subscriber
   |                ↑                 |
   |                |   request(n)    |
   |--------------> |-----------------|
   |                |                 |
   | emit data (onNext)               |
   |--------------> |-----------------|

In essence, reactive programming transforms how we deal with data streams by making them asynchronous, non-blocking, and controllable through backpressure, enabling systems that are highly responsive and resilient to varying workloads. This new paradigm addresses the limitations of traditional synchronous or event-driven push/pull models by explicitly coordinating the data flow between producers and consumers.

Index

19.2 Differences Between Java Streams and Reactive Streams

Java Streams and Reactive Streams both deal with processing sequences of data, but they differ fundamentally in design, execution model, and use cases. Understanding these differences helps choose the right tool for your needs.

Conceptual Differences

Feature Java Streams Reactive Streams
Model Pull-based (consumer requests data) Push-based (producer pushes data)
Execution Synchronous and blocking Asynchronous and non-blocking
Data Size Finite, typically eager processing Potentially infinite or unbounded
Evaluation Lazy intermediate ops, eager terminal op Fully asynchronous, event-driven
Error Handling Errors thrown immediately, stopping the pipeline Errors propagated asynchronously through callbacks
Backpressure Support None (consumer pulls at its own pace) Built-in backpressure controls data flow
Lifecycle Single-use pipeline, terminates after terminal op Long-lived, supports subscription, cancellation, multiple events
Parallelism Supports parallel streams via parallel() Designed for concurrent, non-blocking streams

Key Technical Differences

Code Contrast: Filtering and Summing Integers

Java Streams (Synchronous, Pull-based):

List<Integer> numbers = List.of(1, 2, 3, 4, 5);

int sum = numbers.stream()
    .filter(n -> n % 2 == 0)
    .mapToInt(Integer::intValue)
    .sum();

System.out.println("Sum of even numbers: " + sum);

Reactive Streams (Flow API, Asynchronous, Push-based):

import java.util.concurrent.SubmissionPublisher;
import java.util.concurrent.Flow.*;

public class ReactiveExample {
    public static void main(String[] args) throws InterruptedException {
        SubmissionPublisher<Integer> publisher = new SubmissionPublisher<>();

        Subscriber<Integer> subscriber = new Subscriber<>() {
            private Subscription subscription;
            private int sum = 0;

            @Override
            public void onSubscribe(Subscription subscription) {
                this.subscription = subscription;
                subscription.request(1); // Request one item
            }

            @Override
            public void onNext(Integer item) {
                if (item % 2 == 0) {
                    sum += item;
                }
                subscription.request(1); // Request next item
            }

            @Override
            public void onError(Throwable throwable) {
                throwable.printStackTrace();
            }

            @Override
            public void onComplete() {
                System.out.println("Sum of even numbers: " + sum);
            }
        };

        publisher.subscribe(subscriber);

        // Publish numbers asynchronously
        for (int i = 1; i <= 5; i++) {
            publisher.submit(i);
        }
        publisher.close();

        Thread.sleep(100); // Give time for async processing
    }
}

Summary

Aspect Java Streams Reactive Streams
Data Flow Pull-based, synchronous Push-based, asynchronous
Data Source Finite collections/arrays Potentially infinite streams
Execution Model Lazy intermediates, eager terminal Fully async with backpressure
Error Handling Immediate exceptions Asynchronous error callbacks
Lifecycle Single use pipeline Long-lived, subscribable stream
Use Case Examples Batch data processing Event-driven, live data streams

Java Streams excel in simple, synchronous batch processing, while Reactive Streams provide powerful tools for asynchronous, event-driven, and scalable stream processing—especially suited to modern distributed and interactive systems.

Index

19.3 Simple Examples Using Flow API (Java 9+)

Java 9 introduced the java.util.concurrent.Flow API to provide a standard interface for reactive streams. This API defines four key interfaces:

Here, we'll focus on implementing a simple reactive stream pipeline by creating a custom Publisher and Subscriber and connecting them.

Basic Flow

  1. Publisher sends data asynchronously.
  2. Subscriber subscribes to Publisher and requests data.
  3. Subscription controls the flow, allowing Subscriber to request or cancel.
  4. Data and signals flow via onNext(), onError(), and onComplete() methods.

Example 1: StringPublisher and LoggingSubscriber

This minimal example demonstrates a publisher sending a fixed list of strings to a subscriber that logs received messages.

import java.util.List;
import java.util.concurrent.Flow.*;
import java.util.concurrent.SubmissionPublisher;

public class SimpleFlowExample {
    public static void main(String[] args) throws InterruptedException {
        // Publisher that asynchronously submits strings
        SubmissionPublisher<String> publisher = new SubmissionPublisher<>();

        // Subscriber that logs received items
        Subscriber<String> subscriber = new Subscriber<>() {
            private Subscription subscription;

            @Override
            public void onSubscribe(Subscription subscription) {
                this.subscription = subscription;
                subscription.request(1);  // Request the first item
            }

            @Override
            public void onNext(String item) {
                System.out.println("Received: " + item);
                subscription.request(1);  // Request next item after processing
            }

            @Override
            public void onError(Throwable throwable) {
                System.err.println("Error occurred: " + throwable.getMessage());
            }

            @Override
            public void onComplete() {
                System.out.println("All items processed.");
            }
        };

        // Connect subscriber to publisher
        publisher.subscribe(subscriber);

        // Publish some items asynchronously
        List.of("Hello", "Reactive", "Streams", "with", "Flow API")
            .forEach(publisher::submit);

        // Close the publisher to signal end of stream
        publisher.close();

        // Wait briefly to allow async processing to complete
        Thread.sleep(1000);
    }
}

Explanation:

Example 2: Custom Publisher and Subscriber

To understand internals, here is a minimal custom Publisher and Subscriber illustrating manual data flow control.

import java.util.concurrent.Flow.*;

public class ManualFlowExample {
    public static void main(String[] args) {
        Publisher<String> publisher = new Publisher<>() {
            @Override
            public void subscribe(Subscriber<? super String> subscriber) {
                subscriber.onSubscribe(new Subscription() {
                    private boolean completed = false;
                    @Override
                    public void request(long n) {
                        if (!completed) {
                            subscriber.onNext("Custom");
                            subscriber.onNext("Flow");
                            subscriber.onNext("Example");
                            subscriber.onComplete();
                            completed = true;
                        }
                    }
                    @Override
                    public void cancel() {
                        System.out.println("Subscription cancelled");
                    }
                });
            }
        };

        Subscriber<String> subscriber = new Subscriber<>() {
            private Subscription subscription;
            @Override
            public void onSubscribe(Subscription subscription) {
                this.subscription = subscription;
                subscription.request(1);
            }
            @Override
            public void onNext(String item) {
                System.out.println("Received: " + item);
                subscription.request(1);
            }
            @Override
            public void onError(Throwable throwable) {
                System.err.println("Error: " + throwable.getMessage());
            }
            @Override
            public void onComplete() {
                System.out.println("Processing complete.");
            }
        };

        publisher.subscribe(subscriber);
    }
}

Explanation:

Summary

With these basics, you can build scalable reactive data flows integrated into modern Java applications.

Index