Статьи

3 причины, по которым вы не должны заменять свои циклы for на Stream forEach

Потрясающие! Мы переносим нашу кодовую базу на Java 8. Мы заменим все на функции. Выкинь шаблоны дизайна. Удалить объект ориентации. Правильно! Поехали!

Подожди минуту

Java 8 выпускается уже более года, и острые ощущения вернулись к повседневной работе.

В нерепрезентативном исследовании, проведенном baeldung.com в мае 2015 года, установлено, что 38% их читателей приняли Java 8 . До этого исследование Typsafe, проведенное в конце 2014 года, заявило, что пользователи Java 8% приняли Java 8 .

Что это значит для вашей кодовой базы?

Некоторые рефакторинги миграции Java 7 -> Java 8 не представляют сложности. Например, при передаче Callable в ExecutorService :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
ExecutorService s = ...
 
// Java 7 - meh...
Future<String> f = s.submit(
    new Callable<String>() {
        @Override
        public String call() {
            return "Hello World";
        }
    }
);
 
// Java 8 - of course!
Future<String> f = s.submit(() -> "Hello World");

Стиль анонимного класса действительно не добавляет здесь никакой ценности.

Помимо этих простых задач, есть и другие, менее очевидные темы. Например, использовать ли внешний и внутренний итератор . Смотрите также эту интересную статью Нила Гафтера, опубликованную в 2007 году, на тему вне времени: http://gafter.blogspot.ch/2007/07/internal-versus-external-iterators.html.

Результат следующих двух частей логики одинаков

1
2
3
4
5
6
7
8
List<Integer> list = Arrays.asList(1, 2, 3);
 
// Old school
for (Integer i : list)
    System.out.println(i);
 
// "Modern"
list.forEach(System.out::println);

Я утверждаю, что «современный» подход следует использовать с особой осторожностью, т. Е. Только в том случае, если вы действительно извлекаете выгоду из внутренней функциональной итерации (например, при объединении в цепочку набора операций через map() , flatMap() и другие операции Stream).

Вот краткий список минусов «современного» подхода по сравнению с классическим:

1. Производительность — вы на ней проиграете

Анжелика Лангер достаточно хорошо завершила эту тему в своей статье и связанной с ней лекции на конференциях :

https://jaxenter.com/java-performance-tutorial-how-fast-are-the-java-8-streams-118830.html

Во многих случаях производительность не критична, и вам не следует делать преждевременную оптимизацию — поэтому вы можете утверждать, что этот аргумент на самом деле не является аргументом как таковым. Но я буду противостоять этому Stream.forEach() в этом случае, говоря, что издержки Stream.forEach() по сравнению с обычным циклом for в целом настолько значительны, что его использование по умолчанию просто накапливает множество бесполезных циклов ЦП во всех ваших заявление. Если мы говорим об увеличении потребления ЦП на 10-20% только из-за выбора стиля цикла, то мы сделали что-то принципиально неправильное. Да — отдельные циклы не имеют значения, но нагрузки на всю систему можно было бы избежать.

Вот результат теста Angelika для обычного цикла, который находит максимальное значение в списке упакованных целочисленных значений:

1
2
ArrayList, for-loop : 6.55 ms
ArrayList, seq. stream: 8.33 ms

В других случаях, когда мы выполняем относительно простые вычисления для примитивных типов данных, мы абсолютно ДОЛЖНЫ вернуться к классическому циклу for (и предпочтительно к массивам, а не к коллекциям).

Вот результат теста Angelika для обычного цикла, который находит максимальное значение в массиве примитивных целых:

1
2
int-array, for-loop : 0.36 ms
int-array, seq. stream: 5.35 ms

Преждевременная оптимизация — это не хорошо, но, как следствие, предотвращение преждевременной оптимизации еще хуже. Важно подумать о том, в каком контексте мы находимся, и принять правильные решения в таком контексте. Мы уже писали о производительности раньше, читайте нашу статью Топ 10 простых оптимизаций производительности в Java

2. Читаемость — для большинства людей, по крайней мере

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

Причина, по которой мы это делаем, заключается в том, что обслуживание программного обеспечения сложно. Особенно кода, написанного кем-то другим. Давным давно. Кто, вероятно, написал только код C до перехода на Java.

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

1
2
3
4
5
6
7
8
List<Integer> list = Arrays.asList(1, 2, 3);
 
// Old school
for (Integer i : list)
    System.out.println(i);
 
// "Modern"
list.forEach(System.out::println);

Но что здесь происходит:

01
02
03
04
05
06
07
08
09
10
11
12
13
List<Integer> list = Arrays.asList(1, 2, 3);
 
// Old school
for (Integer i : list)
    for (int j = 0; j < i; j++)
        System.out.println(i * j);
 
