Java 8 Iterable.forEach () ve foreach döngüsü


466

Aşağıdakilerden hangisi Java 8'de daha iyi bir uygulamadır?

Java 8:

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

Java 7:

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

Ben lambdas ile "basitleştirilmiş" olabilir döngüler için bir sürü var, ama onları kullanmanın gerçekten herhangi bir avantajı var mı? Performanslarını ve okunabilirliklerini geliştirir mi?

DÜZENLE

Bu soruyu daha uzun metotlara da taşıyacağım. Ebeveyn işlevini bir lambdadan geri alamayacağınızı veya kıramayacağınızı biliyorum ve bunları karşılaştırırken de dikkate alınmalıdır, ancak dikkate alınması gereken başka bir şey var mı?


10
Birinin diğerine göre gerçek bir performans avantajı yoktur. İlk seçenek FP'den esinlenen bir şeydir (whitch genellikle kodunuzu ifade etmek için daha "güzel" ve "açık" bir yol gibi konuşulur). Gerçekte - bu daha çok "stil" sorusudur.
Eugene Loy

5
@Dwb: bu durumda, ilgili değil. forEach paralel veya bunun gibi bir şey olarak tanımlanmamıştır, bu nedenle bu iki şey anlamsal olarak eşdeğerdir. Tabii ki forEach'in paralel bir versiyonunu uygulamak mümkündür (ve standart kütüphanede zaten mevcut olabilir) ve bu durumda lambda ifade sözdizimi çok yararlı olacaktır.
AardvarkSoup

8
@AardvarkSoup forEach'ın çağrıldığı örnek bir Akıştır ( lambdadoc.net/api/java/util/stream/Stream.html ). Paralel yürütme istemek için biri joins.parallel (). ForEach (...)
mschenk74 yazabilir

13
joins.forEach((join) -> mIrc.join(mSession, join));bir "basitleştirme" gerçekten for (String join : joins) { mIrc.join(mSession, join); }? Noktalama sayısını gizlemek için noktalama sayısını 9'dan 12'ye çıkardınız join. Gerçekten yaptığınız şey bir satıra iki ifade koymak.
Tom Hawtin - tackline

7
Dikkate alınması gereken başka bir nokta da Java'nın sınırlı değişken yakalama yeteneğidir. Stream.forEach () ile, yerel değişkenleri yakalamaları onları sonlandırdığından güncelleyemezsiniz, bu da forEach lambda'da durumsal bir davranışa sahip olabileceğiniz anlamına gelir (sınıf durumu değişkenlerini kullanmak gibi bir çirkinliğe hazırlıklı değilseniz).
RedGlyph

Yanıtlar:


512

