Java 8 Iterable.forEach () vs foreach loop

466

Manakah dari berikut ini yang merupakan praktik yang lebih baik di Java 8?

Java 8:

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

Java 7:

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

Saya punya banyak loop yang bisa "disederhanakan" dengan lambdas, tetapi apakah benar-benar ada keuntungan menggunakannya? Apakah ini akan meningkatkan kinerja dan keterbacaan mereka?

EDIT

Saya juga akan memperluas pertanyaan ini ke metode yang lebih panjang. Saya tahu bahwa Anda tidak dapat mengembalikan atau menghancurkan fungsi induk dari lambda dan ini juga harus dipertimbangkan ketika membandingkannya, tetapi apakah ada hal lain yang perlu dipertimbangkan?

nebkat
sumber
10
Tidak ada keunggulan kinerja nyata satu sama lain. Opsi pertama adalah sesuatu yang terinspirasi oleh FP (whitch biasanya dibicarakan seperti cara yang lebih "bagus" dan "jelas" untuk mengekspresikan kode Anda). Pada kenyataannya - ini adalah pertanyaan "gaya".
Eugene Loy
5
@ WDB: dalam hal ini, itu tidak relevan. forEach tidak didefinisikan sebagai paralel atau semacamnya, jadi kedua hal ini secara semantik setara. Tentu saja dimungkinkan untuk mengimplementasikan versi paralel forEach (dan yang mungkin sudah ada di perpustakaan standar), dan dalam kasus seperti itu sintaks ekspresi lambda akan sangat berguna.
AardvarkSoup
8
@AardvarkSoup Contoh di mana forEach dipanggil adalah Stream ( lambdadoc.net/api/java/util/stream/Stream.html ). Untuk meminta eksekusi paralel, seseorang dapat menulis joins.parallel (). ForEach (...)
mschenk74
13
Apakah joins.forEach((join) -> mIrc.join(mSession, join));benar-benar "penyederhanaan" for (String join : joins) { mIrc.join(mSession, join); }? Anda telah meningkatkan jumlah tanda baca dari 9 menjadi 12, untuk kepentingan menyembunyikan jenis join. Apa yang sebenarnya telah Anda lakukan adalah menempatkan dua pernyataan pada satu baris.
Tom Hawtin - tackline
7
Hal lain yang perlu dipertimbangkan adalah kemampuan menangkap variabel terbatas Java. Dengan Stream.forEach (), Anda tidak dapat memperbarui variabel lokal karena penangkapannya menjadikannya final, yang berarti Anda dapat memiliki perilaku stateful di lambai forEach (kecuali Anda siap untuk beberapa keburukan seperti menggunakan variabel status kelas).
RedGlyph

Jawaban:

512

