Java 8 Iterable.forEach () vs foreach loop


463

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?


10
Nie ma żadnej rzeczywistej przewagi wydajności nad sobą. Pierwsza opcja to coś zainspirowanego przez FP (o którym zwykle mówi się jako o bardziej „miłym” i „czystym” sposobie wyrażania kodu). W rzeczywistości jest to pytanie raczej „stylowe”.
Eugene Loy

5
@Dwb: w tym przypadku nie jest to istotne. forEach nie jest zdefiniowane jako równoległe ani nic podobnego, więc te dwie rzeczy są semantycznie równoważne. Oczywiście możliwe jest zaimplementowanie równoległej wersji forEach (a jedna może już być obecna w standardowej bibliotece), w takim przypadku składnia wyrażeń lambda byłaby bardzo przydatna.
AardvarkSoup,

8
@AardvarkSoup Instancja, na której nazywa się forEach, to Stream ( lambdadoc.net/api/java/util/stream/Stream.html ). Aby zażądać wykonania równoległego, można napisać joins.parallel (). ForEach (...)
mschenk74

13
Czy 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 rodzaju join. To, co naprawdę zrobiłeś, to umieszczenie dwóch instrukcji w jednym wierszu.
Tom Hawtin - tackline

7
Inną kwestią do rozważenia jest ograniczona zdolność przechwytywania zmiennych Java. Dzięki Stream.forEach () nie można aktualizować zmiennych lokalnych, ponieważ ich przechwytywanie czyni je ostatecznymi, co oznacza, że ​​możesz mieć stanowe zachowanie w lambda forEach (chyba że jesteś przygotowany na pewne brzydoty, takie jak używanie zmiennych stanu klasy).
RedGlyph

Odpowiedzi:


509

Lepszą praktyką jest stosowanie for-each. Poza naruszeniem zasady Keep It Simple, Stupid , nowozelandzki forEach()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:

    Object prev = null;
    for(Object curr : list)
    {
        if( prev != null )
            foo(prev, curr);
        prev = curr;
    }
  • 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 Consumerbrak deklarowania. Dlatego każdy kod, który zgłasza sprawdzone wyjątki, musi je owinąć w try-catchlub Throwables.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 returnw lambda jest równe a continuedla for-each, ale nie ma odpowiednika break. 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 Streamnie implementuje się Iterable(pomimo faktycznego posiadania metody iterator) 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 (tj list.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).


95
„Po co zachęcać do ukrywania efektów ubocznych gdzieś w wyrażeniach?” to złe pytanie. Funkcjonalność forEachma na celu zachęcenie do funkcjonalnego stylu, tj. Używanie wyrażeń bez skutków ubocznych. Jeśli napotkasz sytuację, że forEachnie 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. Klasyczna forpętla nie stała się przestarzała…
Holger,

17
@Holger Jak forEachstosować bez skutków ubocznych?
Aleksandr Dubinsky,

14
W porządku, nie byłem dość precyzyjny, forEachto jedyna operacja strumieniowa przeznaczona do efektów ubocznych, ale nie dotyczy efektów ubocznych, takich jak przykładowy kod, liczenie jest typową reduceoperacją. 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 klasycznej forpętli. Jeśli chodzi o pierwotne pytanie, myślę, że problem wynika z faktu, że ktoś używa strumienia, w którym wystarczyłaby prosta forpętla nad źródłem strumienia. Użyj strumienia, w którym forEach()działa tylko
Holger

8
@Holger Jaki jest przykład skutków ubocznych, które forEachbyłyby odpowiednie?
Aleksandr Dubinsky,

29
Coś, co przetwarza każdy element indywidualnie i nie próbuje mutować zmiennych lokalnych. Np. Manipulowanie samymi elementami lub ich drukowanie, zapisywanie / wysyłanie ich do pliku, strumienia sieciowego itp. Nie ma dla mnie problemu, jeśli kwestionujesz te przykłady i nie widzisz dla nich żadnej aplikacji; filtrowanie, mapowanie, zmniejszanie, wyszukiwanie i (w mniejszym stopniu) zbieranie są preferowanymi operacjami strumienia. ForEach wydaje mi się wygodą do łączenia z istniejącymi interfejsami API. Oczywiście w przypadku operacji równoległych. Nie będą działać z forpętlami.
Holger,

169

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ć

     joins.parallelStream().forEach(join -> mIrc.join(mSession, join));

    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.Streaminterfejsu. Jeśli java.util.Iterablesprzężenia implementują tylko interfejs, nie jest to już prawdą.