Daha iyi uygulama kullanmaktır for-each. Basit Tutun, Aptal prensibini ihlal etmenin yanı sıra , yeni kanatlı forEach()en azından aşağıdaki eksikliklere sahiptir:

  • Nihai olmayan değişkenler kullanılamaz . Yani, aşağıdaki gibi bir kod forEach lambda'ya dönüştürülemez:

    Object prev = null;
    for(Object curr : list)
    {
        if( prev != null )
            foo(prev, curr);
        prev = curr;
    }
    
  • İşaretli istisnalar işlenemiyor . Lambdaların aslında kontrol edilmiş istisnalar atması yasak değildir, ancak yaygın fonksiyonel arayüzler Consumerherhangi bir beyan etmemektedir. Bu nedenle, işaretli istisnalar atan herhangi bir kod bunları try-catchveya içine sarmalıdır Throwables.propagate(). Ancak bunu yapsanız bile, atılan istisnaya ne olduğu her zaman açık değildir. Bağırsaklarında bir yerde yutulabilirforEach()

  • Sınırlı akış kontrolü . returnLambdadaki A , continueher biri için a'ya eşittir , ancak a'ya eşdeğer değildir break. Dönüş değerleri, kısa devre veya set bayrakları ( nihai olmayan değişkenler kuralının ihlali olmasaydı, şeyleri biraz hafifletir) gibi şeyler yapmak da zordur . "Bu yalnızca bir optimizasyon değil, bazı dizilerin (bir dosyadaki satırları okumak gibi) yan etkileri olabileceğini veya sonsuz bir dizinin olabileceğini düşündüğünüzde kritik öneme sahiptir."

  • En iyi duruma getirilmesi gereken kodunuzun% 0,1'i hariç herkes için korkunç, korkunç bir şey olan paralel olarak çalışabilir. Herhangi bir paralel kod düşünülmelidir (geleneksel çok iş parçacıklı yürütmenin kilitleri, uçucuları ve diğer özellikle kötü yönlerini kullanmasa bile). Herhangi bir hata bulmak zor olacak.

  • Performans zarar verebilir , çünkü JIT forEach () + lambda'yı düz döngülerle aynı ölçüde optimize edemez, özellikle şimdi lambdalar yeni. "Optimizasyon" ile, lambdaları çağırma yükünü (küçüktür) değil, modern JIT derleyicisinin çalışan kod üzerinde gerçekleştirdiği karmaşık analiz ve dönüşümü kastediyorum.

  • Paralelliğe ihtiyacınız varsa, bir ExecutorService kullanmak muhtemelen çok daha hızlıdır ve çok daha zor değildir . Akımlar hem otomajiktir (okuma: probleminiz hakkında çok şey bilmiyorum) ve özel (okuma: genel durum için verimsiz) paralelleştirme stratejisi ( çatal birleştirmeli özyinelemeli ayrışma ) kullanın.

  • İç içe çağrı hiyerarşisi ve tanrı yasaklayan paralel yürütme nedeniyle hata ayıklamayı daha karmaşık hale getirir. Hata ayıklayıcı, çevresindeki koddan değişkenleri görüntüleme konusunda sorun yaşayabilir ve adım adım geçiş gibi işlemler beklendiği gibi çalışmayabilir.

  • Genel olarak akışların kodlanması, okunması ve hatalarının ayıklanması daha zordur . Aslında, bu genel olarak karmaşık " akıcı " API'ler için geçerlidir . Karmaşık tek ifadelerin kombinasyonu, jeneriklerin yoğun kullanımı ve ara değişkenlerin olmaması, kafa karıştırıcı hata mesajları üretmek ve hata ayıklamayı hayal kırıklığına uğratır. "Bu yöntem X türü için aşırı yüke sahip değil" yerine "türleri karıştırdığınız bir yere daha yakın bir hata iletisi alırsınız, ancak nerede veya nasıl olduğunu bilmiyoruz." Benzer şekilde, hata ayıklayıcıdaki şeyleri kodun birden çok ifadeye bölünmesi ve ara değerlerin değişkenlere kaydedilmesi gibi kolayca inceleyemezsiniz. Son olarak, kodun okunması ve yürütmenin her aşamasında tür ve davranışların anlaşılması önemsiz olabilir.

  • Ağrılı bir başparmak gibi yapışır . Java dili zaten for-each deyimine sahiptir. Neden bir işlev çağrısı ile değiştirilmeli? İfadelerde bir yerde yan etkileri gizlemeyi neden teşvik edesiniz? Neden hantal tek gömlekleri teşvik etmeliyim? Her biri için düzenli ve yeni için karıştırmak Her willy-nilly kötü tarzıdır. Kod deyimler içinde konuşmalıdır (tekrarları nedeniyle hızlı bir şekilde anlaşılabilecek kalıplar) ve kod ne kadar az sayıda deyim kullanılırsa o kadar çok deyim kullanılır ve hangi deyimin kullanılacağına karar vermek için daha az zaman harcanır (benim gibi mükemmeliyetçiler için büyük bir zaman kaybı! ).

Gördüğünüz gibi, mantıklı olduğu durumlar dışında forEach () 'ın büyük bir hayranı değilim.

Özellikle bana saldırgan gerçektir Streamuygulamıyor Iterable(aslında sahip yönteme rağmen iterator) ve for-each, sadece bir foreach ile () içinde kullanılamaz. Akımları Iterables'a dökmenizi öneririm (Iterable<T>)stream::iterator. Daha iyi bir alternatif, uygulama da dahil olmak üzere bir dizi Stream API sorununu çözen StreamEx kullanmaktır Iterable.

