1. Overview
One of the most exciting features of Java 8 is the Stream API – which, simply put, is a powerful tool for processing sequences of elements.
StreamEx is a library that provides additional functionality for the standard Stream API along with the performance improvements.
Here are a few core features:
- Shorter and convenient ways of doing the everyday tasks
- 100% compatibility with original JDK Streams
- Friendliness for parallel processing: any new feature takes the advantage on parallel streams as much as possible
- Performance and minimal overhead. If StreamEx allows solving the task using less code compared to standard Stream, it should not be significantly slower than the usual way (and sometimes it’s even faster)
In this tutorial, we’ll present some of the features of StreamEx API.
2. Setting up the Example
To use StreamEx, we need to add the following dependency to the pom.xml:
<dependency>
<groupId>one.util</groupId>
<artifactId>streamex</artifactId>
<version>0.6.5</version>
</dependency>
The latest version of the library can be found on Maven Central.
Through this tutorial, we’re going to use a simple User class:
public class User {
int id;
String name;
Role role = new Role();
// standard getters, setters, and constructors
}
And a simple Role class:
public class Role {
}
3. Collectors Shortcut Methods
One of the most popular terminal operations of Streams is the collect operation; this allows for repackaging Stream elements to a collection of our choice.
The problem is that code can get unnecessarily verbose for simple scenarios:
users.stream()
.map(User::getName)
.collect(Collectors.toList());
3.1. Collecting to a Collection
Now, with StreamEx, we don’t need to provide a Collector to specify that we need a List, Set, Map, InmutableList, etc.:
List<String> userNames = StreamEx.of(users)
.map(User::getName)
.toList();
The collect operation is still available in the API if we want to perform something more complicated than taking elements from a Stream and putting them in a collection.
3.2. Advanced Collectors
Another shorthand is groupingBy:
Map<Role, List<User>> role2users = StreamEx.of(users)
.groupingBy(User::getRole);
This will produce a Map with the key type specified in the method reference, producing something similar to the group by operation in SQL.
Using plain Stream API, we’d need to write:
Map<Role, List<User>> role2users = users.stream()
.collect(Collectors.groupingBy(User::getRole));
A similar shorthand form can be found for the Collectors.joining():
StreamEx.of(1, 2, 3)
.joining("; "); // "1; 2; 3"
Which takes all the elements in the Stream a produces a String concatenating all of them.
4. Adding, Removing and Selecting Elements
In some scenarios, we’ve got a list of objects of different types, and we need to filter them by type:
List usersAndRoles = Arrays.asList(new User(), new Role());
List<Role> roles = StreamEx.of(usersAndRoles)
.select(Role.class)
.toList();
We can add elements to the start or end of our Stream, with this handy operations:
List<String> appendedUsers = StreamEx.of(users)
.map(User::getName)
.prepend("(none)")
.append("LAST")
.toList();
We can remove unwanted null elements using nonNull() and use the Stream as an Iterable:
for (String line : StreamEx.of(users).map(User::getName).nonNull()) {
System.out.println(line);
}
5. Math Operations and Primitive Types Support
StreamEx adds supports for primitive types, as we can see in this self-explaining example:
short[] src = {1,2,3};
char[] output = IntStreamEx.of(src)
.map(x -> x * 5)
.toCharArray();
Now let’s take an array of double elements in an unordered manner. We want to create an array consisting of the difference between each pair.
We can use the pairMap method to perform this operation:
public double[] getDiffBetweenPairs(double... numbers) {
return DoubleStreamEx.of(numbers)
.pairMap((a, b) -> b - a)
.toArray();
}
6. Map Operations
6.1. Filtering by Keys
Another useful feature is an ability to create a Stream from a Map and filter the elements by using the values they point at.
In this case, we’re taking all non-null values:
Map<String, Role> nameToRole = new HashMap<>();
nameToRole.put("first", new Role());
nameToRole.put("second", null);
Set<String> nonNullRoles = StreamEx.ofKeys(nameToRole, Objects::nonNull)
.toSet();
6.2. Operating on Key-Value Pairs
We can also operate on key-value pairs by creating an EntryStream instance:
public Map<User, List<Role>> transformMap(
Map<Role, List<User>> role2users) {
Map<User, List<Role>> users2roles = EntryStream.of(role2users)
.flatMapValues(List::stream)
.invert()
.grouping();
return users2roles;
}
The special operation EntryStream.of takes a Map and transforms it into a Stream of key-value objects. Then we use flatMapValues operation to transform our list of roles to a Stream of single values.
Next, we can invert the key-value pair, making the User class the key and the Role class the value.
And finally, we can use the grouping operation to transform our map to the inversion of the one received, all with just four operations.
6.3. Key-Value Mapping
We can also map keys and values independently:
Map<String, String> mapToString = EntryStream.of(users2roles)
.mapKeys(String::valueOf)
.mapValues(String::valueOf)
.toMap();
With this, we can quickly transform our keys or values to another required type.
7. File Operations
Using StreamEx, we can read files efficiently, i.e., without loading full files at once. It’s handy while processing large files:
StreamEx.ofLines(reader)
.remove(String::isEmpty)
.forEach(System.out::println);
Note that we’ve used remove() method to filter away empty lines.
Point to note here is that StreamEx won’t automatically close the file. Hence, we must remember to manually perform closing operation on both file reading and writing occasion to avoid unnecessary memory overhead.
8. Conclusion
In this tutorial, we’ve learned about StreamEx, and it’s different utilities. There is a lot more to go through – and they have a handy cheat sheet here.
As always, the full source code is available over on GitHub.