4
Slajdy inżyniera wyroczni, do którego się odwołuje ( blogs.oracle.com/darcy/resource/Devoxx/… ), nie wspominają o równoległości w tych wyrażeniach lambda. Równoległość może wystąpić w metodach zbiorczego zbierania, takich jak mapi foldktóre tak naprawdę nie są powiązane z lambdami.
Thomas Jungblut

1
Nie wydaje się, aby kod OP zyskał na automatycznej równoległości (zwłaszcza, że ​​nie ma gwarancji, że będzie). Tak naprawdę nie wiemy, co to jest „mIrc”, ale „dołącz” nie wydaje się tak naprawdę czymś, co można wydobyć poza kolejnością.
Eugene Loy

10
Stream#forEachi Iterable#forEachnie są tym samym. OP pyta o Iterable#forEach.
gvlasov

2
Użyłem stylu UPDATEX, ponieważ nastąpiły zmiany w specyfikacji między czasem zadawania pytania a aktualizacją odpowiedzi. Pomyślałbym, że bez historii odpowiedzi byłoby to jeszcze bardziej zagmatwane.
mschenk74

1
Czy ktoś mógłby mi wyjaśnić, dlaczego ta odpowiedź jest nieprawidłowa, jeśli joinsimplementuje Iterablezamiast Stream? Z kilku rzeczy, które przeczytałem, OP powinien być w stanie to zrobić, joins.stream().forEach((join) -> mIrc.join(mSession, join));a joins.parallelStream().forEach((join) -> mIrc.join(mSession, join));jeśli joinsimplementujeIterable
Blueriver

112

Czytając to pytanie, można odnieść wrażenie, że Iterable#forEachw 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:

joins.forEach(join -> mIrc.join(mSession, join));

jest nie służyć jako skrót do pisania

for (String join : joins) {
    mIrc.join(mSession, join);
}

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

joins.forEach(new Consumer<T>() {
    @Override
    public void accept(T join) {
        mIrc.join(mSession, join);
    }
});

I zastępuje następujący kod Java 7:

final Consumer<T> c = new Consumer<T>() {
    @Override
    public void accept(T join) {
        mIrc.join(mSession, join);
    }
};
for (T t : joins) {
    c.accept(t);
}

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#forEachod semantyki tradycyjnej pętli dla każdej z nich. Kiedy przyzwyczaisz się do składni Iterable#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#forEachnależ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 nich Iterable#forEach, tam gdzie ma to sens.

Ponieważ wady Iterable#forEachtego 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:

    joins.forEach(getJoinStrategy());

    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ąpione Iterable#forEach.

    Aby poprzeć to stwierdzenie faktami, zrobiłem kilka mikro-testów z Caliperem . Oto kod testowy (potrzebny jest najnowszy suwak gita):

    @VmOptions("-server")
    public class Java8IterationBenchmarks {
    
        public static class TestObject {
            public int result;
        }
    
        public @Param({"100", "10000"}) int elementCount;
    
        ArrayList<TestObject> list;
        TestObject[] array;
    
        @BeforeExperiment
        public void setup(){
            list = new ArrayList<>(elementCount);
            for (int i = 0; i < elementCount; i++) {
                list.add(new TestObject());
            }
            array = list.toArray(new TestObject[list.size()]);
        }
    
        @Benchmark
        public void timeTraditionalForEach(int reps){
            for (int i = 0; i < reps; i++) {
                for (TestObject t : list) {
                    t.result++;
                }
            }
            return;
        }
    
        @Benchmark
        public void timeForEachAnonymousClass(int reps){
            for (int i = 0; i < reps; i++) {
                list.forEach(new Consumer<TestObject>() {
                    @Override
                    public void accept(TestObject t) {
                        t.result++;
                    }
                });
            }
            return;
        }
    
        @Benchmark
        public void timeForEachLambda(int reps){
            for (int i = 0; i < reps; i++) {
                list.forEach(t -> t.result++);
            }
            return;
        }
    
        @Benchmark
        public void timeForEachOverArray(int reps){
            for (int i = 0; i < reps; i++) {
                for (TestObject t : array) {
                    t.result++;
                }
            }
        }
    }

    A oto wyniki:

    Podczas pracy z „-client”, Iterable#forEachprzewyż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#forEachró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ście list.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:

    public abstract class MyOptimizedCollection<E> implements Collection<E>{
        private enum OperatingSystem{
            LINUX, WINDOWS, ANDROID
        }
        private OperatingSystem operatingSystem = OperatingSystem.WINDOWS;
        private int numberOfCores = Runtime.getRuntime().availableProcessors();
        private Collection<E> delegate;
    
        @Override
        public Stream<E> parallelStream() {
            if (!System.getProperty("parallelSupport").equals("true")) {
                return this.delegate.stream();
            }
            switch (operatingSystem) {
                case WINDOWS:
                    if (numberOfCores > 3 && delegate.size() > 10000) {
                        return this.delegate.parallelStream();
                    }else{
                        return this.delegate.stream();
                    }
                case LINUX:
                    return SomeVerySpecialStreamImplementation.stream(this.delegate.spliterator());
                case ANDROID:
                default:
                    return this.delegate.stream();
            }
        }
    }

    Zaletą jest to, że implementacja pętli nie musi znać ani dbać o te szczegóły.


