Java 8 Streams
This article will introduce you to latest Stream API introduced in Java 8. This example-driven tutorial gives an in-depth overview of Java 8 streams. When I first read about the Stream API, I was confused about the name as it sounds very similar to InputStream and OutputStream from Java I/O package. But Java 8 streams are completely different. Streams are Monads, thus playing a big part in bringing a functional programming paradigm to Java. It added many ways to modify Collections and make your code shorter and smarter. You’ll learn about the execution order followed in Streams and how that order effects the execution as well as the runtime performance of a Java program.
Let’s go over a few examples how Streams work in Java and how can we fit them into our world-class apps.
Streams
As understood from the term ‘Stream’, it represents a sequence of elements in a collection and supports different functions to be operated upon those elements to read, update, or remove those elements.
Examples:
To start, let’s look at a simple example of Streams:
Filter
Let’s filter empty string from a list:
List<String> strList = Arrays.asList("discover", "", "sdk", "awesome", "java","");
List<String> noEmptyStr = strList.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
In the code above, noEmptyStr will contain only the strings which are not empty.
>>> Most of the time, streams will have ~equal performance; they’ll just be shorter to write.
forEach
We can use the forEach method to iterate over a list and perform an action as well.
Normal way to loop a Map:
Map<String, Integer> myMap = new HashMap<>();
myMap.put("Discover", 1);
myMap.put("Sdk", 2);
myMap.put(".com", 3);
for (Map.Entry<String, Integer> entry : myMap.entrySet()) {
System.out.println("Key : " + entry.getKey() + " Value : " + entry.getValue());
}
With streams you do the 2nd part easily:
myMap.forEach((k,v)->System.out.println("Key : " + k + " Value : " + v));
Terminal operations
A terminal operation is always eagerly executed. This operation will kick off the execution of all previous lazy operations present in the stream. Terminal operations either return concrete types or produce a side effect.
The 3 terminal operations are:
- reduce : It’s output is always a concrete type and is used to cumulate elements.
- collect : It’s output is either a List, a Map or a Set. As clear, it is used to group elements.
- forEach : It actually produces a side-effect and performs a specific operation on elements which is usually provided.
How are Streams created?
Streams can be created from various data sources, especially Collections. Sets and Lists have methods stream() and parallelStream() to either create a sequential or a parallel stream. Parallel streams are capable of operating on multiple threads and will be covered in a later section of this tutorial. Let’s look at sequential streams for now:
Arrays.asList("my1", "my2", "my3")
.stream()
.findFirst()
.ifPresent(System.out::println); // my1
Calling the method stream() on a list of objects returns a regular object stream. But we don't have to create collections in order to work with streams as we see in the next code sample:
Stream.of("my1", "my2", "my3")
.findFirst()
.ifPresent(System.out::println); // my1
Using Stream.of() alone will create a stream from a bunch of objects.
>>> Parallel streams are capable of operating on multiple threads.
Statistics
With Java 8, statistics collectors are also introduced to calculate complete statistics when processing is done.
List<Integer> nums = Arrays.asList(21, 9, 56, 98, 19, 100, 12);
IntSummaryStatistics stats = nums.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("Highest num : " + stats.getMax());
System.out.println("Sum : " + stats.getSum());
System.out.println("Average : " + stats.getAverage());
System.out.println("Lowest num : " + stats.getMin());
Processing order of Streams
We’ve looked at how to create and work with different kinds of streams, let's look deeper into how stream operations are executed under the bench.
An important feature of intermediate operations is laziness. Look at this example code where a terminal operation is absent:
Stream.of("alpha", "bravo", "charlie", "delta", "echo")
.filter(str -> {
System.out.println("Filter: " + str);
return true;
});
When executing this code snippet, nothing is printed to the console. That is because intermediate operations will only be executed when a terminal operation is present. It is so important that we will say it again:
>>> Intermediate operations will only be executed when a terminal operation is present.
Let's extend the above example by the terminal operation forEach:
Stream.of("alpha", "bravo", "charlie", "delta", "echo")
.filter(str -> {
System.out.println("filter: " + str);
return true;
})
.forEach(str -> System.out.println("forEach: " + str));
Executing the code sample above results in the following output on the console:
filter: alpha
forEach: alpha
filter: bravo
forEach: bravo
filter: charlie
forEach: charlie
filter: delta
forEach: delta
filter: echo
forEach: echo
Amazed with the print order? A simple approach would be to process the operations horizontally one after another on all elements of the stream. But instead each element moves along the chain vertically. The first string "alpha" passes filter then forEach, only then the second string "bravo" is executed.
Why order matters
The next example is comprised of two intermediate operations, map and filter, and one terminal operation, forEach. Let's once again look at how these operations are processed:
Stream.of("d2", "a2", "b1", "b3", "c")
.map(s -> {
System.out.println("Map: " + s);
return s.toUpperCase();
})
.filter(s -> {
System.out.println("Filter: " + s);
return s.startsWith("A");
})
.forEach(s -> System.out.println("forEach: " + s));
// map: d2
// filter: D2
// map: a2
// filter: A2
// forEach: A2
// map: b1
// filter: B1
// map: b3
// filter: B3
// map: c
// filter: C
As you might have guessed both map and filter are called five times for every string in the underlying collection whereas forEach is only called once.
We can still reduce the actual number of executions if we change the order of the operations, moving filter to the beginning of the chain:
Stream.of("e3", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));
// filter: e3
// filter: a2
// map: a2
// forEach: A2
// filter: b1
// filter: b3
// filter: c
Now, map is called only once so the operation pipeline performs much faster for larger numbers of input collections.
>>> Complex method chains needs to be observed carefully as output might not be what was guessed.
Let's extend the above example by an additional operation, sorted:
Stream.of("d2", "a2", "b1", "b3", "c")
.sorted((s1, s2) -> {
System.out.printf("sort: %s; %s\n", s1, s2);
return s1.compareTo(s2);
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));
>>> Sorting is a different and a special kind of intermediate operation. It's called stateful operation because in order to sort a collection of elements, we need to maintain state when we perform ordering.
Executing this example results in the following output:
sort: a2; d2
sort: b1; a2
sort: b1; d2
sort: b1; a2
sort: b3; b1
sort: b3; d2
sort: c; b3
sort: c; d2
filter: a2
map: a2
forEach: A2
filter: b1
filter: b3
filter: c
filter: d2
First, the sort operation is executed on the entire input collection. In other words sorted is executed horizontally. So in this case sorted is called eight times for multiple combinations on every element in the input collection.
Once again we can optimize the performance by reordering the chain:
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.sorted((s1, s2) -> {
System.out.printf("sort: %s; %s\n", s1, s2);
return s1.compareTo(s2);
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));
// filter: d2
// filter: a2
// filter: b1
// filter: b3
// filter: c
// map: a2
// forEach: A2
In this example, sorted is actually never called because filter reduces the input collection to just a single element. So the performance is greatly turned up for more input collections.
Reusing Streams
Streams work opposite from what you might be expecting, they actually don’t support reuse. Just follow one rule, as soon as you use one terminal operation, the stream is closed, period, done, finished ! Let’s look at an example:
Stream<String> myStream =
Stream.of("charlie", "discover", "alpha", "bravo", "sdk")
.filter(s -> s.startsWith("d"));
myStream.anyMatch(s -> true); // fine
myStream.noneMatch(s -> true); // oops ! IllegalStateException !
This will cause a hefty IllegalStateException. Still, there is a way out. You can certainly use Supplier to take out a new stream whenever you want one. Let’s have a look at another example where we use stream again and again:
Supplier<Stream<String>> myStreamSupplier =
() -> Stream.of("charlie", "discover", "alpha", "bravo", "sdk")
.filter(s -> s.startsWith("d"));
myStreamSupplier.get().anyMatch(s -> true); // fine
myStreamSupplier.get().noneMatch(s -> true); // fine
Using a Supplier makes sure that we get a new Stream whenever we create a new call to the get() method on which we are free to call the desired terminal method any number of times we want.
>>> As soon as you use one terminal operation, the stream is closed.
Conclusion
My programming article to Java 8 streams ends here. If you're interested in reading more about Java 8 streams, I recommend you to read the Stream Javadoc package documentation. This article presented the true power introduced by Java Streams and we focused on parallel streams as well. Use them to make better apps!
See my article introducing the Spring Framework for Java or
Visit the homepage to discover the best Java development platforms.
Recent Stories
Top DiscoverSDK Experts
Compare Products
Select up to three two products to compare by clicking on the compare icon () of each product.
{{compareToolModel.Error}}
{{CommentsModel.TotalCount}} Comments
Your Comment