Kapan saya harus menggunakan aliran?

99

Saya baru saja menemukan pertanyaan saat menggunakan a Listdan stream()metodenya. Meskipun saya tahu cara menggunakannya, saya tidak yakin kapan harus menggunakannya.

Misalnya, saya punya daftar, berisi berbagai jalur ke lokasi berbeda. Sekarang, saya ingin memeriksa apakah satu jalur tertentu berisi salah satu jalur yang ditentukan dalam daftar. Saya ingin mengembalikan booleanberdasarkan apakah kondisi terpenuhi atau tidak.

Ini tentu saja, bukanlah tugas yang sulit. Tapi saya bertanya-tanya apakah saya harus menggunakan stream, atau loop for (-each).

Daftar

private static final List<String> EXCLUDE_PATHS = Arrays.asList(new String[]{
    "my/path/one",
    "my/path/two"
});

Contoh - Streaming

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream()
                        .map(String::toLowerCase)
                        .filter(path::contains)
                        .collect(Collectors.toList())
                        .size() > 0;
}

Contoh - Untuk-Setiap Loop

private boolean isExcluded(String path){
    for (String excludePath : EXCLUDE_PATHS) {
        if(path.contains(excludePath.toLowerCase())){
            return true;
        }
    }
    return false;
}

Perhatikan bahwa pathparameternya selalu huruf kecil .

Tebakan pertama saya adalah pendekatan untuk setiap lebih cepat, karena loop akan segera kembali, jika kondisinya terpenuhi. Sedangkan aliran masih akan mengulang semua entri daftar untuk menyelesaikan pemfilteran.

Apakah asumsi saya benar? Jika ya, mengapa (atau lebih tepatnya kapan ) saya akan menggunakannya stream()?

Mcuenez.dll
sumber
11
Streaming lebih ekspresif dan mudah dibaca daripada for-loop tradisional. Nanti Anda perlu berhati-hati tentang intrinsik jika-maka dan kondisi, dll. Ekspresi aliran sangat jelas: ubah nama file menjadi huruf kecil, kemudian filter berdasarkan sesuatu dan kemudian hitung, kumpulkan, dll. Hasilnya: sangat berulang ekspresi aliran perhitungan.
Jean-Baptiste Yunès
12
Tidak perlu new String[]{…}disini. Cukup gunakanArrays.asList("my/path/one", "my/path/two")
Holger
4
Jika sumber Anda adalah String[], tidak perlu menelepon Arrays.asList. Anda bisa melakukan streaming melalui array menggunakan Arrays.stream(array). Ngomong-ngomong, saya kesulitan memahami tujuan isExcludedtes sama sekali. Sungguh menarik apakah elemen dari EXCLUDE_PATHSbenar-benar terkandung di suatu tempat di dalam jalur? Yaitu isExcluded("my/path/one/foo/bar/baz")akan kembali true, serta isExcluded("foo/bar/baz/my/path/one/")...
Holger
3
Hebat, saya tidak mengetahui Arrays.streammetodenya, terima kasih telah menunjukkannya. Memang, contoh yang saya posting sepertinya tidak berguna bagi orang lain selain saya. Saya mengetahui perilaku isExcludedmetode ini, tetapi itu benar-benar hanya sesuatu yang saya butuhkan untuk diri saya sendiri, oleh karena itu, untuk menjawab pertanyaan Anda: ya , ini menarik karena alasan yang tidak ingin saya sebutkan, karena tidak sesuai dengan ruang lingkup dari pertanyaan awal.
mcuenez
1
Mengapa toLowerCasediterapkan pada konstanta yang sudah berbentuk huruf kecil? Bukankah seharusnya itu diterapkan pada pathargumen?
Sebastian Redl

Jawaban:

78

Asumsi Anda benar. Implementasi streaming Anda lebih lambat daripada for-loop.

Penggunaan aliran ini harus secepat loop-for:

EXCLUDE_PATHS.stream()  
                               .map(String::toLowerCase)
                               .anyMatch(path::contains);

Ini mengulangi item, menerapkan String::toLowerCasedan menyaring item satu per satu dan mengakhiri item pertama yang cocok.

Keduanya collect()& anyMatch()adalah operasi terminal. anyMatch()keluar di item pertama yang ditemukan, sementara collect()mengharuskan semua item untuk diproses.

Stefan Pries
sumber
2
Luar biasa, tidak tahu tentang findFirst()kombinasi dengan filter(). Ternyata, saya tidak tahu bagaimana menggunakan aliran seperti yang saya pikirkan.
mcuenez
4
Ada beberapa artikel blog & presentasi yang sangat menarik di web tentang kinerja API streaming, yang menurut saya sangat membantu untuk memahami cara kerja hal-hal ini di balik terpal. Saya pasti dapat merekomendasikan untuk sedikit meneliti, jika Anda tertarik dengan itu.
Stefan Pries
Setelah Anda mengedit, saya merasa jawaban Anda adalah yang harus diterima, karena Anda juga menjawab pertanyaan saya di komentar jawaban lainnya. Padahal, saya ingin memberi @ rvit34 kredit untuk memposting kode :-)
mcuenez
34

Keputusan apakah akan menggunakan Stream atau tidak tidak harus didasarkan pada pertimbangan performa, melainkan oleh keterbacaan. Jika menyangkut kinerja, ada pertimbangan lain.

Dengan .filter(path::contains).collect(Collectors.toList()).size() > 0pendekatan Anda, Anda memproses semua elemen dan mengumpulkannya menjadi sementara List, sebelum membandingkan ukurannya, tetap saja, ini hampir tidak pernah menjadi masalah bagi Stream yang terdiri dari dua elemen.

