Dhruv Sharma
Dhruv Sharma

Reputation: 25

not able to create combinations of arrays with java streams

I am trying to create different combinations from multiple sized arrays, picking one element from each. check below code, collect variable is giving correct output, but collect1 is giving error.

int[] pants= {3, 5, 7}, shirts = {4, 7, 8}, 
                skirts = {5, 8}, shoes = {3};

List<int[]> collect = Arrays.stream(pants)
            .mapToObj(i -> Arrays.stream(shirts)
            .mapToObj(j -> new int[] {i,j}))
            
            .flatMap(Function.identity())
            .collect(Collectors.toList());
    
    List<int[]> collect1 = Arrays.stream(pants)
            .mapToObj(i -> Arrays.stream(shirts)
            .mapToObj(j -> Arrays.stream(skirts)
            .mapToObj(k -> Arrays.stream(shoes)
            .mapToObj(l -> new int[] {i,j,k,l}))))
            
            .flatMap(Function.identity())
            .collect(Collectors.toList());

collect.forEach(ar->System.out.println(Arrays.toString(ar)));

error for collect1 is: Type mismatch: cannot convert from List<Stream<Stream<int[]>>> to List<int[]>

Upvotes: 1

Views: 308

Answers (2)

GBlodgett
GBlodgett

Reputation: 12819

According to the docs for MapToObj: "Returns an object-valued Stream consisting of the results of applying the given function to the elements of this stream."

So every time you call it, it returns a Stream. Thus if you want just the values in the stream, you must add in a call to flatmap for every call to MapToObj:

List<int[]> collect1 = Arrays.stream(pants)
        .mapToObj(i -> Arrays.stream(shirts)
          .mapToObj(j -> Arrays.stream(skirts)
            .mapToObj(k -> Arrays.stream(shoes)
              .mapToObj(l -> new int[] {i,j,k,l}))))
        .flatMap(Function.identity())
        .flatMap(Function.identity())
        .flatMap(Function.identity())
        .collect(Collectors.toList());

Upvotes: 1

knittl
knittl

Reputation: 265547

It's often a good idea to reduce a problem to the smallest for possible. Here it is 2 lists with 2 items. Combining them means to take the first item of the first list and then produce a combination with each each of the second list. Then take the second item of the first list and combine it with each item from the second list. And so on …

Pseudo code:

combine({1, 2}, {3, 4}) == { { 1, 3 }, { 1, 4 }, { 2, 3 }, { 2, 4 } };

Now let's try to express this with streams:

int[] first = { 1, 2 }, second = { 3, 4 };

int[][] combinations = Arrays.stream(first)
        .flatMap(a -> Arrays.stream(second)
                .map(b -> new int[]{a, b}))
        .toArray();

The tricky part is, that this does not work! Streams (and generics in general) in Java can only used reference types, they do not support simple value types such as int. This makes the code a bit more complicated. The type int[] extends Object, so we can transform the code as follows:

Object[] combinations = Arrays.stream(first)
        .mapToObj(a -> Arrays.stream(second)
                .mapToObj(b -> new int[]{a, b}))
        .flatMap(x -> x)
        .toArray();

Adding a third input array makes this:

Object[] combinations = Arrays.stream(first)
        .mapToObj(a -> Arrays.stream(second)
                .mapToObj(b -> Arrays.stream(third)
                        .mapToObj(c -> new int[]{a, b, c}))
                .flatMap(x -> x))
        .flatMap(x -> x)
        .toArray();

or, with a list of int arrays:

List<int[]> combinations = Arrays.stream(first)
        .mapToObj(a -> Arrays.stream(second)
                .mapToObj(b -> Arrays.stream(third)
                        .mapToObj(c -> new int[]{a, b, c}))
                .flatMap(x -> x))
        .flatMap(x -> x)
        .collect(Collectors.toList());

Rinse and repeat, until you have added all inputs:

List<int[]> combinations = Arrays.stream(first)
        .mapToObj(a -> Arrays.stream(second)
                .mapToObj(b -> Arrays.stream(third)
                        .mapToObj(c -> Arrays.stream(fourth).mapToObj(d -> new int[]{a, b, c, d}))
                        .flatMap(x -> x))
                .flatMap(x -> x))
        .flatMap(x -> x)
        .collect(Collectors.toList());

If you were to start out with boxed Integers, your stream would become a lot simpler:

Integer[] first = { 1, 2 }, second = { 3, 4 }, third = { 5, 6 }, fourth = { 7, 8 };

List<int[]> combinations = Arrays.stream(first)
        .flatMap(a -> Arrays.stream(second)
                .flatMap(b -> Arrays.stream(third)
                        .flatMap(c -> Arrays.stream(fourth)
                                .map(d -> new int[]{a, b, c, d}))))
        .collect(Collectors.toList());

A similar effect can be achieved by converting each IntStream to a Stream<Integer> first. I find this is easier to read and understand than the mapToObj.flatMap style:

List<int[]> combinations = Arrays.stream(first)
        .boxed()
        .flatMap(a -> Arrays.stream(second)
                .boxed()
                .flatMap(b -> Arrays.stream(third)
                        .boxed()
                        .flatMap(c -> Arrays.stream(fourth)
                                .boxed()
                                .map(d -> new int[]{a, b, c, d}))))
        .collect(Collectors.toList());

Be aware of the performance impact when creating lots of nested and small streams and when boxing/unboxing primitive values.

Upvotes: 1

Related Questions