Learn more on Java with our tutorial Bridging Android and Java in Android Development on SitePoint.
After shying away from them for years, Java finally embraced functional programming constructs in the spring of 2014. Java 8 includes support for lambda expressions, and offers a powerful Streams API which allows you to work with sequences of elements, such as lists and arrays, in a whole new way.
In this tutorial, I’m going to show you how to create streams and then transform them using three widely used higher-order methods named map
, filter
and reduce
.
The code for this post can be found here.
Creating a Stream
As you can tell from its name, a stream is just a sequence of items. Although there are lots of approaches to stream creation, for now, we’ll be focusing only on generating streams from lists and arrays.
In Java 8, every class which implements the java.util.Collection
interface has a stream
method which allows you to convert its instances into Stream
objects. Therefore, it’s trivially easy to convert any list into a stream. Here’s an example which converts an ArrayList
of Integer
objects into a Stream
:
// Create an ArrayList
List<Integer> myList = new ArrayList<Integer>();
myList.add(1);
myList.add(5);
myList.add(8);
// Convert it into a Stream
Stream<Integer> myStream = myList.stream();
If you prefer arrays over lists, you can use the stream
method available in the Arrays
class to convert any array into a stream. Here’s another example:
// Create an array
Integer[] myArray = {1, 5, 8};
// Convert it into a Stream
Stream<Integer> myStream = Arrays.stream(myArray);
The map
Method
Once you have a Stream
object, you can use a variety of methods to transform it into another Stream
object. The first such method we’re going to look at is the map
method. It takes a lambda expression as its only argument, and uses it to change every individual element in the stream. Its return value is a new Stream
object containing the changed elements.
To give you a realistic example, let me show you how you can use map
to convert all elements in an array of strings to uppercase.
You start by converting the array into a Stream
:
String[] myArray = new String[]{"bob", "alice", "paul", "ellie"};
Stream<String> myStream = Arrays.stream(myArray);
Then you call the map
method, passing a lambda expression, one which can convert a string to uppercase, as its sole argument:
Stream<String> myNewStream =
myStream.map(s -> s.toUpperCase());
The Stream
object returned contains the changed strings. To convert it into an array, you use its toArray
method:
String[] myNewArray =
myNewStream.toArray(String[]::new);
At this point, you have an array of strings, all of which are in uppercase.
I hope you are now beginning to realize that with this style of programming, you can do away with loops, and the code you write can be very concise and readable.
The filter
Method
In the previous section, you saw that the map
method processes every single element in a Stream
object. You might not always want that. Sometimes, you might want to work with only a subset of the elements. To do so, you can use the filter
method.
Just like the map
method, the filter
method expects a lambda expression as its argument. However, the lambda expression passed to it must always return a boolean
value, which determines whether or not the processed element should belong to the resulting Stream
object.
For example, if you have an array of strings, and you want to create a subset of it which contains only those strings whose length is more than four characters, you would have to write the following code:
Arrays.stream(myArray)
.filter(s -> s.length() > 4)
.toArray(String[]::new);
The code above looks a lot more concise than the code we wrote in the previous example because I’ve chained all the Stream
methods. Most developers prefer writing functional code in this manner because, usually, there’s no need to store references to intermediate Stream
objects.
Reduction Operations
A reduction operation is one which allows you to compute a result using all the elements present in a stream. Reduction operations are also called terminal operations because they are always present at the end of a chain of Stream
methods. We’ve already been using a reduction method in our previous examples: the toArray
method. It’s a terminal operation because it converts a Stream
object into an array.
Java 8 includes several reduction methods, such as sum
, average
and count
, which allow to perform arithmetic operations on Stream
objects and get numbers as results. For example, if you want to find the sum of an array of integers, you can use the following code:
int myArray[] = { 1, 5, 8 };
int sum = Arrays.stream(myArray).sum();
If you want to perform more complex reduction operations, however, you must use the reduce
method. Unlike the map
and filter
methods, the reduce
method expects two arguments: an identity element, and a lambda expression. You can think of the identity element as an element which does not alter the result of the reduction operation. For example, if you are trying to find the product of all the elements in a stream of numbers, the identity element would be the number 1.
The lambda expression you pass to the reduce
method must be capable of handling two inputs: a partial result of the reduction operation, and the current element of the stream. If you are wondering what a partial result is, it’s the result obtained after processing all the elements of the stream that came before the current element.
The following is a sample code snippet which uses the reduce
method to concatenate all the elements in an array of String
objects:
String[] myArray = { "this", "is", "a", "sentence" };
String result = Arrays.stream(myArray)
.reduce("", (a,b) -> a + b);
Conclusion
You now know enough to start using the map
, filter
and reduce
methods in your projects. For the sake of the brevity, throughout this tutorial, I’ve used only serial streams. If you have computationally intensive map operations, or if you expect your streams to be very large, you should consider using parallel streams instead. Fortunately, its very easy to convert any stream into its parallel equivalent. All you need to do is call its parallel
method.
I’d also like to let you know that if you prefer not to use lambda expressions while working with the map
, filter
, and reduce
methods, you can always use method references instead.
To learn more about the Streams API and the other methods it has to offer, you can refer to its documentation.
Learn more on Java with our tutorial Bridging Android and Java in Android Development on SitePoint.
Frequently Asked Questions (FAQs) about Java 8 Streams, Filter, Map, and Reduce
What is the significance of Java 8 Streams in programming?
Java 8 Streams are a significant feature introduced in Java 8. They provide a new abstraction of dealing with sequences of data in a declarative way. The main advantage of Java 8 Streams is that they allow for significant optimizations through lazy evaluations and parallel execution by dividing the data into multiple chunks. This makes it easier to work with large data sets, especially in multi-threaded environments. Streams also support functional-style operations on streams of elements, such as map-reduce transformations on collections.
How does the filter operation work in Java 8 Streams?
The filter operation in Java 8 Streams is used to filter out elements from a stream that don’t match a given predicate. A predicate is a functional interface that represents a boolean-valued function of one argument. The filter operation is an intermediate operation, meaning that it can be chained with other stream operations and doesn’t perform any processing until a terminal operation is invoked on the stream pipeline.
Can you explain the map operation in Java 8 Streams?
The map operation in Java 8 Streams is a transformation operation. It takes a Function as an argument, which is applied to each element in the stream and transforms it into a new element. The Function is a functional interface that represents a function that takes one argument and produces a result. The map operation is useful for converting or transforming a Stream of one type into a Stream of another type.
How does the reduce operation work in Java 8 Streams?
The reduce operation in Java 8 Streams is a terminal operation that takes a BinaryOperator as an argument and returns an Optional describing the reduced value, if any. This operation processes all the elements in the stream to produce a single result. The BinaryOperator is a functional interface that represents an operation upon two operands of the same type, producing a result of the same type as the operands.
What is the difference between a sequential and a parallel stream?
A sequential stream is a stream where the elements are processed one after the other, while a parallel stream is a stream where the elements can be processed in parallel, meaning multiple elements can be processed at the same time. Parallel streams utilize the Fork/Join Framework under the hood to use multiple threads for processing, which can significantly increase the performance of the operation on large datasets.
How can I convert a Stream into a Collection?
You can convert a Stream into a Collection using the collect method. The collect method is a terminal operation that transforms the elements in the stream into a different kind of result, such as a Collection. You can use the Collectors utility class to provide a Collector that accumulates the input elements into a new Collection.
Can I use Streams to perform operations on data from a database?
Yes, you can use Streams to perform operations on data from a database. You can use the Stream API to process the data in a declarative way, which can lead to more readable and maintainable code. However, keep in mind that the Stream API is not a replacement for SQL and that the performance characteristics can be different.
What is the difference between an intermediate and a terminal operation?
Intermediate operations are operations that transform a Stream into another Stream, such as filter and map. They are always lazy, meaning that they don’t perform any processing until a terminal operation is invoked on the stream pipeline. Terminal operations, on the other hand, are operations that produce a result or a side-effect, such as reduce or collect.
Can I reuse a Stream?
No, you cannot reuse a Stream. Once a terminal operation is invoked on a Stream, it is consumed and cannot be used again. If you need to traverse the same data source again, you will have to create a new Stream.
How can I debug a Stream?
You can debug a Stream using the peek method. The peek method is an intermediate operation that takes a Consumer and returns a new Stream consisting of the elements of the original Stream, additionally performing the provided action on each element as elements are consumed from the resulting Stream.
Hathibelagal is an independent developer and blogger who loves tinkering with new frameworks, SDKs, and devices. Read his blog here.