Bununla forEach()birlikte, aşağıdakiler için yararlıdır:

  • Senkronize bir liste üzerinden atomik olarak yineleme . Bundan önce, ile oluşturulan bir liste Collections.synchronizedList(), get veya set gibi şeylere göre atomikti, ancak yineleme sırasında iş parçacığı için güvenli değildi.

  • Paralel yürütme (uygun bir paralel akış kullanarak) . Bu, sorununuz Akışlar ve Ayırıcılar içine yerleştirilmiş performans varsayımlarıyla eşleşiyorsa, bir ExecutorService kullanarak size birkaç satır kod kaydeder.

  • Senkronize liste gibi, yinelemenin kontrolünde olmaktan yararlanan belirli kaplar (insanlar daha fazla örnek getiremedikçe bu büyük ölçüde teoriktir)

  • Tek bir işleviforEach() ve yöntem başvuru bağımsız değişkenini (ör list.forEach (obj::someMethod).) Kullanarak daha temiz çağırma . Bununla birlikte, kontrol edilen istisnalar, daha zor hata ayıklama ve kod yazarken kullandığınız deyimlerin sayısını azaltma hususlarını unutmayın.

Referans için kullandığım makaleler:

DÜZENLEME: Görünüşe göre lambdas için bazı orijinal öneriler ( http://www.javac.info/closures-v06a.html gibi ) bahsettiğim bazı sorunları (elbette kendi komplikasyonlarını eklerken) çözdü.


96
“Neden yan etkilerin ifadelerde bir yerde saklanmasını teşvik edelim?” yanlış soru. İşlevsel forEach, işlevsel stili teşvik etmek için vardır, yani yan etkileri olmayan ifadeleri kullanmak . Durumla karşılaşırsanız, forEachyan etkilerinizle iyi çalışmaz, iş için doğru aracı kullanmadığınız hissini almalısınız. O zaman basit cevap şu ki, duygularınız doğru, bu yüzden bunun için her döngüde kalın. Klasik fordöngü kullanımdan kaldırılmadı…
Holger

17
@Holger forEachYan etkiler olmadan nasıl kullanılabilir?
Aleksandr Dubinsky

14
Pekala, yeterince hassas değildim, forEachyan etkiler için tasarlanan tek akış işlemi, ancak örnek kodunuz gibi yan etkiler için değil, sayma tipik bir reduceişlemdir. Yumru kuralı olarak, yerel değişkenleri manipüle eden veya klasik bir fordöngüde kontrol akışını (istisna işleme dahil) etkileyecek her işlemi tutmanızı öneririm . Bence asıl soru ile ilgili olarak, sorun birisinin akışın forkaynağı üzerinde basit bir döngünün yeterli olacağı bir akışı kullanmasından kaynaklanmaktadır . forEach()Yalnızca işe yarayan bir akış kullanın
Holger

8
@Holger forEachUygun olabilecek yan etkilere örnek nedir ?
Aleksandr Dubinsky

30
Her öğeyi ayrı ayrı işleyen ve yerel değişkenleri değiştirmeye çalışmayan bir şey. Örneğin, öğeleri manipüle etmek veya yazdırmak, bir dosyaya, ağ akışına vb. Yazmak / göndermek. Bu örnekleri sorgularsanız ve bunun için herhangi bir uygulama görmezseniz benim için sorun değil; filtreleme, haritalama, azaltma, arama ve (daha az derecede) toplama bir akışın tercih edilen işlemleridir. ForEach, mevcut API'larla bağlantı kurmak için bana kolaylık sağlıyor. Ve paralel operasyonlar için elbette. Bunlar fordöngülerle çalışmaz .
Holger

169

Avantaj, işlemler paralel olarak gerçekleştirilebildiğinde dikkate alınır. (Bkz. Http://java.dzone.com/articles/devoxx-2012-java-8-lambda-and - iç ve dış yineleme hakkındaki bölüm)

  • Benim açımdan en büyük avantaj, döngü içinde ne yapılacağının uygulanmasının, paralel mi yoksa sıralı mı yürütüleceğine karar vermek zorunda kalmadan tanımlanabilmesidir.

  • Döngünün paralel olarak yürütülmesini istiyorsanız,

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

    İplik işleme vb. İçin bazı ekstra kodlar yazmanız gerekecektir.

Not: Cevabım için java.util.Streamarayüzü uyguladığım varsayımlarda bulundum . Eğer birleşimler yalnızca java.util.Iterablearabirimi uygularsa, bu artık doğru değildir.


4
Başvurduğu kehanet mühendisinin slaytları ( blogs.oracle.com/darcy/resource/Devoxx/… ) bu lambda ifadelerindeki paralellikten bahsetmiyor. Paralellik gibi toplu toplama yöntemleri içinde gerçekleşebilir map& foldgerçekten lambdas ile ilgili değildir.
Thomas Jungblut

1
OP'nin kodunun burada otomatik paralellikten fayda sağlayacağı gerçekten görülmüyor (özellikle bir tane olacağının garantisi olmadığından). "MIrc" nin ne olduğunu gerçekten bilmiyoruz, ancak "join" gerçekten sıra dışı bir şekilde dışarı atılabilecek bir şey gibi görünmüyor.
Eugene Loy

11
Stream#forEachve Iterable#forEachaynı şey değil. OP soruyor Iterable#forEach.
gvlasov

2
UPDATEX stilini kullandım, çünkü sorunun sorulduğu zaman ile cevabın güncellenme zamanı arasındaki şartnamede değişiklikler oldu. Cevabın tarihi olmasaydı daha da kafa karıştırıcı olacağını düşündüm.
mschenk74

1
Birisi bana joinsuygulamak Iterableyerine bu cevabın neden geçerli olmadığını açıklayabilir Streammi? Okuduğum birkaç şeyden OP yapmalı joins.stream().forEach((join) -> mIrc.join(mSession, join));ve joins.parallelStream().forEach((join) -> mIrc.join(mSession, join));eğer joinsuygularsaIterable
Blueriver

112

Bu soruyu okurken Iterable#forEach, lambda ifadeleriyle birlikte, her biri için geleneksel bir döngü yazmak için bir kısayol / değiştirme olduğu izlenimi edinilebilir. Bu doğru değil. OP bu kod:

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

olduğu değil yazma için kısayol olarak tasarlanan

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

ve kesinlikle bu şekilde kullanılmamalıdır. (Bu olsa Bunun yerine kısayol olarak tasarlanmıştır değil yazma için tam olarak aynı)

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

Ve aşağıdaki Java 7 kodunun yerine geçer:

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

Yukarıdaki örneklerde olduğu gibi bir döngü gövdesini işlevsel bir arabirimle değiştirmek, kodunuzu daha açık hale getirir: (1) döngü gövdesinin çevresindeki kodu ve kontrol akışını etkilemediğini ve (2) Döngünün gövdesi, çevreleyen kodu etkilemeden işlevin farklı bir uygulamasıyla değiştirilebilir. Dış kapsamın nihai olmayan değişkenlerine erişememek, işlevlerin / lambdaların bir açığı değildir , anlambilimini her bir geleneksel döngü için anlambilimden ayıran bir özelliktirIterable#forEach . Birinin sözdizimine alıştıktan sonra Iterable#forEach, kodu daha okunabilir hale getirir, çünkü kod hakkında bu ek bilgileri hemen alırsınız.

Her biri için geleneksel döngüler Java'da kesinlikle iyi uygulama (aşırı kullanım " en iyi uygulama " önlemek için) kalacak . Ancak bu, Iterable#forEachkötü uygulama veya kötü stil olarak düşünülmesi gerektiği anlamına gelmez . İşi yapmak için doğru aracı kullanmak her zaman iyi bir uygulamadır ve bu, her bir döngü için geleneksel olanların Iterable#forEachmantıklı olduğu yerlerle karıştırılmasını içerir .

Iterable#forEachBu iş parçacığının dezavantajları zaten tartışıldığı için, muhtemelen neden kullanmak isteyebileceğinizin bazı nedenleri şunlardır Iterable#forEach:

  • Kodunuzu daha açık hale getirmek için: gibi, yukarıda açıklanan Iterable#forEach olabilir , bazı durumlarda kodunuzu daha açık ve okunabilir hale.

  • Kodunuzu daha genişletilebilir ve bakımı daha kolay hale getirmek için: Bir işlevi döngü gövdesi olarak kullanmak, bu işlevi farklı uygulamalarla değiştirmenize olanak tanır (bkz. Strateji Deseni ). Örneğin lambda ifadesini, alt sınıflar tarafından üzerine yazılabilecek bir yöntem çağrısı ile kolayca değiştirebilirsiniz:

    joins.forEach(getJoinStrategy());

    Ardından, işlevsel arabirimi uygulayan bir enum kullanarak varsayılan stratejiler sağlayabilirsiniz. Bu, yalnızca kodunuzu daha genişletilebilir hale getirmekle kalmaz, aynı zamanda döngü uygulamasını döngü bildiriminden ayırdığı için sürdürülebilirliği de artırır.

  • Kodunuzda daha hata ayıklanabilir hale getirmek için: Döngü uygulamasını bildirimden ayırmak, hata ayıklamayı daha kolay hale getirebilir, çünkü hata ayıklama iletilerini basan özel bir hata ayıklama uygulamanız olabilir if(DEBUG)System.out.println(). Ayıklama uygulaması örneğin bir olabilir temsilci , süsleyen asıl işlevi uygulama.

  • Optimize performansı açısından kritik kod için: iddiaların bazıları aksine bu thread, Iterable#forEach yok zaten daha iyi performans sağlamak geleneksel for-each döngüsü, en azından ArrayList kullanarak ve "-istemci" modunda Hotspot'unuzu çalıştırırken. Bu performans artışı çoğu kullanım durumu için küçük ve ihmal edilebilir olsa da, bu ekstra performansın fark yaratabileceği durumlar vardır. Örneğin kütüphane sahipleri, mevcut döngü uygulamalarının bir kısmının değiştirilmesi gerekiyorsa, kesinlikle değerlendirmek isteyecektir Iterable#forEach.

    Bu ifadeyi gerçeklerle desteklemek için, Caliper ile bazı mikro ölçütler yaptım . İşte test kodu (git'ten en son Kaliper gereklidir):

    @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++;
                }
            }
        }
    }

    İşte sonuçlar:

    "-Client" ile çalışırken Iterable#forEach, geleneksel bir ArrayList üzerindeki döngüden daha iyi performans gösterir, ancak yine de bir dizi üzerinden doğrudan yinelemekten daha yavaştır. "-Server" ile çalışırken, tüm yaklaşımların performansı hemen hemen aynıdır.

  • Paralel yürütme için isteğe bağlı destek sağlamak için: Burada, akışlarıIterable#forEach kullanarak paralel olarak işlevsel arabirimini yürütme olasılığının kesinlikle önemli bir yönü olduğu söylenmiştir . Yana garanti etmez, döngü aslında paralel bir şekilde gerçekleştirilmesini, kimse bu bir göz önüne almalıyız opsiyonel özellik. İle listenizi tekrarlayarak açık bir şekilde söylersiniz: Bu döngü paralel yürütmeyi destekler , ancak buna bağlı değildir. Yine, bu bir özellik değil, bir açık!Collection#parallelStream()list.parallelStream().forEach(...);

    Paralel yürütme kararını gerçek döngü uygulamanızdan uzaklaştırarak, kodun kendisini etkilemeden kodunuzun isteğe bağlı optimizasyonuna izin verirsiniz, bu iyi bir şeydir. Ayrıca, varsayılan paralel akış uygulaması ihtiyaçlarınızı karşılamıyorsa, hiç kimse kendi uygulamanızı sağlamanızı engellemez. Örneğin, temel işletim sistemine, koleksiyonun boyutuna, çekirdek sayısına ve bazı tercih ayarlarına bağlı olarak optimize edilmiş bir koleksiyon sağlayabilirsiniz:

    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();
            }
        }
    }

    Buradaki güzel şey, döngü uygulamanızın bu ayrıntıları bilmesine veya önemsemesine gerek olmamasıdır.


