Stream of cartesian product of other streams, each element as a List?

1.1k views Asked by At

How can I implement a function using Java 8 to take some number of streams, and produce a stream in which each element is a list consisting of one member of the Cartesian product of the streams?

I've looked at this question -- that question uses an aggregator that is a BinaryOperator (taking two items of like type and producing an item of the same type). I'd like the items in the end result to be Lists rather than the types of the elements in the input streams.

Concretely, supposing my desired function is called product, the following:

Stream<List<String>> result =
    product(
        Stream.of("A", "B", "C", "D"),
        Stream.of("I", "J", "K"),
        Stream.of("Y", "Z")
    );

result.forEach(System.out::println);

should print:

[A, I, Y]
[A, I, Z]
[A, J, Y]
[A, J, Z]
[A, K, Y]
[A, K, Z]
[B, I, Y]
...
[D, K, Y]
[D, K, Z]

Ideally, I'd like for this operation to be as lazy as possible. For example, if the input streams are produced by Stream.generate(), it'd be great if the suppliers of those streams weren't executed until absolutely needed.

2

There are 2 answers

0
marco On BEST ANSWER

A possible solution is as follows:

private static <T> Stream<List<T>> product(Stream<T>... streams) {
    if (streams.length == 0) {
        return Stream.empty();
    }
    List<List<T>> cartesian = streams[streams.length - 1]
            .map(x -> Collections.singletonList(x))
            .collect(Collectors.toList());
    for (int i = streams.length - 2; i >= 0; i--) {
        final List<List<T>> previous = cartesian;
        cartesian = streams[i].flatMap(x -> previous.stream().map(p -> {
            final List<T> list = new ArrayList<T>(p.size() + 1);
            list.add(x);
            list.addAll(p);
            return list;
        })).collect(Collectors.toList());
    }
    return cartesian.stream();
}

public static void main(String... args) {
    final Stream<List<String>> result =
            product(
                    Stream.of("A", "B", "C", "D"),
                    Stream.of("I", "J", "K"),
                    Stream.of("Y", "Z")
            );

    result.forEach(System.out::println);
}

The product-call returns a Stream<List<String>> result which prints as

[A, I, Y]
[A, I, Z]
[A, J, Y]
[A, J, Z]
[A, K, Y]
[A, K, Z]
[B, I, Y]
[B, I, Z]
[B, J, Y]
[B, J, Z]
[B, K, Y]
[B, K, Z]
[C, I, Y]
[C, I, Z]
[C, J, Y]
[C, J, Z]
[C, K, Y]
[C, K, Z]
[D, I, Y]
[D, I, Z]
[D, J, Y]
[D, J, Z]
[D, K, Y]
[D, K, Z]
0
AudioBubble On

You can implement something like this:

List<Stream<String>> listStreams = List.of(
        Stream.of("A", "B", "C", "D"),
        Stream.of("I", "J", "K"),
        Stream.of("Y", "Z"));

Stream<List<String>> streamLists = listStreams.stream()
        // represent each list element as SingletonList<Object>
        .map(stream -> stream.map(Collections::singletonList))
        // summation of pairs of inner lists
        .reduce((stream1, stream2) -> {
            // list of lists from second stream
            List<List<String>> list2 = stream2.collect(Collectors.toList());
            // append to the first stream
            return stream1.flatMap(inner1 -> list2.stream()
                    // combinations of inner lists
                    .map(inner2 -> {
                        List<String> list = new ArrayList<>();
                        list.addAll(inner1);
                        list.addAll(inner2);
                        return list;
                    }));
        }).orElse(Stream.empty());

// output
streamLists.forEach(System.out::println);

Output:

[A, I, Y]
[A, I, Z]
[A, J, Y]
[A, J, Z]
[A, K, Y]
[A, K, Z]
[B, I, Y]
[B, I, Z]
[B, J, Y]
[B, J, Z]
[B, K, Y]
[B, K, Z]
[C, I, Y]
[C, I, Z]
[C, J, Y]
[C, J, Z]
[C, K, Y]
[C, K, Z]
[D, I, Y]
[D, I, Z]
[D, J, Y]
[D, J, Z]
[D, K, Y]
[D, K, Z]

See also: Find cartesian product of 2 lists