In this article, we are going to explore the Java Stream API with some real-time use cases from the projects I’ve worked.
What is Stream?
A Stream in Java can be defined as a sequence of elements from a source that supports aggregate operations on them. The source here refers to a Collections or Arrays that provides data to a Stream. Stream keeps the ordering of the data as it is in the source.
In simple terms, Java streams represent a pipeline through which the data will flow and the functions to operate on the data.
Stream Operations
Stream operations are divided into intermediate and terminal operations and are combined to form stream pipelines.
Intermediate Operation
Intermediate operations are always lazy and return a new stream that, when traversed, contains the elements of the initial stream that match the given predicate.
Moreover, the traversal of the pipeline source does not begin until the terminal operation of the pipeline is executed. So, executing an intermediate operation such as filter()
does not actually perform any filtering, but instead creates a new stream
List of Intermediate Operations
map()
, filter()
, distinct()
, sorted()
, limit()
, skip()
Terminal Operation
Terminal operations, such as Stream.forEach
or IntStream.sum
, may traverse the stream to produce a result or a side-effect. After the terminal operation is performed, the stream pipeline is considered consumed, and can no longer be used.
If you need to traverse the same data source again, you must return to the data source to get a new stream. In almost all cases, terminal operations are eager, completing their traversal of the data source and processing of the pipeline before returning. Only the terminal operations iterator()
and spliterator()
are not. These are provided as an “escape hatch” to enable arbitrary client-controlled pipeline traversals in the event that the existing operations are not sufficient to the task.
Intermediate operations are further divided into stateless and stateful operations.
Stateless operations
Stateless operations, such as filter
and map
, retain no state from previously seen elements when processing a new element. Each element can be processed independently of operations on other elements.
Stateful operations
Stateful operations, such as distinct
and sorted
, may incorporate state from previously seen elements when processing new elements.
It may need to process the entire input before producing a result. For example, one cannot produce any results from sorting a stream until one has seen all elements of the stream.
List of Terminal Operations
forEach()
, toArray()
, reduce()
, collect()
, min()
, max()
, count()
, anyMatch()
, allMatch()
, noneMatch()
, findFirst()
, findAny()
Map Operation
map()
is an intermediate operation that returns a stream consisting of the results of applying the given function to the elements of this stream.
Filter Operation
filter()
returns a stream consisting of the elements of this stream that match the given predicate. It is an intermediate operation.
FlatMap Operation
flatMap()
is a combination of map()
and flat()
operation. It first applies the given function to the elements of this stream and then flattens the resulting elements into a new stream. It is an intermediate operation.
Distinct Operation
distinct()
returns a stream consisting of the distinct elements (according to Object.equals(Object)
) of this stream. It is an intermediate operation.
Collect Operation
collect()
is a terminal operation, and it performs a mutable reduction operation on the elements of this stream using a Collector.
It accepts an argument of the type Collector
that implement various useful reduction operations, such as accumulating elements into collections, summarizing elements according to various criteria, etc. There are many predefined collectors such as Collectors.toList()
, Collectors.joining()
, Collectors.toSet()
, etc., for most common operations
Collector
A Collector
is specified by four functions that work together to accumulate entries into a mutable result container, and optionally perform a final transform on the result. They are:
- creation of a new result container (
supplier()
) - incorporating a new data element into a result container (
accumulator()
) - combining two result containers into one (
combiner()
) - performing an optional final transform on the container (
finisher()
)
ForEach Operation
foreach()
is a terminal operation that performs an action for each element of the stream.
Realtime Examples
Map and Collect
Example 1
Let’s assume that we have a list of users and we need to get the list of user Ids from the list
List<Long> userIds = users.stream().map(u -> u.getId()).collect(Collectors.toList());
Example 2
Let’s assume that we have a list of user privileges and we need to convert that to a list of GrantedAuthority
with the privilege name
List<GrantedAuthority> authorities = privileges.stream().map(p -> new SimpleGrantedAuthority(p.getName())).collect(Collectors.toList());
Example 3
Let’s assume that we have a list of validation errors obtained from BindingResult.getAllErrors()
and we want to get all the error messages with field names and convert that to a JSON format string.
String temp = allErrors.stream().map(e -> {
if (e instanceof FieldError) {
return "{\"field\":\"" + ((FieldError) e).getField() + "\",\"defaultMessage\":\"" + e.getDefaultMessage() + "\"}";
} else {
return "{\"object\":\"" + e.getObjectName() + "\",\"defaultMessage\":\"" + e.getDefaultMessage() + "\"}";
}
}).collect(Collectors.joining(","));
Filter, Map, and Collect
Let’s assume that we have a list of users and some of the users having a profile image with a unique user key stored in the server.
If we need to delete the profile images with the user key, then we can filter
the users having an avatar, map
the user key and then collect
them in a list.
storageService.deleteProfileImages(users.stream().filter(u -> u.hasAvatar()).map(u -> u.getUserKey()).collect(Collectors.toList()));
Map, FlatMap, Distinct, and Collect
Let’s assume that we have a list of user roles and each role has a list of privileges.
If we need to get the list of all privileges of all user roles, then we can do so with the combination of map
, flatMap
, distinct
, and collect
operations.
List<Privilege> privileges = user.getRoles().stream().map(role -> role.getPrivileges()).flatMap(list -> list.stream()).distinct().collect(Collectors.toList());
The following diagram depicts what each operation does to get the list of distinct privileges from the collection of roles.
map
returns a new stream of lists and each list containing all the privileges of a role.
flatMap
produces a new stream containing all the privileges in all the lists. As a result, this new stream may contain duplicates since different user roles can have the same privileges.
distinct
returns a stream consisting of the distinct privileges by eliminating the duplicates.
collect
operation collects all the distinct privileges into a list.
Map and ForEach
Let’s assume that we have a list of roles and each role has a list of privileges. If we need to convert all the roles and privileges to list of SimpleGrantedAuthority
, then we can use map
and forEach
functions.
public static List<SimpleGrantedAuthority> buildSimpleGrantedAuthorities(final Set<Role> roles) {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
role.getPrivileges().stream().map(p -> new SimpleGrantedAuthority(p.getName())).forEach(authorities::add);
}
return authorities;
}
map
operation returns a new stream mapping the privilege name to SimpleGrantedAuthority
forEach
adds each element of the stream to the authorities
list.
References
https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html
Conclusion
That’s all folks. In this article, we have explored the Stream API functions.
Please share this article with your friends if you like it. Thank you for reading.
Nice try for examples … Please append few more, like have particular set of record for User info in flat-file what can be translated back to Map. Logger system have timestamp: FileName and info/debug statements …. System have page to check the log pattern per file.