Które z poniższych jest lepszą praktyką w Javie 8?
Java 8:
joins.forEach(join -> mIrc.join(mSession, join));
Java 7:
for (String join : joins) {
mIrc.join(mSession, join);
}
Mam wiele pętli for, które można by „uprościć” za pomocą lambdas, ale czy naprawdę jest jakaś korzyść z ich używania? Czy poprawiłoby to ich wydajność i czytelność?
EDYTOWAĆ
Rozszerzę to pytanie na dłuższe metody. Wiem, że nie można zwrócić funkcji rodzicielskiej z lambda lub zepsuć jej, i należy to również wziąć pod uwagę, porównując je, ale czy jest coś jeszcze do rozważenia?
java
for-loop
java-8
java-stream
nebkat
źródło
źródło
joins.forEach((join) -> mIrc.join(mSession, join));
naprawdę jest to „uproszczenie”for (String join : joins) { mIrc.join(mSession, join); }
? Zwiększyłeś liczbę znaków interpunkcyjnych z 9 do 12, dla korzyści ukrywania rodzajujoin
. To, co naprawdę zrobiłeś, to umieszczenie dwóch instrukcji w jednym wierszu.Odpowiedzi:
Lepszą praktyką jest stosowanie
for-each
. Poza naruszeniem zasady Keep It Simple, Stupid , nowozelandzkiforEach()
ma co najmniej następujące wady:Nie można użyć zmiennych nie-końcowych . Tak więc kodu takiego jak poniższy nie można przekształcić w lambda forEach:
Nie można obsłużyć sprawdzonych wyjątków . Lambdy nie są w rzeczywistości zabronione zgłaszania sprawdzonych wyjątków, ale popularne interfejsy funkcjonalne, takie jak
Consumer
brak deklarowania. Dlatego każdy kod, który zgłasza sprawdzone wyjątki, musi je owinąć wtry-catch
lubThrowables.propagate()
. Ale nawet jeśli to zrobisz, nie zawsze jest jasne, co stanie się z rzuconym wyjątkiem. Może zostać połknięty gdzieś w trzewiachforEach()
Ograniczona kontrola przepływu . A
return
w lambda jest równe acontinue
dla for-each, ale nie ma odpowiednikabreak
. Trudno jest również wykonywać takie czynności, jak zwracanie wartości, zwarcie lub ustawianie flag (co nieco by to złagodziło, gdyby nie było to niezgodne z zasadą „ nie-końcowe zmienne” ). „Nie jest to tylko optymalizacja, ale jest krytyczna, gdy weźmie się pod uwagę, że niektóre sekwencje (np. Czytanie linii w pliku) mogą mieć skutki uboczne lub sekwencję nieskończoną”.Może działać równolegle , co jest okropną, okropną rzeczą dla wszystkich oprócz 0,1% kodu, który należy zoptymalizować. Każdy równoległy kod musi zostać przemyślany (nawet jeśli nie używa blokad, składników lotnych i innych szczególnie nieprzyjemnych aspektów tradycyjnego wykonywania wielowątkowego). Każdy błąd będzie trudny do znalezienia.
Może to zaszkodzić wydajności , ponieważ JIT nie może zoptymalizować forEach () + lambda w takim samym stopniu jak zwykłe pętle, szczególnie teraz, gdy lambdy są nowe. Przez „optymalizację” nie mam na myśli narzutu wywoływania lambdas (który jest mały), ale wyrafinowanej analizy i transformacji, które współczesny kompilator JIT wykonuje na działającym kodzie.
Jeśli potrzebujesz równoległości, prawdopodobnie skorzystanie z usługi ExecutorService jest prawdopodobnie znacznie szybsze i nie jest trudniejsze . Strumienie są zarówno automagiczne (czytaj: niewiele wiem o twoim problemie), jak i wykorzystują specjalną (czytaj: nieefektywną w ogólnym przypadku) strategię paralelizacji ( rekursywny rozkład sprzężenia wideł ).
Sprawia, że debugowanie jest bardziej mylące ze względu na zagnieżdżoną hierarchię połączeń i, na wszelki wypadek, równoległe wykonywanie. W debugerze mogą występować problemy z wyświetlaniem zmiennych z otaczającego kodu, a takie czynności jak przejście może nie działać zgodnie z oczekiwaniami.
Strumienie są ogólnie trudniejsze do kodowania, odczytu i debugowania . W rzeczywistości dotyczy to ogólnie złożonych „ płynnych ” interfejsów API. Kombinacja złożonych pojedynczych instrukcji, intensywne użycie generycznych i brak zmiennych pośrednich powoduje sprzeniewierzenie komunikatów o błędach i udaremnienie debugowania. Zamiast „ta metoda nie ma przeciążenia dla typu X” pojawia się komunikat o błędzie bliżej „gdzieś pomieszałeś typy, ale nie wiemy gdzie i jak”. Podobnie, nie można tak łatwo przejść i zbadać rzeczy w debuggerze, jak wtedy, gdy kod jest podzielony na wiele instrukcji, a wartości pośrednie są zapisywane w zmiennych. Wreszcie, odczytanie kodu i zrozumienie rodzajów i zachowania na każdym etapie wykonania może być trywialne.
Wystaje jak obolały kciuk . Język Java ma już instrukcję dla każdego. Po co zastępować go wywołaniem funkcji? Po co zachęcać do ukrywania efektów ubocznych w wyrażeniach? Po co zachęcać do nieporęcznych jedno-liniowców? Mieszanie regularnych dla każdego i nowego dla każdego chcącego nie chce to zły styl. Kod powinien wypowiadać się w idiomach (wzorach, które są szybkie do zrozumienia ze względu na ich powtarzanie), a im mniej idiomów jest używanych, tym jaśniejszy jest kod i mniej czasu spędza się na decydowaniu, którego idiomu użyć (duży czasochłonność dla perfekcjonistów takich jak ja! ).
Jak widać, nie jestem wielkim fanem forEach (), z wyjątkiem przypadków, gdy ma to sens.
Szczególnie obraźliwy jest dla mnie fakt, że
Stream
nie implementuje sięIterable
(pomimo faktycznego posiadania metodyiterator
) i nie może być użyty w for-each, tylko w forEach (). Polecam rzutowanie strumieni na Iterables przy pomocy(Iterable<T>)stream::iterator
. Lepszą alternatywą jest użycie StreamEx, która rozwiązuje szereg problemów z Stream API, w tym implementacjęIterable
.To powiedziawszy,
forEach()
jest przydatne w następujących przypadkach:Atomowe iterowanie po zsynchronizowanej liście . Wcześniej lista generowana za pomocą
Collections.synchronizedList()
była atomowa w odniesieniu do rzeczy takich jak get lub set, ale nie była bezpieczna dla wątków podczas iteracji.Wykonanie równoległe (przy użyciu odpowiedniego strumienia równoległego) . Oszczędza to kilka linii kodu w porównaniu do korzystania z usługi ExecutorService, jeśli twój problem odpowiada założeniom dotyczącym wydajności wbudowanym w strumienie i spliteratory.
Określone kontenery, które podobnie jak lista zsynchronizowana, czerpią korzyści z kontrolowania iteracji (chociaż jest to w dużej mierze teoretyczne, chyba że ludzie mogą podać więcej przykładów)
Wywołanie jednej funkcji w bardziej przejrzysty sposób za pomocą
forEach()
argumentu referencyjnego metody (tjlist.forEach (obj::someMethod)
.). Pamiętaj jednak o sprawdzonych wyjątkach, trudniejszym debugowaniu i zmniejszeniu liczby idiomów używanych podczas pisania kodu.Artykuły, które wykorzystałem w celach informacyjnych:
EDYCJA: Wygląda na to, że niektóre z oryginalnych propozycji lambdas (takie jak http://www.javac.info/closures-v06a.html ) rozwiązały niektóre z wymienionych przeze mnie problemów (oczywiście dodając własne komplikacje).
źródło
forEach
ma na celu zachęcenie do funkcjonalnego stylu, tj. Używanie wyrażeń bez skutków ubocznych. Jeśli napotkasz sytuację, żeforEach
nie działa ona dobrze z efektami ubocznymi, powinieneś mieć wrażenie, że nie używasz odpowiedniego narzędzia do pracy. Zatem prosta odpowiedź brzmi: to dlatego, że masz dobre przeczucie, więc trzymaj się pętli for-each. Klasycznafor
pętla nie stała się przestarzała…forEach
stosować bez skutków ubocznych?forEach
to jedyna operacja strumieniowa przeznaczona do efektów ubocznych, ale nie dotyczy efektów ubocznych, takich jak przykładowy kod, liczenie jest typowąreduce
operacją. Zasugerowałbym, z reguły, by każdą operację, która manipuluje zmiennymi lokalnymi lub wpłynęła na przepływ sterowania (w tym obsługę wyjątków), zachowała w klasycznejfor
pętli. Jeśli chodzi o pierwotne pytanie, myślę, że problem wynika z faktu, że ktoś używa strumienia, w którym wystarczyłaby prostafor
pętla nad źródłem strumienia. Użyj strumienia, w którymforEach()
działa tylkoforEach
byłyby odpowiednie?for
pętlami.Korzyść bierze się pod uwagę, gdy operacje można wykonywać równolegle. (Patrz http://java.dzone.com/articles/devoxx-2012-java-8-lambda-and - sekcja o iteracji wewnętrznej i zewnętrznej)
Główną zaletą z mojego punktu widzenia jest to, że można zdefiniować realizację tego, co ma zostać wykonane w pętli, bez konieczności decydowania, czy będzie ona wykonywana równolegle czy sekwencyjnie
Jeśli chcesz, aby twoja pętla była wykonywana równolegle, możesz po prostu napisać
Będziesz musiał napisać dodatkowy kod do obsługi wątków itp.
Uwaga: W mojej odpowiedzi założyłem, że dołącza się do implementacji
java.util.Stream
interfejsu. Jeślijava.util.Iterable
sprzężenia implementują tylko interfejs, nie jest to już prawdą.źródło
map
ifold
które tak naprawdę nie są powiązane z lambdami.Stream#forEach
iIterable#forEach
nie są tym samym. OP pyta oIterable#forEach
.joins
implementujeIterable
zamiastStream
? Z kilku rzeczy, które przeczytałem, OP powinien być w stanie to zrobić,joins.stream().forEach((join) -> mIrc.join(mSession, join));
ajoins.parallelStream().forEach((join) -> mIrc.join(mSession, join));
jeślijoins
implementujeIterable
Czytając to pytanie, można odnieść wrażenie, że
Iterable#forEach
w połączeniu z wyrażeniami lambda jest skrótem / zamiennikiem do pisania tradycyjnej pętli dla każdej z nich. To po prostu nieprawda. Ten kod z PO:jest nie służyć jako skrót do pisania
i na pewno nie powinien być używany w ten sposób. Zamiast tego jest on przeznaczony jako skrót (chociaż to nie dokładnie to samo) do pisania
I zastępuje następujący kod Java 7:
Zastąpienie treści pętli funkcjonalnym interfejsem, jak w powyższych przykładach, czyni kod bardziej wyraźnym: Mówisz, że (1) treść pętli nie wpływa na otaczający kod i przepływ sterowania, oraz (2) treść pętli można zastąpić inną implementacją funkcji, bez wpływu na otaczający kod. Brak dostępu do zmiennych końcowych zewnętrznego zakresu nie jest deficytem funkcji / lambdów, jest to cecha, która odróżnia semantykę
Iterable#forEach
od semantyki tradycyjnej pętli dla każdej z nich. Kiedy przyzwyczaisz się do składniIterable#forEach
, kod staje się bardziej czytelny, ponieważ natychmiast otrzymujesz dodatkowe informacje o kodzie.Tradycyjne pętle dla każdej z pewnością pozostaną dobrą praktyką (aby uniknąć nadużywanego terminu „ najlepsza praktyka ”) w Javie. Ale to nie znaczy, że
Iterable#forEach
należy to uznać za złą praktykę lub zły styl. Dobrą praktyką jest zawsze używanie odpowiedniego narzędzia do wykonywania pracy, a to obejmuje mieszanie tradycyjnych pętli dla każdej z nichIterable#forEach
, tam gdzie ma to sens.Ponieważ wady
Iterable#forEach
tego wątku zostały już omówione, oto kilka powodów, dla których prawdopodobnie możesz chcieć użyćIterable#forEach
:Aby uczynić kod bardziej wyraźne: Jak opisano powyżej,
Iterable#forEach
mogą uczynić kod bardziej wyraźne i czytelne w niektórych sytuacjach.Aby uczynić kod bardziej rozszerzalnym i łatwym w utrzymaniu: Użycie funkcji jako treści pętli pozwala zastąpić tę funkcję różnymi implementacjami (patrz Wzorzec strategii ). Można np. Łatwo zastąpić wyrażenie lambda wywołaniem metody, które może zostać zastąpione przez podklasy:
Następnie możesz podać domyślne strategie za pomocą wyliczenia, które implementuje funkcjonalny interfejs. To nie tylko czyni twój kod bardziej rozszerzalnym, ale także zwiększa łatwość konserwacji, ponieważ oddziela implementację pętli od deklaracji pętli.
Aby uczynić kod bardziej debugowalnym: Oddzielenie implementacji pętli od deklaracji może również ułatwić debugowanie, ponieważ możesz mieć specjalną implementację debugowania, która drukuje komunikaty debugowania, bez potrzeby zaśmiecania głównego kodu
if(DEBUG)System.out.println()
. Implementacja debugowania może np. Być delegatem , który zdobi faktyczną implementację funkcji.Aby zoptymalizować wydajność krytycznych kodu: W przeciwieństwie do niektórych z tych twierdzeń w tym wątku,
Iterable#forEach
ma już zapewnić lepszą wydajność niż tradycyjne dla-każdej pętli, przynajmniej przy użyciu ArrayList i działa w trybie Hotspot „-client”. Chociaż to zwiększenie wydajności jest niewielkie i nieistotne w większości przypadków użycia, istnieją sytuacje, w których ta dodatkowa wydajność może mieć znaczenie. Np. Opiekunowie bibliotek z pewnością będą chcieli ocenić, czy niektóre z ich istniejących implementacji pętli powinny zostać zastąpioneIterable#forEach
.Aby poprzeć to stwierdzenie faktami, zrobiłem kilka mikro-testów z Caliperem . Oto kod testowy (potrzebny jest najnowszy suwak gita):
A oto wyniki:
Podczas pracy z „-client”,
Iterable#forEach
przewyższa tradycyjną pętlę for przez ArrayList, ale nadal jest wolniejszy niż bezpośrednie iterowanie po tablicy. Podczas pracy z „-server” wydajność wszystkich podejść jest prawie taka sama.Aby zapewnić opcjonalne wsparcie dla równoległego wykonywania: Powiedziano już tutaj, że możliwość wykonania interfejsu funkcjonalnego
Iterable#forEach
równoległego przy użyciu strumieni , jest z pewnością ważnym aspektem. PonieważCollection#parallelStream()
nie gwarantuje to, że pętla jest faktycznie wykonywana równolegle, należy uznać to za funkcję opcjonalną . Iterując po liścielist.parallelStream().forEach(...);
, wyraźnie mówisz: Ta pętla obsługuje wykonywanie równoległe, ale nie zależy od niej. Ponownie, jest to cecha, a nie deficyt!Odsuwając decyzję o równoległym wykonywaniu od rzeczywistej implementacji pętli, umożliwiasz opcjonalną optymalizację kodu, bez wpływu na sam kod, co jest dobrą rzeczą. Ponadto, jeśli domyślna implementacja równoległego strumienia nie odpowiada Twoim potrzebom, nikt nie przeszkadza w zapewnieniu własnej implementacji. Możesz np. Zapewnić zoptymalizowaną kolekcję w zależności od podstawowego systemu operacyjnego, wielkości kolekcji, liczby rdzeni i niektórych ustawień preferencji:
Zaletą jest to, że implementacja pętli nie musi znać ani dbać o te szczegóły.
źródło
forEach
i wfor-each
oparciu o niektóre kryteria dotyczące natury ciała pętli. Mądrość i dyscyplina w przestrzeganiu takich zasad są cechą dobrego programisty. Takie zasady są również jego zgubą, ponieważ ludzie wokół niego albo ich nie przestrzegają, albo się nie zgadzają. Np. Za pomocą zaznaczonych vs niesprawdzonych wyjątków. Ta sytuacja wydaje się jeszcze bardziej niuansowa. Ale jeśli ciało „nie wpływa na kod surround ani kontrolę przepływu”, to czy nie jest ono lepiej traktowane jako funkcja?But, if the body "does not affect surround code or flow control," isn't factoring it out as a function better?
. Tak, moim zdaniem często tak będzie - uwzględnianie tych pętli jako funkcji jest naturalną konsekwencją.Iterable#forEach
wcześniejszych niż Java 8 tylko ze względu na wzrost wydajności. Projekt ma jedną główną pętlę podobną do pętli gry, z nieokreśloną liczbą zagnieżdżonych podpętli, w których klienci mogą podłączać uczestników pętli jako funkcje. Z takiej struktury oprogramowania wiele się korzystaIteable#forEach
.forEach()
może być zaimplementowany tak, aby był szybszy niż dla każdej pętli, ponieważ iterowalny zna najlepszy sposób na iterację swoich elementów, w przeciwieństwie do standardowego sposobu iteratora. Różnica polega więc na zapętleniu wewnętrznym lub zapętleniu zewnętrznym.Na przykład
ArrayList.forEach(action)
może być po prostu zaimplementowany jakow przeciwieństwie do pętli for-each, która wymaga dużo rusztowań
Jednak musimy również uwzględnić dwa koszty ogólne przy użyciu
forEach()
, jeden tworzy obiekt lambda, drugi wywołuje metodę lambda. Prawdopodobnie nie są znaczące.zobacz także http://journal.stuffwithstuff.com/2013/01/13/iteration-inside-and-out/, aby porównać wewnętrzne / zewnętrzne iteracje dla różnych przypadków użycia.
źródło
void forEach(Consumer<T> v){leftTree.forEach(v);v.accept(rootElem);rightTree.forEach(v);}
jest to bardziej eleganckie niż iteracja zewnętrzna i możesz zdecydować, jak najlepiej zsynchronizowaćString.join
metodach (w porządku, nieprawidłowe łączenie) jest „Liczba elementów, które prawdopodobnie nie są warte narzutu Arrays.stream”. więc używają szykownej pętli for.TL; DR :
List.stream().forEach()
był najszybszy.Czułem, że powinienem dodać moje wyniki z iteracji testów porównawczych. Przyjąłem bardzo proste podejście (bez ram benchmarkingowych) i porównałem 5 różnych metod:
for
List.forEach()
List.stream().forEach()
List.parallelStream().forEach
procedura testowa i parametry
Lista w tej klasie powinna być iterowana i mieć
doIt(Integer i)
zastosowanie do wszystkich jej członków, za każdym razem inną metodą. w klasie głównej trzy razy uruchomiłem testowaną metodę, aby rozgrzać JVM. Następnie uruchamiam metodę testową 1000 razy, sumując czas potrzebny dla każdej metody iteracji (używającSystem.nanoTime()
). Po tym dzielę tę sumę przez 1000 i to jest wynik, średni czas. przykład:Uruchomiłem to na 4-rdzeniowym procesorze i5 z Javą w wersji 1.8.0_05
klasyczny
for
czas wykonania: 4,21 ms
klasyczne foreach
czas wykonania: 5,95 ms
List.forEach()
czas wykonania: 3,11 ms
List.stream().forEach()
czas wykonania: 2,79 ms
List.parallelStream().forEach
czas wykonania: 3,6 ms
źródło
System.out.println
wyświetlasz naiwnie te dane, wszystkie wyniki są bezużyteczne.System.nanoTime()
. Jeśli przeczytasz odpowiedź, zobaczysz, jak to zrobiono. Nie sądzę, żeby to czyniło bezużyteczne widzenie, ponieważ jest to pytanie względne . Nie obchodzi mnie, jak dobrze dała metoda, zależy mi na tym, jak dobrze wypadła w porównaniu do innych metod.Wydaje mi się, że muszę trochę rozszerzyć swój komentarz ...
O stylu paradygmatu
To chyba najważniejszy aspekt. FP stało się popularne ze względu na to, co można uzyskać, unikając skutków ubocznych. Nie będę zagłębiał się w to, jakie zalety i zalety możesz z tego uzyskać, ponieważ nie ma to związku z pytaniem.
Powiem jednak, że iteracja przy użyciu Iterable.forEach jest zainspirowana FP i raczej wynikiem wprowadzenia większej liczby FP do Javy (jak na ironię, powiedziałbym, że forEach w czystym FP nie ma większego zastosowania, ponieważ nie robi nic poza wprowadzeniem skutki uboczne).
Na koniec powiedziałbym, że jest to raczej kwestia gustu \ stylu \ paradygmatu, w którym obecnie piszesz.
O równoległości.
Z punktu widzenia wydajności nie ma obiecanych znaczących korzyści ze stosowania Iterable.forEach ponad foreach (...).
Według oficjalnych dokumentów na Iterable.forEach :
... tj. doktorzy całkiem jasno, że nie będzie żadnego ukrytego paralelizmu. Dodanie jednego byłoby naruszeniem LSP.
Teraz istnieją „równoległe kolekcje”, które są obiecane w Javie 8, ale aby pracować z tymi, musisz mi bardziej precyzyjnie i dołożyć starań, aby z nich korzystać (patrz na przykład odpowiedź mschenk74).
BTW: w tym przypadku zostanie użyty Stream.forEach i nie gwarantuje to, że faktyczna praca zostanie wykonana równolegle (zależy od podstawowej kolekcji).
AKTUALIZACJA: może nie jest to tak oczywiste i trochę rozciągnięte na pierwszy rzut oka, ale jest jeszcze jeden aspekt stylu i perspektywy czytelności.
Przede wszystkim - zwykłe stare forloopy są proste i stare. Wszyscy już je znają.
Po drugie, i ważniejsze - prawdopodobnie chcesz użyć Iterable. Dla każdego tylko z lambdami jednowarstwowymi. Jeśli „ciało” staje się cięższe - wydają się być nieczytelne. Masz dwie opcje stąd - użyj klas wewnętrznych (fuj) lub użyj zwykłego starego forloopa. Ludzie często denerwują się, gdy widzą, że te same czynności (iteratyny nad kolekcjami) są wykonywane w różnych stylach / stylach w tej samej bazie kodu, i wydaje się, że tak jest.
Ponownie może to stanowić problem. Zależy od ludzi pracujących nad kodem.
źródło
collection.forEach(MyClass::loopBody);
Jednym z najbardziej upraszczających
forEach
ograniczeń funkcjonalnych jest brak sprawdzonej obsługi wyjątków.Jednym z możliwych obejść jest zastąpienie terminala
forEach
zwykłą starą pętlą foreach:Oto lista najpopularniejszych pytań z innymi obejściami dotyczącymi sprawdzania obsługi wyjątków w lambdach i strumieniach:
Java Lambda funkcja, która generuje wyjątek?
Java 8: Lambda-Streams, Filtruj według metody z wyjątkiem
Jak mogę zgłaszać wyjątki SPRAWDZONE z wewnętrznych strumieni Java 8?
Java 8: Obowiązkowo sprawdzono obsługę wyjątków w wyrażeniach lambda. Dlaczego obowiązkowe, a nie opcjonalne?
źródło
Zaletą metody Java 1.8 forEach w porównaniu z wersją 1.7 Enhanced for loop jest to, że podczas pisania kodu można skupić się wyłącznie na logice biznesowej.
Metoda forEach przyjmuje jako argument argument java.util.function.Consumer, więc pomaga mieć logikę biznesową w oddzielnej lokalizacji, dzięki czemu można jej ponownie użyć w dowolnym momencie.
Spójrz na poniższy fragment,
Tutaj utworzyłem nową klasę, która zastąpi metodę klasy akceptującej z klasy konsumenckiej, w której możesz dodać dodatkową funkcjonalność, Więcej niż iterację .. !!!!!!
źródło