Penggunaan .map(String::toLowerCase).anyMatch(path::contains)dapat menghemat siklus CPU dan memori, jika Anda memiliki jumlah elemen yang jauh lebih besar. Namun, ini mengubah masing String- masing menjadi representasi huruf kecil, sampai ditemukan kecocokan. Jelas, ada gunanya menggunakan

private static final List<String> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .collect(Collectors.toList());

private boolean isExcluded(String path) {
    return EXCLUDE_PATHS.stream().anyMatch(path::contains);
}

sebagai gantinya. Jadi, Anda tidak perlu mengulangi konversi ke huruf kecil di setiap permintaan isExcluded. Jika jumlah elemen dalam EXCLUDE_PATHSatau panjang senar menjadi sangat besar, Anda dapat mempertimbangkan untuk menggunakan

private static final List<Predicate<String>> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .map(s -> Pattern.compile(s, Pattern.LITERAL).asPredicate())
          .collect(Collectors.toList());

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream().anyMatch(p -> p.test(path));
}

Mengompilasi string sebagai pola regex dengan LITERALbendera, membuatnya berperilaku seperti operasi string biasa, tetapi memungkinkan mesin menghabiskan beberapa waktu dalam persiapan, misalnya menggunakan algoritme Boyer Moore, agar lebih efisien dalam hal perbandingan aktual.

Tentu saja, ini hanya terbayar jika ada cukup tes berikutnya untuk mengkompensasi waktu yang dihabiskan dalam persiapan. Menentukan apakah ini akan menjadi kasusnya, adalah salah satu pertimbangan kinerja yang sebenarnya, selain pertanyaan pertama apakah operasi ini akan pernah menjadi kinerja yang kritis sama sekali. Bukan pertanyaan apakah akan menggunakan Stream atau forloop.

Ngomong-ngomong, contoh kode di atas tetap menggunakan logika kode asli Anda, yang terlihat meragukan bagi saya. isExcludedMetode Anda mengembalikan true, jika jalur yang ditentukan berisi salah satu elemen dalam daftar, sehingga mengembalikan trueuntuk /some/prefix/to/my/path/one, serta my/path/one/and/some/suffixatau bahkan /some/prefix/to/my/path/one/and/some/suffix.

Even dummy/path/onerousdianggap memenuhi kriteria sebagai containsstring my/path/one

Holger
sumber
Wawasan bagus tentang kemungkinan pengoptimalan kinerja, terima kasih. Mengenai bagian terakhir dari jawaban Anda: jika balasan saya atas komentar Anda tidak memuaskan, pertimbangkan kode contoh saya sebagai bantuan belaka bagi orang lain untuk memahami apa yang saya tanyakan - daripada kode sebenarnya. Selain itu, Anda selalu dapat mengedit pertanyaan, jika Anda memiliki contoh yang lebih baik.
mcuenez
3
Saya menerima komentar Anda bahwa operasi ini adalah yang Anda inginkan, jadi tidak perlu mengubahnya. Saya hanya akan menyimpan bagian terakhir untuk pembaca mendatang, sehingga mereka sadar bahwa ini bukan operasi biasa, tetapi juga, bahwa ini telah dibahas dan tidak memerlukan komentar lebih lanjut…
Holger
Sebenarnya aliran sangat cocok digunakan untuk pengoptimalan memori ketika jumlah memori yang bekerja melanggar batas server
ColacX
21

Ya. Kamu benar. Pendekatan aliran Anda akan memiliki beberapa overhead. Tetapi Anda dapat menggunakan konstruksi seperti itu:

private boolean isExcluded(String path) {
    return  EXCLUDE_PATHS.stream().map(String::toLowerCase).anyMatch(path::contains);
}

Alasan utama menggunakan aliran adalah karena aliran membuat kode Anda lebih sederhana dan mudah dibaca.

rvit34
sumber
3
Apakah anyMatchjalan pintas untuk filter(...).findFirst().isPresent()?
mcuenez
6
Ya itu! Itu bahkan lebih baik dari saran pertama saya.
Stefan Pries
8

Tujuan stream di Java adalah untuk menyederhanakan kerumitan penulisan kode paralel. Ini terinspirasi oleh pemrograman fungsional. Aliran serial hanya untuk membuat kode lebih bersih.

Jika kita menginginkan kinerja kita harus menggunakan parallelStream, yang telah dirancang untuk. Yang serial, secara umum, lebih lambat.

Ada artikel bagus untuk dibaca tentang , dan Kinerja . ForLoopStreamParallelStream

Dalam kode Anda, kami dapat menggunakan metode terminasi untuk menghentikan pencarian pada pertandingan pertama. (anyMatch ...)

Paulo Ricardo Almeida
sumber
5
Perhatikan bahwa untuk streaming kecil dan dalam beberapa kasus lain, streaming paralel bisa lebih lambat karena biaya mulai. Dan jika Anda memiliki operasi terminal yang dipesan, daripada yang tidak dapat diparalelkan tanpa urutan, sinkronisasi ulang di bagian akhir.
CAD97
0

Seperti orang lain telah menyebutkan banyak poin bagus, tetapi saya hanya ingin menyebutkan evaluasi malas dalam evaluasi aliran. Saat kami map()membuat aliran jalur huruf kecil, kami tidak langsung membuat seluruh aliran, melainkan aliran dibuat secara malas , itulah sebabnya kinerja harus setara dengan perulangan for tradisional. Itu tidak melakukan pemindaian penuh, map()dan anyMatch()dijalankan pada saat yang bersamaan. Setelah anyMatch()mengembalikan nilai true, itu akan dihubung pendek.

Kaicheng Hu
sumber