5
Masz ciekawy pogląd w tej dyskusji i przywołujesz kilka punktów. Spróbuję się do nich odnieść. Proponujesz przełączać się między forEachi w for-eachoparciu 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?
Aleksandr Dubinsky

4
Dzięki za szczegółowe komentarze Aleksandr. 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ą.
Balder

2
Jeśli chodzi o problem z wydajnością - myślę, że zależy to w dużej mierze od charakteru pętli. W projekcie, nad którym pracuję, używam pętli w stylu funkcji podobnych do Iterable#forEachwcześ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ę korzysta Iteable#forEach.
Balder

7
Na końcu mojej krytyki znajduje się zdanie: „Kod powinien mówić idiomami, a im mniej idiomów, tym jaśniejszy jest kod i mniej czasu spędza się decydując, którego idiomu użyć”. Zacząłem głęboko doceniać ten punkt, kiedy przestawiłem się z C # na Javę.
Aleksandr Dubinsky

7
To zły argument. Możesz go użyć do uzasadnienia wszystkiego, czego chcesz: dlaczego nie powinieneś używać pętli for, ponieważ pętla while jest wystarczająco dobra, a to o jeden idiom mniej. Do licha, po co używać dowolnej instrukcji pętli, przełącznika lub try / catch, gdy goto potrafi to wszystko i jeszcze więcej.
tapichu

13

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 jako

for(int i=0; i<size; i++)
    action.accept(elements[i])

w przeciwieństwie do pętli for-each, która wymaga dużo rusztowań

Iterator iter = list.iterator();
while(iter.hasNext())
    Object next = iter.next();
    do something with `next`

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.


8
dlaczego iteracyjny zna najlepszy sposób, a iterator nie?
mschenk74,

2
nie ma zasadniczej różnicy, ale potrzebny jest dodatkowy kod, aby dostosować się do interfejsu iteratora, co może być bardziej kosztowne.
ZhongYu

1
@ zhong.j.yu, jeśli wdrażasz Collection, i tak wdrażasz Iterable. Zatem nie ma narzutu kodu w postaci „dodawania więcej kodu w celu zaimplementowania brakujących metod interfejsu”, jeśli o to ci chodzi. Jak powiedział mschenk74, wydaje się, że nie ma żadnych powodów, dla których nie można dostosować iteratora, aby wiedzieć, jak iterować swoją kolekcję w najlepszy możliwy sposób. Zgadzam się, że tworzenie iteratora może wiązać się z dodatkowymi kosztami, ale poważnie, te rzeczy zwykle są tak tanie, że można powiedzieć, że mają zerowy koszt ...
Eugene Loy

4
na przykład iteracja drzewa: 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ć
maniak zapadkowy

1
Zabawne jest to, że jedynym komentarzem w String.joinmetodach (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.
Tom Hawtin - tackline

7

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:

  1. klasyczny for
  2. klasyczne foreach
  3. List.forEach()
  4. List.stream().forEach()
  5. List.parallelStream().forEach

procedura testowa i parametry

private List<Integer> list;
private final int size = 1_000_000;

public MyClass(){
    list = new ArrayList<>();
    Random rand = new Random();
    for (int i = 0; i < size; ++i) {
        list.add(rand.nextInt(size * 50));
    }    
}
private void doIt(Integer i) {
    i *= 2; //so it won't get JITed out
}

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ąc System.nanoTime()). Po tym dzielę tę sumę przez 1000 i to jest wynik, średni czas. przykład:

myClass.fored();
myClass.fored();
myClass.fored();
for (int i = 0; i < reps; ++i) {
    begin = System.nanoTime();
    myClass.fored();
    end = System.nanoTime();
    nanoSum += end - begin;
}
System.out.println(nanoSum / reps);

Uruchomiłem to na 4-rdzeniowym procesorze i5 z Javą w wersji 1.8.0_05

klasyczny for

for(int i = 0, l = list.size(); i < l; ++i) {
    doIt(list.get(i));
}

czas wykonania: 4,21 ms

klasyczne foreach

for(Integer i : list) {
    doIt(i);
}

czas wykonania: 5,95 ms

