Haruskah saya mengembalikan Koleksi atau Aliran?

163

Misalkan saya memiliki metode yang mengembalikan tampilan baca-saja ke daftar anggota:

class Team {
    private List < Player > players = new ArrayList < > ();

    // ...

    public List < Player > getPlayers() {
        return Collections.unmodifiableList(players);
    }
}

Lebih lanjut anggap bahwa semua klien lakukan adalah beralih daftar sekali, segera. Mungkin untuk memasukkan pemain ke JList atau sesuatu. Klien tidak menyimpan referensi ke daftar untuk pemeriksaan nanti!

Dengan skenario umum ini, haruskah saya mengembalikan aliran?

public Stream < Player > getPlayers() {
    return players.stream();
}

Atau apakah mengembalikan aliran non-idiomatis di Jawa? Apakah aliran dirancang untuk selalu "dihentikan" di dalam ekspresi yang sama dengan yang mereka buat?

fredoverflow
sumber
12
Jelas tidak ada yang salah dengan ini sebagai ungkapan. Bagaimanapun, players.stream()hanyalah metode yang mengembalikan aliran ke pemanggil. Pertanyaan sebenarnya adalah, apakah Anda benar-benar ingin membatasi penelepon ke satu traversal, dan juga menolaknya akses ke koleksi Anda melalui CollectionAPI? Mungkin penelepon hanya ingin ke addAllkoleksi lain?
Marko Topolnik
2
Semuanya tergantung. Anda selalu dapat melakukan collection.stream () serta Stream.collect (). Jadi terserah Anda dan penelepon yang menggunakan fungsi itu.
Raja Anbazhagan

Jawaban:

222

Jawabannya, seperti biasa, "itu tergantung". Itu tergantung pada seberapa besar koleksi yang dikembalikan. Itu tergantung pada apakah hasilnya berubah dari waktu ke waktu, dan seberapa penting konsistensi dari hasil yang dikembalikan. Dan itu sangat tergantung pada bagaimana pengguna cenderung menggunakan jawabannya.

Pertama, perhatikan bahwa Anda selalu bisa mendapatkan Koleksi dari Stream, dan sebaliknya:

// If API returns Collection, convert with stream()
getFoo().stream()...

// If API returns Stream, use collect()
Collection<T> c = getFooStream().collect(toList());

Jadi pertanyaannya adalah, mana yang lebih bermanfaat bagi penelepon Anda.

Jika hasil Anda mungkin tidak terbatas, hanya ada satu pilihan: Streaming.

Jika hasil Anda mungkin sangat besar, Anda mungkin lebih suka Stream, karena mungkin tidak ada nilai apa pun dalam mewujudkannya sekaligus, dan melakukan hal itu dapat menciptakan tekanan tumpukan yang signifikan.

Jika semua yang akan dilakukan pemanggil adalah mengulanginya (pencarian, filter, agregat), Anda harus memilih Stream, karena Stream sudah memiliki bawaan ini dan tidak perlu mematerialisasi koleksi (terutama jika pengguna mungkin tidak memproses seluruh hasil.) Ini adalah kasus yang sangat umum.

Bahkan jika Anda tahu bahwa pengguna akan mengulanginya beberapa kali atau mempertahankannya, Anda masih mungkin ingin mengembalikan Stream sebagai gantinya, karena fakta sederhana bahwa Koleksi apa pun yang Anda pilih untuk dimasukkan ke dalamnya (mis., ArrayList) mungkin bukan formulir yang mereka inginkan, dan kemudian pemanggil harus menyalinnya. jika Anda mengembalikan aliran, mereka dapat melakukannya collect(toCollection(factory))dan mendapatkannya dalam bentuk yang mereka inginkan.

Kasus "prefer Stream" di atas sebagian besar berasal dari fakta bahwa Stream lebih fleksibel; Anda dapat terlambat mengikat ke bagaimana Anda menggunakannya tanpa menimbulkan biaya dan kendala mewujudkannya ke Koleksi.

Satu-satunya kasus di mana Anda harus mengembalikan Koleksi adalah ketika ada persyaratan konsistensi yang kuat, dan Anda harus menghasilkan snapshot konsisten dari target yang bergerak. Kemudian, Anda akan ingin memasukkan elemen ke dalam koleksi yang tidak akan berubah.

