Stream enables the concept of filtering where one method filters data and passes it to another method after processing. The intermediate operations transform a stream into another stream. Stream can only be used once, once a terminal operation (such as forEach, collect, or reduce) is called on a stream, the stream is consumed and cannot be reused.

A stream is not a data structure but take input from the Collections, Arrays, and I/O channels. They provide methods to create streams (e.g. stream()). Streams don’t change the original data structure. Instead, streams operate on the data source to produce a transformed or filtered result. By calling the stream() method on an appropriate data source, it will return a stream and you can perform a range of operations on the stream.

Terminal operations

For each

The forEach method in Java is a convenient way to iterate over elements in a collection or other data structures. It is introduced as a part of the stream API and the Iterable interface and is a terminal operation.

Warning

The semantics of forEach() is different when working on a stream than a collection. The Collection.forEach() uses the collection’s iterator (if one is specified), so the processing order of the items is defined. In contrast, the processing order of Collection.stream().forEach() is undefined.

default void forEach(Consumer<? super T> action)
@FunctionalInterface
public interface Consumer<T> { void accept(T t); }

The forEach() method takes a Consumer type. The Consumer is a functional interface that has a single method accept(). This method performs an operation on the given input argument of type T. Consider the below example that iterates through a list, we implement the consumer interface through anonymous inner class.

public class Demo{
	public static void main(String[] args) {
		List<Integer> nums = Arrays.asList(3, 4, 2, 7 ,5);
		
		Consumer<Integer> con = new Consumer<Integer>() {
			@Override
			public void accept(Integer n) {
				// In this example we just want to print each number
				System.out.println(n);
			}
		};
		// The forEach method will provide one value at a time to consumer object
		nums.forEach(con);
	}
}
3
4
2
7
5

Since the consumer interface is a functional interface, we can implement it using lambda expression.

public class Demo{
	public static void main(String[] args) {
		List<Integer> nums = Arrays.asList(3, 4, 2, 7 ,5);
		
		Consumer<Integer> con = n -> System.out.println(n);
		
		nums.forEach(con);
	}
}

We can further simply the code by passing the lambda expression as the forEach parameter.

public class Demo{
	public static void main(String[] args) {
		List<Integer> nums = Arrays.asList(3, 4, 2, 7 ,5);
		
		nums.forEach(n -> System.out.println(n));
	}
}

We can use method reference to further simply this expression:

public class Demo{
	public static void main(String[] args) {
		List<Integer> nums = Arrays.asList(3, 4, 2, 7 ,5);  
		
		nums.forEach(System.out::println);
	}
}

Reduce

The reduce() method is a terminal operation that combines the elements of a stream to create a single accumulated result by repeatedly applying a specified operation (e.g., summing numbers or concatenating strings).

With identity value

The reduce method can take a identity and a accumulator as parameter.

T reduce(T identity, BinaryOperator<T> accumulator);
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> { }
@FunctionalInterface  
public interface BiFunction<T, U, R> { R apply(T t, U u); }
  • Identity
    • Acts as a starting point and is also the default result for empty streams.
  • Accumulator
    • The accumulator is a function that combines two elements of the same type (current result and next element) and produce a result of the same type. The BinaryOperator functional interface a subtype of BiFunction and has a method apply() that takes the two elements.

This example adds the numbers in the list together.

public static void main(String[] args) {
	List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
	
	// Create the stream
	Stream<Integer> s1 = list.stream();  
	
	BinaryOperator<Integer> bo = new BinaryOperator<>() {  
		@Override  
		public Integer apply(Integer integer, Integer integer2) {  
			return integer+integer2;  
		}  
	};  
	
	// The identify value 0 serves as the starting point for the operation
	// The initial value is set as 0, which means the accumulation starts at 0
	int result = s1.reduce(0, bo);  
	System.out.println(result);  
}

This is how the reduction work, for each element in the stream, the reduce() method:

  1. Combines the current result (starting with the identity 0) with the next stream element using the BinaryOperator.
  2. The process continues for all elements in the stream.
  • Initial value = 0
  • Step 1: 0 + 1 = 1
  • Step 2: 1 + 2 = 3
  • Step 3: 3 + 3 = 6
  • Step 4: 6 + 4 = 10
  • Step 5: 10 + 5 = 15