5
Bu tartışmada ilginç bir görüşünüz var ve birkaç noktaya değindiniz. Onlara hitap etmeye çalışacağım. Döngü gövdesinin doğasıyla ilgili bazı ölçütler arasında geçiş yapmayı forEachve bunları for-eachtemel almayı önerirsiniz . Bilgelik ve disiplin bu kurallara uymak, iyi bir programcının ayırt edici özelliğidir. Bu tür kurallar da onun yasağıdır, çünkü etrafındaki insanlar ya onları takip etmez ya da katılmazlar. Örneğin, işaretli veya işaretsiz İstisnalar kullanılarak. Bu durum daha da incelikli görünüyor. Ancak, gövde "surround kodu veya akış kontrolünü etkilemiyorsa", işlevi daha iyi bir işlev olarak görmezden gelmiyor mu?
Aleksandr Dubinsky

4
Ayrıntılı yorumlar için teşekkürler Aleksandr. But, if the body "does not affect surround code or flow control," isn't factoring it out as a function better?. Evet, bence bu sık sık olacak - işlevler doğal bir sonuç olduğu için bu döngüleri çarpanlarına ayırmak.
Balder

2
Performans sorunuyla ilgili olarak - sanırım bu, döngünün doğasına bağlıdır. Üzerinde çalıştığım bir projede Iterable#forEach, performans artışı nedeniyle Java 8'den önce benzer işlev stili döngüler kullanıyorum . Söz konusu projede, istemcilerin döngü katılımcılarını işlev olarak ekleyebileceği tanımlanmamış sayıda iç içe alt döngüye sahip bir oyun döngüsüne benzer bir ana döngü vardır. Böyle bir yazılım yapısı büyük ölçüde yararlanır Iteable#forEach.
Balder