Jadi saya akan mengatakan bahwa sebagian besar waktu, Stream adalah jawaban yang tepat - lebih fleksibel, tidak memaksakan biasanya biaya materialisasi yang tidak perlu, dan dapat dengan mudah diubah menjadi Koleksi pilihan Anda jika diperlukan. Tetapi kadang-kadang, Anda mungkin harus mengembalikan Koleksi (katakanlah, karena persyaratan konsistensi yang kuat), atau Anda mungkin ingin mengembalikan Koleksi karena Anda tahu bagaimana pengguna akan menggunakannya dan tahu ini adalah hal yang paling nyaman bagi mereka.

Brian Goetz
sumber
6
Seperti yang saya katakan, ada beberapa kasus di mana ia tidak akan terbang, seperti ketika Anda ingin mengembalikan foto pada saat target bergerak, terutama ketika Anda memiliki persyaratan konsistensi yang kuat. Tetapi sebagian besar waktu, Stream tampaknya menjadi pilihan yang lebih umum, kecuali Anda tahu sesuatu yang spesifik tentang bagaimana itu akan digunakan.
Brian Goetz
8
@Marko Bahkan jika Anda membatasi pertanyaan Anda begitu sempit, saya masih tidak setuju dengan kesimpulan Anda. Mungkin Anda berasumsi bahwa membuat Stream entah bagaimana jauh lebih mahal daripada membungkus koleksi dengan pembungkus abadi? (Dan, bahkan jika tidak, tampilan aliran yang Anda dapatkan di bungkus lebih buruk daripada yang Anda dapatkan dari aslinya; karena UnmodifiableList tidak mengesampingkan spliterator (), Anda secara efektif akan kehilangan semua paralelisme.) Intinya: waspadalah bias keakraban; Anda sudah tahu Koleksi selama bertahun-tahun, dan itu mungkin membuat Anda tidak percaya pada pendatang baru.
Brian Goetz
5
@MarkoTopolnik Tentu. Tujuan saya adalah untuk menjawab pertanyaan desain API umum, yang menjadi FAQ. Mengenai biaya, perhatikan bahwa, jika Anda belum memiliki koleksi terwujud Anda dapat kembali atau membungkus (OP tidak, tetapi sering tidak ada), mematerialisasikan koleksi dalam metode pengambil tidak lebih murah daripada mengembalikan aliran dan membiarkan pemanggil terwujud satu (dan tentu saja pematerialisasi awal mungkin jauh lebih mahal, jika pemanggil tidak membutuhkannya atau jika Anda mengembalikan ArrayList tetapi pemanggil menginginkan TreeSet.) Tetapi Stream adalah baru, dan orang-orang sering menganggap itu lebih dari $$$ daripada ini.
Brian Goetz
4
@MarkoTopolnik Walaupun in-memory adalah use-case yang sangat penting, ada juga beberapa case lain yang memiliki dukungan paralelisasi yang baik, seperti stream yang dihasilkan tanpa urutan (misalnya, Stream.generate). Namun, di mana Streams tidak sesuai adalah kasus penggunaan reaktif, di mana data tiba dengan latensi acak. Untuk itu, saya sarankan RxJava.
Brian Goetz
4
@MarkoTopolnik Saya rasa kami tidak setuju, kecuali mungkin Anda mungkin menyukai kami untuk memfokuskan upaya kami sedikit berbeda. (Kami sudah terbiasa dengan ini; tidak bisa membuat semua orang senang.) Pusat desain untuk Streams berfokus pada struktur data dalam memori; pusat desain untuk RxJava berfokus pada acara yang dibuat secara eksternal. Keduanya adalah perpustakaan yang bagus; keduanya juga tidak berjalan dengan baik ketika Anda mencoba menerapkannya pada casing yang jauh dari pusat desain mereka. Tetapi hanya karena palu adalah alat yang mengerikan untuk sulaman, itu tidak menyarankan ada yang salah dengan palu.
Brian Goetz
63

Saya punya beberapa poin untuk ditambahkan ke jawaban luar biasa Brian Goetz .