// "Modern"
list.forEach(i -> {
    IntStream.range(0, i).forEach(j -> {
        System.out.println(i * j);
    });
});

Все становится немного интереснее и необычнее. Я не говорю «хуже». Это вопрос практики и привычки. И нет черно-белого ответа на проблему. Но если остальная часть кода является императивной (и, вероятно, таковой является), то вложение объявлений диапазона и вызовы forEach() и лямбда-выражения, безусловно, являются необычными, вызывая когнитивные трения в команде.

Вы можете построить примеры, в которых императивный подход действительно кажется более неловким, чем эквивалентный функциональный, как показано здесь:

Но во многих ситуациях это не так, и написание функционального эквивалента чего-то относительно простого императива довольно сложно (и опять же, неэффективно). Пример можно увидеть в этом блоге в предыдущем посте: http://blog.jooq.org/2015/09/09/how-to-use-java-8-functional-programming-to-generate-an-alphabetic -последовательность/

В этом посте мы сгенерировали последовательность символов:

1
A, B, ..., Z, AA, AB, ..., ZZ, AAA

… Похоже на столбцы в MS Excel:

Fig1_50047

Императивный подход ( изначально неназванный пользователь в переполнении стека ):

01
02
03
04
05
06
07
08
09
10
11
import static java.lang.Math.*;
  
private static String getString(int n) {
    char[] buf = new char[(int) floor(log(25 * (n + 1)) / log(26))];
    for (int i = buf.length - 1; i >= 0; i--) {
        n--;
        buf[i] = (char) ('A' + n % 26);
        n /= 26;
    }
    return new String(buf);
}

… Вероятно, превосходит функциональность на уровне краткости:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.List;
  
import org.jooq.lambda.Seq;
  
public class Test {
    public static void main(String[] args) {
        int max = 3;
  
        List<String> alphabet = Seq
            .rangeClosed('A', 'Z')
            .map(Object::toString)
            .toList();
  
        Seq.rangeClosed(1, max)
           .flatMap(length ->
               Seq.rangeClosed(1, length - 1)
                  .foldLeft(Seq.seq(alphabet), (s, i) ->
                      s.crossJoin(Seq.seq(alphabet))
                       .map(t -> t.v1 + t.v2)))
           .forEach(System.out::println);
    }
}

И это уже использует jOOλ , чтобы упростить написание функциональной Java.

3. Ремонтопригодность

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

01
02
03
04
05
06
07
08
09
10
11
12
13
List<Integer> list = Arrays.asList(1, 2, 3);
 
// Old school
for (Integer i : list)
    for (int j = 0; j < i; j++)
        System.out.println(i / j);
 
// "Modern"
list.forEach(i -> {
    IntStream.range(0, i).forEach(j -> {
        System.out.println(i / j);
    });
});

Очевидно, это вызывает проблемы, и мы можем сразу увидеть проблему в трассировке стека исключений.

Старая школа

1
2
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Test.main(Test.java:13)

Современный

1
2
3
4
5
6
7
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Test.lambda$1(Test.java:18)
    at java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:110)
    at java.util.stream.IntPipeline$Head.forEach(IntPipeline.java:557)
    at Test.lambda$0(Test.java:17)
    at java.util.Arrays$ArrayList.forEach(Arrays.java:3880)
    at Test.main(Test.java:16)

Ух ты. Мы были просто …? Да. Это те же самые причины, по которым у нас были проблемы с производительностью в пункте № 1. Внутренняя итерация — это просто намного больше работы для JVM и библиотек. И это чрезвычайно простой вариант использования, мы могли бы показать то же самое с поколением серий AA, AB, .., ZZ .

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

Вывод

Обычно это блог о функциональном программировании, декларативном программировании. Мы любим лямбды. Мы любим SQL. И вместе они могут творить чудеса .

Но когда вы переходите на Java 8 и рассматриваете возможность использования более функционального стиля в своем коде, помните, что FP не всегда лучше — по разным причинам. На самом деле, он никогда не «лучше», он просто другой и позволяет нам по-разному рассуждать о проблемах.

Мы, Java-разработчики, должны потренироваться и прийти к интуитивному пониманию того, когда использовать FP, а когда придерживаться OO / imperative. При правильном опыте комбинирование того и другого поможет нам улучшить наше программное обеспечение.

Или, говоря словами дяди Боба:

Нижняя, нижняя строка здесь просто так. ОО программирование — это хорошо, когда знаешь, что это такое. Функциональное программирование хорошо, когда вы знаете, что это такое. И функциональное ОО-программирование тоже хорошо, если вы знаете, что это такое.

http://blog.cleancoder.com/uncle-bob/2014/11/24/FPvsOO.html