7
Eleştirimin en sonunda bir cümle var: "Kod deyimlerle konuşmalı ve kod ne kadar az sayıda deyim kullanılırsa, kod ne kadar açık ve hangi deyimin kullanılacağına karar vermek için daha az zaman harcanır". C # 'dan Java' ya geçerken bu noktayı derinden takdir etmeye başladım.
Aleksandr Dubinsky

7
Bu kötü bir argüman. İstediğiniz her şeyi haklı çıkarmak için kullanabilirsiniz: neden bir for döngüsü kullanmamalısınız, çünkü while döngüsü yeterince iyidir ve bu daha az bir deyimdir. Heck, neden tüm bunları ve daha fazlasını yapabildiğinde neden herhangi bir döngü, anahtar veya try / catch ifadesi kullanın.
tapichu

13

forEach()her döngü için daha hızlı olacak şekilde uygulanabilir, çünkü yinelenebilir standart yineleyici yolunun aksine öğelerini yinelemenin en iyi yolunu bilir. Böylece fark dahili döngü veya harici döngüdür.

Örneğin ArrayList.forEach(action),

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

çok sayıda iskele gerektiren for-loop'a karşılık

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

Bununla birlikte, forEach()biri lambda nesnesini yapıyor, diğeri lambda yöntemini çağırıyor. Muhtemelen önemli değiller.

