Wednesday, 24 April 2024

Understanding Intermediate and Terminal Operations in Stream Processing

Java streams, introduced in Java 8, improvise how the collections of objects are processed. Streams represent a sequence of elements that can be processed using operations provided by the Stream API. In general, streams act as wrappers around a data source like an array, collection, or I/O channel.

 

How do streams streamline processing?

Stream processing entails below steps:

a.   creating a stream from a data source such as collections, arrays etc.,

b.   Apply a sequence of intermediate operations—like filter(), map(), and reduce()—to manipulate the elements.

Finally, use a terminal operation such as collect() or forEach() to yield the desired outcome.


Stream advantages

a.   Conciseness: Stream operations can  represent the complex data processing logic in just a few lines of code, unlike traditional loops.

b.   Immutability: Streams do not modify/alter the original data source, encouraging immutability and enhancing code safety.

c.    Parallelization: Stream operations have the potential to be executed in parallel, leading to better performance on processors with multiple cores.

 

Intermediate and terminal operations

In Java streams, operations can be categorized into two types: intermediate and terminal. Intermediate operations transform one stream into another, processing elements one by one in a lazy manner. These operations have no effect until the pipeline starts. On the other hand, terminal operations mark the end of the stream lifecycle and initiate the pipeline's execution. In a stream pipeline, composed of a source, zero or more intermediate operations, and a terminal operation, the computation is performed lazily, only when the terminal operation is invoked.

 

HelloWorld.java

package com.sample.app.streams;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class HelloWorld {

	public static void main(String args[]) {
		List<String> names = Arrays.asList("Thulasi", "Ram", "Hari", "Chamu", "Tataji");

		List<String> namesStartsWithTAndInUppercase = names.stream().filter(name -> name.startsWith("T"))
				.map(name -> name.toUpperCase()).collect(Collectors.toList());

		namesStartsWithTAndInUppercase.forEach(System.out::println);

	}

}

 

Output

THULASI
TATAJI

 

List<String> namesStartsWithTAndInUppercase = names
    .stream()
    .filter(name -> name.startsWith("T"))
    .map(name -> name.toUpperCase())
    .collect(Collectors.toList());

 

Above Java code snippet demonstrates the usage of Java streams to filter and transform a list of names.

 

Let's break it down.

a.   stream(): This is a terminal operation that converts the names list into a stream. It marks the beginning of the stream pipeline.

b.   filter(): This is an intermediate operation that filters the stream based on the given predicate. In this case, it filters names that start with the letter "T".

c.    map(): This is another intermediate operation that transforms each element of the stream. Here, it converts each name to uppercase using the toUpperCase() method.

d.   collect(Collectors.toList()): This is a terminal operation that collects the elements of the stream into a list. It consumes the stream and produces a new list containing the filtered and transformed elements.

So, in summary:

 

a.   Intermediate operations: filter() and map()

b.   Terminal operation: collect(Collectors.toList())

 

Intermediate operations will not get triggered until the terminal operation is initiated

 

PeekDemo.java
package com.sample.app.streams;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class PeekDemo {

	public static void main(String args[]) {
		List<String> names = Arrays.asList("Thulasi", "Ram", "Hari", "Chamu", "Tataji");

		Stream<String> namesStartsWithTAndInUppercase = names
				.stream()
				.peek(ele -> System.out.println("Processing the element " + ele))
				.filter(name -> name.startsWith("T"))
				.peek(ele -> System.out.println("\tElement received after applying the filter " + ele))
				.map(name -> name.toUpperCase())
				.peek(ele -> System.out.println("\tElement received after applying the transformation " + ele));

		System.out.println("Streams is defined with intermittent operations");
	
		namesStartsWithTAndInUppercase.forEach(ele -> System.out.println("Final element : " + ele + "\n"));

	}

}

Output

Streams is defined with intermittent operations
Processing the element Thulasi
	Element received after applying the filter Thulasi
	Element received after applying the transformation THULASI
Final element : THULASI

Processing the element Ram
Processing the element Hari
Processing the element Chamu
Processing the element Tataji
	Element received after applying the filter Tataji
	Element received after applying the transformation TATAJI
Final element : TATAJI

In this Java code snippet, a stream pipeline is defined for processing a list of names. The pipeline includes intermediate operations such as peek() to log information about each element as it passes through the stream, filter() to select only names starting with "T", and map() to convert each name to uppercase. However, despite defining these intermediate operations, no processing occurs until a terminal operation is invoked. Only when forEach() is called to print the final elements do the intermediate operations get triggered, executing the pipeline and producing the desired output. This highlights the lazy evaluation nature of Java streams, where intermediate operations are deferred until a terminal operation initiates the stream processing.

 

Previous                                                 Next                                                 Home

No comments:

Post a Comment