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?
java
for-loop
java-8
java-stream
nebkat
sumber
sumber
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 jenisjoin
. Apa yang sebenarnya telah Anda lakukan adalah menempatkan dua pernyataan pada satu baris.Jawaban:
Praktik yang lebih baik adalah menggunakan
for-each
. Selain melanggar prinsip Keep It Simple, Stupid , fangled-baruforEach()
memiliki setidaknya kekurangan berikut:Tidak dapat menggunakan variabel non-final . Jadi, kode seperti berikut tidak dapat diubah menjadi lambda forEach:
Tidak dapat menangani pengecualian yang dicentang . Lambdas sebenarnya tidak dilarang melemparkan pengecualian yang diperiksa, tetapi antarmuka fungsional umum seperti
Consumer
tidak menyatakan apa pun. Oleh karena itu, kode apa pun yang melempar pengecualian yang diperiksa harus membungkusnya dengantry-catch
atauThrowables.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
return
dalam lambda sama dengan acontinue
dalam masing-masing, tetapi tidak ada yang setara dengan abreak
. 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
Stream
tidak diterapkanIterable
(meskipun sebenarnya memiliki metodeiterator
) 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 mengimplementasikanIterable
.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).
sumber
forEach
ada untuk mendorong gaya fungsional, yaitu menggunakan ekspresi tanpa efek samping. Jika Anda menghadapi situasi yangforEach
tidak 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.for
Loop klasik tidak menjadi usang ...forEach
digunakan tanpa efek samping?forEach
adalah satu-satunya operasi aliran yang ditujukan untuk efek samping, tetapi ini bukan untuk efek samping seperti contoh kode Anda, penghitungan adalahreduce
operasi yang umum . Saya menyarankan, sebagai aturan buk, untuk menjaga setiap operasi yang memanipulasi variabel lokal atau akan mempengaruhi aliran kontrol (termasuk penanganan eksepsi) dalamfor
loop klasik . Mengenai pertanyaan awal, saya pikir, masalahnya berasal dari kenyataan bahwa seseorang menggunakan aliran di manafor
loop sederhana atas sumber aliran akan cukup. Gunakan aliran yangforEach()
hanya berfungsiforEach
sesuai untuk Anda?for
loop.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
Anda harus menulis beberapa kode tambahan untuk penanganan utas dll.
Catatan: Untuk jawaban saya, saya berasumsi bergabung dengan mengimplementasikan
java.util.Stream
antarmuka. Jika bergabung hanya mengimplementasikanjava.util.Iterable
antarmuka, ini tidak lagi benar.sumber
map
&fold
yang tidak benar-benar terkait dengan lambdas.Stream#forEach
danIterable#forEach
bukan hal yang sama. OP bertanya tentangIterable#forEach
.joins
diterapkanIterable
bukanStream
? Dari beberapa hal yang saya baca, OP seharusnya dapat melakukanjoins.stream().forEach((join) -> mIrc.join(mSession, join));
danjoins.parallelStream().forEach((join) -> mIrc.join(mSession, join));
jikajoins
mengimplementasikanIterable
Ketika membaca pertanyaan ini, orang bisa mendapatkan kesan, bahwa
Iterable#forEach
dalam kombinasi dengan ekspresi lambda adalah jalan pintas / pengganti untuk menulis perulangan tradisional untuk setiap loop. Ini tidak benar. Kode ini dari OP:ini tidak dimaksudkan sebagai jalan pintas untuk menulis
dan tentunya tidak boleh digunakan dengan cara ini. Sebaliknya ini dimaksudkan sebagai jalan pintas (meskipun tidak persis sama) untuk menulis
Dan itu sebagai pengganti kode Java 7 berikut:
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#forEach
dari semantik tradisional untuk setiap loop. Setelah seseorang terbiasa dengan sintaksIterable#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#forEach
harus 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 denganIterable#forEach
, di mana masuk akal.Karena kelemahan dari
Iterable#forEach
sudah dibahas di utas ini, berikut adalah beberapa alasan, mengapa Anda mungkin ingin menggunakanIterable#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:
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 digantiIterable#forEach
.Untuk mendukung pernyataan ini dengan fakta, saya telah melakukan beberapa tolok ukur mikro dengan Caliper . Inilah kode tes (Caliper terbaru dari git diperlukan):
Dan inilah hasilnya:
Saat menjalankan dengan "-client",
Iterable#forEach
mengungguli 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#forEach
secara paralel menggunakan stream , tentu saja merupakan aspek penting. KarenaCollection#parallelStream()
tidak menjamin, bahwa loop sebenarnya dieksekusi secara paralel, kita harus menganggap ini sebagai fitur opsional . Dengan mengulangi daftar Anda denganlist.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:
Yang menyenangkan di sini adalah, bahwa implementasi loop Anda tidak perlu tahu atau peduli tentang detail ini.
sumber
forEach
danfor-each
berdasarkan 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?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.Iterable#forEach
sebelum 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 diuntungkanIteable#forEach
.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 sebagaisebagai lawan dari untuk-setiap loop yang membutuhkan banyak perancah
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.
sumber
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 terbaikString.join
metode (oke, salah gabung) adalah "Jumlah elemen yang kemungkinan tidak layak untuk biaya overhead Arrays.stream." jadi mereka menggunakan posh untuk loop.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:
for
List.forEach()
List.stream().forEach()
List.parallelStream().forEach
prosedur dan parameter pengujian
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 (menggunakanSystem.nanoTime()
). Setelah selesai saya membagi jumlah itu dengan 1000 dan itulah hasilnya, waktu rata-rata. contoh:Saya menjalankan ini pada CPU core i5 4, dengan versi java 1.8.0_05
klasik
for
waktu eksekusi: 4,21 ms
foreach klasik
waktu eksekusi: 5,95 ms
List.forEach()
waktu eksekusi: 3.11 ms
List.stream().forEach()
waktu eksekusi: 2.79 ms
List.parallelStream().forEach
waktu eksekusi: 3,6 ms
sumber
System.out.println
untuk menampilkan data ini secara naif, maka semua hasilnya tidak berguna.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.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 :
... 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.
sumber
collection.forEach(MyClass::loopBody);
Salah satu keterbatasan fungsional yang paling mengecewakan
forEach
adalah kurangnya dukungan pengecualian yang diperiksa.Salah satu solusi yang mungkin adalah mengganti terminal
forEach
dengan loop foreach tua polos: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?
sumber
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 .. !!!!!!
sumber