Praktik yang lebih baik adalah menggunakan for-each. Selain melanggar prinsip Keep It Simple, Stupid , fangled-baru forEach()memiliki setidaknya kekurangan berikut:

  • Tidak dapat menggunakan variabel non-final . Jadi, kode seperti berikut tidak dapat diubah menjadi lambda forEach:

    Object prev = null;
    for(Object curr : list)
    {
        if( prev != null )
            foo(prev, curr);
        prev = curr;
    }
    
  • Tidak dapat menangani pengecualian yang dicentang . Lambdas sebenarnya tidak dilarang melemparkan pengecualian yang diperiksa, tetapi antarmuka fungsional umum seperti Consumertidak menyatakan apa pun. Oleh karena itu, kode apa pun yang melempar pengecualian yang diperiksa harus membungkusnya dengan try-catchatau Throwables.propagate(). Tetapi bahkan jika Anda melakukan itu, tidak selalu jelas apa yang terjadi pada pengecualian yang dilemparkan. Itu bisa tertelan di suatu tempat di nyaliforEach()

  • Kontrol aliran terbatas . A returndalam lambda sama dengan a continuedalam masing-masing, tetapi tidak ada yang setara dengan a break. Juga sulit untuk melakukan hal-hal seperti nilai balik, korsleting, atau set flag (yang akan meringankan banyak hal, jika itu bukan pelanggaran aturan variabel non-final ). "Ini bukan hanya optimasi, tetapi penting ketika Anda mempertimbangkan bahwa beberapa urutan (seperti membaca baris dalam file) mungkin memiliki efek samping, atau Anda mungkin memiliki urutan yang tak terbatas."

  • Dapat dieksekusi secara paralel , yang merupakan hal yang mengerikan, mengerikan untuk semua kecuali 0,1% dari kode Anda yang perlu dioptimalkan. Kode paralel apa pun harus dipikirkan (bahkan jika itu tidak menggunakan kunci, volatil, dan aspek jahat lainnya dari eksekusi multi-threaded tradisional). Bug apa pun akan sulit ditemukan.

  • Mungkin melukai kinerja , karena JIT tidak dapat mengoptimalkan forEach () + lambda ke tingkat yang sama seperti loop biasa, terutama sekarang bahwa lambda baru. Yang dimaksud dengan "optimisasi" bukan berarti overhead dari memanggil lambdas (yang kecil), tetapi pada analisis dan transformasi canggih yang dilakukan oleh kompiler JIT modern pada menjalankan kode.

  • Jika Anda memang membutuhkan paralelisme, mungkin jauh lebih cepat dan tidak jauh lebih sulit untuk menggunakan ExecutorService . Streaming keduanya otomatis (baca: tidak tahu banyak tentang masalah Anda) dan menggunakan strategi paralelisasi khusus (baca: tidak efisien untuk kasus umum) ( dekomposisi fork-join recursive ).

  • Membuat debugging lebih membingungkan , karena hierarki panggilan bersarang dan, dilarang, eksekusi paralel. Debugger mungkin memiliki masalah menampilkan variabel dari kode di sekitarnya, dan hal-hal seperti langkah-langkah mungkin tidak berfungsi seperti yang diharapkan.

  • Streaming secara umum lebih sulit untuk dikodekan, dibaca, dan didebug . Sebenarnya, ini berlaku untuk API " lancar " yang kompleks secara umum. Kombinasi dari pernyataan tunggal yang kompleks, penggunaan generik yang banyak, dan kurangnya variabel perantara berkonspirasi untuk menghasilkan pesan kesalahan yang membingungkan dan membuat frustrasi proses debugging. Alih-alih "metode ini tidak memiliki kelebihan untuk tipe X" Anda mendapatkan pesan kesalahan lebih dekat ke "di suatu tempat Anda mengacaukan jenis, tetapi kami tidak tahu di mana atau bagaimana." Demikian pula, Anda tidak dapat melangkah dan memeriksa hal-hal dalam debugger semudah ketika kode tersebut dipecah menjadi beberapa pernyataan, dan nilai-nilai perantara disimpan ke variabel. Akhirnya, membaca kode dan memahami jenis dan perilaku pada setiap tahap eksekusi mungkin tidak sepele.

  • Keluar seperti ibu jari yang sakit . Bahasa Jawa sudah memiliki untuk-setiap pernyataan. Mengapa menggantinya dengan panggilan fungsi? Mengapa mendorong menyembunyikan efek samping di suatu tempat dalam ekspresi? Mengapa mendorong one-liners yang sulit? Memadukan secara teratur untuk masing-masing dan yang baru untuk masing-masing mau tak mau adalah gaya yang buruk. Kode harus berbicara dalam idiom (pola yang cepat untuk dipahami karena pengulangannya), dan semakin sedikit idiom yang digunakan, semakin jelas kodenya dan semakin sedikit waktu yang dihabiskan untuk memutuskan idiom mana yang akan digunakan (penguras waktu besar bagi perfeksionis seperti saya! ).

Seperti yang Anda lihat, saya bukan penggemar forEach () kecuali dalam kasus ketika itu masuk akal.

Yang sangat menyinggung saya adalah fakta yang Streamtidak diterapkan Iterable(meskipun sebenarnya memiliki metode iterator) dan tidak dapat digunakan untuk masing-masing, hanya dengan forEach (). Saya sarankan casting Streaming ke Iterables dengan (Iterable<T>)stream::iterator. Alternatif yang lebih baik adalah menggunakan StreamEx yang memperbaiki sejumlah masalah Stream API, termasuk mengimplementasikan Iterable.