Sangat umum untuk mengembalikan Stream dari panggilan metode gaya "pengambil". Lihat halaman penggunaan Stream di Java 8 javadoc dan cari "metode ... yang mengembalikan Stream" untuk paket selain java.util.Stream. Metode ini biasanya pada kelas yang mewakili atau dapat berisi beberapa nilai atau agregasi sesuatu. Dalam kasus seperti itu, API biasanya telah mengembalikan koleksi atau arraynya. Untuk semua alasan yang dicatat Brian dalam jawabannya, sangat fleksibel untuk menambahkan metode Stream-return di sini. Banyak dari kelas-kelas ini sudah memiliki metode collection-atau array-return, karena kelas mendahului API Streams. Jika Anda merancang API baru, dan masuk akal untuk memberikan metode pengembalian-arus, mungkin juga tidak perlu menambahkan metode pengembalian-pengumpulan.

Brian menyebutkan biaya "mewujudkan" nilai-nilai menjadi koleksi. Untuk memperkuat poin ini, sebenarnya ada dua biaya di sini: biaya menyimpan nilai dalam koleksi (alokasi memori dan penyalinan) dan juga biaya untuk menciptakan nilai di tempat pertama. Biaya yang terakhir sering dapat dikurangi atau dihindari dengan mengambil keuntungan dari perilaku mencari kemalasan Stream. Contoh yang baik dari ini adalah API di java.nio.file.Files:

static Stream<String>  lines(path)
static List<String>    readAllLines(path)

Tidak hanya itu readAllLines harus menyimpan seluruh isi file dalam memori untuk menyimpannya ke dalam daftar hasil, tetapi juga harus membaca file sampai akhir sebelum mengembalikan daftar. The linesMetode dapat segera setelah itu telah dilakukan beberapa setup kembali, meninggalkan membaca file dan garis breaking sampai nanti ketika itu diperlukan - atau tidak sama sekali. Ini adalah keuntungan besar, jika misalnya, penelepon hanya tertarik pada sepuluh baris pertama:

try (Stream<String> lines = Files.lines(path)) {
    List<String> firstTen = lines.limit(10).collect(toList());
}

Tentu saja ruang memori yang cukup dapat disimpan jika pemanggil menyaring aliran untuk hanya mengembalikan garis yang cocok dengan pola, dll.

Sebuah idiom yang tampaknya muncul adalah menamai metode aliran-kembali setelah jamak dari nama hal-hal yang diwakilinya atau mengandung, tanpa getawalan. Selain itu, walaupun stream()adalah nama yang masuk akal untuk metode pengembalian aliran ketika hanya ada satu set nilai yang mungkin untuk dikembalikan, kadang-kadang ada kelas yang memiliki agregasi dari beberapa jenis nilai. Misalnya, Anda memiliki beberapa objek yang berisi atribut dan elemen. Anda mungkin menyediakan dua API yang mengembalikan aliran:

Stream<Attribute>  attributes();
Stream<Element>    elements();
Stuart Marks
sumber
3
Poin bagus. Bisakah Anda mengatakan lebih banyak tentang di mana Anda melihat idiom penamaan yang muncul, dan seberapa banyak daya tarik (uap?) Yang didapat? Saya suka ide konvensi penamaan yang membuatnya jelas bahwa Anda mendapatkan aliran vs koleksi - meskipun saya juga sering berharap penyelesaian IDE pada "mendapatkan" untuk memberi tahu saya apa yang bisa saya dapatkan.
Joshua Goldberg
1
Saya juga sangat tertarik dengan idiom penamaan itu
terpilih
5
@JoshuaGoldberg JDK tampaknya telah mengadopsi idiom penamaan ini, meskipun tidak secara eksklusif. Pertimbangkan: CharSequence.chars () dan .codePoints (), BufferedReader.lines (), dan Files.lines () ada di Java 8. Di Java 9, berikut ini telah ditambahkan: Process.children (), NetworkInterface.addresses ( ), Scanner.tokens (), Matcher.results (), java.xml.catalog.Catalog.catalogs (). Metode stream-return lainnya telah ditambahkan yang tidak menggunakan idiom ini - Scanner.findAll () datang ke pikiran - tetapi idiom nomina jamak tampaknya mulai digunakan secara adil di JDK.
Stuart Marks
1

Apakah aliran dirancang untuk selalu "dihentikan" di dalam ekspresi yang sama dengan yang mereka buat?

Begitulah cara mereka digunakan dalam kebanyakan contoh.

Catatan: mengembalikan Stream tidak jauh berbeda dengan mengembalikan Iterator (diakui dengan kekuatan yang jauh lebih ekspresif)

IMHO solusi terbaik adalah merangkum mengapa Anda melakukan ini, dan tidak mengembalikan koleksi.

misalnya

public int playerCount();
public Player player(int n);

atau jika Anda bermaksud menghitungnya

public int countPlayersWho(Predicate<? super Player> test);
Peter Lawrey
sumber
2
Masalah dengan jawaban ini adalah itu akan mengharuskan penulis untuk mengantisipasi setiap tindakan klien ingin melakukan itu akan sangat meningkatkan jumlah metode di kelas.
dkatzel
@dkatzel Tergantung pada apakah pengguna akhir adalah penulis atau seseorang yang bekerja dengan mereka. Jika pengguna akhir tidak diketahui, maka Anda memerlukan solusi yang lebih umum. Anda mungkin masih ingin membatasi akses ke koleksi yang mendasarinya.
Peter Lawrey
1

Jika aliran terbatas, dan ada operasi yang diharapkan / normal pada objek yang dikembalikan yang akan melempar pengecualian yang diperiksa, saya selalu mengembalikan Koleksi. Karena jika Anda akan melakukan sesuatu pada masing-masing objek yang dapat melempar pengecualian cek, Anda akan membenci aliran. Satu kekurangan nyata dengan aliran saya ada ketidakmampuan untuk menangani pengecualian diperiksa dengan elegan.

Sekarang, mungkin itu adalah tanda bahwa Anda tidak perlu pengecualian yang diperiksa, yang adil, tetapi kadang-kadang tidak dapat dihindari.

designbygravity
sumber
1

Berbeda dengan koleksi, stream memiliki karakteristik tambahan . Aliran yang dikembalikan dengan metode apa pun mungkin:

  • terbatas atau tak terbatas
  • paralel atau berurutan (dengan threadpool bersama global default yang dapat memengaruhi bagian aplikasi lainnya)
  • dipesan atau tidak dipesan

Perbedaan-perbedaan ini juga ada dalam koleksi, tetapi ada mereka bagian dari kontrak yang jelas:

  • Semua Koleksi memiliki ukuran, Iterator / Iterable dapat tak terbatas.
  • Koleksi secara eksplisit dipesan atau tidak dipesan
  • Untungnya, paralelitas bukanlah sesuatu yang dipedulikan koleksi di luar keamanan benang.

Sebagai konsumen aliran (baik dari pengembalian metode atau sebagai parameter metode) ini adalah situasi yang berbahaya dan membingungkan. Untuk memastikan algoritme mereka berperilaku dengan benar, konsumen aliran perlu memastikan algoritma tidak membuat asumsi yang salah tentang karakteristik aliran. Dan itu adalah hal yang sangat sulit untuk dilakukan. Dalam pengujian unit, itu berarti Anda harus melipatgandakan semua tes Anda untuk diulangi dengan konten aliran yang sama, tetapi dengan aliran yang

  • (terbatas, dipesan, berurutan)
  • (terbatas, teratur, paralel)
  • (terbatas, tidak dipesan, berurutan) ...

Penjaga metode penulisan untuk stream yang melempar IllegalArgumentException jika aliran input memiliki karakteristik melanggar algoritma Anda sulit, karena properti disembunyikan.

Yang meninggalkan Stream hanya sebagai pilihan yang valid dalam metode tanda tangan ketika tidak ada masalah di atas masalah, yang jarang terjadi.

Jauh lebih aman untuk menggunakan tipe data lain dalam tanda tangan metode dengan kontrak eksplisit (dan tanpa melibatkan pemrosesan thread-pool) yang membuatnya tidak mungkin untuk secara tidak sengaja memproses data dengan asumsi yang salah tentang keteraturan, ukuran atau paralelitas (dan penggunaan threadpool).

tkruse
sumber
2
Kekhawatiran Anda tentang aliran tak terbatas tidak berdasar; pertanyaannya adalah "haruskah saya mengembalikan koleksi atau aliran". Jika Collection kemungkinan, hasilnya adalah dengan definisi yang terbatas. Jadi kekhawatiran bahwa penelepon akan mengambil risiko iterasi tanpa batas, mengingat Anda bisa mengembalikan koleksi , tidak berdasar. Saran lainnya dalam jawaban ini hanya buruk. Kedengarannya bagi saya seperti Anda bertemu seseorang yang terlalu sering menggunakan Stream, dan Anda terlalu berputar ke arah lain. Dapat dimengerti, tetapi saran yang buruk.
Brian Goetz
0

