I have been thinking about how to collect “two adjacent elements” in a stream, for example transform a stream of Long
into a stream of Pair<Long,Long>
(where Pair<A,B>
is a little record that does just what it says). I only came up with the idea of a stateful lambda to be used inside a Stream.map()
that buffers every second element and sends an Optional<Pair>
rightwards that can then be filtered by its not-emptyness" (Good idea? It won’t support parallel streams for sure; this could also be used for illustration in Chapter 12 - “Avoid Side-Effects in Functional Pipelines”)
import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; public class Experimental { record PairOfInt(Integer a, Integer b) { @Override public String toString() { return "(" + a + ", " + b + ")"; } } // Problems: // 1) We lost the last element in a stream with an odd number of elements // 1) If the stream is run "in parallel" anything can happen here. // It would definitely be necessary to synchronize the "stash" // 2) Is there a way to make sure and make evident in code that // a stream cannot be run in parallel so that the next developer // doesn't try something stupid? public Function<Integer, Optional<PairOfInt>> buildPairBuilder() { List<Integer> stash = new ArrayList<>(1); return (x) -> { synchronized (stash) { if (stash.isEmpty()) { stash.add(x); return Optional.empty(); } else { return Optional.of(new PairOfInt(stash.remove(0), x)); } } }; } private static String stringify(List<PairOfInt> pairs) { return pairs.stream() .map(PairOfInt::toString) .collect(Collectors.joining(", ")); } // Behaves well, prints out // (0, 1), (2, 3), (4, 5), (6, 7), (8, 9), (10, 11), (12, 13), (14, 15), (16, 17), (18, 19) ... @Test public void runStreamSequentially() { var pairBuilder = buildPairBuilder(); List<PairOfInt> pairs = IntStream.rangeClosed(0, 33) .boxed() .map(pairBuilder) .filter(Optional::isPresent) .map(Optional::orElseThrow) .toList(); System.out.println(stringify(pairs)); } // Behaves badly, prints out for example // (0, 1), (2, 3), (25, 4), (31, 5), (7, 6), (10, 11), (26, 13), (12, 14), ... @Test public void runStreamParallel() { var pairBuilder = buildPairBuilder(); List<PairOfInt> pairs = IntStream.rangeClosed(0, 33) .parallel() // **** DANGER, WILL ROBINSON! **** .boxed() .map(pairBuilder) .filter(Optional::isPresent) .map(Optional::orElseThrow) .toList(); System.out.println(stringify(pairs)); } }
As StackOverflow exists, one can get pointers on how to (nearly) do that:
The proposed solution is to write one’s own collector, which works at the “business tail” of the stream only of course.
I haven’t tried this yet but one the idea arises that one might want to add a write your own collector subchapter to the book.
The problem of elegantly generating pair in the middle of the stream is still open but a reader points to a 3rd-party library called
StreamEx
:
where you can do things like
DoubleStreamEx.of(input).pairMap((a, b) -> b-a).toArray();
But I haven’t looked at that at all.