Статьи

Сравнение императивных и функциональных алгоритмов в Java 8

Популярный твит Марио Фуско впечатляюще показывает, в чем главное отличие императивного и функционального подходов к подобным алгоритмам:

Императив против функциональности — разделение интересов pic.twitter.com/G2cC6iBkDJ

— Марио Фуско (@mariofusco) 1 марта 2015 г.

Оба алгоритма делают одно и то же, вероятно, они одинаково быстры и разумны Тем не менее, один из алгоритмов гораздо проще писать и читать, чем другой. Разница заключается в том, что в императивном программировании различные алгоритмические требования распределены по всему блоку кода, тогда как в функциональном программировании каждое требование имеет свою собственную небольшую строку кода. Для сравнения:

  • Зеленый: обработка ошибок
  • Синий: критерии Стоп
  • Красный: операции ввода-вывода
  • Желтый: «Бизнес логика»

Функциональное программирование не всегда превосходит императивное программирование, как показано в других примерах в блоге jOOQ :

Но вот пример из переполнения стека пользователем Aurora_Titanium , где разница столь же очевидна, как и в примере с Марио Фуско:

Вычисление повторяющихся значений в массиве

Идея состоит в том, чтобы вычислить сумму всех этих значений, которые дублируются в наборе значений. Например, следующий массив:

1
int[] list = new int[]{1,2,3,4,5,6,7,8,8,8,9,10};

… должен дать в результате что-то вроде:

1
Duplicate: 8. Sum of all duplicate values: 24

Императивный подход

Один из ответов пользователя Volkan Ozkan использует императивный подход и вычисляет сумму следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
int[] array = new int[] {
    1, 2, 3, 4, 5, 6, 7, 8, 8, 8, 9, 10
};
 
int sum = 0;
for (int j = 0; j < array.length; j++)
{
    for (int k = j + 1; k < array.length; k++)
    {
        if (k != j && array[k] == array[j])
        {
            sum = sum + array[k];
            System.out.println(
                "Duplicate found: "
              + array[k]
              + " "
              + "Sum of the duplicate value is " + sum);
        }
    }
}

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

Функциональный подход

Если для вас приемлемо небольшое снижение производительности (боксирование, сбор их в карты), и, возможно, это так, вы можете заменить вышеприведенный трудно читаемый код следующим фрагментом функциональной логики в стиле Java-8, которая гораздо яснее рассказывает, что делает:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
int[] array = new int[] {
    1, 2, 3, 4, 5, 6, 7, 8, 8, 8, 9, 10
};
 
IntStream.of(array)
         .boxed()
         .collect(groupingBy(i -> i))
         .entrySet()
         .stream()
         .filter(e -> e.getValue().size() > 1)
         .forEach(e -> {
             System.out.println(
                 "Duplicates found for : "
               + e.getKey()
               + " their sum being : "
               + e.getValue()
                  .stream()
                  .collect(summingInt(i -> i)));
         });

или с объяснениями:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int[] array = new int[] {
    1, 2, 3, 4, 5, 6, 7, 8, 8, 8, 9, 10
};
 
// Create a Stream<Integer> from your data
IntStream.of(array)
         .boxed()
 
// Group values into a Map<Integer, List<Integer>>
         .collect(groupingBy(i -> i))
 
// Filter out those map values that have only
// 1 element in their group
         .entrySet()
         .stream()
         .filter(e -> e.getValue().size() > 1)
 
// Print the sum for the remaining groups
         .forEach(e -> {
             System.out.println(
                 "Duplicates found for : "
               + e.getKey()
               + " their sum being : "
               + e.getValue()
                  .stream()
                  .collect(summingInt(i -> i)));
         });

(обратите внимание, что функциональный подход вычисляет суммы для каждого дублирующего значения, а не общую сумму, как императивный подход. Из первоначального вопроса это требование было не очень ясным)

Как мы уже говорили в предыдущей статье в нашем блоге, мощь функционального программирования с помощью API, такого как Java 8 Stream API, заключается в том, что мы приближаемся к выразительной силе декларативного программирования в стиле SQL. Мы больше не заботимся о запоминании индексов отдельных массивов и о том, как их вычислять и сохранять промежуточные результаты в некоторых буферах. Теперь мы можем сосредоточиться на действительно интересной логике, такой как: «что такое дубликат?» или «какая сумма меня интересует?»

Прочтите о том, как SQL сравнивается с потоками Java 8: общие предложения SQL и их эквиваленты в потоках Java 8