не удается создавать комбинации массивов с потоками java

#java #java-stream

Вопрос:

Я пытаюсь создавать различные комбинации из массивов нескольких размеров, выбирая по одному элементу из каждого. проверьте код ниже, переменная collect выдает правильный вывод, но collect1 выдает ошибку.

 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)));
 

ошибка для collect1: Несоответствие типов: не удается преобразовать из списка<Поток<Поток<int[]><int[]>><int []>> в список<int[]>>><int[]>

Ответ №1:

Часто бывает хорошей идеей свести проблему к минимуму, насколько это возможно. Вот 2 списка с 2 пунктами. Объединить их означает взять первый элемент первого списка, а затем создать комбинацию с каждым из каждого второго списка. Затем возьмите второй пункт первого списка и объедините его с каждым пунктом из второго списка. И так далее …

Псевдокод:

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

Теперь давайте попробуем выразить это потоками:

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

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

Самое сложное в том, что это не работает! Потоки (и обобщения в целом) в Java могут использовать только ссылочные типы, они не поддерживают простые типы значений, такие как int . Это немного усложняет код. Тип int[] расширяется Object , поэтому мы можем преобразовать код следующим образом:

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

Добавление третьего входного массива делает это:

 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();
 

или со списком массивов int:

 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());
 

Промойте и повторяйте, пока не добавите все входные данные:

 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());
 

Если бы вы начали с целых чисел в коробках, ваш поток стал бы намного проще:

 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());
 

Аналогичного эффекта можно достичь, преобразовав каждый IntStream из них в Stream<Integer> первый. Я нахожу, что это легче читать и понимать, чем стиль mapToObj.flatMap:

 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());
 

Помните о влиянии на производительность при создании большого количества вложенных и небольших потоков, а также при упаковке/распаковке примитивных значений.

Ответ №2:

Согласно документам для MapToObj : «Возвращает поток, возвращающий объектное значение, состоящий из результатов применения данной функции к элементам этого потока».

Поэтому каждый раз, когда вы звоните ему, он возвращает Stream «а». Таким образом, если вам нужны только значения в потоке, вы должны добавить вызов flatmap для каждого вызова в 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());