DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Java Stream API: 3 Things Every Developer Should Know About
  • The Long Road to Java Virtual Threads
  • Java Virtual Threads and Scaling
  • Java’s Next Act: Native Speed for a Cloud-Native World

Trending

  • Hybrid Cloud vs Multi-Cloud: Choosing the Right Strategy for AI Scalability and Security
  • My LLM Journey as a Software Engineer Exploring a New Domain
  • While Performing Dependency Selection, I Avoid the Loss Of Sleep From Node.js Libraries' Dangers
  • Solid Testing Strategies for Salesforce Releases
  1. DZone
  2. Coding
  3. Java
  4. Understanding Lazy Evaluation in Java Streams

Understanding Lazy Evaluation in Java Streams

Java Streams offer a high-level, declarative approach to data processing, but one of their most intriguing features is lazy evaluation.

By 
Andrei Tuchin user avatar
Andrei Tuchin
DZone Core CORE ·
Jan. 18, 24 · Tutorial
Likes (7)
Comment
Save
Tweet
Share
18.3K Views

Join the DZone community and get the full member experience.

Join For Free

Java Streams, introduced in Java 8, have revolutionized how we handle collections of data in Java. They offer a high-level, declarative approach to data processing, but one of their most intriguing features is lazy evaluation. This article delves into what lazy evaluation means in the context of Java Streams and why it's beneficial, accompanied by practical examples.

Basics of Java Streams

Java Streams provide a way to sequentially or parallelly process sequences of elements. A stream pipeline consists of a source (like collections), followed by zero or more intermediate operations and a terminal operation.

  • Intermediate Operations: These operations (such as filter, map, and sorted) transform the stream into another one and are lazy.
  • Terminal Operations: Operations (like forEach, collect, and reduce) that produce a result or a side-effect. After a terminal operation is performed, the stream can no longer be used.

What Is Lazy Evaluation?

Lazy evaluation means that the computation on the elements of the stream is only performed when it's necessary, usually at the point of the terminal operation. This is in contrast to eager evaluation, where computations are performed immediately.

Lazy Evaluation in Java Streams

In Java Streams, intermediate operations are not executed until a terminal operation is invoked. This approach can optimize performance, especially for large datasets, by reducing the number of iterations and computations.

The Role of JVM

The JVM plays a crucial role in orchestrating lazy evaluation within the Java stream pipeline. Here's how it works internally:

  1. Stream Initialization: When you create a stream, such as stream() on a collection, the JVM sets up the initial configuration for the stream, including a reference to the source data (e.g., a collection or an array).

  2. Intermediate Operations: When you chain intermediate operations (e.g., filter, map) on the stream, the JVM builds a pipeline of operations by creating a new stream object that is associated with the previous one. However, it does not perform any computation at this stage.

  3. Terminal Operation Invocation: Lazy evaluation comes into play when a terminal operation is invoked. At this point, the JVM triggers the entire pipeline to start processing. It does so by traversing the pipeline from the source to the terminal operation and applying the intermediate operations as it goes along.

  4. Processing Elements: While processing the elements, the JVM optimizes performance and memory usage by fetching and processing one element at a time. This ensures that unnecessary elements are not loaded into memory, especially when dealing with large collections.

  5. Short-Circuiting: For operations that support short-circuiting, such as findFirst or limit, the JVM stops processing as soon as the desired condition is met. This behavior reduces unnecessary computation, making the stream processing more efficient.

Example 1: Basic Lazy Operation

Java
 
List<String> strings = Arrays.asList("one", "two", "three", "four");
Stream<String> longStringsStream = strings.stream().filter(s -> {
    System.out.println("Filtering: " + s);
    return s.length() > 3;
});
System.out.println("Stream created, filter not applied yet!");
longStringsStream.forEach(System.out::println);

In this example, the filter operation only gets executed when the forEach terminal operation starts. 

Example 2: Combining Multiple Lazy Operations

Java
 
strings.stream()
       .filter(s -> {
           System.out.println("Filter: " + s);
           return s.length() > 3;
       })
       .map(s -> {
           System.out.println("Map: " + s);
           return s.toUpperCase();
       })
       .forEach(s -> System.out.println("Processed: " + s));

Here, each element goes through filter and then map, but only when the forEach operation is executing.

Example 3: Infinite Streams

Java
 
Stream.iterate(0, n -> n + 1)
      .filter(n -> n % 2 == 0)
      .limit(10)
      .forEach(System.out::println);

This example creates an infinite stream of natural numbers, filters even numbers, and limits the output to the first 10 even numbers.

Example 4: Terminal Operation (Execution Trigger)

Lazy evaluation continues until a terminal operation is invoked. Terminal operations are actions that trigger the processing of the data. Examples include collect, forEach, and reduce.

When a terminal operation is called, the JVM starts the data processing pipeline, and the following happens:

  • The JVM begins iterating over the source data (e.g., the list of numbers).
  • It applies the recorded intermediate operations one by one in the order they were specified.
  • The result is computed and returned, or the final action specified in the terminal operation is executed.
Java
 
List<Integer> result = filteredStream.collect(Collectors.toList());

