Почему я должен использовать «функциональные операции» вместо цикла for?

39
for (Canvas canvas : list) {
}

NetBeans предлагает мне использовать «функциональные операции»:

list.stream().forEach((canvas) -> {
});

Но почему это предпочтительнее ? Во всяком случае, труднее читать и понимать. Вы звоните stream(), затем forEach()используете лямбда-выражение с параметром canvas. Я не вижу, как это лучше, чем forцикл в первом фрагменте.

Очевидно, я говорю только из эстетики. Возможно, здесь есть техническое преимущество, которого мне не хватает. Что это такое? Почему я должен использовать второй метод вместо?

Омега
источник
15
В вашем конкретном примере это не будет предпочтительным.
Роберт Харви,
1
Пока единственной операцией является одна forEach, я склонен с вами согласиться. Как только вы добавляете другие операции в конвейер или создаете выходную последовательность, тогда потоковый подход становится предпочтительным.
JacquesB
@ РобертХарви, не так ли? почему бы нет?
Сара
@RobertHarvey хорошо Я согласен, что принятый ответ действительно показывает, как for-версия выдувается из воды для более сложных случаев, но я не понимаю, почему для «побед» в тривиальном случае. Вы заявляете, что это само собой разумеющееся, но я этого не вижу, поэтому я спросил.
Сара

Ответы:

45

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

Ваш пример не очень практичен. Рассмотрим следующий код с сайта Oracle .

List<Transaction> groceryTransactions = new Arraylist<>();
for(Transaction t: transactions){
  if(t.getType() == Transaction.GROCERY){
    groceryTransactions.add(t);
  }
}
Collections.sort(groceryTransactions, new Comparator(){
  public int compare(Transaction t1, Transaction t2){
    return t2.getValue().compareTo(t1.getValue());
  }
});
List<Integer> transactionIds = new ArrayList<>();
for(Transaction t: groceryTransactions){
  transactionsIds.add(t.getId());
}

может быть написано с использованием потоков:

List<Integer> transactionsIds = 
    transactions.stream()
                .filter(t -> t.getType() == Transaction.GROCERY)
                .sorted(comparing(Transaction::getValue).reversed())
                .map(Transaction::getId)
                .collect(toList());

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

luboskrnac
источник
5
Есть методы оптимизации, такие как карты слияние ( stream.map(f).map(g)stream.map(f.andThen(g))) сборки / уменьшение синтеза (при создании потока в одном методе , а затем передать его другой метод , который потребляет его, компилятор может исключить поток) и поток слияние (который может расплавить много потоков опов вместе в один императивный цикл), который может сделать потоковые операции намного более эффективными. Они реализованы в компиляторе GHC Haskell, а также в некоторых других компиляторах Haskell и других функциональных языках, и есть экспериментальные реализации для Scala.
Йорг Миттаг
Производительность, вероятно, не является фактором при рассмотрении функционального цикла против цикла, поскольку компилятор может / должен выполнить преобразование из цикла for в функциональную операцию, если Netbeans может, и это определено как оптимальный путь.
Райан
Я бы не согласился, что второе более читабельно. Требуется время, чтобы понять, что происходит. Есть ли преимущество в производительности, если вы делаете это вторым способом, потому что иначе я не вижу его?
Бок МакДонах
@BokMcDonagh, по моему опыту, он менее читабелен для разработчиков, которые не удосужились ознакомиться с новыми абстракциями. Я бы предложил больше использовать такие API, чтобы стать более знакомым, потому что это будущее. Не только в мире Java.
luboskrnac
16

Еще одно преимущество использования функционального потокового API заключается в том, что он скрывает детали реализации. Он описывает только то, что должно быть сделано, а не как. Это преимущество становится очевидным при рассмотрении изменений, которые необходимо выполнить, чтобы перейти от однопоточного к параллельному выполнению кода. Просто измените .stream()к .parallelStream().

Стефан Долласе
источник
13

Во всяком случае, труднее читать и понимать.

Это очень субъективно. Я считаю, что вторую версию гораздо легче читать и понимать. Он соответствует тому, как это делают другие языки (например, Ruby, Smalltalk, Clojure, Io, Ioke, Seph), требует меньше концепций для понимания (это просто обычный вызов метода, как и любой другой, тогда как первый пример - специализированный синтаксис).

Во всяком случае, это вопрос знакомства.

Йорг Миттаг
источник
6
Да, это правда. Но это больше похоже на комментарий, чем на мой ответ.
Омега
Функциональная операция также использует специальный синтаксис: «->» является новым
Ryan