Java Streams: Stream Operation with Examples

As we learned in Java Streams: stream creation with examples that a stream pipeline consists of a source, zero or more intermediate operations, and a terminal operation.

We also learned that streams are lazy; computation on the source data is only performed when the terminal operation is initiated.

In this article, we are going to explore more about stream operation.

Stream Operation

Stream operations can be either intermediate or terminal. An intermediate operation produces another stream. Likewise, a terminal operation produces a result or side-effect.

Let’s take a look at some of the operations provides by the Java Stream.

Filtering a stream

You can filter a stream by passing a predicate (T -> boolean) to filter method. The filter is an intermediate operation that returns a filtered stream. If you are familiar with SQL, you can think of a predicate as a where clause.


List<Book> javaBooks =                                                                          
  books.stream()
  .filter(book -> book.getCategory().equals(JAVA))
  .collect(Collectors.toList());

Here stream of books is filtered based on the category. The predicate function is a lambda function book -> book.getCategory().equals(JAVA).

Take-While and Drop-While

Let’s say you need to find all the books whose price is less than $42. You can do something like:


List<Book> lessThan42 =                                                              
  books.stream()
  .filter(book -> book.getPrice() < 42)
  .collect(Collectors.toList());

One of the problems with the above approach is that filter iterates through the whole list even if the books are sorted. We can short-circuit the stream by using takeWhile as:


List<Book> priceLessThan42 =                                                            
  books.stream()
  .takeWhile(book -> book.getPrice() < 42)
  .collect(Collectors.toList());

The takeWhile was introduced in Java 9.

Similarly, you can use dropWhile to drop the element of the stream which matches the given condition. This operation is the complement of takeWhile.


List<Book> priceGreaterThan42 =                                                            
  books.stream()
  .dropWhile(book -> book.getPrice() < 42)
  .collect(Collectors.toList());

The behavior of the takeWhile/dropWhile operation is nondeterministic if the stream is unordered, and some (but not all) elements of this stream match the given predicate. In this case, it can return any subset of matching elements including an empty stream.

Finding distinct element of a stream

The distinct() method returns a stream consisting of the distinct element by applying equals() method. This is a stateful intermediate operation.


List<String> publisher =                                                                    
  books.stream()
  .map(book -> book.getPublisher())
  .distinct()
  .collect(Collectors.toList());

Problem with Stateful Operation

A stream pipeline results may be nondeterministic or incorrect if function passed to the stream operations are stateful. A stateful operation is one whose result depends on any state which might change during the execution of the stream pipeline.

An example of a stateful lambda is the parameter to map() in:


Set<Integer> seen = Collections
  .synchronizedSet(new HashSet<>());

stream.parallel()
  .map(e -> { if (seen.add(e)) return 0; else return e; })...

Here, if the mapping operation is performed in parallel, the results for the same input could vary from run to run, due to thread scheduling differences. Whereas, with a stateless lambda expression the results would always be the same.

Truncating a stream

You can truncate a stream by short-circuiting stream by using limit operation on the stream. The limit is a stateful operation. It is a very cheap operation on a sequential stream. But, can be quite expensive on an ordered parallel stream.


books.stream().limit(2)
  .collect(Collectors.toList());

You can use skip(n) operation to discard the first n elements of a stream. Similar to limit, this can be quite an expensive operation on an ordered parallel pipeline.


books.stream().skip(2)
  .collect(Collectors.toList());

Mapping a Stream

You can map one stream to another stream using mapping functions.

Map

We can apply map function to transform one stream to another stream. For example, mapping Stream<Book> to Stream<String> of authors can be done as books.stream().map(Book::getAuthor). The result of map operation is another stream after applying the function (T) -> R on each element of the stream.


static List<String> mapToAuthors(List<Book> books) {               
  return books.stream()
  .map(Book::getAuthor)
  .collect(Collectors.toList()); 
}                                                                          

The function passed to the map operation should be non-interfering and stateless; otherwise, there can be undesired behavior with the possibility of code breaking in the future.

FlatMap

The flatMap operation applies a one-to-many transformation to a stream’s elements and flattens the resulting elements into a new stream.


<R> Stream<R> flatMap(Function<? super T
    , ? extends Stream<? extends R>> mapper);

Like map, the mapper function passed to the flatMap operation should be non-interfering and stateless.


Set<String> s = 
  Stream.of(set1, set2)
  .flatMap(Set::stream)
  .collect(Collectors.toSet());

The difference between map and flatMap operation is – map only applies transformation. Whereas, flatMap also flattens the stream.


static void flatMap() {

  List<Integer> primeNumbers = 
    Arrays.asList(2, 3, 5, 7, 11, 13);

  List<Integer> evenNumbers = 
    Arrays.asList(2, 4, 6, 8);

  List<List<Integer>> evenOrPrime = 
    Arrays.asList(primeNumbers, evenNumbers);

  List<Integer> numbers =
     evenOrPrime.stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList());

    log.info("Flattened map : {}", numbers);
  }