The final result is 15.

As discussed before, the entire process can be more succinct using the lambda expression.

public static void main(String[] args){  
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);  
    Stream<Integer> s1 = list.stream();  
    BinaryOperator<Integer> bo = (integer, integer2) -> {  
		return integer+integer2;  
    };  
    int result = s1.reduce(0, bo);  
    System.out.println(result);  
}

We can further simply it by passing the expression as argument.

public static void main(String[] args){  
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);  
    Stream<Integer> s1 = list.stream();  
    int result = s1.reduce(0, (integer, integer2) -> integer + integer2);  
    System.out.println(result);  
}
15

This is an example only, stream provides a terser way to sum the numbers by mapping stream into a IntStream using mapToInt() method, and the IntStream provides a sum() method for calculating the sum of a number stream.

Without identity value

The reduce() method can be used without identity provided. When no identity value is provided, the method returns an Optional because there may not always be a result (e.g., when the stream is empty). If the stream has one element, that element becomes the result.

Optional<T> reduce(BinaryOperator<T> accumulator);

When no identity is provided:

  • The first element of the stream is treated as the initial value.
  • The accumulator is applied to combine elements from the stream.
public static void main(String[] args){  
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);  
    Optional<Integer> result = list.stream().reduce((a,b) -> a+b);  
    System.out.println(result.get());  
}
15

Intermediate operations

Filter

The filter method is used to process elements of a stream and produce a new stream consisting of elements that satisfy a given predicate.

Stream<T> filter(Predicate<? super T> predicate);
@FunctionalInterface  
public interface Predicate<T> { boolean test(T t); }

The Predicate is also a functional interface that represents a condition (boolean valued function). It takes one input and returns true if the input satisfies the condition, false otherwise. It works as described below.

  1. The filter method iterates over the elements of the stream.
  2. For each element, it evaluates the predicate condition.
  3. If the predicate returns true, the element is included in the resulting stream.
  4. If the predicate returns false, the element is excluded.
public class Demo{
	public static void main() {
		List<Integer> nums = Arrays.asList(3, 4, 2, 7 ,5);
		
		Stream<Integer> s1 = nums.stream();
		
		Stream<Integer> s2 = s1.filter(n -> n%2==0);
	}
}

Map

The map intermediate method transforms each element in a stream by applying a specific function.

<R> Stream<R> map(Function<? super T, ? extends R> mapper);
@FunctionalInterface  
public interface Function<T, R> { R apply(T t); }

The Function functional interface has a apply() method to transform elements.

public class Demo{
	public static void main() {
		List<Integer> nums = Arrays.asList(3, 4, 2, 7 ,5);
		
		Stream<Integer> s1 = nums.stream();
		
		// This will double the value of each element in the stream
		Stream<Integer> s2 = s1.map(n -> n*2);
	}
}

In one expression

You can combine all stream operations in a single expression.

public class Demo{
	public static void main() {
		List<Integer> nums = Arrays.asList(3, 4, 2, 7 ,5);
		
		int result = nums.stream()
						.filter(n -> n%2 == 0)
						.map(n -> n*2)
						.reduce(0, (c,e) -> c+e);
	}
}

Parallel stream

ParallelStream enables parallel processing of data. Unlike a normal stream (sequential stream), a parallel stream divides the data into multiple chunks and processes them concurrently using multiple threads. It is useful for stream with independent values - the computation on one value in the stream does not depend on the results or processing of other values, such as filtering, doubling each element, summing up elements. It is not for performing on dependent values where the computation or processing of one value in the stream depends on the relative position or relationship with other elements, such as sorting. You can create a parallel stream using the parallelStream() method.

List<Integer> list = Arrays.asList(3, 4, 2, 7 ,5);
Stream<Integer> parallelStream = list.parallelStream();

Back to parent page: Java Standard Edition (Java SE) and Java Programming

Web_and_App_Development Programming_Languages Java Stream For_Each ParallelStream

Reference: