Cara menambahkan elemen aliran Java8 ke Daftar yang ada


Jawaban:


198

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 Collectoruntuk mengubah koleksi yang ada.

Jawaban singkatnya adalah tidak , setidaknya, tidak secara umum, Anda tidak boleh menggunakan a Collectoruntuk 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.


Penjelasan hebat! - terima kasih telah menjelaskan ini. Saya akan mengedit jawaban saya untuk merekomendasikan tidak pernah melakukan ini dengan kemungkinan aliran paralel.
Balder

3
Jika pertanyaannya adalah, jika ada satu baris untuk menambahkan elemen aliran ke daftar yang ada, maka jawaban singkatnya adalah ya . Lihat jawaban saya. Namun, saya setuju dengan Anda, bahwa menggunakan Collectors.toCollection () dalam kombinasi dengan daftar yang ada adalah cara yang salah.
nosid

Benar. Saya kira kita semua memikirkan kolektor.
Stuart Marks

Jawaban bagus! Saya sangat tergoda untuk menggunakan solusi sekuensial bahkan jika Anda dengan jelas menyarankan karena seperti yang dinyatakan itu harus bekerja dengan baik. Tetapi fakta bahwa javadoc mengharuskan argumen pemasok toCollectionmetode ini harus mengembalikan koleksi baru dan kosong setiap kali meyakinkan saya untuk tidak melakukannya. Saya benar-benar ingin mematahkan kontrak javadoc kelas Java inti.
tampilannya

1
@AlexCurvers Jika Anda ingin streaming memiliki efek samping, Anda hampir pasti ingin menggunakannya 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().
Stuart Marks

169

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.


6
+1 Sangat lucu betapa banyak orang mengklaim bahwa tidak ada kemungkinan ketika ada satu. Btw. Saya dimasukkan forEach(existing::add)sebagai kemungkinan dalam jawaban dua bulan lalu . Saya seharusnya menambahkan forEachOrderedjuga ...
Holger

5
Apakah ada alasan apapun Anda digunakan forEachOrderedbukan forEach?
memberound

6
@membersound: forEachOrderedberfungsi untuk aliran sekuensial dan paralel . Sebaliknya, forEachmungkin menjalankan objek fungsi yang diteruskan bersamaan untuk aliran paralel. Dalam hal ini, objek fungsi harus disinkronkan dengan benar, misalnya dengan menggunakan a Vector<Integer>.
nosid

@BrianGoetz: Saya harus mengakui, bahwa dokumentasi Stream.forEachOrdered agak tidak tepat. Namun, saya tidak bisa melihat interpretasi yang masuk akal dari spesifikasi ini , di mana tidak ada hubungan sebelum-terjadi antara dua panggilan target::add. Terlepas dari thread mana metode ini dipanggil, tidak ada ras data . Saya berharap Anda tahu itu.
nosid

Ini adalah jawaban yang paling berguna, sejauh yang saya ketahui. Ini sebenarnya menunjukkan cara praktis untuk memasukkan item ke daftar yang ada dari aliran, yang merupakan pertanyaan yang ditanyakan (meskipun kata "kumpulkan" yang menyesatkan)
Wheezil

12

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 listvariabel — 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.


@assylias: secara logis, itu tidak salah karena ada bagian atau ; Lagi pula, menambahkan catatan.
Erik Kaplun

1
Jawaban singkatnya benar. Satu kalimat yang diusulkan akan berhasil dalam kasus-kasus sederhana tetapi gagal dalam kasus umum.
Stuart Marks

Jawaban yang lebih panjang sebagian besar benar, tetapi desain API terutama tentang paralelisme dan kurang tentang pemrograman fungsional. Meskipun tentu saja ada banyak hal tentang FP yang bisa menerima paralelisme, jadi kedua konsep ini selaras.
Stuart Marks

@StuartMarks: Menarik: dalam hal apa solusi yang disediakan dalam jawaban assylias rusak? (dan poin bagus tentang paralelisme — saya kira saya terlalu bersemangat untuk mengadvokasi FP)
Erik Kaplun

@ErikAllik Saya telah menambahkan jawaban yang mencakup masalah ini.
Stuart Marks

11

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

ahh, itu memalukan: P
Erik Kaplun

2
Teknik ini akan gagal total jika aliran dijalankan secara paralel.
Stuart Marks

1
Ini akan menjadi tanggung jawab penyedia pengumpulan untuk memastikan tidak gagal - misalnya dengan menyediakan koleksi bersamaan.
Balder

2
Tidak, kode ini melanggar persyaratan toCollection (), yaitu bahwa pemasok mengembalikan koleksi kosong baru dari jenis yang sesuai. Bahkan jika tujuannya adalah thread-safe, penggabungan yang dilakukan untuk kasus paralel akan menimbulkan hasil yang salah.
Stuart Marks

1
@Balder Saya telah menambahkan jawaban yang harus menjelaskan ini.
Stuart Marks

4

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 bermaksud mengatakan bagaimana cara menambah / mengumpulkan ke dalam daftar yang ada bukan hanya menugaskan kembali.
codefx

1
Nah, secara teknis Anda tidak bisa melakukan hal-hal semacam itu dalam paradigma Pemrograman Fungsional, yang merupakan aliran. Dalam Pemrograman Fungsional, negara tidak diubah, sebaliknya, negara-negara baru dibuat dalam struktur data persisten, membuatnya aman untuk keperluan konkurensi, dan lebih fungsional seperti. Pendekatan yang saya sebutkan adalah apa yang dapat Anda lakukan, atau Anda dapat menggunakan pendekatan berorientasi objek gaya lama di mana Anda mengulangi setiap elemen, dan menyimpan atau menghapus elemen sesuai keinginan Anda.
Aman Agnihotri

0

targetList = sourceList.stream (). flatmap (List :: stream) .collect (Collectors.toList ());


0

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.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.