When I first read about the Stream API, I found the name since it sounds similar to InputStream and OutputStream from Java I/O. But Java 8 streams are a completely different thing. Streams are Segments, thus playing a big part in bringing functional programming to Java – if you’re not yet familiar with Java 8 Lambda expressions, functional interfaces, and method references, you should probably read my previous article first before starting with this tutorial.
1. Streams
A stream represents a sequence of elements supporting sequential and parallel aggregate operations. Since Java 8, we can generate a stream from a collection, an array or an I/O channel.
Every collection class now has the stream() method that returns a stream of elements in the collections.The Stream interface is defined in java.util.stream package. Starting from Java 8, the Java collections will begin to have methods that return Stream.
2. Stream Pipeline
- We can say that a stream is a pipeline of aggregate operations that can be evaluated. A pipeline consists of the following parts:
- S source: can be a collection, an array or an I/O channel.
- Zero or more intermediate operations which produce new streams, such as filter, map, sorted, etc.
- A terminal operation that produces a non-stream result such as a primitive value, a collection, or void (such as the forEach operation).
Examples:
1 |
Stream<Student> stream = listStudents.stream(); |
Obtaining a stream from an array:
1 |
int[] arrayIntegers = {1, 8, 2, 3, 98, 11, 35, 91}; |
Obtaining a stream from a file:
1 2 |
BufferedReader bufferReader = new BufferedReader(new FileReader("students.txt")); Stream<String> streamLines = bufferReader.lines(); |
Operations that can be performed on a stream are categorized into intermediate operations and terminal operations. We’ll see details of these operations shortly. Consider the following code:
1 2 3 |
List<Student> top3Students = listStudents.stream().filter(s ->s.getScore() >= 70).sorted().limit(3).collect(Collectors.toList()); System.out.println("Top 3 Students by Score:"); top3Students.forEach(s -> System.out.println(s)); |
This code can be read as: select top 3 students whose scores >= 70, and sort them by score in descending order (the natural ordering of the Student class). Here we can see the following intermediate operations: filter, sorted and limit; and the terminal operation is collect.
As you can see, the operations on a stream can be chained together (intermediate operations) and end with a terminal operation. Such a chain of stream operations is called stream pipeline.
3. Intermediate Operations
An intermediate operation processes over a stream and return a new stream as a response. Then we can execute another intermediate operation on the new stream, and so on, and finally execute the terminal operation.
One interesting point about intermediate operations is that they are lazily executed. That means they are not run until a terminal operation is executed.
The Stream API provides the following common intermediate operations:
- map()
- filter()
- sorted()
- limit()
- distinct()
1. filter:
Stream.filter operation will return a new stream that contains elements that match its predicate. Below we will use a lambda expression to create a java.util.function.Predicate that will check if the integer is less than three. The terminal Stream.count operation will return the number of elements in the stream.
1 2 3 4 5 |
public void intermediate_filter() { long elementsLessThanThree = Stream.of(1, 2, 3, 4) .filter(p -> p.intValue() < 3).count(); assertEquals(2, elementsLessThanThree); } |
2. map:
Stream.map will transform the elements in a stream using the provided java.util.function.Function. A function is a method that accepts an argument and produces a result. They are commonly used for transforming collections and can be seen in the transforming a list to a map example.
In the snippet below we will create a function using a lambda expression that will replace any null strings in the stream with [unknown].
1 2 3 4 5 6 7 8 9 |
public void intermediate_map() { List<String> strings = Stream.of("one", null, "three").map(s -> { if (s == null) return "[unknown]"; else return s; }).collect(Collectors.toList()); assertThat(strings, contains("one", "[unknown]", "three")); } |
3. flatmap:
Stream.flatmap will transform each element into zero or more elements by a way of another stream. To demonstrate, we pulled code snippet from how to count unique words in a file example. Using java 7 file API, we will read all the lines from a file as a Stream. Then calling Stream.flatmap we will break the line into words elements. If we had a line made up of “the horse was walking down the street”, this line would be broken into [“the”, “horse”, “was”, “walking”, “down”, “the”, “street”]. Calling the Stream.distinct method will find all unique occurrences of words.
1 2 3 4 5 6 7 8 |
public void count_distinct_words_java8() throws IOException { File file = new File(sourceFileURI); long uniqueWords = java.nio.file.Files .lines(Paths.get(file.toURI()), Charset.defaultCharset()) .flatMap(line -> Arrays.stream(line.split(" ."))).distinct() .count(); assertEquals(80, uniqueWords); } |
4. Peek:
The Stream.peek is extremely useful during debugging. It allows you to peek into the stream before an action is encountered. In this snippet we will filter any strings with a size greater than four then call the peek at the stream before the map is called. The peek operation will print out the elements of ‘Badgers’, ‘finals’ and ‘four’.
1 2 3 4 5 6 |
public void intermediate_peek() { List<String> strings = Stream.of("Badgers", "finals", "four") .filter(s -> s.length() >= 4).peek(s -> System.out.println(s)) .map(s -> s.toUpperCase()).collect(Collectors.toList()); assertThat(strings, contains("BADGERS", "FINALS", "FOUR")); } |
5. distinct:
Stream.distinct will find unique elements in a stream according to their .equals behavior. A use case to use distinct is when you want to find the distinct number of words in a file.
1 2 3 4 5 6 7 8 9 |
public void intermediate_distinct() { List<Integer> distinctIntegers = IntStream.of(5, 6, 6, 6, 3, 2, 2) .distinct() .boxed() .collect(Collectors.toList()); assertEquals(4, distinctIntegers.size()); assertThat(distinctIntegers, contains( 5, 6, 3, 2)); } |
6. sorted:
The Stream.sorted method will return a stream sorted according to natural order. Below, we will create a stream of ints then calling the sort which will return the numbers in ascending order.
1 2 3 4 5 |
public void intermediate_sorted() { List<Integer> sortedNumbers = Stream.of(5, 3, 1, 3, 6).sorted() .collect(Collectors.toList()); assertThat(sortedNumbers, contains(1, 3, 3, 5, 6)); } |
7. limit:
Stream.limit is a useful technique to limit the number or truncate elements to be processed in the stream. Similar to limit elements in a list, we can perform the same behavior within a stream as shown below.
1 2 3 4 5 |
public void intermediate_limit() { List<String> vals = Stream.of("limit", "by", "two").limit(2) .collect(Collectors.toList()); assertThat(vals, contains("limit", "by")); } |
Terminal operations provided by the Stream API in details:
- The allMatch() operation
- The collect() operation
- The forEach() operation
- The max() operation
- The reduce() operation
Remember the following two key characteristics of terminal operations:
They can return a primitive value (boolean or long), a concrete type (Optional value object), or void (creating side effect).
They are eagerly executed, and a terminal operation is always the last operation in a Stream pipeline.
Note that the following examples are still based on the sample data (a list of Person objects) in the tutorial Java Stream Aggregate Functions Examples (Intermediate Operations).
- The allMatch operation
The allMatch()operation answers the question: Do all elements in the stream meet this condition? It returns true if and only if all elements match a provided predicate, otherwise return false.
The following example checks if all persons are male:
1 2 3 |
boolean areAllMale = listPersons.stream() .allMatch(p -> p.getGender().equals(Gender.MALE)); System.out.println("Are all persons male? " + areAllMale); |
Result:
1 |
Are all persons male? false |
- The collect operation
The collect() operation accumulates elements in a stream into a container such as a collection. It performs mutable reduction operation in which the reduced (final) value is a mutable result. The following example accumulates emails of the persons into a list collection:
1 2 3 4 5 6 7 8 |
List<String> listEmails = listPersons.stream() .map(p -> p.getEmail()) .collect(Collectors.toList()); System.out.println("List of Emails: " + listEmails); Output: List of Emails: [alice@gmail.com, bob@gmail.com, carol@gmail.com, david@gmail.com, eric@gmail.com, frank@gmail.com, gibb@gmail.com, henry@gmail.com, isabell@gmail.com, jane@gmail.com] |
- The forEach operation
The forEach() operation performs an action for each element in the stream, thus creating a side effect, such as print out information of each female person as shown in the following example:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
System.out.println("People are female:"); listPersons.stream() .filter(p -> p.getGender().equals(Gender.FEMALE)) .forEach(System.out::println); Output: People are female: Alice Brown Carol Hill Isabell Hill Jane Smith |
4. The max operation
the max() is a special reduction operation that returns the maximum element in the stream according to the specified comparator. The following example finds the oldest male person in the list:
1 2 3 4 5 6 7 8 9 10 11 |
Optional<Person> max = listPersons.stream() .filter(p -> p.getGender().equals(Gender.MALE)) .max((p1, p2) -> p1.getAge() - p2.getAge()); if (max.isPresent()) { Person oldestMan = max.get(); System.out.println("The oldest man is: " + oldestMan + " (" + oldestMan.getAge() + ")"); } Result: The oldest man is: David Green (39) |
5. The reduce operation
The Stream API provides three versions of reduce() methods which are general reduction operations. Let’s look at one version.
This method performs a reduction on the elements of the stream, using an associative accumulation function, and returns an Optional object describing the reduced value. For example, the following code accumulates first names of all persons into a String:
1 2 3 4 5 6 7 8 9 10 |
Optional<String> reducedValue = listPersons.stream() .map(p -> p.getFirstName()) .reduce((name1, name2) -> name1 + ", " + name2); if (reducedValue.isPresent()) { String names = reducedValue.get(); System.out.println(names); } Output: Alice, Bob, Carol, David, Eric, Frank, Gibb, Henry, Isabell, Jane |