Bagaimana cara memeriksa apakah Java 8 Stream kosong?

100

Bagaimana cara memeriksa apakah a Streamkosong dan mengeluarkan pengecualian jika tidak, sebagai operasi non-terminal?

Pada dasarnya, saya mencari sesuatu yang setara dengan kode di bawah ini, tetapi tanpa mewujudkan aliran di antaranya. Secara khusus, pemeriksaan tidak boleh dilakukan sebelum aliran benar-benar dikonsumsi oleh operasi terminal.

public Stream<Thing> getFilteredThings() {
    Stream<Thing> stream = getThings().stream()
                .filter(Thing::isFoo)
                .filter(Thing::isBar);
    return nonEmptyStream(stream, () -> {
        throw new RuntimeException("No foo bar things available")   
    });
}

private static <T> Stream<T> nonEmptyStream(Stream<T> stream, Supplier<T> defaultValue) {
    List<T> list = stream.collect(Collectors.toList());
    if (list.isEmpty()) list.add(defaultValue.get());
    return list.stream();
}
Cephalopoda
sumber
23
Anda tidak dapat memiliki kue dan memakannya juga - dan secara harfiah begitu, dalam konteks ini. Anda harus menggunakan streaming untuk mengetahui apakah itu kosong. Itulah inti dari semantik Stream (kemalasan).
Marko Topolnik
Ini akan dikonsumsi pada akhirnya, pada saat ini pemeriksaan harus dilakukan
Cephalopod
12
Untuk memastikan bahwa streaming tidak kosong, Anda harus mencoba mengonsumsi setidaknya satu elemen. Pada saat itu, aliran tersebut telah kehilangan "keperawanannya" dan tidak dapat dikonsumsi lagi dari awal.
Marko Topolnik

Jawaban:

24

Jika Anda dapat hidup dengan kapabilitas paralel terbatas, solusi berikut akan berfungsi:

private static <T> Stream<T> nonEmptyStream(
    Stream<T> stream, Supplier<RuntimeException> e) {

    Spliterator<T> it=stream.spliterator();
    return StreamSupport.stream(new Spliterator<T>() {
        boolean seen;
        public boolean tryAdvance(Consumer<? super T> action) {
            boolean r=it.tryAdvance(action);
            if(!seen && !r) throw e.get();
            seen=true;
            return r;
        }
        public Spliterator<T> trySplit() { return null; }
        public long estimateSize() { return it.estimateSize(); }
        public int characteristics() { return it.characteristics(); }
    }, false);
}

Berikut beberapa contoh kode yang menggunakannya:

List<String> l=Arrays.asList("hello", "world");
nonEmptyStream(l.stream(), ()->new RuntimeException("No strings available"))
  .forEach(System.out::println);
nonEmptyStream(l.stream().filter(s->s.startsWith("x")),
               ()->new RuntimeException("No strings available"))
  .forEach(System.out::println);

Masalah dengan eksekusi paralel (efisien) adalah bahwa mendukung pemisahan Spliteratormemerlukan cara yang aman untuk thread untuk mengetahui apakah salah satu fragmen telah melihat nilai dengan cara yang aman untuk thread. Kemudian fragmen terakhir yang dieksekusi tryAdvanceharus menyadari bahwa itu adalah yang terakhir (dan juga tidak bisa maju) untuk mengeluarkan pengecualian yang sesuai. Jadi saya tidak menambahkan dukungan untuk pemisahan di sini.

Holger
sumber
33

Jawaban dan komentar lain benar karena untuk memeriksa konten aliran, seseorang harus menambahkan operasi terminal, dengan demikian "menghabiskan" aliran. Namun, seseorang dapat melakukan ini dan mengubah hasilnya kembali menjadi aliran, tanpa menyangga seluruh konten aliran. Berikut ini beberapa contoh:

static <T> Stream<T> throwIfEmpty(Stream<T> stream) {
    Iterator<T> iterator = stream.iterator();
    if (iterator.hasNext()) {
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false);
    } else {
        throw new NoSuchElementException("empty stream");
    }
}

static <T> Stream<T> defaultIfEmpty(Stream<T> stream, Supplier<T> supplier) {
    Iterator<T> iterator = stream.iterator();
    if (iterator.hasNext()) {
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false);
    } else {
        return Stream.of(supplier.get());
    }
}

Pada dasarnya ubah aliran menjadi an Iteratoruntuk memanggilnya hasNext(), dan jika benar, ubah Iteratorkembali menjadi a Stream. Ini tidak efisien karena semua operasi selanjutnya di aliran akan melalui Iterator hasNext()dannext() metode , yang juga menyiratkan bahwa aliran diproses secara efektif secara berurutan (bahkan jika kemudian berubah paralel). Namun, ini memungkinkan Anda untuk menguji streaming tanpa membuat buffer ke semua elemennya.

Mungkin ada cara untuk melakukan ini menggunakan Spliteratorbukan sebuah Iterator. Hal ini berpotensi memungkinkan aliran yang dikembalikan memiliki karakteristik yang sama dengan aliran masukan, termasuk berjalan secara paralel.