List.forEach()

list.forEach((i) -> doIt(i));

czas wykonania: 3,11 ms

List.stream().forEach()

list.stream().forEach((i) -> doIt(i));

czas wykonania: 2,79 ms

List.parallelStream().forEach

list.parallelStream().forEach((i) -> doIt(i));

czas wykonania: 3,6 ms


23
Jak zdobyć te liczby? Z jakich ram testu porównawczego korzystasz? Jeśli nie używasz żadnego i po prostu System.out.printlnwyświetlasz naiwnie te dane, wszystkie wyniki są bezużyteczne.
Luiggi Mendoza

2
Bez ram. Używam 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.
Assaf,

31
I to jest cel dobrego mikro-testu. Ponieważ nie spełniasz takich wymagań, wyniki są bezużyteczne.
Luiggi Mendoza

6
Zamiast tego mogę polecić poznanie JMH, to jest to, co jest używane w samej Javie i wkłada wiele wysiłku w uzyskanie poprawnych liczb: openjdk.java.net/projects/code-tools/jmh
dsvensson

1
Zgadzam się z @LuiggiMendoza. Nie ma sposobu, aby wiedzieć, że wyniki te są spójne lub prawidłowe. Bóg jeden wie, ile testów, które wykonałem, które ciągle raportują różne wyniki, zwłaszcza w zależności od kolejności iteracji, wielkości i tego, co nie.
mmm,

6

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 :

Wykonuje podaną akcję na zawartości Iterable, w kolejności , w której elementy występują podczas iteracji, aż wszystkie elementy zostaną przetworzone lub akcja wygeneruje wyjątek.

... 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.


1
Równoległość nie potrzebuje nowych „równoległych kolekcji”. Zależy to tylko od tego, czy poprosiłeś o sekwencyjny strumień (używając collection.stream ()) czy równoległy (używając collection.parallelStream ()).
JB Nizet

@JBNizet Według docs Collection.parallelStream () nie gwarantuje, że wdrożenie kolekcji zwróci strumień równoległy. Właściwie zastanawiam się, kiedy to się stanie, ale prawdopodobnie zależy to od kolekcji.
Eugene Loy

Zgoda. To zależy również od kolekcji. Ale chodziło mi o to, że równoległe pętle foreach były już dostępne ze wszystkimi standardowymi kolekcjami (ArrayList itp.). Nie trzeba czekać na „równoległe kolekcje”.
JB Nizet

@JBNizet zgadza się co do twojego punktu widzenia, ale tak naprawdę nie to rozumiałem przez „równoległe kolekcje”. Odwołuję się do Collection.parallelStream (), który został dodany w Javie 8, jako „kolekcje równoległe” przez analogię do koncepcji Scali, która robi prawie to samo. Ponadto, nie jestem pewien, jak to się nazywa w kawałku JSR. Widziałem kilka artykułów, które używają tej samej terminologii dla tej funkcji Java 8.
Eugene Loy

1
do ostatniego akapitu można użyć odwołania do funkcji:collection.forEach(MyClass::loopBody);
maniak zapadkowy

6

Jednym z najbardziej upraszczających forEachograniczeń funkcjonalnych jest brak sprawdzonej obsługi wyjątków.

Jednym z możliwych obejść jest zastąpienie terminala forEachzwykłą starą pętlą foreach:

    Stream<String> stream = Stream.of("", "1", "2", "3").filter(s -> !s.isEmpty());
    Iterable<String> iterable = stream::iterator;
    for (String s : iterable) {
        fileWriter.append(s);
    }

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?


3

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ę .. !!!!!!

    class MyConsumer implements Consumer<Integer>{
    
        @Override
        public void accept(Integer o) {
            System.out.println("Here you can also add your business logic that will work with Iteration and you can reuse it."+o);
        }
    }
    
    public class ForEachConsumer {
    
        public static void main(String[] args) {
    
            // Creating simple ArrayList.
            ArrayList<Integer> aList = new ArrayList<>();
            for(int i=1;i<=10;i++) aList.add(i);
    
            //Calling forEach with customized Iterator.
            MyConsumer consumer = new MyConsumer();
            aList.forEach(consumer);
    
    
            // Using Lambda Expression for Consumer. (Functional Interface) 
            Consumer<Integer> lambda = (Integer o) ->{
                System.out.println("Using Lambda Expression to iterate and do something else(BI).. "+o);
            };
            aList.forEach(lambda);
    
            // Using Anonymous Inner Class.
            aList.forEach(new Consumer<Integer>(){
                @Override
                public void accept(Integer o) {
                    System.out.println("Calling with Anonymous Inner Class "+o);
                }
            });
        }
    }
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.