Yang mengatakan, forEach()berguna untuk hal berikut:

  • Pengulangan secara atomis pada daftar yang disinkronkan . Sebelum ini, daftar yang dibuat dengan Collections.synchronizedList()adalah atom sehubungan dengan hal-hal seperti mendapatkan atau mengatur, tetapi tidak aman thread saat iterasi.

  • Eksekusi paralel (menggunakan aliran paralel yang sesuai) . Ini menghemat beberapa baris kode vs menggunakan ExecutorService, jika masalah Anda cocok dengan asumsi kinerja yang dibangun ke dalam Streaming dan Pembagi.

  • Wadah khusus yang , seperti daftar yang disinkronkan, mendapat manfaat dari kontrol iterasi (meskipun ini sebagian besar teoretis kecuali orang dapat memunculkan lebih banyak contoh)

  • Memanggil fungsi tunggal lebih bersih dengan menggunakan forEach()dan argumen referensi metode (yaitu, list.forEach (obj::someMethod)). Namun, ingatlah poin-poin pada pengecualian diperiksa, debugging lebih sulit, dan mengurangi jumlah idiom yang Anda gunakan saat menulis kode.

Artikel yang saya gunakan untuk referensi:

EDIT: Sepertinya beberapa proposal asli untuk lambdas (seperti http://www.javac.info/closures-v06a.html ) menyelesaikan beberapa masalah yang saya sebutkan (sambil menambahkan komplikasinya sendiri, tentu saja).

Aleksandr Dubinsky
sumber
95
"Mengapa mendorong menyembunyikan efek samping di suatu tempat dalam ekspresi?" adalah pertanyaan yang salah. Fungsional forEachada untuk mendorong gaya fungsional, yaitu menggunakan ekspresi tanpa efek samping. Jika Anda menghadapi situasi yang forEachtidak bekerja dengan baik dengan efek samping Anda, Anda harus merasa bahwa Anda tidak menggunakan alat yang tepat untuk pekerjaan itu. Maka jawaban sederhananya adalah, itu karena perasaan Anda benar, jadi tetap di setiap loop untuk itu. forLoop klasik tidak menjadi usang ...
Holger
17
@ Holger Bagaimana bisa forEachdigunakan tanpa efek samping?
Aleksandr Dubinsky
14
Baiklah, saya tidak cukup tepat, forEachadalah satu-satunya operasi aliran yang ditujukan untuk efek samping, tetapi ini bukan untuk efek samping seperti contoh kode Anda, penghitungan adalah reduceoperasi yang umum . Saya menyarankan, sebagai aturan buk, untuk menjaga setiap operasi yang memanipulasi variabel lokal atau akan mempengaruhi aliran kontrol (termasuk penanganan eksepsi) dalam forloop klasik . Mengenai pertanyaan awal, saya pikir, masalahnya berasal dari kenyataan bahwa seseorang menggunakan aliran di mana forloop sederhana atas sumber aliran akan cukup. Gunakan aliran yang forEach()hanya berfungsi
Holger
8
@ Holger Apa contoh efek samping yang forEachsesuai untuk Anda?
Aleksandr Dubinsky
29
Sesuatu yang memproses setiap item secara individual dan tidak mencoba mengubah variabel lokal. Misalnya memanipulasi item itu sendiri atau mencetaknya, menulis / mengirimnya ke file, aliran jaringan, dll. Tidak masalah bagi saya jika Anda mempertanyakan contoh-contoh ini dan tidak melihat aplikasi untuk itu; memfilter, memetakan, mengurangi, mencari, dan (pada tingkat yang lebih rendah) mengumpulkan adalah operasi aliran yang disukai. ForEach terlihat seperti kenyamanan bagi saya untuk menautkan dengan API yang ada. Dan untuk operasi paralel, tentu saja. Ini tidak akan bekerja dengan forloop.
Holger
169

Keuntungan muncul saat operasi dapat dilakukan secara paralel. (Lihat http://java.dzone.com/articles/devoxx-2012-java-8-lambda-and - bagian tentang iterasi internal dan eksternal)

  • Keuntungan utama dari sudut pandang saya adalah bahwa implementasi apa yang harus dilakukan dalam loop dapat didefinisikan tanpa harus memutuskan apakah akan dieksekusi secara paralel atau berurutan

  • Jika Anda ingin loop Anda dieksekusi secara paralel, Anda cukup menulis

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

    Anda harus menulis beberapa kode tambahan untuk penanganan utas dll.

Catatan: Untuk jawaban saya, saya berasumsi bergabung dengan mengimplementasikan java.util.Streamantarmuka. Jika bergabung hanya mengimplementasikan java.util.Iterableantarmuka, ini tidak lagi benar.

mschenk74
sumber
4
Slide-slide insinyur oracle yang dia maksud ( blogs.oracle.com/darcy/resource/Devoxx/… ) tidak menyebutkan paralelisme dalam ekspresi lambda itu. Paralelisme dapat terjadi dalam metode pengumpulan massal seperti map& foldyang tidak benar-benar terkait dengan lambdas.
Thomas Jungblut
1
Tampaknya kode OP tidak akan mendapat manfaat dari paralelisme otomatis di sini (terutama karena tidak ada jaminan bahwa akan ada satu). Kami tidak benar-benar tahu apa itu "mIrc", tetapi "bergabung" tidak benar-benar tampak seperti sesuatu yang dapat dieksekusi di luar urutan.
Eugene Loy
11
Stream#forEachdan Iterable#forEachbukan hal yang sama. OP bertanya tentang Iterable#forEach.
gvlasov
2
Saya menggunakan gaya UPDATEX karena ada perubahan dalam spesifikasi antara waktu pertanyaan diajukan dan waktu jawabannya diperbarui. Tanpa sejarah jawaban itu akan lebih membingungkan lagi, saya pikir.
mschenk74
1
Adakah yang bisa menjelaskan kepada saya mengapa jawaban ini tidak valid jika joinsditerapkan Iterablebukan Stream? Dari beberapa hal yang saya baca, OP seharusnya dapat melakukan joins.stream().forEach((join) -> mIrc.join(mSession, join));dan joins.parallelStream().forEach((join) -> mIrc.join(mSession, join));jika joinsmengimplementasikanIterable
Blueriver
112

Ketika membaca pertanyaan ini, orang bisa mendapatkan kesan, bahwa Iterable#forEachdalam kombinasi dengan ekspresi lambda adalah jalan pintas / pengganti untuk menulis perulangan tradisional untuk setiap loop. Ini tidak benar. Kode ini dari OP:

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

ini tidak dimaksudkan sebagai jalan pintas untuk menulis

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

dan tentunya tidak boleh digunakan dengan cara ini. Sebaliknya ini dimaksudkan sebagai jalan pintas (meskipun tidak persis sama) untuk menulis

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

Dan itu sebagai pengganti kode Java 7 berikut:

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

Mengganti badan loop dengan antarmuka fungsional, seperti pada contoh di atas, membuat kode Anda lebih eksplisit: Anda mengatakan bahwa (1) tubuh loop tidak mempengaruhi kode di sekitarnya dan aliran kontrol, dan (2) tubuh loop dapat diganti dengan implementasi fungsi yang berbeda, tanpa mempengaruhi kode di sekitarnya. Tidak dapat mengakses variabel non final dari lingkup luar bukan defisit fungsi / lambdas, itu adalah fitur yang membedakan semantik dari Iterable#forEachdari semantik tradisional untuk setiap loop. Setelah seseorang terbiasa dengan sintaks Iterable#forEach, itu membuat kode lebih mudah dibaca, karena Anda segera mendapatkan informasi tambahan tentang kode ini.

Setiap loop tradisional tentu akan tetap menjadi praktik yang baik (untuk menghindari istilah " praktik terbaik " yang terlalu sering digunakan ) di Jawa. Tapi ini tidak berarti, itu Iterable#forEachharus dianggap praktik buruk atau gaya buruk. Itu selalu merupakan praktik yang baik, untuk menggunakan alat yang tepat untuk melakukan pekerjaan, dan ini termasuk mencampur tradisional untuk setiap loop dengan Iterable#forEach, di mana masuk akal.

Karena kelemahan dari Iterable#forEachsudah dibahas di utas ini, berikut adalah beberapa alasan, mengapa Anda mungkin ingin menggunakan Iterable#forEach:

  • Untuk membuat kode Anda lebih eksplisit: Seperti dijelaskan di atas, Iterable#forEach dapat membuat kode Anda lebih eksplisit dan mudah dibaca dalam beberapa situasi.

  • Untuk membuat kode Anda lebih bisa diperluas dan dipelihara: Menggunakan fungsi sebagai badan loop memungkinkan Anda untuk mengganti fungsi ini dengan implementasi yang berbeda (lihat Pola Strategi ). Misalnya Anda dapat dengan mudah mengganti ekspresi lambda dengan pemanggilan metode, yang mungkin ditimpa oleh sub-kelas:

    joins.forEach(getJoinStrategy());

    Kemudian Anda bisa memberikan strategi standar menggunakan enum, yang mengimplementasikan antarmuka fungsional. Ini tidak hanya membuat kode Anda lebih dapat diperluas, tetapi juga meningkatkan pemeliharaan karena memisahkan implementasi loop dari deklarasi loop.

  • Untuk membuat kode Anda lebih dapat di-debuggable: Memisahkan implementasi loop dari deklarasi juga dapat membuat debugging lebih mudah, karena Anda bisa memiliki implementasi debug khusus, yang mencetak pesan debug, tanpa perlu mengacaukan kode utama Anda if(DEBUG)System.out.println(). Implementasi debug misalnya bisa menjadi delegasi , yang menghiasi implementasi fungsi yang sebenarnya.

  • Untuk mengoptimalkan kode penting-kinerja: Bertentangan dengan beberapa pernyataan di utas ini, Iterable#forEach memang sudah memberikan kinerja yang lebih baik daripada perulangan tradisional untuk setiap loop, setidaknya saat menggunakan ArrayList dan menjalankan Hotspot dalam mode "-client". Meskipun peningkatan kinerja ini kecil dan dapat diabaikan untuk sebagian besar kasus penggunaan, ada beberapa situasi, di mana kinerja ekstra ini dapat membuat perbedaan. Misalnya pengelola perpustakaan tentu ingin mengevaluasi, jika beberapa implementasi loop yang ada harus diganti Iterable#forEach.

    Untuk mendukung pernyataan ini dengan fakta, saya telah melakukan beberapa tolok ukur mikro dengan Caliper . Inilah kode tes (Caliper terbaru dari git diperlukan):

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

    Dan inilah hasilnya:

    Saat menjalankan dengan "-client", Iterable#forEachmengungguli tradisional untuk loop di atas ArrayList, tetapi masih lebih lambat daripada secara langsung beralih di atas array. Saat dijalankan dengan "-server", kinerja semua pendekatan hampir sama.

  • Untuk memberikan dukungan opsional untuk eksekusi paralel: Telah dikatakan di sini, bahwa kemungkinan untuk mengeksekusi antarmuka fungsional Iterable#forEachsecara paralel menggunakan stream , tentu saja merupakan aspek penting. Karena Collection#parallelStream()tidak menjamin, bahwa loop sebenarnya dieksekusi secara paralel, kita harus menganggap ini sebagai fitur opsional . Dengan mengulangi daftar Anda dengan list.parallelStream().forEach(...);, Anda secara eksplisit mengatakan: Loop ini mendukung eksekusi paralel, tetapi tidak bergantung padanya. Sekali lagi, ini adalah fitur dan bukan defisit!

    Dengan memindahkan keputusan untuk eksekusi paralel dari implementasi loop aktual Anda, Anda mengizinkan optimalisasi opsional kode Anda, tanpa memengaruhi kode itu sendiri, yang merupakan hal yang baik. Juga, jika implementasi aliran paralel paralel tidak sesuai dengan kebutuhan Anda, tidak ada yang mencegah Anda menyediakan implementasi Anda sendiri. Anda dapat mis. Memberikan koleksi yang dioptimalkan tergantung pada sistem operasi yang mendasarinya, pada ukuran koleksi, pada jumlah core, dan pada beberapa pengaturan preferensi:

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

    Yang menyenangkan di sini adalah, bahwa implementasi loop Anda tidak perlu tahu atau peduli tentang detail ini.

Balder
sumber
5
Anda memiliki pandangan yang menarik dalam diskusi ini dan memunculkan sejumlah poin. Saya akan mencoba mengatasinya. Anda mengusulkan untuk beralih di antara forEachdan for-eachberdasarkan pada beberapa kriteria mengenai sifat tubuh loop. Kebijaksanaan dan disiplin untuk mengikuti aturan seperti itu adalah ciri khas seorang programmer yang baik. Aturan seperti itu juga kutukannya, karena orang-orang di sekitarnya tidak mengikuti atau tidak setuju. Misalnya, menggunakan Pengecualian yang dicentang dan tidak dicentang. Situasi ini tampaknya semakin bernuansa. Tetapi, jika tubuh "tidak mempengaruhi kode surround atau kontrol aliran," bukankah memfaktorkannya sebagai fungsi yang lebih baik?
Aleksandr Dubinsky
4
Terima kasih atas komentar terinci Aleksandr. But, if the body "does not affect surround code or flow control," isn't factoring it out as a function better?. Ya, ini akan sering menjadi kasus menurut saya - memfaktorkan loop ini sebagai fungsi adalah konsekuensi alami.
Balder
2
Mengenai masalah kinerja - saya kira itu sangat tergantung pada sifat loop. Dalam proyek yang sedang saya kerjakan, saya telah menggunakan fungsi-loop seperti Iterable#forEachsebelum Java 8 hanya karena peningkatan kinerja. Proyek tersebut memiliki satu loop utama yang mirip dengan loop game, dengan jumlah sub-loop bersarang yang tidak ditentukan, di mana klien dapat plug-in loop peserta sebagai fungsi. Struktur perangkat lunak seperti itu sangat diuntungkan Iteable#forEach.
Balder
7
Ada kalimat di bagian paling akhir kritik saya: "Kode harus berbicara dalam idiom, dan semakin sedikit idiom yang digunakan semakin jelas kodenya dan semakin sedikit waktu yang dihabiskan untuk memutuskan idiom mana yang akan digunakan". Saya mulai sangat menghargai titik ini ketika saya beralih dari C # ke Jawa.
Aleksandr Dubinsky
7
Itu argumen yang buruk. Anda dapat menggunakannya untuk menjustifikasi apa pun yang Anda inginkan: mengapa Anda tidak harus menggunakan for for loop, karena perulangan sementara cukup baik dan itu satu ungkapan yang kurang. Heck, mengapa menggunakan loop, switch, atau mencoba / menangkap pernyataan ketika goto dapat melakukan semua itu dan banyak lagi.
tapichu
13

forEach()dapat diimplementasikan menjadi lebih cepat daripada untuk-setiap loop, karena iterable tahu cara terbaik untuk mengulangi elemen-elemennya, sebagai lawan dari cara iterator standar. Jadi perbedaannya adalah loop internal atau loop eksternal.

Misalnya ArrayList.forEach(action)dapat dengan mudah diimplementasikan sebagai

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

sebagai lawan dari untuk-setiap loop yang membutuhkan banyak perancah

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

Namun, kita juga perlu memperhitungkan dua biaya overhead dengan menggunakan forEach(), satu membuat objek lambda, yang lain menggunakan metode lambda. Mereka mungkin tidak signifikan.

lihat juga http://journal.stuffwithstuff.com/2013/01/13/iteration-inside-and-out/ untuk membandingkan iterasi internal / eksternal untuk berbagai kasus penggunaan.

ZhongYu
sumber
8
mengapa iterable tahu cara terbaik tetapi iterator tidak?
mschenk74
2
tidak ada perbedaan mendasar, tetapi kode tambahan diperlukan untuk menyesuaikan dengan antarmuka iterator, yang mungkin lebih mahal.
ZhongYu
1
@ zhong.j.yu jika Anda menerapkan Koleksi, Anda juga menerapkan Iterable. Jadi, tidak ada overhead kode dalam hal "menambahkan lebih banyak kode untuk mengimplementasikan metode antarmuka yang hilang", jika itu yang Anda maksud. Seperti yang dikatakan mschenk74 tampaknya tidak ada alasan mengapa Anda tidak dapat mengubah iterator Anda untuk mengetahui cara beralih pada koleksi Anda dengan cara terbaik. Saya setuju bahwa mungkin ada overhead untuk pembuatan iterator, tapi serius, hal-hal itu biasanya sangat murah, sehingga Anda dapat mengatakan bahwa mereka memiliki biaya nol ...
Eugene Loy
4
misalnya iterasi pohon:, void forEach(Consumer<T> v){leftTree.forEach(v);v.accept(rootElem);rightTree.forEach(v);}ini lebih elegan daripada iterasi eksternal, dan Anda dapat memutuskan cara menyinkronkan terbaik
ratchet freak
1
Lucunya, satu-satunya komentar dalam String.joinmetode (oke, salah gabung) adalah "Jumlah elemen yang kemungkinan tidak layak untuk biaya overhead Arrays.stream." jadi mereka menggunakan posh untuk loop.
Tom Hawtin - tackline
7

TL; DR : List.stream().forEach()adalah yang tercepat.

Saya merasa saya harus menambahkan hasil saya dari tolok ukur iterasi. Saya mengambil pendekatan yang sangat sederhana (tanpa kerangka kerja pembandingan) dan membandingkan 5 metode berbeda:

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

prosedur dan parameter pengujian

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
}

Daftar di kelas ini akan diulang dan beberapa doIt(Integer i)diterapkan untuk semua anggotanya, setiap kali melalui metode yang berbeda. di kelas Utama saya menjalankan metode yang diuji tiga kali untuk memanaskan JVM. Saya kemudian menjalankan metode pengujian 1000 kali menjumlahkan waktu yang dibutuhkan untuk setiap metode iterasi (menggunakan System.nanoTime()). Setelah selesai saya membagi jumlah itu dengan 1000 dan itulah hasilnya, waktu rata-rata. contoh:

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

Saya menjalankan ini pada CPU core i5 4, dengan versi java 1.8.0_05

klasik for

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

waktu eksekusi: 4,21 ms

foreach klasik

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

waktu eksekusi: 5,95 ms

List.forEach()

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

waktu eksekusi: 3.11 ms

List.stream().forEach()

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

waktu eksekusi: 2.79 ms

List.parallelStream().forEach

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

waktu eksekusi: 3,6 ms

Assaf
sumber
23
Bagaimana Anda mendapatkan angka-angka itu? Kerangka kerja untuk benchmark apa yang Anda gunakan? Jika Anda tidak menggunakan dan hanya polos System.out.printlnuntuk menampilkan data ini secara naif, maka semua hasilnya tidak berguna.
Luiggi Mendoza
2
Tidak ada kerangka Saya menggunakan System.nanoTime(). Jika Anda membaca jawabannya Anda akan melihat bagaimana hal itu dilakukan. Saya tidak berpikir itu membuatnya sia-sia karena ini adalah pertanyaan yang relatif . Saya tidak peduli seberapa baik metode tertentu, saya peduli seberapa baik itu dibandingkan dengan metode lain.
Assaf
31
Dan itulah tujuan dari tolok ukur mikro yang baik. Karena Anda belum memenuhi persyaratan seperti itu, hasilnya tidak berguna.
Luiggi Mendoza
6
Saya dapat merekomendasikan untuk mengenal JMH sebagai gantinya, ini adalah apa yang digunakan untuk Java itu sendiri dan menempatkan banyak upaya untuk mendapatkan angka yang benar: openjdk.java.net/projects/code-tools/jmh
dsvensson
1
Saya setuju dengan @LuiggiMendoza. Tidak ada cara untuk mengetahui bahwa hasil ini konsisten atau valid. Tuhan tahu berapa banyak tolok ukur yang telah saya lakukan yang terus melaporkan hasil yang berbeda, terutama tergantung pada urutan iterasi, ukuran dan apa yang tidak.
mmm
6

Saya merasa bahwa saya perlu sedikit memperluas komentar saya ...

Tentang paradigma \ gaya

Itu mungkin aspek yang paling menonjol. FP menjadi populer karena Anda bisa menghindari efek samping. Saya tidak akan menggali jauh ke dalam apa pro \ kontra yang bisa Anda dapatkan dari ini, karena ini tidak terkait dengan pertanyaan.

Namun, saya akan mengatakan bahwa iterasi menggunakan Iterable. ForEach terinspirasi oleh FP dan lebih sebagai hasil membawa lebih banyak FP ke Jawa (ironisnya, saya akan mengatakan bahwa tidak ada banyak gunanya forEach di FP murni, karena tidak melakukan apa-apa selain memperkenalkan efek samping).

Pada akhirnya saya akan mengatakan bahwa ini lebih merupakan masalah selera \ gaya \ paradigma yang sedang Anda tulis.

Tentang paralelisme.

Dari sudut pandang kinerja, tidak ada manfaat penting yang dijanjikan dari menggunakan Iterable. Untuk setiap pertemuan (...).

Menurut dokumen resmi di Iterable. ForEach :

Melakukan tindakan yang diberikan pada isi Iterable, dalam urutan elemen terjadi saat iterasi, hingga semua elemen diproses atau tindakan melempar pengecualian.

... yaitu dokumen cukup jelas bahwa tidak akan ada paralelisme implisit. Menambahkan satu akan menjadi pelanggaran LSP.

Sekarang, ada "koleksi paralel" yang dijanjikan di Java 8, tetapi untuk bekerja dengan yang Anda butuhkan untuk saya lebih eksplisit dan menaruh perhatian ekstra untuk menggunakannya (lihat jawaban mschenk74 misalnya).

BTW: dalam hal ini Stream.forEach akan digunakan, dan itu tidak menjamin bahwa pekerjaan yang sebenarnya akan dilakukan secara paralel (tergantung pada koleksi yang mendasarinya).

UPDATE: mungkin tidak begitu jelas dan sedikit melebar sekilas tetapi ada sisi lain gaya dan perspektif keterbacaan.

Pertama-tama - forloop tua polos polos dan tua. Semua orang sudah mengenal mereka.

Kedua, dan yang lebih penting - Anda mungkin ingin menggunakan Iterable. Karena hanya dengan lambda satu-liner. Jika "tubuh" menjadi lebih berat - mereka cenderung tidak bisa dibaca. Anda memiliki 2 opsi dari sini - gunakan kelas dalam (yuck) atau gunakan forloop lama biasa. Orang-orang sering kesal ketika mereka melihat hal yang sama (iteratins atas koleksi) sedang dilakukan berbagai vays / gaya dalam basis kode yang sama, dan ini tampaknya menjadi kasusnya.

Sekali lagi, ini mungkin atau mungkin tidak menjadi masalah. Tergantung orang yang mengerjakan kode.

Eugene Loy
sumber
1
Paralelisme tidak membutuhkan "koleksi paralel" baru. Itu hanya tergantung pada apakah Anda meminta aliran sekuensial (menggunakan collection.stream ()) atau untuk yang paralel (menggunakan collection.parallelStream ()).
JB Nizet
@JBNizet Menurut docs Collection.parallelStream () tidak menjamin bahwa pengumpulan koleksi akan mengembalikan aliran paralel. Saya, sebenarnya bertanya-tanya sendiri, kapan ini mungkin terjadi, tetapi, mungkin ini tergantung pada koleksi.
Eugene Loy
sepakat. Itu juga tergantung pada koleksinya. Tapi poin saya adalah bahwa foreach loop paralel sudah tersedia dengan semua koleksi standar (ArrayList, dll). Tidak perlu menunggu "koleksi paralel".
JB Nizet
@JBNizet setuju pada poin Anda, tapi itu tidak benar-benar apa yang saya maksudkan dengan "koleksi paralel" di tempat pertama. Saya mereferensikan Collection.parallelStream () yang ditambahkan di Java 8 sebagai "koleksi paralel" dengan analogi konsep Scala yang tidak jauh berbeda. Juga, tidak yakin bagaimana itu disebut dalam bit JSR saya melihat beberapa makalah yang menggunakan terminologi yang sama untuk fitur Java 8 ini.
Eugene Loy
1
untuk paragraf terakhir Anda dapat menggunakan referensi fungsi:collection.forEach(MyClass::loopBody);
ratchet freak
6

Salah satu keterbatasan fungsional yang paling mengecewakan forEachadalah kurangnya dukungan pengecualian yang diperiksa.

Salah satu solusi yang mungkin adalah mengganti terminal forEachdengan loop foreach tua polos:

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

Berikut adalah daftar pertanyaan paling populer dengan solusi lain tentang penanganan pengecualian diperiksa dalam lambdas dan stream:

Fungsi Java 8 Lambda yang melempar pengecualian?

Java 8: Lambda-Streams, Saring berdasarkan Metode dengan Pengecualian

Bagaimana saya bisa membuang pengecualian CHECKED dari dalam aliran Java 8?

Java 8: Pengecualian wajib wajib menangani ekspresi lambda. Mengapa wajib, bukan opsional?

Vadzim
sumber
3

Keuntungan metode Java 1.8 forEach lebih dari 1.7 Enhanced for loop adalah bahwa saat menulis kode Anda dapat fokus pada logika bisnis saja.

Metode forEach mengambil objek java.util.function.Consumer sebagai argumen, sehingga membantu memiliki logika bisnis kami di lokasi terpisah yang dapat Anda gunakan kembali kapan saja.

Lihat di cuplikan di bawah ini,

  • Di sini saya telah membuat Kelas baru yang akan menggantikan metode kelas dari Kelas Konsumen, di mana Anda dapat menambahkan fungsionalitas tambahan, Lebih dari Iterasi .. !!!!!!

    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);
                }
            });
        }
    }
Hardik Patel
sumber