Stuart Marks
sumber
1
Saya tidak berpikir bahwa ada solusi yang dapat dipelihara yang akan mendukung pemrosesan paralel yang efisien karena sulit untuk mendukung pemisahan, namun memiliki estimatedSizedan characteristicsbahkan dapat meningkatkan kinerja single-threaded. Kebetulan saya menulis Spliteratorsolusi saat Anda memposting Iteratorsolusi…
Holger
3
Anda dapat meminta Spliterator pada stream, memanggil tryAdvance (lambda) di mana lambda Anda menangkap apa pun yang diteruskan padanya, lalu mengembalikan Spliterator yang mendelegasikan hampir semuanya ke Spliterator yang mendasarinya, kecuali bahwa ia menempelkan elemen pertama kembali ke potongan pertama ( dan memperbaiki hasil estimationSize).
Brian Goetz
1
@BrianGoetz Ya, itulah yang saya pikirkan, saya hanya belum bersusah payah menangani semua detail itu.
Stuart Marks
3
@ Brian Goetz: Itu yang saya maksud dengan "terlalu rumit". Memanggil tryAdvancesebelum Streammelakukannya akan mengubah sifat malas Streammenjadi aliran "malas sebagian". Ini juga menyiratkan bahwa mencari elemen pertama bukanlah operasi paralel lagi karena Anda harus membelah terlebih dahulu dan melakukan tryAdvancepada bagian yang terbelah secara bersamaan untuk melakukan operasi paralel yang sebenarnya, sejauh yang saya mengerti. Jika satu-satunya operasi terminal adalah findAnyatau serupa itu akan menghancurkan seluruh parallel()permintaan.
Holger
2
Jadi, untuk dukungan paralel penuh, Anda tidak boleh memanggil tryAdvancesebelum streaming dan harus menggabungkan setiap bagian yang terpisah menjadi proxy dan mengumpulkan informasi "hasAny" dari semua operasi bersamaan Anda sendiri dan memastikan bahwa operasi bersamaan terakhir menampilkan pengecualian yang diinginkan jika aliran kosong. Banyak hal…
Holger
25

Ini mungkin cukup dalam banyak kasus

stream.findAny().isPresent()
kenglxn
sumber
15

Anda harus melakukan operasi terminal di Arus agar salah satu filter dapat diterapkan. Oleh karena itu Anda tidak dapat mengetahui apakah itu akan kosong sampai Anda mengkonsumsinya.

Hal terbaik yang dapat Anda lakukan adalah menghentikan Stream dengan file findAny() operasi terminal, yang akan berhenti ketika menemukan elemen apa pun, tetapi jika tidak ada, ia harus mengulang semua daftar input untuk mengetahuinya.

Ini hanya akan membantu Anda jika daftar input memiliki banyak elemen, dan salah satu dari beberapa yang pertama melewati filter, karena hanya sebagian kecil dari daftar yang harus dipakai sebelum Anda mengetahui Stream tidak kosong.

Tentu saja Anda masih harus membuat Stream baru untuk menghasilkan daftar output.

Eran
sumber
7
Ada anyMatch(alwaysTrue()), saya pikir itu yang paling dekat hasAny.
Marko Topolnik
1
@MarkoTopolnik Baru saja memeriksa referensi - yang ada dalam pikiran saya adalah findAny (), meskipun anyMatch () juga akan berfungsi.
Eran
3
anyMatch(alwaysTrue())sangat cocok dengan semantik yang Anda inginkan hasAny, memberi Anda booleanalih - alih Optional<T>--- tetapi kami membagi rambut di sini :)
Marko Topolnik
1
Catatan alwaysTrueadalah predikat Jambu Biji.
Jean-François Savard
11
anyMatch(e -> true)kemudian.
FBB
6

Saya pikir harus cukup untuk memetakan boolean

Dalam kode ini adalah:

boolean isEmpty = anyCollection.stream()
    .filter(p -> someFilter(p)) // Add my filter
    .map(p -> Boolean.TRUE) // For each element after filter, map to a TRUE
    .findAny() // Get any TRUE
    .orElse(Boolean.FALSE); // If there is no match return false
Luis Roberto
sumber
1
Jika hanya ini yang Anda butuhkan, jawaban kenglxn lebih baik.
Dominykas Mostauskis
tidak berguna, itu menduplikasi Collection.isEmpty ()
Krzysiek
@Krzyek itu bukan percuma jika perlu menyaring koleksinya. Namun, saya setuju dengan Dominyka bahwa jawaban
kenglxn
Itu karena itu juga duplikatStream.anyMatch()
Krzysiek
4

Mengikuti ide Stuart, ini bisa dilakukan dengan cara Spliteratorseperti ini:

static <T> Stream<T> defaultIfEmpty(Stream<T> stream, Stream<T> defaultStream) {
    final Spliterator<T> spliterator = stream.spliterator();
    final AtomicReference<T> reference = new AtomicReference<>();
    if (spliterator.tryAdvance(reference::set)) {
        return Stream.concat(Stream.of(reference.get()), StreamSupport.stream(spliterator, stream.isParallel()));
    } else {
        return defaultStream;
    }
}

Saya pikir ini berfungsi dengan Stream paralel karena stream.spliterator()operasi akan menghentikan streaming, dan kemudian membangunnya kembali sesuai kebutuhan

Dalam kasus penggunaan saya, saya membutuhkan default Streamdaripada nilai default. itu cukup mudah untuk diubah jika bukan ini yang Anda butuhkan

phoenix7360
sumber
Saya tidak tahu apakah ini akan memengaruhi performa secara signifikan dengan streaming paralel. Mungkin sebaiknya mengujinya jika ini merupakan persyaratan
phoenix7360
Maaf tidak menyadari bahwa @Holger juga punya solusi dengan Spliteratorsaya bertanya-tanya bagaimana keduanya dibandingkan.
phoenix7360
0

Saya hanya akan menggunakan:

stream.count()>0
daniel sp
sumber