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
CityandRegion.
Given as input a collection
regionsof regions, we can use the functionflatMapto 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();