Appearance
Output Collections
We have introduced several common Stream
operations: map()
, filter()
, and reduce()
. These operations can be categorized into two types. The first type is transformation operations, which convert one Stream
into another Stream
, such as map()
and filter()
. The second type is aggregation operations, which calculate each element in a Stream
to obtain a specific result, such as reduce()
.
Distinguishing between these two operations is crucial because, for Stream
, performing a transformation operation does not trigger any computation! Let's run an experiment:
java
import java.util.function.Supplier;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream<Long> s1 = Stream.generate(new NatualSupplier());
Stream<Long> s2 = s1.map(n -> n * n);
Stream<Long> s3 = s2.map(n -> n - 1);
System.out.println(s3); // java.util.stream.ReferencePipeline$3@49476842
}
}
class NatualSupplier implements Supplier<Long> {
long n = 0;
public Long get() {
n++;
return n;
}
}
In this example, s1
is a Long
sequence with up to 922 quintillion elements. Despite executing the above code, there will be no memory growth or computation, as transformation operations only save the transformation rules. No matter how many times we transform a Stream
, no actual computation will occur.
In contrast, aggregation operations will immediately force the Stream
to output each element and aggregate them to get the final result. Therefore, performing an aggregation operation on a Stream
will trigger a series of chained requests:
java
Stream<Long> s1 = Stream.generate(new NatualSupplier());
Stream<Long> s2 = s1.map(n -> n * n);
Stream<Long> s3 = s2.map(n -> n - 1);
Stream<Long> s4 = s3.limit(10);
s4.reduce(0, (acc, n) -> acc + n);
When we aggregate s4
using reduce()
, it continuously requests each element from s4
, which in turn requests elements from s3
, s2
, and eventually from s1
, where real elements are fetched from the Supplier
instance, transformed through various steps, and finally aggregated by reduce()
.
Thus, an aggregation operation truly demands data from the Stream
, and the result is not another Stream
but some other Java object.
Output as List
reduce()
is just one type of aggregation operation. If we want to save the elements of a Stream
into a collection, such as a List
, it is an aggregation operation because a List
contains concrete Java objects. This means converting a Stream
to a List
forces the Stream
to output each element.
The following example demonstrates how to filter out empty strings from a list of String
values and save the non-empty strings into a List
:
java
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
Stream<String> stream = Stream.of("Apple", "", null, "Pear", " ", "Orange");
List<String> list = stream.filter(s -> s != null && !s.isBlank()).collect(Collectors.toList());
System.out.println(list);
}
}
To collect each element of a Stream
into a List
, call the collect()
method with a Collectors.toList()
object. This object acts as a Collector
that uses a reduce()
-like operation to add each element to a collector (actually an ArrayList
).
Similarly, collect(Collectors.toSet())
can collect each element of a Stream
into a Set
.
Output as Array
Outputting Stream
elements as an array is similar to outputting them as a List
. Simply call the toArray()
method and pass in the array constructor:
java
List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);
Here, the array constructor is String[]::new
, which actually implements the IntFunction<String[]>
interface, with the signature String[] apply(int)
, meaning it takes an int
parameter and returns a String[]
array.
Output as Map
Collecting Stream
elements into a Map
is a bit more involved, as each element needs a key and a value. Thus, we must specify two mapping functions to map elements to the key and value, respectively:
java
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
Map<String, String> map = stream
.collect(Collectors.toMap(
// Map element s to key:
s -> s.substring(0, s.indexOf(':')),
// Map element s to value:
s -> s.substring(s.indexOf(':') + 1)));
System.out.println(map);
}
}
Grouping Output
Stream
also supports powerful grouping capabilities. Here’s an example:
java
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<String> list = List.of("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");
Map<String, List<String>> groups = list.stream()
.collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
System.out.println(groups);
}
}
Grouping output uses Collectors.groupingBy()
, which requires two functions: one for the group key (s -> s.substring(0, 1)
to group strings by the first letter) and the other for the group value (Collectors.toList()
to output as a List
). The output of this code is:
java
{
A=[Apple, Avocado, Apricots],
B=[Banana, Blackberry],
C=[Coconut, Cherry]
}
This results in three groups, sorted by "A," "B," and "C," with each group being a List
.
If there is a Student
class with properties like student name, grade, and score:
java
class Student {
int gradeId; // Grade
int classId; // Class
String name; // Name
int score; // Score
}
Given a Stream<Student>
, grouping students by grade or class can be achieved with ease using grouping functions.
Summary
A Stream
can be collected into various collections:
- Use
Stream.collect()
to easily output to aList
,Set
, orMap
, and also perform grouped output.