Above example prints – Flattened map : [2, 3, 5, 7, 11, 13, 2, 4, 6, 8]. As you can see, flatMap flattens a stream of List<Integer> to stream of Integer.

Mapping to primitive stream

A Stream<T> can be mapped to specialized primitive stream IntStream, DoubleStream and LongStream by calling methods mapToInt, mapToDouble, and mapToLong respectively.


static DoubleStream mapToPrice(List<Book> books) {  
  return books.stream()
  .mapToDouble(Book::getPrice);        
}                                                           

A specialized primitive stream can be converted back to a nonspecialized stream by calling boxed as:


static Stream<Double> box(DoubleStream doubleStream) { 
  return doubleStream.boxed();                                         
}                                                                      

Matching elements of the Stream

Stream class provides method to find matching elements in a stream. There are two types of method. One returns boolean, for example anyMatch. And another returns Optional, for example findAny.

Any Match

The anyMatch method returns true if any match for a given predicate is found. This is a short-circuiting terminal operation. This operation returns false for empty streams and stream is not evaluated.


static boolean hasAnyJavaBook(List<Book> books) {                            
  return books.stream()
  .anyMatch(book -> book.getCategory().equals(JAVA));
}                                                                                 

All Match

The allMatch method returns true when elements of the stream match given Predicate. This is a short-circuiting terminal operation. For an empty stream, this operation returns true and the stream is not evaluated.


static boolean hasAllJavaBook(List<Book> books) {                         
  return books.stream()
  .allMatch(book -> book.getCategory().equals(JAVA));
}                                                                                 

None Match

The noneMatch method returns true if no elements of the stream match given Predicate. This is a short-circuiting terminal operation. For an empty stream, this operation returns true and the stream is not evaluated.


static boolean hasNoJavaBook(List<Book> books) {                           
  return books.stream()
  .noneMatch(book -> book.getCategory().equals(JAVA));
}                                                                                  

Finding elements of the Stream

Find Any

The findAny method returns an Optional describing an arbitrary element of the stream, or an empty Optional if the stream is empty. This is a short-circuiting terminal operation. The behavior of this operation is nondeterministic; it can return any element of the stream.

The nondeterministic behavior allows for optimal performance in the case of a parallel stream.


static Optional<Book> findAnyJavaBook(List<Book> books) {                         
  return books.stream()
    .filter(book -> book.getCategory().equals(JAVA))
    .findAny();
}                                                                                         

The Optional<T> class (java.util.Optional) is a container object to represent the existence or absence of a value T. If findAny doesn’t find any element then it returns empty Optional instead of returning null.

Find First

The findFirst method returns an Optional which represents the first element of the stream, or an empty Optional if the stream is empty. If the stream has no encountered order then any element can be returned.


static Optional<Book> findFirstJavaBook(List<Book> books) {                          
  return books.stream()
    .filter(book -> book.getCategory().equals(Category.JAVA))
    .findFirst();
}                                                                                            

Finding the first element in a parallel stream is expensive. If you don’t care about which element is returned, we should use findAny.

Reduction Operation

A reduction operation (also called fold) takes a sequence of input elements and combines them into a single result by repeated application of combining operation. Stream classes have some generic reduction operations such as reduce() and collect(), and some specialized ones such as min(), max(),etc.

Reduce

The reduce method T reduce(T identity, BinaryOperator<T> accumulator) performs reduction operation on the stream using an associative accumulator function (BinaryOperator: T, U -> R) based on initial identity value. This is a terminal operation.


static double sum() {                                
  List<Integer> numbers = 
    List.of(1, 2, 3, 4, 5); 
          
  return numbers.stream()
  .reduce(0, (a, b) -> a + b);        
}                                                                                                                                          

or using method reference as:


private static double sum() {                             
  List<Integer> numbers = 
    List.of(1, 2, 3, 4, 5);         

  return numbers.stream()
  .reduce(0, Integer::sum);        
}                                                                                                                                

If a stream is empty then the identity (initial) value is returned. There is another variant of the reduce method ‘Optional<T> reduce(BinaryOperator<T> accumulator)‘ which returns Optional<T> (for an empty stream).

Min & Max

The reduce operation can be used to find min, max as:


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

Optional<Integer> min = 
  numbers.stream().reduce(Integer::min);

Optional<Integer> max = 
  numbers.stream().reduce(Integer::max);

Summary

Stream operations can be either intermediate or terminal. An intermediate operation produces another stream. Likewise, a terminal operation produces a result or side-effect.

  • You can use filter, distinct, takeWhile,dropWhile, skip, and limit method to filter and slice a stream.
  • You can transform elements of a stream using the ‘map‘ and ‘flatMap‘ method. The flatMap operation also flattens the stream.
  • You can find elements in a stream using findFirst and findAny method.
  • You can match a stream by using allMatch, anyMatch and anyMatch method for a given predicate.
  • You can combine elements of a stream using reduce.
Social Share !
Default image
Pankaj
Software Architect @ Schlumberger ``` Cloud | Microservices | Programming | Kubernetes | Architecture | Machine Learning | Java | Python ```