Terminal operation #
A stream pipeline contains one terminal operation.
This operation may for instance:
- execute a method for each element of the stream (e.g. print the value of each of them), or
- collect these elements (e.g. into a list), or
- inspect these elements (check whether all of them satisfy a certain condition), or
- aggregate these elements (e.g. sum the values of a stream of integers).
Stream.forEach
#
This instance method is similar to Stream.map
,
in the sense that it also applies a callback method to each element of the stream.
However, in this case, the callback method is not a function (i.e. it has no return value).
In other words, if the stream has type Stream<
$\mathit{T}$>
,
then foreach
takes as argument a callback method of type
$\qquad \mathit{T} \to$ void
(equivalently, in Java’s terminology, the callback method must implement the native functional interface Consumer<
$\mathit{T}$>
, seen earlier).
Example.
List<Integer>list = List.of(4, 1, 3, 2, 4); // outputs `4, `2` and `4` list.stream() .filter(x -> x % 2 == 0) .forEach(System.out::println);
Hint. In the absence of intermediate operation, the instance method
Collection.forEach
can be used instead, with the same effect:List<Integer>list = List.of(4, 1, 3, 2, 4); // ouputs `4`, `1`, `3`, `2`, and `4` list.forEach(System.out::println);
Collecting a stream #
Stream.toList
#
This instance method outputs a final list with the elements of the stream.
Example. We already used this method in several of our examples. For instance
List<Integer>list = List.of(2, 1, 3, 1, 2); // Contains [1, 1, 2, 2, 3] List<Integer> ouputList = list.stream() .sorted() .toList();
Stream.collect
#
This instance method takes as argument a so-called Collector, which is in charge of collecting the elements of the stream into a Collection (e.g. List
or Set
), or a Map
, a String
, etc.
The utility class Collectors
provides a variety of static methods that return a collector.
Collectors.joining
#
This static method returns a collector that concatenates a stream of strings.
joining
(optionally) take a string as argument, which is used to separate the concatenated elements in the output string.
Example
City ancona = new City("Ancona", 60100); City bologna = new City("Bologna", 40100); City matera = new City("Matera", 75100); List<City> cities = List.of(ancona, bologna, matera); // Contains the string `Ancona:Bologna:Matera` String concat = cities.stream() .map(c -> c.name) .collect(Collectors.joining(":"));
The method Files.list(path)
takes as argument a path to a directory,
and returns the content of the directory,
as a stream of paths (i.e. a stream with type Stream<Path>
).
What does the method mysteriousMethod
below return?
String mysteriousMethod(Path path) throws IOException {
if(!Files.isDirectory(path)) {
return "";
}
return Files.list(path)
.filter(p -> path.toString().endsWith(".txt"))
.flatMap(p -> readLines(p))
.filter(l -> l.contains("foo"))
.collect(Collectors.joining(System.lineSeparator()));
}
Stream<String> readLines(Path path){
try {
return Files.lines(path);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Collectors.toCollection
#
This static method takes as argument the constructor of a Java collection (e.g. HashSet::new
, TreeSet::new
, ArrayList::new
, etc.), and returns a collector for this type of collection.
List<Integer>list = List.of(2, 1, 3, 1, 2);
// This is the TreeSet {1, 2, 3}
Set<Integer> ouputSet = list.stream()
.collect(Collectors.toCollection(TreeSet::new));
Collectors.toSet
#
As we saw in our initial pipeline example, this static method takes no argument, and returns a collector that produces a Java Set
.
The Java specification does not require a specific implementation.
So this set could for instance be a HashSet
or a TreeSet
, depending on the Java Virtual Machine that executes the program.
List<Integer>list = List.of(2, 1, 3, 1, 2);
// Contains {1, 2, 3}
Set<Integer> ouputSet = list.stream()
.collect(Collectors.toSet());
Collectors.toList
#
This static method is analogous to Collectors.toSet
, but the collector that it returns produces a Java List
.
Again, there is no requirement on the specific implementation of this list.
Collectors.toMap
#
This static method is analogous to Collectors.toSet
and Collectors.toList
, but the collector that it returns produces a Java Map
.
If the stream has type Stream<
$\mathit{T}$>
,
then toMap
takes as argument two callback functions, of type
$\qquad \mathit{T} \to K$
and
$\qquad \mathit{T} \to V$
respectively, where $K$ and $V$ are arbitrary types.
The first callback method defines the keys of the map, and the second defines the values.
Example. The following method takes a list of cities, and returns a map from city name to city.
Map<String, City> buildMap(List<City> cities){ return cities.stream() .collect(Collectors.toMap( c -> c.name, c -> c )); }
Warning. This method may throw an exception at runtime, in case of conflicting keys (in other words, if the first callback function is not injective). To make it robust, it is possible to pass it a third callback method, in charge of solving conflicts.
Collectors.groupingBy
#
This static method returns a collector that groups the elements of the stream, based on some input grouping criterion. The grouping criterion is specified as a callback function.
Example. The following method take a list of units as argument, and returns a map from a color to a list of units that share this color:
Map<String, List<Unit>> groupByColor(List<Unit> units){ return units.stream() .collect(Collectors.groupingBy(u -> u.color)); }
If the stream has type Stream<
$\mathit{T}$>
,
then Collectors.groupingBy
takes as argument a callback function of type
$\qquad \mathit{T} \to K$
where $K$ is an arbitrary type. This callback function computes a key for each element of the stream.
The terminal operation returns a map with type Map<
$K$, List<
$T$>>
that maps each key to the list of elements that share this key.
Alternatively, groupingBy
may be called with a Collector
as second argument, which aggregates the elements of each list.
Example. The following method take a list of units as argument, and returns a map from color to number of units that share this color.
Map<String, Long> groupByColorAndCount(List<Unit> units) { return units.stream() .collect(Collectors.groupingBy( u -> u.color, Collectors.counting() )); }
Boolean checks #
Stream.anyMatch
#
This instance method checks whether at least one element in the stream satisfies a certain Boolean condition.
Similarly to the method filter, if the stream has type Stream<
$\mathit{T}$>
,
then anyMatch
takes as argument a callback function of type
$\qquad \mathit{T} \to$ Boolean
which specifies the Boolean condition.
Warning.
anyMatch
is a short-circuiting operation, meaning that the stream is interrupted after the first match (if any).
Example. The following two programs are equivalent:
List<Integer> integers = getIntegers(); boolean containsEvenNumber = integers.stream() .anyMatch(x -> x % 2 == 0);
$\qquad$
List<Integer> integers = getIntegers(); boolean containsEvenNumber = false; for(int x : integers){ if(x % 2 == 0){ containsEvenNumber = true; break; } }
Coding style. Programs with multiple
break
statements can be difficult to read and/or debug. A stream with a short-circuiting operation (likeanyMatch
) can be a way to avoid them.
What does the method mysteriousMethod
below return?
List<Path> mysteriousMethod(Path path) throws IOException {
if (!Files.isDirectory(path)) {
return List.of();
}
return Files.list(path)
.filter(p -> path.toString().endsWith(".txt"))
.filter(p -> readLines(p).anyMatch(l -> l.contains("foo")))
.toList();
}
Stream<String> readLines(Path path){
try {
return Files.lines(path);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Stream.allMatch
#
This instance method is analogous to Stream.anyMatch
, but checks whether all elements in the stream satisfy the Boolean condition.
This is also a short-circuiting operation (if an element does not satisfy the criterion).
Aggregation #
Stream.findFirst
#
This instance method returns the first element in the stream.
This is a short-circuiting operation.
In combination with Stream.filter
, it can simulate Stream.anyMatch
.
The output value has type Optional<
$T$>
if the elements of the stream have type $T$ (we will see the meaning of Optional
in the dedicated section).
Example The following two programs are equivalent:
List<Integer> integers = getIntegers(); Optional<Integer> firstEvenNumber = integers.stream() .filter(x -> x % 2 == 0) .findFirst();
$\qquad$
List<Integer> integers = getIntegers(); Optional<Integer> firstEvenNumber = Optional.empty(); for(int x : integers){ if(x % 2 == 0){ firstEvenNumber = Optional.of(x); break; } }
Stream.count
#
This instance method returns the size of the stream.
Example.
List<Integer>list = List.of(2, 1, 3, 1, 2); // has value 2 int size = list.stream() .filter(x -> x % 2 == 0) .count();
Stream.max
#
This instance method takes as argument a Comparator, and returns (one of) the) maximal element(s) in the stream w.r.t. this comparator (if any).
The output value has type Optional<
$T$>
if the elements of the stream have type $T$ (we will see the meaning of Optional
in the dedicated section).
Example.
City ancona = new City("Ancona", 60100);, City bologna = new City("Bologna", 40100);, City matera = new City("Matera", 75100);, List<City> cities = List.of(ancona, bologna, matera); // Contains matera Optional<City> selectedCity = cities.stream() .max((c1, c2) -> c1.zipCode - c2.zipCode);
Stream.reduce
#
This instance method aggregates the elements of the stream based on a binary operator $\circ$.
For instance, $\circ$ could be +
or *
for a stream of numbers, or concatenation for a stream of strings.
As an illustration, if the stream has elements
$\qquad (a, b, c, d)$
then the method reduce
returns
$\qquad (((a \circ b) \circ c) \circ d)$
Let us assume that the stream has type Stream<
$\mathit{T}$>
.
The method reduce
takes two arguments:
-
a neutral element for the operator $\circ$, i.e. a value $a$ such that $a \circ x = x$ for any $x \in T$. For instance,
- $0$ is the neutral element for $+$,
- $1$ is the neutral element for $*$,
""
is the neutral element for string concatenation,- etc.
-
the binary operator $\circ$, specified as a callback function.
What is the type of this callback function?
Example.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4); // Has value 24 int product = numbers.stream() .reduce(1, (subtotal, element) -> subtotal * element);