How to Use the Java Stream API
What Is the Stream API?
The Stream API is an extended iteration API added in Java SE 8. It makes it possible to write complex processing that was previously performed on collections in clearer code.
This page does not explain lambda expressions, so if you are not familiar with them, first read Lambda Functions.
A stream can be thought of as another form of a collection that allows access to collection elements.
Basic Flow
The structure of a stream is broadly divided into three steps.
- Create a Stream from a collection.
- Execute as many “intermediate operations” as needed on the stream, combining and transforming the collection contents.
- Apply processing to the transformed collection contents with a “terminal operation”.
In actual use, it is written in a form like createStream().intermediateOperation().terminalOperation().
Now let’s look at an actual example. The following example displays only even numbers from 1 to 5.
Without the Stream API
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
for (Integer i : list) {
if (i % 2 == 0) {
System.out.println(i);
}
}
With the Stream API
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
list.stream() // Create Stream
.filter(i -> i % 2 == 0) // Intermediate operation
.forEach(i -> System.out.println(i)); // Terminal operation
Let’s look at the difference in processing. Without the Stream API, the structure is “display only when the remainder after division by 2 is 0”. With the Stream API, the structure is “collect all elements divisible by 2 and display them all”.
It becomes possible to separate these two kinds of processing. This improves readability and modularity, and also enables code abstraction.
In this example, a stream is obtained and the filter method is used as an intermediate operation. As the name suggests, it filters collection elements. The filter method receives a lambda expression such as T -> boolean. (If you do not know what T means, check generics.) It returns a stream containing only elements for which the lambda expression is true.
The forEach method is used as the terminal operation. As its name suggests, it is a method that “executes processing by taking out elements one by one”. It should be familiar from other languages. In Java, it corresponds to the enhanced for statement.
This example separates the intermediate operation that gets “only items whose remainder after division by 2 is 0” from the terminal operation that “prints each item to the screen”.
Because an intermediate operation returns a stream after applying its work, it can be written as a chain as in the example.
Of course, it can also be written as follows.
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = integerList.stream(); // Create Stream
Stream<Integer> stream2 = stream.filter(i -> i % 2 == 0); // Intermediate operation
stream2.forEach(i -> System.out.println(i)); // Terminal operation
Chaining is more readable, so unless there is a special reason, write it continuously.
Intermediate Operations
This section explains commonly used representative intermediate operations.
filter
As mentioned in the previous example, this is an intermediate operation for filtering. It receives a lambda expression such as T -> boolean as an argument. It collects only elements whose condition is true.
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() // Create Stream
.filter(i -> i % 2 == 0) // Intermediate operation
.forEach(i -> System.out.println(i)); // Terminal operation
map
The argument of map is a lambda expression such as T -> U. It is an intermediate operation that transforms elements.
It accesses each element inside the stream one by one, executes the function passed as a parameter, and then returns the result in the format specified by the terminal operation.
As in the example below, you can multiply values by 2.
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() // Create Stream
.map(i -> i * 2) // Intermediate operation
.forEach(i -> System.out.println(i)); // Terminal operation
You can also return a different type, such as from Integer to String, as in the example below.
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() // Create Stream
.map(i -> "요소는" + i + "입니다.") // Intermediate operation
.forEach(i -> System.out.println(i)); // Terminal operation
Also, suppose there is an Animal interface like the following.
interface Animal {
/**
* Returns the cry.
*/
String getCry();
}
You can use map on an animal list and print each cry.
List<Animal> animalList = Arrays.asList(dog, cat, elephant);
animalList.stream() // Create Stream
.map(animal -> animal.getCry()) // Intermediate operation
.forEach(cry -> System.out.println(cry)); // Terminal operation
flatMap
flatMap is also called stream flattening. When the data is a two-dimensional array or list, flatMap converts it into one dimension.
Let’s look at the following code.
List<List<String>> alphabets = Arrays.asList(Arrays.asList("a", "b", "c"), Arrays.asList("d", "e", "f"));
List<String> result = alphabets.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println(result);
Output:
[a, b, c, d, e, f]
sorted
The argument of sorted is a lambda expression such as (T, T) -> int. It is an intermediate operation that sorts elements. It takes two elements and passes them to the lambda expression. If the return value is positive, the order is descending; if it is negative, the order is ascending.
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() // Create Stream
.sorted((a, b) -> Integer.compare(a, b)) // Intermediate operation
.forEach(i -> System.out.println(i)); // Terminal operation
For simpler sorting in ascending or descending order, the clearest approach is to pass the Comparator interface methods naturalOrder() or reverseOrder() as in the example below.
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() // Create Stream
.sorted(Comparator.reverseOrder()) // Intermediate operation
.forEach(i -> System.out.println(i)); // Terminal operation
So far, we have looked at three representative intermediate operations that are used often. There are many others, so look them up if you are interested.
Terminal Operations
forEach
The argument of forEach is a lambda expression such as (T) -> void. It is a terminal operation that takes all elements one by one and performs some processing.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
numbers.stream()
.map(x -> x + 3) // Add 4.
.filter(x -> x % 2 == 0) // Filter only even numbers.
.limit(3) // Take up to 3.
.forEach(System.out::println);
Output:
4
6
8
When forEach is called, the entire stream is consumed.
peek
Unlike forEach, which consumes the entire stream when called, peek does not actually consume stream elements. peek passes the element it inspected as-is to the next operation in the pipeline and prints intermediate values before and after each operation.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
List<Integer> result = numbers.stream()
.peek(v -> System.out.println("From stream: " + v)) // Print the first consumed element
.map(v -> v + 3)
.peek(v -> System.out.println("Mapped value: " + v)) // Print the result after map
.filter(v -> v % 2 == 0)
.peek(v -> System.out.println("Filtered value: " + v)) // Print the result after filter
.limit(3)
.peek(v -> System.out.println("limited value: " + v)) // Print the result after limit
.collect(Collectors.toList());
System.out.println(result);
Output:
From stream: 1
Mapped value: 4
Filtered value: 4
limited value: 4
From stream: 2
Mapped value: 5
From stream: 3
Mapped value: 6
Filtered value: 6
limited value: 6
From stream: 4
Mapped value: 7
[4, 6]
Various Stream Examples
Extracting Only Non-Numeric Values from a String and Converting Them Back to a String
String input = "1Ca2Adf34eB342";
String output = input.chars()
.mapToObj(ch -> (char) ch)// Stream<Character>
.sorted()
.filter(ch -> !Character.isDigit(ch))
.map(Object::toString)
.collect(Collectors.joining());
System.out.println(output);
Output:
ABCadef
Combining Two Lists into Pairs
List<Integer> numberArray1 = Arrays.asList(1, 2);
List<Integer> numberArray2 = Arrays.asList(3, 4, 5);
List<int[]> pairs = numberArray1.stream()
.flatMap(i -> numberArray2.stream()
.map(j -> new int[]{i, j}))
.collect(Collectors.toList());
for (int[] a : pairs) {
System.out.println(Arrays.toString(a));
}
Output:
[1, 3]
[1, 4]
[1, 5]
[2, 3]
[2, 4]
[2, 5]
Extracting Numbers from a String and Calculating the Sum
String input = "1Ca2Adf9";
int sum = input.chars()
.filter(Character::isDigit)
.map(a -> Character.digit(a, 10))
.sum();
System.out.println(sum); // 1 + 2 + 9
Output:
12
Performing joining Only When Strings Are Not null
String front = "abc";
String middle = null;
String end = "ghi";
String output = Stream.of(front, middle, end)
.filter(Objects::nonNull)
.collect(Collectors.joining());
System.out.println(output);
Output:
abcghi