farklı kullanım durumları için dahili / harici yinelemeleri karşılaştırmak için ayrıca bkz. http://journal.stuffwithstuff.com/2013/01/13/iteration-inside-and-out/ .


8
neden yinelenebilir en iyi yolu biliyor ama yineleyici bilmiyor?
mschenk74

2
önemli bir fark yoktur, ancak yineleyici arayüzüne uymak için ekstra kod gereklidir, bu da daha maliyetli olabilir.
ZhongYu

1
@ zhong.j.yu Koleksiyon uygularsanız yine de Yinelenebilir uygulama yaparsınız. Yani, "eksik arabirim yöntemlerini uygulamak için daha fazla kod eklemek" anlamında herhangi bir kod yükü yoktur. Mschenk74'ün dediği gibi, koleksiyonunuz üzerinde nasıl en iyi şekilde yineleneceğini bilmek için yineleyicinizi değiştirememeniz için hiçbir neden yok gibi görünüyor. Yineleyici oluşturma için ek yük olabileceğini kabul ediyorum, ama cidden, bu şeyler genellikle çok ucuz, sıfır maliyete sahip olduklarını söyleyebilirsiniz ...
Eugene Loy

4
örneğin bir ağacı yineleme:, void forEach(Consumer<T> v){leftTree.forEach(v);v.accept(rootElem);rightTree.forEach(v);}bu dış yinelemeden daha zariftir ve en iyi nasıl senkronize edileceğine karar verebilirsiniz
mandal ucube

1
Tuhaf bir şekilde, String.joinyöntemlerde tek yorum (tamam, yanlış birleştirme) "Arrays.stream yükü değerinde olması muhtemel olmayan öğe sayısıdır." bu yüzden döngü için bir lüks kullanırlar.
Tom Hawtin - tackline

7

TL; DR : List.stream().forEach()en hızlısıydı.

