Dlaczego powinienem używać „operacji funkcjonalnych” zamiast pętli for?

39
for (Canvas canvas : list) {
}

NetBeans sugeruje użycie „operacji funkcjonalnych”:

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

Ale dlaczego jest to preferowane ? Jeśli tak, to trudniej jest je przeczytać i zrozumieć. Dzwonisz stream(), a następnie forEach()używasz wyrażenia lambda z parametrem canvas. Nie rozumiem, jak to jest ładniejsze niż forpętla w pierwszym fragmencie.

Oczywiście mówię tylko z estetyki. Być może brakuje mi tutaj przewagi technicznej. Co to jest? Dlaczego zamiast tego powinienem użyć drugiej metody?

Omega
źródło
15
W twoim przykładzie nie byłoby to preferowane.
Robert Harvey,
1
Tak długo, jak jedyną operacją jest pojedynczy forEach, zgadzam się z tobą. Gdy tylko dodasz inne operacje do potoku lub utworzysz sekwencję wyjściową, wtedy preferowane jest podejście strumieniowe.
JacquesB,
@RobertHarvey, prawda? Dlaczego nie?
sara
@RobertHarvey dobrze Zgadzam się, że zaakceptowana odpowiedź naprawdę pokazuje, jak wersja dla wersji jest wydmuchiwana z wody w bardziej skomplikowanych przypadkach, ale nie rozumiem, dlaczego dla „wygranych” w trywialnej sprawie. twierdzisz, że to oczywiste, ale nie widzę tego, więc zapytałem.
sara

Odpowiedzi:

45

Strumienie zapewniają znacznie lepszą abstrakcję dla kompozycji różnych operacji, które chcesz wykonywać na podstawie kolekcji lub strumieni danych, które nadchodzą. Zwłaszcza, gdy trzeba mapować elementy, filtrować je i konwertować.

Twój przykład nie jest zbyt praktyczny. Rozważ następujący kod z witryny 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());
}

można zapisać za pomocą strumieni:

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

Druga opcja jest znacznie bardziej czytelna. Więc kiedy zagnieżdżasz pętle lub różne pętle wykonujące częściowe przetwarzanie, jest to bardzo dobry kandydat do użycia interfejsu API Streams / Lambda.

luboskrnac
źródło
5
Istnieją techniki optymalizacji, takie jak fusion map ( stream.map(f).map(g)stream.map(f.andThen(g))) budowanie / zmniejszanie fuzji (podczas budowania strumienia w jednej metodzie, a następnie przekazanie go do innej metody, która go zużywa, kompilator może wyeliminować strumień) i fuzji strumienia (która może łączyć wiele operacji strumienia razem w jedną pętlę imperatywną), co może znacznie zwiększyć wydajność operacji strumieniowych. Są one zaimplementowane w kompilatorze GHC Haskell, a także w niektórych innych kompilatorach języka Haskell i innych językach funkcjonalnych. Istnieją także implementacje badań eksperymentalnych dla Scali.
Jörg W Mittag,
Wydajność prawdopodobnie nie jest czynnikiem branym pod uwagę przy rozważaniu pętli funkcjonalnej vs, ponieważ z pewnością kompilator mógłby / powinien wykonać konwersję z pętli for na operację funkcjonalną, jeśli Netbeans może i jest określona jako optymalna ścieżka.
Ryan
Nie zgodziłbym się, że ten drugi jest bardziej czytelny. Zrozumienie, co się dzieje, zajmuje trochę czasu. Czy wykonanie drugiej metody ma przewagę pod względem wydajności, ponieważ w przeciwnym razie jej nie widzę?
Bok McDonagh,
@BokMcDonagh, Me doświadczenie jest takie, że jest mniej czytelny dla programistów, którzy nie zadali sobie trudu, aby zapoznać się z nowymi abstrakcjami. Sugerowałbym, aby częściej korzystać z takich interfejsów API, aby lepiej się zapoznać, ponieważ jest to przyszłość. Nie tylko w świecie Java.
luboskrnac
16

Kolejną zaletą korzystania z funkcjonalnego API przesyłania strumieniowego jest to, że ukrywa on szczegóły implementacji. Opisuje tylko, co należy zrobić, a nie jak. Ta zaleta staje się oczywista, gdy przyjrzymy się zmianom, które należy wprowadzić, aby zmienić z jednowątkowego na równoległe wykonywanie kodu. Wystarczy zmienić .stream()się .parallelStream().

Stefan Dollase
źródło
13

Jeśli tak, to trudniej jest je przeczytać i zrozumieć.

To jest bardzo subiektywne. Uważam, że druga wersja jest znacznie łatwiejsza do odczytania i zrozumienia. Pasuje do tego, jak robią to inne języki (np. Ruby, Smalltalk, Clojure, Io, Ioke, Seph), wymaga mniej pojęć do zrozumienia (jest to zwykłe wywołanie metody, jak każdy inny, podczas gdy pierwszy przykład to specjalistyczna składnia).

Jeśli już, to kwestia znajomości.

Jörg W Mittag
źródło
6
Tak to prawda. Ale dla mnie wygląda to bardziej na komentarz niż odpowiedź.
Omega
Operacja fakultatywna używa również specjalnej składni: „->” jest nowy
Ryan