The makers of Java 8 recently introduced a new feature called Stream API. The Stream API along with lambda expressions can be used to perform bulk operations on a sequence of elements, but that's not all it can (or should) be used for. Multiple Stream operations can also be chained to perform a number of sequential operations.
In order to fully understand the concept behind Stream API, you'll need a basic understanding of lambda expressions and functional interfaces in Java 8.
Why use the Stream API?
Suppose you have an integer array and you want to find all of the elements in that array that are even numbers. Before having Java 8 Streams, you’d need to write code similar to the following example:
public static void main(String[] args) {
List<Integer> aList = Arrays.asList(6,88,23,14,17,12,32,51,79,94);
List<Integer> evenNumbersList = new ArrayList<Integer>();
for(int num:aList){
if(num %2 == 0)
evenNumbersList.add(num);
}
}
Essentially, you'd need to iterate through the list and check if each number is even. If so, you'll need to add this information to an output list.
By using Java 8 streams instead, you can re-write the above code as follows:
public static void main(String[] args) {
List<Integer> aList = Arrays.asList(6,88,23,14,17,12,32,51,79,94);
aList.stream().filter(num -> num %2 == 0);
}
Compared to the pre-Java 8 code, the code using Streams is far more concise. The stream API allows you to perform operations on collections without external iteration. In this case, we’re performing a filter operation which will filter the input collection based on the condition specified.
How do Streams work?
As we mentioned earlier, the Stream API in conjunction with lambda expressions can be used to perform bulk operations on Collections without the need for external iteration. The basic interface in the Stream API is called the 'java.util.Stream', and there are various methods on the Stream interface that perform various operations on the Stream instance.
In the above example, we're invoking the filter method. This accepts a Predicate instance which is an in-built functional interface. Here, a lambda expression is used to implement the Predicate interface. This lambda expression accepts an Integer value, checks if it is even via the modulus operator and returns a Boolean value accordingly. The filter operation runs this lambda expression on every element in the input Stream and creates a new Stream with the output. The result? A Stream with only even numbers.
An important point to remember is that Streams do not modify the underlying collection and the data in the underlying collection will not be affected by the Stream operations.
Creating Streams
There are several ways to create a Stream, including the following:
From a Collection
Our first example shows how a method called stream has been added to all the Collection interfaces. When this method is invoked a Stream corresponding to the underlying Collection is returned. The following code demonstrates this:
List<Integer> aList = Arrays.asList(6,88,23,14,17,12);
Stream<Integer> stream = aList.stream();
From an Array
Streams can also be created using an array. There is an Arrays.stream method available which can be used as follows:
int[] array = new int[] {5,8,12};
IntStream arrayStream= Arrays.stream(array);
From a List of elements
Streams can also be created using a List of values using the Stream.of method like so:
Stream<Integer> anotherStream = Stream.of(5,7,14);
Using Stream.generate method
There is a Stream.generate method available which can also be used to create a Stream as follows:
Stream<Integer> stream = Stream.generate(() -> new Random().nextInt(100));
The generate method accepts a Supplier instance which is a functional interface. Here, a lambda expression is passed which generates a random number less than 100. This code will create a Stream of random integers less than 100.
Iterating Through a Stream
There's a forEach method available on the Stream interface that can be used to iterate through a Stream similar to the forEach method on the Collection interfaces. The following code demonstrates this:
public static void main(String[] args) {
List<Integer> aList = Arrays.asList(6,88,23,14,17,12,32,51,79,94);
Stream<Integer> stream = aList.stream().filter(num -> num %2 == 0);
stream.forEach(num -> System.out.println(num));
}
Here, the Stream.filter is used to filter the Stream and create a new Stream of even numbers. Then the code invokes the forEach method on the Stream. This method accepts a Consumer instance which is another in-built functional interface. Here, a lambda expression is used to implement the Consumer interface. This lambda expression accepts an integer value and simply prints it.
When you execute this code, it will provide the following output:
6
88
14
12
32
94
Types of Stream Operations
There are 2 types of operations that can be performed via the Streams. These can be done as follows:
Intermediate
Intermediate operations operate on a Stream and return a Stream instance. Intermediate operations can be chained to perform a series of operations. The filter operation seen above is an intermediate operation. Another intermediate operation is map. Using the map operation, you can apply some function to each element in the Stream. The following code demonstrates this:
public static void main(String[] args) {
List<Integer> inputList = Arrays.asList(49,8,11,15,22);
Stream<Integer> inputStream = inputList.stream().map(num -> num +5);
System.out.println("Stream with each element incremented by 5:");
inputStream.forEach(num -> System.out.print(num+","));
}
Here, the map operation is used to increment each element in the input Stream by 5. The map operation accepts a Function instance which is another in-built functional interface. In this case, the Function interface is implemented via a lambda expression which accepts an integer value, increments it by 5 and returns it. When this code is executed, it will provide the following output:
Stream with each element incremented by 5:
54,13,16,20,27,
Terminal
Terminal operations return a non-stream result, meaning that they can return a result of any data type. Once a terminal operation is applied, no other Stream operation can be applied and an example of a terminal operation is anyMatch. The anyMatch operation checks if any element in the input stream matches the condition specified. Consider the following code:
public static void main(String[] args) {
List<String> strList = Arrays.asList("Cat","Dog","Cow","Horse");
boolean anyMatch = strList.stream().anyMatch( str -> str.startsWith("C"));
System.out.println("Animal starting with C present: "+anyMatch);
}
This above code checks if there is any Animal in the input list that starts with “C”. The anyMatch operation accepts a Predicate instance. As before, it is implemented via a lambda expression that accepts a String value, checks if the String starts with “C” and returns a boolean value accordingly. When this code is executed, it will print the following output:
Animal starting with C present: true
Chaining Operations
Streams allow you to chain a number of operations, which makes the Stream API very powerful. The following code demonstrates this:
public static void main(String args[]){
List<String> names = Arrays.asList("Sunday","Monday","January","Wednesday","February","December","Saturday","Monday","Friday");
names.stream().filter(name -> name.endsWith("day")).sorted().map(name -> name.toUpperCase()).forEach(name -> System.out.println(name));
}
The code above creates a list of String values that has days of the week as well as some months, and a Stream is created on this input list. The filter operation is first used to filter out only the days (i.e. it checks if the String ends with “day”). After this the sorted operation is invoked to sort the stream after which the map operation is invoked to convert each String in the Stream to uppercase. The filter, sort and map operations are chained, each operation is performed on the output of the previous operation. When this code is executed, it will print the following output:
FRIDAY
MONDAY
MONDAY
SATURDAY
SUNDAY
WEDNESDAY
From the example above, you can see that all the month names are eliminated. Also, the Strings are sorted alphabetically and converted to uppercase.
Parallel Stream
The Stream API also supports parallel streams. Parallel streams allow executing stream operations concurrently on the elements in a stream. Just like the stream method, another method called parallelStream has been added to all the Collection interfaces; this returns a parallel stream. When parallel streams are used on streams with a large number of elements, there is a huge performance improvement provided the underlying platform supports parallel programming.
Consider the following code:
public static void main(String[] args) {
List<String> names = Arrays.asList("Sunday","Monday","January","Wednesday","February","December","Saturday","Monday","Friday");
names.parallelStream().filter(name -> name.endsWith("day")).sorted().map(name -> name.toUpperCase()).forEach(name -> System.out.println(name));
}
Code-wise, this code is almost the same as the sequential stream example. Instead of invoking the stream method on the input ArrayList, the parallelStream method is invoked.
When this code is executed, it will print the following output:
FRIDAY
MONDAY
SATURDAY
WEDNESDAY
SUNDAY
MONDAY
Irrespective of whether a sequential stream is user or parallel, the output is the same.
Converting Stream to a Collection
There is a method available on the Stream interface called collect. It can be used to convert a Stream to a List,Set, Map or Collection. The following code demonstrates this:
public static void main(String[] args) {
List<Integer> inputList = Arrays.asList(49,8,11,15,22);
List<Integer> sortedList = inputList.stream().sorted().collect(Collectors.toList());
System.out.println("Sorted Stream:");
sortedList.forEach(num -> System.out.print(num+","));
}
In the code above, the stream.sorted method is used to sort the elements in the input stream. After that, the code invokes the collect method. This method accepts an argument of type Collector. The collect method reduces the input stream to an object of any data type based on the Collector specified. Here, the Collectors.toList method is used, this returns a Collector that converts the input Stream to a List. When this code is executed, it will print the following output:
Sorted Stream:
8,11,15,22,49,
A powerful API that can be used to process a sequence of elements, the Stream API can help to make code more concise, as well as increase its readability.
Interested in finding a role based on your skills, not your CV? Make sure to visit our homepage to find out more.