Here, collect is a terminal operation that triggers the execution of the entire pipeline. The JVM iterates through the source list, applies the filter and map transformations, and collects the filtered and mapped values into a new list. 

Example 5: Short-Circuiting (Efficiency)

Java streams also support short-circuiting operations. These operations stop processing as soon as a certain condition is met. For example, findFirst, findAny, and limit are short-circuiting operations.

Java
 
Optional<Integer> firstEven = numbers.stream()
    .filter(n -> n % 2 == 0)
    .findFirst();

In this case, if an even number is found early in the stream, the processing stops immediately, which is an efficiency optimization. 

Example 6: Custom Spliterators

Custom Spliterators allow you to specify how a stream should be divided into smaller segments for parallel processing. To illustrate this concept, let's create an example of a custom Spliterator for a data structure called "Range," representing a range of integers. We aim to create a stream that encompasses all the numbers within this range and split it into smaller parts to enable parallel processing. Achieving this involves implementing a custom Spliterator for the "Range" data structure.

Imagine you have a unique data structure known as "Range," which defines a span of integers with a starting and ending point. The objective is to construct a stream that covers all the integers within this range and efficiently divide it into smaller segments to facilitate parallel computation. This can be accomplished by designing a custom Spliterator tailored to the "Range" data structure.

Java
 
import java.util.Spliterator;
import java.util.function.Consumer;

class Range {
    private final int start;
    private final int end;

    public Range(int start, int end) {
        this.start = start;
        this.end = end;
    }

    public int getStart() {
        return start;
    }

    public int getEnd() {
        return end;
    }
}

class RangeSpliterator implements Spliterator<Integer> {
    private final Range range;
    private int current;

    public RangeSpliterator(Range range) {
        this.range = range;
        this.current = range.getStart();
    }

    @Override
    public boolean tryAdvance(Consumer<? super Integer> action) {
        if (current <= range.getEnd()) {
            action.accept(current);
            current++;
            return true;
        }
        return false;
    }

    @Override
    public Spliterator<Integer> trySplit() {
        int mid = (current + range.getEnd()) / 2;
        if (current >= mid) {
            return null; // No more splitting
        }

        int start = current;
        int end = mid;
        current = mid + 1;

        return new RangeSpliterator(new Range(start, end));
    }

    @Override
    public long estimateSize() {
        return range.getEnd() - current + 1;
    }

    @Override
    public int characteristics() {
        return SIZED | SUBSIZED | NONNULL | IMMUTABLE;
    }
}

In this example:

  • "Range" represents a custom data structure denoting an integer range.
  • "RangeSpliterator" is a customized Spliterator responsible for segmenting the range into more manageable portions.

We can utilize this custom Spliterator to create a stream and process it concurrently:

Java
 
public class CustomSpliteratorExample {
    public static void main(String[] args) {
        Range range = new Range(1, 100);

        Stream<Integer> parallelStream = StreamSupport.stream(
            new RangeSpliterator(range), true); // We use 'true' to enable parallel processing

        parallelStream
            .parallel() // This line is optional but explicitly activates parallel processing
            .forEach(System.out::println);
    }
}

In summary, custom Spliterators complement lazy evaluation in Java streams by enabling efficient parallel processing and customization of how data is split and processed. Lazy evaluation ensures that transformations and operations are deferred until necessary, and custom Spliterators dictate how data should be partitioned for parallel execution, resulting in efficient and optimized stream processing. This synergy between custom Spliterators and lazy evaluation contributes to the overall efficiency and flexibility of Java stream operations.

Benefits and Considerations

  • Performance Optimization: For large datasets, only processing the required data can lead to significant performance improvements.
  • Memory Efficiency: Lazy evaluation allows for the processing of data streams that wouldn't fit into memory if fully realized.
  • Flexibility: You can build complex stream pipelines that are efficient and readable.
  • Caveats: The order of operations matters. Also, side effects should be avoided in lambda expressions used with streams.

Advanced Lazy Evaluation Techniques

  • Custom Lazy Operations: Advanced users can create their lazy operations by implementing custom Spliterators or using the flatMap operation creatively.
  • Infinite Streams: Lazy evaluation is what makes working with infinite streams possible. You can generate or iterate over an infinite stream, and the stream will only process the elements necessary for the terminal operation.

Real-World Applications

  • Data Processing: Lazy evaluation is ideal for scenarios like big data processing where datasets are too large to be processed in memory.
  • Web Services: When dealing with paginated results from web services, you can process only the required pages of results lazily.

Conclusion

Understanding and leveraging lazy evaluation in Java Streams is crucial for writing efficient, effective, and elegant Java code. It allows developers to write more expressive, concise, and performant data processing code, making Java an even more powerful tool for handling complex data processing tasks.

Evaluation Java virtual machine Java (programming language) Stream (computing) Java performance

Opinions expressed by DZone contributors are their own.

Related

  • Java Stream API: 3 Things Every Developer Should Know About
  • The Long Road to Java Virtual Threads
  • Java Virtual Threads and Scaling
  • Java’s Next Act: Native Speed for a Cloud-Native World

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!