Intermediate operations #
In a stream pipeline, an intermediate operation transforms a stream into another stream.
Stateless operations #
A stateless intermediate operation processes each element of the input stream independently, and without the need to memorize information about previous elements in the stream.
Stream.filter
and Stream.map
#
These two instance methods allow specifying a stateless operation, via a callback method. We have already explained their behavior in detail in our initial pipeline example.
Stream.flatMap
#
This is another very useful instance method, which can produce several objects out a single object in the stream.
Example. Consider the following classes
City
andRegion
.
Given as input a collection
regions
of regions, we can use the functionflatMap
to create a stream that consists of all cities in these regions, as follows:regions.stream() .flatMap(r -> r.cities.stream())
For instance, the following method takes as argument a list of regions, and returns the zip codes of all cities in these regions, with the exceptions of the zip code of Bologna:
List<Integer> allZipCodesButBologna(List<Region> regions) { return regions.stream() .flatMap(r -> r.cities.stream()) .filter(c -> !c.name.equals("Bologna")) .map(c -> c.zipCode) .toList(); }
Let us assume that the stream has type Stream<
$\mathit{T}$>
(for instance, in our example, $\mathit{T}$ is Region
).
The method flatMap
takes as argument a callback function of type
$\qquad T \to$ Stream<
$T’$>
where $T’$ can be any type.
In this example, the callback function is
r -> r.cities.stream()
which has type
$\qquad$ Region
$\to$ Stream<City>
Let us name this callback function $f$.
The method flatMap
returns a Stream<
$\mathit{T’}$>
(e.g. in this example a Stream<City>
).
This stream is the concatenation of all streams $f(a)$ such that $a$ belongs to the original stream.
Stateful operations #
Stateful intermediate operations are the ones that are not stateless.
We list here some of the most useful ones.
Stream.distinct
#
This instance method produces an identical stream, but without duplicates.
Example.
List<Integer>list = List.of(2, 1, 1, 3, 1, 2); // Contains [2, 1, 3] List<Integer> ouputList = list.stream() .distinct() .toList();
For a stream of objects, duplicates are identified based on Java’s object equality (i.e. as specified by the method Object.equals
).
Reminder. If a class overrides the method
Object.equals
, then it should also overrideObject.hashCode
(and these two implementations should be consistent with each other).
Stream.sorted
#
This instance method outputs a sorted stream. It can be called with or without a Comparator.
Its behavior (with and without a comparator) is analogous to the methods Arrays.sort and Collection.sort.
Example.
List<Integer>list = List.of(2, 1, 3, 1, 2); // Contains [1, 1, 2, 2, 3] List<Integer> ouputList = list.stream() .sorted() .toList();
Hint. Recall that
Comparator<
$T$>
is a functional interface, whose (only) method iscompare(
$T$e1
, $T$e2)
. As a result, it can be implemented with an anonymous method.For instance, the following method removes duplicates in a list of cities, and returns it sorted:
List<City> sortDistinctCitiesByName(List<City> cities){ return cities.stream() .distinct() .sorted((c1, c2) -> c1.name.compareTo(c2.name)) .toList(); }
Stream.limit
#
This instance method takes as argument an integer $n$, and outputs the $n$ first elements of the stream.
Example.
List<Integer>list = List.of(2, 1, 3, 1, 2); // Contains [2, 1, 3, 1] List<Integer> ouputList = list.stream() .limit(4) .toList();
Stream.skip
#
This instance method takes as argument an integer $n$, and outputs the stream without its $n$ first elements.
Example.
List<Integer>list = List.of(2, 1, 3, 1, 2); // Contains [3, 1, 2] List<Integer> ouputList = list.stream() .skip(2) .toList();