Kıyaslama yinelemesinden elde ettiğim sonuçları eklemem gerektiğini hissettim. Çok basit bir yaklaşım izledim (karşılaştırma çerçevesi yok) ve 5 farklı yöntemi kıyasladım:

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

test prosedürü ve parametreleri

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
}

Bu sınıftaki liste tekrarlanmalı ve doIt(Integer i)her seferinde tüm üyelerine farklı bir yöntemle uygulanmalıdır. Ana sınıfta JVM'yi ısıtmak için test edilen yöntemi üç kez çalıştırıyorum. Daha sonra her yineleme yöntemi (kullanarak System.nanoTime()) için geçen zamanı toplayarak test yöntemini 1000 kez çalıştırırım . Bunu yaptıktan sonra bu toplamı 1000'e bölüyorum ve sonuç ortalama zaman. misal:

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);

Bunu java sürüm 1.8.0_05 ile i5 4 çekirdekli CPU'da çalıştırdım

klasik for

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

yürütme süresi: 4,21 ms

klasik foreach

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

yürütme süresi: 5,95 ms

List.forEach()

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

yürütme süresi: 3,11 ms

List.stream().forEach()

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

yürütme süresi: 2,79 ms

List.parallelStream().forEach

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

yürütme süresi: 3,6 ms


23
Bu numaraları nasıl elde edersiniz? Karşılaştırma için hangi çerçeveyi kullanıyorsunuz? Hiçbiri kullanmıyorsanız ve System.out.printlnbu verileri saf bir şekilde görüntülemek için sade kullanıyorsanız , tüm sonuçlar işe yaramaz.
Luiggi Mendoza

2
Çerçeve yok. Ben kullanıyorum System.nanoTime(). Cevabı okursanız, nasıl yapıldığını göreceksiniz. Bunun göreceli bir soru olduğu için görmeyi gereksiz kılıyor sanmıyorum . Belli bir yöntemin ne kadar iyi olduğu umurumda değil, diğer yöntemlerle karşılaştırıldığında ne kadar iyi olduğu umurumda.
Assaf

31
Bu da iyi bir mikro ölçütün amacı. Bu gereksinimleri karşılamadığınız için sonuçlar işe yaramaz.
Luiggi Mendoza

6
Bunun yerine JMH'yi tanımayı
dsvensson

1
@LuiggiMendoza'ya katılıyorum. Bu sonuçların tutarlı veya geçerli olduğunu bilmenin bir yolu yoktur. Tanrı, özellikle yineleme sırasına, büyüklüğüne ve neye bağlı olmadığına bağlı olarak farklı sonuçları raporlamaya devam eden kaç ölçüt yaptığımı biliyor.
mmm

6

Yorumumu biraz uzatmam gerektiğini hissediyorum ...

Paradigma \ style hakkında

Muhtemelen en dikkat çekici yönü budur. FP, yan etkilerden kaçınabileceğiniz için popüler oldu. Bu soru ile ilgili olmadığından, bundan ne tür artıları alabileceğinizi derinlemesine araştırmayacağım.

Bununla birlikte, Iterable.forEach kullanarak yinelemenin FP'den ilham aldığını ve daha çok Java'ya daha fazla FP getirmenin sonucundan kaynaklandığını söyleyeceğim (ironik olarak, saf FP'de forEach için çok fazla kullanım olmadığını söyleyebilirim, çünkü tanıtmaktan başka bir şey yapmaz yan etkiler).

Sonunda bunun şu anda yazdığınız bir tat \ stil \ paradigma meselesi olduğunu söyleyebilirim.

Paralellik hakkında.

Performans açısından bakıldığında Iterable.forEach üzerinden foreach (...) kullanmanın vaat edilen kayda değer bir faydası yoktur.

Iterable.forEach'taki resmi belgelere göre :

Yinelenebilir içerikler üzerinde verilen eylemi, tüm öğeler işleninceye veya eylem bir istisna atana kadar, yineleme sırasında oluşan sipariş öğelerinde gerçekleşir .

... yani örtük paralellik olmayacağına dair dokümanlar oldukça açık. Birini eklemek LSP ihlali olacaktır.

Şimdi, Java 8'de vaat edilen "parallell koleksiyonları" var, ancak bunlarla çalışmak için bana daha açık olmanız ve bunları kullanmak için biraz daha dikkatli olmanız gerekiyor (örneğin, mschenk74'ün cevabına bakın).

