Kami telah lama menunggu lambda menghadirkan konsep penutupan ke Java, namun jika kami tidak menggunakannya dalam koleksi, kami kehilangan banyak nilai. Masalah migrasi antarmuka yang ada ke gaya lambda telah diselesaikan melalui metode default. Pada artikel ini, kami akan menganalisis secara mendalam operasi data massal (operasi massal) dalam koleksi Java dan mengungkap misteri peran lambda yang paling kuat.
1.Tentang JSR335
JSR adalah singkatan dari Java Spesification Requests, yang berarti permintaan spesifikasi Java. Peningkatan utama dari versi Java 8 adalah proyek Lambda (JSR 335), yang bertujuan untuk membuat Java lebih mudah dalam menulis kode untuk prosesor multi-core. JSR 335=ekspresi lambda + peningkatan antarmuka (metode default) + operasi data batch. Bersama dengan dua artikel sebelumnya, kami telah mempelajari sepenuhnya konten yang relevan dari JSR335.
2. Iterasi eksternal vs. internal
Di masa lalu, koleksi Java tidak dapat mengekspresikan iterasi internal, tetapi hanya menyediakan satu cara iterasi eksternal, yaitu for atau while loop.
Copy kode kodenya sebagai berikut:
Daftar orang = asList(Orang baru("Joe"), Orang baru("Jim"), Orang baru("John"));
untuk (Orang p : orang) {
p.setLastName("Doe");
}
Contoh di atas adalah pendekatan kami sebelumnya, yang disebut iterasi eksternal. Perulangan adalah perulangan urutan tetap. Di era multi-core saat ini, jika kita ingin melakukan loop secara paralel, kita harus memodifikasi kode di atas. Seberapa besar efisiensi dapat ditingkatkan masih belum pasti, dan hal ini akan membawa risiko tertentu (masalah keamanan benang, dll.).
Untuk mendeskripsikan iterasi internal, kita perlu menggunakan perpustakaan kelas seperti Lambda. Mari kita menulis ulang loop di atas menggunakan lambda dan Collection.forEach.
Salin kode sebagai berikut: person.forEach(p->p.setLastName("Doe"));
Sekarang perpustakaan jdk mengontrol loop. Kita tidak perlu peduli tentang bagaimana nama belakang disetel ke setiap objek orang. Perpustakaan dapat memutuskan bagaimana melakukannya sesuai dengan lingkungan yang sedang berjalan, paralel, rusak, atau malas memuat. Ini adalah iterasi internal, dan klien meneruskan perilaku p.setLastName sebagai data ke dalam api.
Faktanya, iterasi internal tidak terkait erat dengan operasi kumpulan koleksi. Dengan bantuannya, kita dapat merasakan perubahan dalam ekspresi tata bahasa. Hal yang sangat menarik terkait operasi batch adalah API aliran baru. Paket java.util.stream baru telah ditambahkan ke JDK 8.
3.API Aliran
Stream hanya mewakili aliran data dan tidak memiliki struktur data, sehingga tidak dapat lagi dilalui setelah dilalui satu kali (hal ini perlu diperhatikan saat memprogram, tidak seperti Collection, tetap ada data di dalamnya berapa kali pun itu dilintasi). Sumbernya dapat berupa Koleksi, array, io, dll.
3.1 Metode perantara dan titik akhir
Streaming menyediakan antarmuka untuk mengoperasikan data besar, membuat pengoperasian data menjadi lebih mudah dan cepat. Ia memiliki metode seperti pemfilteran, pemetaan, dan pengurangan jumlah traversal. Metode ini dibagi menjadi dua jenis: metode perantara dan metode terminal. Abstraksi "aliran" pada dasarnya harus kontinu kami ingin mendapatkan hasil akhir. Jika demikian, operasi titik akhir harus digunakan untuk mengumpulkan hasil akhir yang dihasilkan oleh aliran. Perbedaan antara kedua metode ini adalah melihat nilai kembaliannya. Jika itu adalah Aliran, itu adalah metode perantara, jika tidak maka itu adalah metode akhir. Silakan merujuk ke API Stream untuk detailnya.
Perkenalkan secara singkat beberapa metode perantara (filter, peta) dan metode titik akhir (kumpulkan, jumlahkan)
3.1.1Saring
Menerapkan fungsi pemfilteran dalam aliran data adalah operasi paling alami yang dapat kami pikirkan. Antarmuka Stream memperlihatkan metode filter, yang menerima implementasi Predikat yang mewakili operasi untuk menggunakan ekspresi lambda yang mendefinisikan kondisi filter.
Copy kode kodenya sebagai berikut:
Daftar orang = …
Aliran orangOver18 = orang.stream().filter(p -> p.getAge() > 18);//Filter orang yang berusia di atas 18 tahun
3.1.2Peta
Misalkan kita memfilter beberapa data sekarang, misalnya saat mengonversi objek. Operasi Map memungkinkan kita mengeksekusi implementasi suatu Fungsi (T dan R generik dari Function<T, R> masing-masing mewakili input eksekusi dan hasil eksekusi), yang menerima parameter input dan mengembalikannya. Pertama, mari kita lihat bagaimana mendeskripsikannya sebagai kelas dalam anonim:
Copy kode kodenya sebagai berikut:
Aliran dewasa = orang
.sungai kecil()
.filter(p -> p.getAge() > 18)
.peta(Fungsi baru() {
@Mengesampingkan
publik Dewasa melamar(Orang orang) {
return new Adult(person);//Mengubah seseorang yang berusia di atas 18 tahun menjadi dewasa
}
});
Sekarang, ubah contoh di atas menjadi ekspresi lambda:
Copy kode kodenya sebagai berikut:
Peta aliran = orang.aliran()
.filter(p -> p.getAge() > 18)
.map(orang -> Dewasa baru(orang));
3.1.3Hitungan
Metode penghitungan adalah metode titik akhir suatu aliran, yang dapat membuat statistik akhir dari hasil aliran dan mengembalikan sebuah int. Sebagai contoh, mari kita hitung jumlah total orang berusia 18 tahun ke atas:
Copy kode kodenya sebagai berikut:
int countOfAdult=orang.stream()
.filter(p -> p.getAge() > 18)
.map(orang -> Dewasa baru(orang))
.menghitung();
3.1.4Kumpulkan
Metode pengumpulan juga merupakan metode titik akhir suatu aliran, yang dapat mengumpulkan hasil akhir.
Copy kode kodenya sebagai berikut:
Daftar Daftar dewasa= orang.stream()
.filter(p -> p.getAge() > 18)
.map(orang -> Dewasa baru(orang))
.collect(Collectors.toList());
Atau jika kita ingin menggunakan kelas implementasi tertentu untuk mengumpulkan hasilnya:
Copy kode kodenya sebagai berikut:
Daftar dewasaList = orang
.sungai kecil()
.filter(p -> p.getAge() > 18)
.map(orang -> Dewasa baru(orang))
.collect(Collectors.toCollection(ArrayList::new));
Karena keterbatasan ruang, metode perantara dan metode titik akhir lainnya tidak akan diperkenalkan satu per satu. Setelah membaca contoh di atas, Anda hanya perlu memahami perbedaan antara kedua metode ini, dan Anda dapat memutuskan untuk menggunakannya sesuai dengan kebutuhan Anda. Nanti.
3.2 Aliran berurutan dan aliran paralel
Setiap Aliran memiliki dua mode: eksekusi berurutan dan eksekusi paralel.
Aliran urutan:
Copy kode kodenya sebagai berikut:
Daftar <Orang> orang = list.getStream.collect(Collectors.toList());
Aliran paralel:
Copy kode kodenya sebagai berikut:
Daftar <Orang> orang = list.getStream.parallel().collect(Collectors.toList());
Sesuai dengan namanya, bila menggunakan metode sekuensial untuk melintasi, setiap item dibaca sebelum item berikutnya dibaca. Saat menggunakan traversal paralel, array akan dibagi menjadi beberapa segmen, yang masing-masing diproses di thread berbeda, dan kemudian hasilnya dikeluarkan bersama-sama.
3.2.1 Prinsip aliran paralel:
Copy kode kodenya sebagai berikut:
Daftar originalList = someData;
split1 = originalList(0, mid);//Membagi data menjadi beberapa bagian kecil
split2 = Daftar asli(pertengahan,akhir);
new Runnable(split1.process());//Jalankan operasi di bagian-bagian kecil
Runnable baru(split2.proses());
Daftar revisiList = split1 + split2;//Gabungkan hasilnya
3.2.2 Perbandingan uji kinerja sekuensial dan paralel
Jika ini adalah mesin multi-inti, secara teoritis aliran paralel akan dua kali lebih cepat dari aliran sekuensial. Berikut ini adalah kode pengujiannya
Copy kode kodenya sebagai berikut:
panjang t0 = Sistem.nanoTime();
//Inisialisasi aliran bilangan bulat dengan kisaran 1 juta dan temukan bilangan yang habis dibagi 2. toArray() adalah metode titik akhir
int a[]=IntStream.range(0, 1_000_000).filter(p -> p % 2==0).toArray();
panjang t1 = Sistem.nanoTime();
//Fungsinya sama seperti di atas, di sini kita menggunakan aliran paralel untuk menghitung
int b[]=IntStream.range(0, 1_000_000).parallel().filter(p -> p % 2==0).toArray();
panjang t2 = Sistem.nanoTime();
//Hasil mesin lokal saya adalah serial: 0.06s, parallel 0.02s, yang membuktikan bahwa aliran paralel memang lebih cepat daripada aliran sekuensial.
System.out.printf("serial: %.2fs, paralel %.2fs%n", (t1 - t0) * 1e-9, (t2 - t1) * 1e-9);
3.3 Tentang kerangka Folk/Join
Paralelisme perangkat keras aplikasi tersedia di Java 7. Salah satu fitur baru dari paket java.util.concurrent adalah kerangka dekomposisi paralel gaya fork-join. Ini juga sangat kuat dan efisien. Saya tidak akan mempelajarinya jelaskan detailnya di sini. Dibandingkan dengan Stream.parallel(), saya lebih suka yang terakhir.
4. Ringkasan
Tanpa lambda, Stream cukup canggung untuk digunakan. Ini akan menghasilkan sejumlah besar kelas internal anonim, seperti contoh 3.1.2map di atas. Jika tidak ada metode default, perubahan pada kerangka koleksi pasti akan menyebabkan banyak perubahan. jadi metode lambda+default membuat perpustakaan jdk Lebih kuat dan fleksibel, peningkatan Stream dan kerangka koleksi adalah bukti terbaik.