Javadoc of Collector menunjukkan cara mengumpulkan elemen aliran ke Daftar baru. Apakah ada satu-liner yang menambahkan hasilnya ke dalam ArrayList yang ada?
Javadoc of Collector menunjukkan cara mengumpulkan elemen aliran ke Daftar baru. Apakah ada satu-liner yang menambahkan hasilnya ke dalam ArrayList yang ada?
Jawaban:
CATATAN: jawaban nosid menunjukkan cara menambahkan ke koleksi yang ada menggunakan forEachOrdered()
. Ini adalah teknik yang berguna dan efektif untuk memutasi koleksi yang ada. Jawaban saya membahas mengapa Anda tidak boleh menggunakan Collector
untuk mengubah koleksi yang ada.
Jawaban singkatnya adalah tidak , setidaknya, tidak secara umum, Anda tidak boleh menggunakan a Collector
untuk memodifikasi koleksi yang ada.
Alasannya adalah bahwa kolektor dirancang untuk mendukung paralelisme, bahkan lebih dari koleksi yang tidak aman. Cara mereka melakukan ini adalah membuat masing-masing utas beroperasi secara independen pada koleksi sendiri hasil antara. Cara setiap utas mendapatkan koleksi sendiri adalah dengan menelepon Collector.supplier()
yang diperlukan untuk mengembalikan koleksi baru setiap kali.
Koleksi-koleksi dari hasil-hasil antara ini kemudian digabungkan, sekali lagi dengan cara yang terbatas, sampai ada satu koleksi hasil tunggal. Ini adalah hasil akhir dari collect()
operasi.
Sepasang jawaban dari Balder dan assylias menyarankan untuk menggunakan Collectors.toCollection()
dan kemudian melewati pemasok yang mengembalikan daftar yang sudah ada alih-alih daftar baru. Ini melanggar persyaratan pemasok, yaitu mengembalikan koleksi baru yang kosong setiap kali.
Ini akan berfungsi untuk kasus-kasus sederhana, seperti yang ditunjukkan contoh-contoh dalam jawaban mereka. Namun, itu akan gagal, terutama jika aliran dijalankan secara paralel. (Versi perpustakaan yang akan datang mungkin berubah dalam beberapa cara yang tidak terduga yang akan menyebabkannya gagal, bahkan dalam kasus berurutan.)
Mari kita ambil contoh sederhana:
List<String> destList = new ArrayList<>(Arrays.asList("foo"));
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");
newList.parallelStream()
.collect(Collectors.toCollection(() -> destList));
System.out.println(destList);
Ketika saya menjalankan program ini, saya sering mendapatkan ArrayIndexOutOfBoundsException
. Ini karena banyak utas beroperasi ArrayList
, struktur data utas tidak aman. Oke, mari kita sinkronkan:
List<String> destList =
Collections.synchronizedList(new ArrayList<>(Arrays.asList("foo")));
Ini tidak akan lagi gagal kecuali. Tetapi alih-alih hasil yang diharapkan:
[foo, 0, 1, 2, 3]
ini memberikan hasil aneh seperti ini:
[foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0]
Ini adalah hasil dari operasi akumulasi / penggabungan terbatas yang saya uraikan di atas. Dengan aliran paralel, setiap utas memanggil pemasok untuk mendapatkan koleksi sendiri untuk akumulasi menengah. Jika Anda melewati pemasok yang mengembalikan koleksi yang sama , setiap utas menambahkan hasilnya ke koleksi itu. Karena tidak ada pemesanan di antara utas, hasilnya akan ditambahkan dalam beberapa urutan sewenang-wenang.
Kemudian, ketika koleksi perantara ini digabungkan, ini pada dasarnya menggabungkan daftar dengan dirinya sendiri. Daftar digabung menggunakan List.addAll()
, yang mengatakan bahwa hasil tidak ditentukan jika kumpulan sumber dimodifikasi selama operasi. Dalam hal ini, ArrayList.addAll()
melakukan operasi array-copy, sehingga akhirnya menduplikasi sendiri, yang merupakan semacam-apa yang diharapkan, saya kira. (Perhatikan bahwa implementasi Daftar lainnya mungkin memiliki perilaku yang sama sekali berbeda.) Bagaimanapun, ini menjelaskan hasil aneh dan elemen digandakan di tujuan.
Anda mungkin berkata, "Saya hanya akan memastikan untuk menjalankan streaming saya secara berurutan" dan teruskan menulis kode seperti ini
stream.collect(Collectors.toCollection(() -> existingList))
bagaimanapun. Saya akan merekomendasikan untuk tidak melakukan ini. Jika Anda mengontrol aliran, tentu saja, Anda dapat menjamin bahwa itu tidak akan berjalan secara paralel. Saya berharap bahwa gaya pemrograman akan muncul di mana aliran diberikan alih-alih koleksi. Jika seseorang memberi Anda aliran dan Anda menggunakan kode ini, itu akan gagal jika aliran itu paralel. Lebih buruk lagi, seseorang mungkin memberi Anda aliran berurutan dan kode ini akan berfungsi dengan baik untuk sementara waktu, lulus semua tes, dll. Kemudian, beberapa waktu sewenang-wenang kemudian, kode di tempat lain dalam sistem mungkin berubah menggunakan aliran paralel yang akan menyebabkan kode Anda untuk istirahat.
OK, lalu pastikan untuk mengingat untuk memanggil sequential()
aliran apa pun sebelum Anda menggunakan kode ini:
stream.sequential().collect(Collectors.toCollection(() -> existingList))
Tentu saja, Anda akan ingat untuk melakukan ini setiap waktu, bukan? :-) Katakanlah Anda lakukan. Kemudian, tim kinerja akan bertanya-tanya mengapa semua implementasi paralel yang dibuat dengan hati-hati tidak memberikan peningkatan. Dan sekali lagi mereka akan melacaknya ke kode Anda yang memaksa seluruh aliran untuk berjalan secara berurutan.
Jangan lakukan itu.
toCollection
metode ini harus mengembalikan koleksi baru dan kosong setiap kali meyakinkan saya untuk tidak melakukannya. Saya benar-benar ingin mematahkan kontrak javadoc kelas Java inti.
forEachOrdered
. Efek samping termasuk menambahkan elemen ke koleksi yang ada, terlepas dari apakah itu sudah memiliki elemen. Jika Anda ingin elemen aliran ditempatkan ke koleksi baru , gunakan collect(Collectors.toList())
atau toSet()
atau toCollection()
.
Sejauh yang saya bisa lihat, semua jawaban lain sejauh ini menggunakan kolektor untuk menambahkan elemen ke aliran yang ada. Namun, ada solusi yang lebih pendek, dan ini berfungsi untuk aliran sekuensial dan paralel. Anda cukup menggunakan metode untukEachOrdered dalam kombinasi dengan referensi metode.
List<String> source = ...;
List<Integer> target = ...;
source.stream()
.map(String::length)
.forEachOrdered(target::add);
Satu-satunya batasan adalah, bahwa sumber dan target adalah daftar yang berbeda, karena Anda tidak diizinkan untuk melakukan perubahan pada sumber aliran selama diproses.
Perhatikan bahwa solusi ini berfungsi untuk aliran sekuensial dan paralel. Namun, itu tidak mendapat manfaat dari konkurensi. Referensi metode yang diteruskan ke forEachOrdered akan selalu dieksekusi secara berurutan.
forEach(existing::add)
sebagai kemungkinan dalam jawaban dua bulan lalu . Saya seharusnya menambahkan forEachOrdered
juga ...
forEachOrdered
bukan forEach
?
forEachOrdered
berfungsi untuk aliran sekuensial dan paralel . Sebaliknya, forEach
mungkin menjalankan objek fungsi yang diteruskan bersamaan untuk aliran paralel. Dalam hal ini, objek fungsi harus disinkronkan dengan benar, misalnya dengan menggunakan a Vector<Integer>
.
target::add
. Terlepas dari thread mana metode ini dipanggil, tidak ada ras data . Saya berharap Anda tahu itu.
Jawaban singkatnya adalah tidak (atau seharusnya tidak). EDIT: yeah, itu mungkin (lihat jawaban assylias di bawah), tetapi terus membaca. EDIT2: tetapi lihat jawaban Stuart Marks untuk alasan lain mengapa Anda masih tidak harus melakukannya!
Jawaban yang lebih panjang:
Tujuan konstruksi ini di Java 8 adalah untuk memperkenalkan beberapa konsep Pemrograman Fungsional ke bahasa; dalam Pemrograman Fungsional, struktur data biasanya tidak dimodifikasi, sebaliknya, yang baru dibuat dari yang lama dengan cara transformasi seperti peta, filter, lipat / perkecil dan banyak lainnya.
Jika Anda harus mengubah daftar lama, cukup kumpulkan item yang dipetakan ke dalam daftar baru:
final List<Integer> newList = list.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
dan kemudian lakukan list.addAll(newList)
- lagi: jika Anda benar-benar harus.
(atau buat daftar baru yang menyatukan yang lama dan yang baru, dan tetapkan kembali ke list
variabel — ini sedikit lebih dalam semangat FP daripada addAll
)
Mengenai API: meskipun API memperbolehkannya (sekali lagi, lihat jawaban assylias), Anda harus menghindari hal itu, setidaknya secara umum. Cara terbaik untuk tidak melawan paradigma (FP) dan mencoba mempelajarinya daripada melawannya (walaupun Jawa umumnya bukan bahasa FP), dan hanya menggunakan taktik "kotor" jika benar-benar diperlukan.
Jawaban yang sangat panjang: (yaitu jika Anda memasukkan upaya untuk benar-benar menemukan dan membaca intro / buku FP seperti yang disarankan)
Untuk mengetahui mengapa memodifikasi daftar yang ada pada umumnya merupakan ide yang buruk dan mengarah pada kode yang kurang dapat dikelola — kecuali Anda memodifikasi variabel lokal dan algoritme Anda pendek dan / atau sepele, yang berada di luar cakupan pertanyaan tentang pemeliharaan kode —Temukan pengantar yang bagus untuk Pemrograman Fungsional (ada ratusan) dan mulai membaca. Penjelasan "pratinjau" akan menjadi sesuatu seperti: itu lebih baik secara matematis dan lebih mudah untuk tidak mengubah data (di sebagian besar program Anda) dan mengarah ke tingkat yang lebih tinggi dan kurang teknis (serta lebih ramah manusia, begitu otak Anda) transisi dari definisi imperatif gaya lama) dari logika program.
Erik Allik sudah memberikan alasan yang sangat bagus, mengapa Anda kemungkinan besar tidak ingin mengumpulkan elemen aliran ke Daftar yang ada.
Bagaimanapun, Anda dapat menggunakan satu-liner berikut, jika Anda benar-benar membutuhkan fungsi ini.
Tetapi seperti yang dijelaskan Stuart Marks dalam jawabannya, Anda seharusnya tidak pernah melakukan ini, jika alirannya mungkin aliran paralel - gunakan dengan risiko Anda sendiri ...
list.stream().collect(Collectors.toCollection(() -> myExistingList));
Anda hanya perlu merujuk daftar asli Anda untuk menjadi orang yang Collectors.toList()
mengembalikan.
Ini demo:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Reference {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
System.out.println(list);
// Just collect even numbers and start referring the new list as the original one.
list = list.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(list);
}
}
Dan inilah cara Anda dapat menambahkan elemen yang baru dibuat ke daftar asli Anda hanya dalam satu baris.
List<Integer> list = ...;
// add even numbers from the list to the list again.
list.addAll(list.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList())
);
Itulah yang disediakan Paradigma Pemrograman Fungsional ini.
Saya akan menggabungkan daftar lama dan daftar baru sebagai aliran dan menyimpan hasilnya ke daftar tujuan. Berfungsi secara paralel juga.
Saya akan menggunakan contoh jawaban yang diterima yang diberikan oleh Stuart Marks:
List<String> destList = Arrays.asList("foo");
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");
destList = Stream.concat(destList.stream(), newList.stream()).parallel()
.collect(Collectors.toList());
System.out.println(destList);
//output: [foo, 0, 1, 2, 3, 4, 5]
Semoga ini bisa membantu.
Collection
"