Learn various intermediate operations Java Stream supports and use them in examples.
We have had an overview of Java 8 Streams API in the last few posts. Until now, we have looked at the basics of the streams, understood how the streams work, how they create and work with streams and learned about streams' laziness and performance optimisation. If you are new to Java 8 Streams, read the previous two posts Understanding Java 8 Streams API and Java 8 Streams API – Laziness and Performance Optimisation.
During the last discussion, we understood that any Stream Operation could be divided into the following steps.
- Creating a Stream: Streams can be made from an existing collection or other ways of creating Streams.
- A Set of Intermediate Operations: Intermediate Operations process over a Stream and return Stream as a response.
- A Terminal Operation: Java 8 Streams Terminal Operation is the end of a Stream flow.
This tutorial will focus on the various Intermediate Operations the Java 8 Streams API made available. Examples used in previous posts demonstrate a few Intermediate Operations like map() and filter(). Here will have a look at all of them in detail.
Mapping Java Stream Elements
Mapping is a process of changing the form of the elements in a stream or a collection. Java Streams API provides map() and flatMap() functions to achieve this.
Java Streams map() function
Sometimes, we may want to apply a change to all the elements of a Stream, or we may wish to change the form of a Stream. We can use Java Stream’s map() method to do so.
The map() method applies the given function to all the elements of this Stream and returns a Stream of the returned elements.
<R> Stream<R> map(Function<? super T,? extends R> mapper)Code language: Java (java)
In the following example, we use the Java Streams map() function on a Stream of Student elements and create a Stream of student names.
Stream<Student> studentsStream = getStudents().stream(); Stream<String> namesStream = studentStream.map(Student::getName);Code language: Java (java)
Java Streams flatMap() function
The flatMap() function is another useful intermediate operation in Java Streams. Like the map() function, the flatMap() function also transforms the form of Stream elements. However, flatMap() is useful when applying one-to-many transformations on stream elements and flattening the output elements in a new Stream.
<R> Stream<R> flatMap (Function<? super T,? extends Stream<? extends R>> mapper)Code language: Java (java)
The flatMap() method takes a function that produces a Stream of elements. The method applies this function to all the elements and the Stream and then combines the elements of each sub-stream into the output stream.
For example, we use the Java Stream’s flatMap() – intermediate operation – to combine elements of multiple Lists into one List. In other words, we merge multiple Java Lists to get a list of combined elements.
List<List<Integer>> listOfLists = List.of( List.of(1, 2, 3), List.of(4, 5), List.of(6, 7, 8, 9, 10) ); List<Integer> flatList = listOfLists.stream() .flatMap(Collection::stream) .toList();Code language: Java (java)
That creates a new List of combined elements.
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]Code language: Bash (bash)
Let’s see another example that the official Java Documentation for the flatMap() function uses. Here, we are reading a Stream of all the words from a file using Java Stream’s intermediate stream operation of flatMap().
Stream<String> lines = Files.lines(path, UTF_8); Stream<String> words = lines .flatMap(line -> Stream.of(line.split(" +")));Code language: Java (java)
Filtering Java Stream Elements
This section uncovers Java Stream’s intermediate operations that we can use to constrain Stream elements.
Java Streams filter() function
We can use the filter() method to choose specific elements from a Stream and create another Stream of the combined elements. The function applies the given predicate to the stream elements and generates a new Stream of matched elements.
For example, we can use the Java Stream filter() method on the Stream of student elements to get a List of students who scored more than 60.
List<Student> passedStudents = getStudents().stream() .filter(student -> student.getScore() >= 60) .toList();Code language: Java (java)
Java Streams distinct() to get Unique Elements
The distinct() method of Java Stream is a powerful Intermediate Stream operation. We can use it on a Stream to create another Stream of unique elements.
In the following example, we find a list of unique names from a student’s List.
List<String> uniqueNames = getStudents().stream() .map(Student::getName) .distinct() .toList();Code language: Java (java)
We can use the distinct() method to remove duplicate elements from a Java Stream. However, the distinct() is a buffered operation as it needs all the elements of the Stream to identify duplicates. Thus, the distinct() function loads all Stream elements into the memory and should be used cautiously.
Java Streams limit() to get limit Stream Elements
The limit() function on a Stream returns a Stream of n elements where n is equal to or less than the given maxSize. This Java Stream intermediate function takes a long maxSize argument and produces a new Stream of truncated elements.
For example, we can use the limit() method and filter() to find the first three students older than 20.
getStudents().stream() .filter(s -> s.getAge() > 20) .map(Student::getName) .limit(3) .toList();Code language: Java (java)
Because of the Stream’s short-circuiting capability, the sequential Stream processing stops as soon as when it finds the first three matching elements. However, the limit operation is expensive for ordered or parallel processing streams, especially when the maxSize is large.
Java Streams skip() to skip Stream Elements
The skip() function in the Stream is an intermediate Stream operation that skips the first n elements in the Stream and returns the remaining elements as a new Stream.
For example, we can use the skip() function with filter() to find students older than 20 years, excluding the first three students.
getStudents().stream() .filter(s -> s.getAge() > 20) .map(Student::getName) .skip(3) .toList();Code language: Java (java)
Like the limit() method, the skip() method is very cheap for sequential streams. However, it is expensive for parallel processing or ordered streams when n is large.
Sorting Java Stream Elements
The Java Streams supports sorting its elements through its intermediate operation of the sorted() method. The sorted() method sorts the elements of the Stream in their natural order and returns a new Stream of sorted elements.
For example, we can use Java Streams sorted() method to sort a List of students.
getStudents().stream() .sorted() .toList();Code language: Java (java)
Additionally, the sorted() method can accept a Comparator instance that we can use to define a custom and more flexible sorting order.
For example, we can use the sorted() method with a Comparator that sorts the student’s collection based on student names.
getStudents().stream() .sorted(Comparator.comparing(Student::getName)) .toList();Code language: Java (java)
We have a detailed guide demonstrating the full power of the sorted() function. Please read it here: Sorting Collections with Java Streams API.
Like distinct, the sort function is also buffered and requires all of the elements of a stream before it performs the sorting. Thus, the operation is memory heavy.
This detailed article uncovered Java Streams intermediate operations that we can use to process the elements in the Stream. Firstly, we learned how to use map() and flatMap() operations to apply given logic on all the elements of the streams. Next, we learned how to use filter(), distinct(), limit(), and skip() operations to manipulate the stream elements. Lastly, we learned how to sort the Stream elements.
It’s really a good explanation and simple to understand the concepts in JAVA8. Thanks for posting your valuable learnings in this blog.