BTW: bu durumda Stream.forEach kullanılacaktır ve gerçek çalışmanın paralel olarak yapılacağını garanti etmez (altta yatan koleksiyona bağlıdır).

GÜNCELLEME: Bir bakışta o kadar açık ve biraz gergin olmayabilir, ancak stil ve okunabilirlik perspektifinin başka bir yönü var.

Her şeyden önce - sade eski forloops düz ve eskidir. Herkes zaten biliyor.

İkincisi ve daha da önemlisi - muhtemelen Yinelenebilir kullanmak istiyorsunuz. Forher one for liner lambdas. Eğer "beden" ağırlaşırsa - okunabilir olma eğilimindedirler. Buradan 2 seçeneğiniz var - iç sınıflar (yuck) kullanın veya düz eski forloop kullanın. İnsanlar genellikle aynı kod tabanında çeşitli vays / stiller yaptıklarını (koleksiyonlar üzerinde iteratinler) gördüklerinde sıkılırlar ve durum böyle görünmektedir.

Yine, bu bir sorun olabilir veya olmayabilir. Kod üzerinde çalışan kişilere bağlıdır.


1
Paralellik yeni "paralel koleksiyonlara" ihtiyaç duymaz. Sadece ardışık bir akış (collection.stream () kullanarak) veya paralel bir akış (collection.parallelStream () kullanarak) isteyip istemediğinize bağlıdır.
JB Nizet

@JBNizet docs Collection.parallelStream () yöntemine göre koleksiyonun uygulanmasının parallell akışı döndüreceği garanti edilmez. Aslında kendimi merak ediyorum, bunun ne zaman olabileceğini, ama muhtemelen bu koleksiyona bağlı.
Eugene Loy

kabul. Ayrıca koleksiyona da bağlıdır. Ama benim açımdan paralel foreach döngülerinin tüm standart koleksiyonlarda (ArrayList vb.) Mevcut olmasıydı. "Paralel koleksiyonlar" için beklemeye gerek yok.
JB Nizet

@JBNizet sizin açınızdan bu konuda hemfikir, ama aslında "paralel koleksiyonlar" ile kastettiğim bu değil. Java 8'de Scala'nın hemen hemen aynı şeyi yapan kavramına benzeterek "paralel koleksiyonlar" olarak eklenen Collection.parallelStream () yöntemine başvuruyorum. Ayrıca, JSR'nin bitinde nasıl adlandırıldığından emin değilim, bu Java 8 özelliği için aynı terminolojiyi kullanan birkaç kağıt gördüm.
Eugene Loy

1
son paragraf için bir fonksiyon referansı kullanabilirsiniz:collection.forEach(MyClass::loopBody);
cırcır ucube

6

En upleasing fonksiyonel forEachsınırlamalarından biri, kontrol edilen istisnalar desteğinin olmamasıdır.

Bir olası bir çözüm , terminal değiştirmektir forEachdüz eski foreach döngüsü ile:

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

Lamdalar ve akarsularda kontrol edilen istisna işleme konusundaki diğer geçici çözümlerle ilgili en popüler soruların listesi:

İstisna atan Java 8 Lambda işlevi?

Java 8: Lambda-Akışları, İstisna ile Yöntemle Filtreleme

Java 8 akışlarının içinden CHECKED istisnalarını nasıl atabilirim?

Java 8: Lambda ifadelerinde zorunlu olarak kontrol edilen özel durumlar işleme. Neden zorunlu, isteğe bağlı değil?


3

Java 1.8 forEach yönteminin 1.7'den fazla avantajı Döngü için geliştirilmiş kod yazarken yalnızca iş mantığına odaklanabilmenizdir.

forEach yöntemi, java.util.function.Consumer nesnesini bağımsız değişken olarak alır, bu nedenle iş mantığımızın her zaman yeniden kullanabileceğiniz ayrı bir konuma sahip olmasına yardımcı olur.

Aşağıdaki snippet'e bakın,

  • Burada ek işlevsellik, yineleme daha fazla ekleyebilirsiniz tüketici sınıfı kabul sınıf yöntemi geçersiz kılan yeni sınıf yarattık .. !!!!!!

    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);
                }
            });
        }
    }
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.