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
, andlimit
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
andfindAny
method. - You can match a stream by using
allMatch
,anyMatch
andanyMatch
method for a given predicate. - You can combine elements of a stream using
reduce
.
Discussion about this post