Saya pikir itu tergantung pada skenario Anda. Mungkin, jika Anda membuat Teamimplementasi Anda Iterable<Player>, itu sudah cukup.

for (Player player : team) {
    System.out.println(player);
}

atau dengan gaya fungsional:

team.forEach(System.out::println);

Tetapi jika Anda ingin api yang lebih lengkap dan lancar, aliran bisa menjadi solusi yang baik.

gontard
sumber
Perhatikan bahwa, dalam kode yang diposting OP, jumlah pemain hampir tidak berguna, selain sebagai perkiraan ('1034 pemain bermain sekarang, klik di sini untuk memulai!') Ini karena Anda mengembalikan tampilan abadi dari koleksi yang bisa diubah , jadi hitungan yang Anda dapatkan sekarang mungkin tidak sama dengan hitungan tiga mikrodetik dari sekarang. Jadi, ketika mengembalikan Koleksi memberi Anda cara "mudah" untuk menghitung (dan sungguh, stream.count()juga cukup mudah), angka itu tidak benar-benar sangat berarti untuk apa pun selain debugging atau memperkirakan.
Brian Goetz
0

Sementara beberapa responden berprofil tinggi memberikan nasihat umum yang bagus, saya terkejut tidak ada yang menyatakan:

Jika Anda sudah memiliki "terwujud" Collectiondi tangan (yaitu sudah dibuat sebelum panggilan - seperti halnya dalam contoh yang diberikan, di mana itu adalah bidang anggota), tidak ada titik mengubahnya menjadi a Stream. Penelepon dapat dengan mudah melakukannya sendiri. Sedangkan, jika penelepon ingin mengkonsumsi data dalam bentuk aslinya, Anda mengubahnya menjadi Streammemaksa mereka untuk melakukan pekerjaan yang berlebihan untuk mematerialisasi ulang salinan dari struktur asli.

Daniel Avery
sumber
-1

Mungkin pabrik Stream akan menjadi pilihan yang lebih baik. Kemenangan besar dari hanya mengekspos koleksi melalui Stream adalah lebih baik merangkum struktur data model domain Anda. Tidak mungkin untuk menggunakan kelas domain Anda untuk memengaruhi pekerjaan bagian dalam Daftar atau Atur Anda hanya dengan mengekspos Stream.

Ini juga mendorong pengguna kelas domain Anda untuk menulis kode dalam gaya Java 8 yang lebih modern. Dimungkinkan untuk secara bertahap menolak gaya ini dengan mempertahankan getter yang ada dan menambahkan getter yang mengembalikan Stream. Seiring waktu, Anda dapat menulis ulang kode lawas hingga Anda akhirnya menghapus semua getter yang mengembalikan Daftar atau Set. Jenis refactoring ini terasa sangat baik setelah Anda menghapus semua kode lawas!

Vazgen Torosyan
sumber
7
adakah alasan ini dikutip sepenuhnya? apakah ada sumbernya?
Xerus
-5

Saya mungkin akan memiliki 2 metode, satu untuk mengembalikan Collectiondan satu untuk mengembalikan koleksi sebagai Stream.

class Team
{
    private List<Player> players = new ArrayList<>();

// ...

    public List<Player> getPlayers()
    {
        return Collections.unmodifiableList(players);
    }

    public Stream<Player> getPlayerStream()
    {
        return players.stream();
    }

}

Ini adalah yang terbaik dari kedua dunia. Klien dapat memilih jika mereka menginginkan Daftar atau Aliran dan mereka tidak harus melakukan pembuatan objek tambahan untuk membuat salinan daftar yang tidak dapat diubah hanya untuk mendapatkan Aliran.

Ini juga hanya menambahkan 1 metode lagi ke API Anda sehingga Anda tidak memiliki terlalu banyak metode

dkatzel
sumber
1
Karena dia ingin memilih di antara dua opsi ini dan meminta pro dan kontra dari masing-masing opsi. Selain itu memberikan pemahaman yang lebih baik tentang konsep-konsep ini kepada semua orang.
Libert Piou Piou
Tolong jangan lakukan itu. Bayangkan API!
François Gautier