Menggunakan Java 8's Opsional dengan Stream :: flatMap

240

Kerangka kerja Java 8 stream baru dan teman-teman membuat beberapa kode java yang sangat ringkas, tapi saya telah menemukan situasi yang tampaknya sederhana yang sulit dilakukan secara ringkas.

Pertimbangkan a List<Thing> thingsdan metode Optional<Other> resolve(Thing thing). Saya ingin memetakan Things ke Optional<Other>s dan mendapatkan yang pertama Other. Solusi yang jelas akan digunakan things.stream().flatMap(this::resolve).findFirst(), tetapi flatMapmengharuskan Anda mengembalikan aliran, dan Optionaltidak memiliki stream()metode (atau itu Collectionatau menyediakan metode untuk mengubahnya atau melihatnya sebagai Collection).

Yang terbaik yang bisa saya pikirkan adalah ini:

things.stream()
    .map(this::resolve)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .findFirst();

Tapi itu sepertinya bertele-tele untuk kasus yang sangat umum. Adakah yang punya ide yang lebih baik?

Yona Appletree
sumber
Setelah sedikit mengkodekan dengan contoh Anda, saya benar-benar menemukan versi eksplisit lebih mudah dibaca daripada yang menyangkut, jika sudah ada .flatMap(Optional::toStream), dengan versi Anda, Anda benar-benar melihat apa yang terjadi.
skiwi
19
@skiwi Nah, Optional.streamada di JDK 9 sekarang ....
Stuart Marks
Saya ingin tahu di mana ini didokumentasikan, dan apa proses untuk mendapatkannya. Ada beberapa metode lain yang benar-benar tampak seperti seharusnya ada, dan saya ingin tahu di mana diskusi untuk perubahan API sedang berlangsung.
Yona Appletree
3
bugs.openjdk.java.net/browse/JDK-8050820
Christoffer Hammarström
10
Yang lucu adalah bahwa JDK-8050820 sebenarnya merujuk pada pertanyaan ini dalam deskripsinya!
Didier L

Jawaban:

265

Jawa 9

Optional.stream telah ditambahkan ke JDK 9. Ini memungkinkan Anda untuk melakukan hal berikut, tanpa perlu metode pembantu:

Optional<Other> result =
    things.stream()
          .map(this::resolve)
          .flatMap(Optional::stream)
          .findFirst();

Java 8

Ya, ini adalah lubang kecil di API, karena itu agak tidak nyaman untuk mengubah Optional<T>panjang nol atau satu Stream<T>. Anda bisa melakukan ini:

Optional<Other> result =
    things.stream()
          .map(this::resolve)
          .flatMap(o -> o.isPresent() ? Stream.of(o.get()) : Stream.empty())
          .findFirst();

Memiliki operator ternary di dalamnya flatMapagak rumit, jadi mungkin lebih baik untuk menulis fungsi pembantu kecil untuk melakukan ini:

/**
 * Turns an Optional<T> into a Stream<T> of length zero or one depending upon
 * whether a value is present.
 */
static <T> Stream<T> streamopt(Optional<T> opt) {
    if (opt.isPresent())
        return Stream.of(opt.get());
    else
        return Stream.empty();
}

Optional<Other> result =
    things.stream()
          .flatMap(t -> streamopt(resolve(t)))
          .findFirst();

Di sini, saya sudah sebaris panggilan untuk resolve()bukannya memiliki map()operasi yang terpisah , tetapi ini adalah masalah selera.

Stuart Marks
sumber
2
Saya tidak berpikir api dapat berubah hingga Java 9 sekarang.
assylias
5
@Hypher, Terima kasih. Teknik .filter (). Map () tidak terlalu buruk dan menghindari ketergantungan pada metode pembantu. 'Akan lebih baik jika ada cara yang lebih ringkas. Saya akan menyelidiki untuk menambahkan Optional.stream ().
Stuart Marks
43
Saya lebih suka:static <T> Stream<T> streamopt(Optional<T> opt) { return opt.map(Stream::of).orElse(Stream.empty()); }
kubek2k
5
Saya berharap mereka hanya akan menambahkan Optionalkelebihan untuk Stream#flatMap... dengan cara itu Anda bisa menulisstream().flatMap(this::resolve)
serpih
4
@ Flkes Ya kami telah menendang sekitar ide ini, tetapi tampaknya tidak menambah semua nilai sekarang (di JDK 9) ada Optional.stream().
Stuart Marks
69

Saya menambahkan jawaban kedua ini berdasarkan sunting yang diajukan oleh pengguna srborlongan ke jawaban saya yang lain . Saya pikir teknik yang diusulkan menarik, tetapi itu tidak benar-benar cocok sebagai pengeditan untuk jawaban saya. Yang lain setuju dan pengeditan yang diusulkan dibatalkan. (Aku bukan salah satu pemilih.) Namun, tekniknya pantas. Akan lebih baik jika srborlongan telah memposting jawabannya sendiri. Ini belum terjadi, dan saya tidak ingin teknik itu hilang dalam kabut StackOverflow menolak sejarah edit, jadi saya memutuskan untuk menampilkannya sebagai jawaban terpisah.

Pada dasarnya tekniknya adalah menggunakan beberapa Optionalmetode dengan cara yang cerdas untuk menghindari keharusan menggunakan operator ternary ( ? :) atau pernyataan if / else.

Contoh inline saya akan ditulis ulang dengan cara ini:

Optional<Other> result =
    things.stream()
          .map(this::resolve)
          .flatMap(o -> o.map(Stream::of).orElseGet(Stream::empty))
          .findFirst();

Contoh saya yang menggunakan metode pembantu akan ditulis ulang dengan cara ini:

/**
 * Turns an Optional<T> into a Stream<T> of length zero or one depending upon
 * whether a value is present.
 */
static <T> Stream<T> streamopt(Optional<T> opt) {
    return opt.map(Stream::of)
              .orElseGet(Stream::empty);
}

Optional<Other> result =
    things.stream()
          .flatMap(t -> streamopt(resolve(t)))
          .findFirst();

KOMENTAR

Mari kita bandingkan versi asli vs yang dimodifikasi secara langsung:

// original
.flatMap(o -> o.isPresent() ? Stream.of(o.get()) : Stream.empty())

// modified
.flatMap(o -> o.map(Stream::of).orElseGet(Stream::empty))

Yang asli adalah pendekatan langsung jika cekatan: kita mendapatkan Optional<Other>; jika memiliki nilai, kami mengembalikan aliran yang berisi nilai itu, dan jika tidak memiliki nilai, kami mengembalikan aliran kosong. Cukup sederhana dan mudah dijelaskan.

Modifikasi cerdas dan memiliki keuntungan bahwa ia menghindari persyaratan. (Saya tahu bahwa beberapa orang tidak menyukai operator ternary. Jika disalahgunakan memang dapat membuat kode sulit untuk dipahami.) Namun, kadang-kadang hal-hal bisa terlalu pintar. Kode yang dimodifikasi juga dimulai dengan Optional<Other>. Kemudian ia memanggil Optional.mapyang didefinisikan sebagai berikut:

Jika suatu nilai hadir, terapkan fungsi pemetaan yang disediakan padanya, dan jika hasilnya bukan nol, kembalikan Opsional yang menggambarkan hasilnya. Kalau tidak, kembalikan Opsional kosong.

The map(Stream::of)panggilan mengembalikan sebuah Optional<Stream<Other>>. Jika ada nilai pada input Opsional, Opsional yang dikembalikan berisi Stream yang berisi hasil Other lainnya. Tetapi jika nilainya tidak ada, hasilnya adalah opsional kosong.

Selanjutnya, panggilan untuk orElseGet(Stream::empty)mengembalikan nilai tipe Stream<Other>. Jika nilai inputnya ada, itu mendapatkan nilai, yang merupakan elemen tunggal Stream<Other>. Jika tidak (jika nilai input tidak ada) mengembalikan nilai yang kosong Stream<Other>. Jadi hasilnya benar, sama dengan kode kondisional asli.

Dalam komentar yang membahas jawaban saya, mengenai hasil edit yang ditolak, saya telah menggambarkan teknik ini sebagai "lebih ringkas tetapi juga lebih tidak jelas". Saya mendukung ini. Butuh beberapa saat untuk mencari tahu apa yang dilakukannya, dan juga butuh waktu untuk menulis deskripsi di atas tentang apa yang dilakukannya. Kehalusan kuncinya adalah transformasi dari Optional<Other>ke Optional<Stream<Other>>. Setelah Anda grok ini masuk akal, tetapi itu tidak jelas bagi saya.

Namun, saya akan mengakui bahwa hal-hal yang awalnya tidak jelas dapat menjadi idiomatis dari waktu ke waktu. Mungkin saja teknik ini akhirnya menjadi cara terbaik dalam praktik, setidaknya sampai Optional.streamditambahkan (jika pernah ada).

PEMBARUAN: Optional.stream telah ditambahkan ke JDK 9.

Stuart Marks
sumber
16

Anda tidak dapat melakukannya lebih ringkas seperti yang sudah Anda lakukan.

Anda mengklaim bahwa Anda tidak mau .filter(Optional::isPresent) dan .map(Optional::get) .

Ini telah diatasi dengan metode yang dijelaskan oleh @StuartMarks, namun sebagai hasilnya Anda sekarang memetakannya menjadi Optional<T>, jadi sekarang Anda harus menggunakan .flatMap(this::streamopt)dan get()pada akhirnya.

Jadi masih terdiri dari dua pernyataan dan sekarang Anda bisa mendapatkan pengecualian dengan metode baru! Karena, bagaimana jika setiap opsi kosong? Maka findFirst()akan mengembalikan opsional kosong dan Anda get()akan gagal!

Jadi apa yang Anda miliki:

things.stream()
    .map(this::resolve)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .findFirst();

sebenarnya adalah cara terbaik untuk mencapai apa yang Anda inginkan, dan itu adalah Anda ingin menyimpan hasilnya sebagai T, bukan sebagai Optional<T>.

Saya mengambil kebebasan menciptakan CustomOptional<T>kelas yang membungkus Optional<T>dan menyediakan metode tambahan flatStream(),. Perhatikan bahwa Anda tidak dapat memperluas Optional<T>:

class CustomOptional<T> {
    private final Optional<T> optional;

    private CustomOptional() {
        this.optional = Optional.empty();
    }

    private CustomOptional(final T value) {
        this.optional = Optional.of(value);
    }

    private CustomOptional(final Optional<T> optional) {
        this.optional = optional;
    }

    public Optional<T> getOptional() {
        return optional;
    }

    public static <T> CustomOptional<T> empty() {
        return new CustomOptional<>();
    }

    public static <T> CustomOptional<T> of(final T value) {
        return new CustomOptional<>(value);
    }

    public static <T> CustomOptional<T> ofNullable(final T value) {
        return (value == null) ? empty() : of(value);
    }

    public T get() {
        return optional.get();
    }

    public boolean isPresent() {
        return optional.isPresent();
    }

    public void ifPresent(final Consumer<? super T> consumer) {
        optional.ifPresent(consumer);
    }

    public CustomOptional<T> filter(final Predicate<? super T> predicate) {
        return new CustomOptional<>(optional.filter(predicate));
    }

    public <U> CustomOptional<U> map(final Function<? super T, ? extends U> mapper) {
        return new CustomOptional<>(optional.map(mapper));
    }

    public <U> CustomOptional<U> flatMap(final Function<? super T, ? extends CustomOptional<U>> mapper) {
        return new CustomOptional<>(optional.flatMap(mapper.andThen(cu -> cu.getOptional())));
    }

    public T orElse(final T other) {
        return optional.orElse(other);
    }

    public T orElseGet(final Supplier<? extends T> other) {
        return optional.orElseGet(other);
    }

    public <X extends Throwable> T orElseThrow(final Supplier<? extends X> exceptionSuppier) throws X {
        return optional.orElseThrow(exceptionSuppier);
    }

    public Stream<T> flatStream() {
        if (!optional.isPresent()) {
            return Stream.empty();
        }
        return Stream.of(get());
    }

    public T getTOrNull() {
        if (!optional.isPresent()) {
            return null;
        }
        return get();
    }

    @Override
    public boolean equals(final Object obj) {
        return optional.equals(obj);
    }

    @Override
    public int hashCode() {
        return optional.hashCode();
    }

    @Override
    public String toString() {
        return optional.toString();
    }
}

Anda akan melihat bahwa saya menambahkan flatStream(), seperti di sini:

public Stream<T> flatStream() {
    if (!optional.isPresent()) {
        return Stream.empty();
    }
    return Stream.of(get());
}

Digunakan sebagai:

String result = Stream.of("a", "b", "c", "de", "fg", "hij")
        .map(this::resolve)
        .flatMap(CustomOptional::flatStream)
        .findFirst()
        .get();

Anda masih perlu mengembalikan sebuah di Stream<T>sini, karena Anda tidak dapat kembali T, karena jika !optional.isPresent(), maka T == nulljika Anda menyatakannya seperti itu, tetapi kemudian Anda .flatMap(CustomOptional::flatStream)akan berusaha untuk menambahkan nullke aliran dan itu tidak mungkin.

Sebagai contoh:

public T getTOrNull() {
    if (!optional.isPresent()) {
        return null;
    }
    return get();
}

Digunakan sebagai:

String result = Stream.of("a", "b", "c", "de", "fg", "hij")
        .map(this::resolve)
        .map(CustomOptional::getTOrNull)
        .findFirst()
        .get();

Sekarang akan membuang NullPointerExceptiondi dalam operasi aliran.

Kesimpulan

Metode yang Anda gunakan, sebenarnya adalah metode terbaik.

skiwi
sumber
6

Versi yang sedikit lebih pendek menggunakan reduce:

things.stream()
  .map(this::resolve)
  .reduce(Optional.empty(), (a, b) -> a.isPresent() ? a : b );

Anda juga bisa memindahkan fungsi pengurangan ke metode utilitas statis dan kemudian menjadi:

  .reduce(Optional.empty(), Util::firstPresent );
Andrejs
sumber
6
Saya suka ini, tetapi perlu ditunjukkan bahwa ini akan mengevaluasi setiap item dalam Stream, sedangkan findFirst () akan mengevaluasi hanya sampai menemukan item yang ada.
Duncan McGregor
1
Dan sayangnya, menjalankan setiap resolusi adalah pemecah kesepakatan. Tapi itu pintar.
Yona Appletree
5

Karena jawaban saya sebelumnya tampaknya tidak terlalu populer, saya akan mencoba ini lagi.

Jawaban singkat:

Anda sebagian besar berada di jalur yang benar. Kode terpendek untuk mendapatkan hasil yang diinginkan yang dapat saya buat adalah ini:

things.stream()
      .map(this::resolve)
      .filter(Optional::isPresent)
      .findFirst()
      .flatMap( Function.identity() );

Ini akan sesuai dengan semua kebutuhan Anda:

  1. Ini akan menemukan respons pertama yang menyelesaikan nonempty Optional<Result>
  2. Itu panggilan this::resolvemalas sesuai kebutuhan
  3. this::resolve tidak akan dipanggil setelah hasil non-kosong pertama
  4. Itu akan kembali Optional<Result>

Jawaban yang lebih panjang

Satu-satunya modifikasi dibandingkan dengan versi awal OP adalah bahwa saya menghapus .map(Optional::get)sebelum panggilan .findFirst()dan ditambahkan .flatMap(o -> o)sebagai panggilan terakhir dalam rantai.

Ini memiliki efek yang bagus untuk menyingkirkan double-Opsional, setiap kali streaming menemukan hasil yang sebenarnya.

Anda tidak bisa benar-benar lebih pendek dari ini di Jawa.

Potongan kode alternatif menggunakan forteknik loop yang lebih konvensional adalah sekitar jumlah baris kode yang sama dan memiliki urutan dan jumlah operasi yang kurang lebih sama dan perlu Anda lakukan:

  1. memanggil this.resolve,
  2. penyaringan berdasarkan Optional.isPresent
  3. mengembalikan hasilnya dan
  4. beberapa cara berurusan dengan hasil negatif (ketika tidak ada yang ditemukan)

Hanya untuk membuktikan bahwa solusi saya berfungsi seperti yang diiklankan, saya menulis sebuah program uji kecil:

public class StackOverflow {

    public static void main( String... args ) {
        try {
            final int integer = Stream.of( args )
                    .peek( s -> System.out.println( "Looking at " + s ) )
                    .map( StackOverflow::resolve )
                    .filter( Optional::isPresent )
                    .findFirst()
                    .flatMap( o -> o )
                    .orElseThrow( NoSuchElementException::new )
                    .intValue();

            System.out.println( "First integer found is " + integer );
        }
        catch ( NoSuchElementException e ) {
            System.out.println( "No integers provided!" );
        }
    }

    private static Optional<Integer> resolve( String string ) {
        try {
            return Optional.of( Integer.valueOf( string ) );
        }
        catch ( NumberFormatException e )
        {
            System.out.println( '"' + string + '"' + " is not an integer");
            return Optional.empty();
        }
    }

}

(Ini memang memiliki beberapa baris tambahan untuk debugging dan memverifikasi bahwa hanya banyak panggilan untuk diselesaikan sesuai kebutuhan ...)

Menjalankan ini pada baris perintah, saya mendapat hasil berikut:

$ java StackOferflow a b 3 c 4
Looking at a
"a" is not an integer
Looking at b
"b" is not an integer
Looking at 3
First integer found is 3
Roland Tepp
sumber
Saya pikir sama dengan Roland Tepp. Mengapa seseorang membuat aliran <stream <? >> dan datar ketika Anda hanya bisa rata dengan satu opsional <opsional <? >>
Young Hyun Yoo
3

Jika Anda tidak keberatan menggunakan perpustakaan pihak ketiga, Anda dapat menggunakan Javaslang . Ini seperti Scala, tetapi diterapkan di Jawa.

Muncul dengan perpustakaan koleksi lengkap abadi yang sangat mirip dengan yang dikenal dari Scala. Koleksi ini menggantikan koleksi Java dan Java 8's Stream. Ini juga memiliki implementasi Opsi sendiri.

import javaslang.collection.Stream;
import javaslang.control.Option;

Stream<Option<String>> options = Stream.of(Option.some("foo"), Option.none(), Option.some("bar"));

// = Stream("foo", "bar")
Stream<String> strings = options.flatMap(o -> o);

Berikut adalah solusi untuk contoh pertanyaan awal:

import javaslang.collection.Stream;
import javaslang.control.Option;

public class Test {

    void run() {

        // = Stream(Thing(1), Thing(2), Thing(3))
        Stream<Thing> things = Stream.of(new Thing(1), new Thing(2), new Thing(3));

        // = Some(Other(2))
        Option<Other> others = things.flatMap(this::resolve).headOption();
    }

    Option<Other> resolve(Thing thing) {
        Other other = (thing.i % 2 == 0) ? new Other(i + "") : null;
        return Option.of(other);
    }

}

class Thing {
    final int i;
    Thing(int i) { this.i = i; }
    public String toString() { return "Thing(" + i + ")"; }
}

class Other {
    final String s;
    Other(String s) { this.s = s; }
    public String toString() { return "Other(" + s + ")"; }
}

Penafian: Saya pencipta Javaslang.

Daniel Dietrich
sumber
3

Terlambat ke pesta, tapi bagaimana

things.stream()
    .map(this::resolve)
    .filter(Optional::isPresent)
    .findFirst().get();

Anda dapat menyingkirkan get terakhir () jika Anda membuat metode util untuk mengonversi opsional untuk streaming secara manual:

things.stream()
    .map(this::resolve)
    .flatMap(Util::optionalToStream)
    .findFirst();

Jika Anda langsung kembali aliran dari fungsi tekad Anda, Anda menyimpan satu baris lagi.

Ljubopytnov
sumber
3

Saya ingin mempromosikan metode pabrik untuk membuat helper untuk API fungsional:

Optional<R> result = things.stream()
        .flatMap(streamopt(this::resolve))
        .findFirst();

Metode pabrik:

<T, R> Function<T, Stream<R>> streamopt(Function<T, Optional<R>> f) {
    return f.andThen(Optional::stream); // or the J8 alternative:
    // return t -> f.apply(t).map(Stream::of).orElseGet(Stream::empty);
}

Pemikiran:

  • Seperti dengan referensi metode secara umum, dibandingkan dengan ekspresi lambda, Anda tidak dapat secara tidak sengaja menangkap variabel dari ruang lingkup yang dapat diakses, seperti:

    t -> streamopt(resolve(o))

  • Ini komposable, misalnya Anda dapat memanggil Function::andThenhasil metode pabrik:

    streamopt(this::resolve).andThen(...)

    Sedangkan dalam kasus lambda, Anda harus melemparkannya terlebih dahulu:

    ((Function<T, Stream<R>>) t -> streamopt(resolve(t))).andThen(...)

charlie
sumber
3

Null didukung oleh Stream yang disediakan My library AbacusUtil . Ini kode:

Stream.of(things).map(e -> resolve(e).orNull()).skipNull().first();
user_3380739
sumber
3

Jika Anda terjebak dengan Java 8 tetapi memiliki akses ke Guava 21.0 atau lebih baru, Anda dapat menggunakan Streams.streamuntuk mengonversi opsi menjadi aliran.

Demikian diberikan

import com.google.common.collect.Streams;

kamu bisa menulis

Optional<Other> result =
    things.stream()
        .map(this::resolve)
        .flatMap(Streams::stream)
        .findFirst();
Nicolas Payette
sumber
0

Bagaimana dengan itu?

private static List<String> extractString(List<Optional<String>> list) {
    List<String> result = new ArrayList<>();
    list.forEach(element -> element.ifPresent(result::add));
    return result;
}

https://stackoverflow.com/a/58281000/3477539

rastaman
sumber
Mengapa melakukan ini ketika Anda dapat melakukan streaming dan mengumpulkan?
OneCricketeer
return list.stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList())), sama seperti pertanyaan (dan jawaban Anda yang ditautkan) memiliki ...
OneCricketeer
Saya mungkin salah, tetapi saya menganggap menggunakan isPresent () dan kemudian get () bukan praktik yang baik. Jadi saya mencoba untuk menjauh dari itu.
rastaman
Jika Anda menggunakan .get() tanpa isPresent() , maka Anda mendapatkan peringatan di IntelliJ
OneCricketeer
-5

Kemungkinan besar Anda salah melakukannya.

Java 8 Opsional tidak dimaksudkan untuk digunakan dengan cara ini. Biasanya hanya dicadangkan untuk operasi aliran terminal yang mungkin atau mungkin tidak mengembalikan nilai, seperti menemukan misalnya.

Dalam kasus Anda, mungkin lebih baik untuk pertama-tama mencoba menemukan cara yang murah untuk memfilter item-item yang dapat diselesaikan dan kemudian mendapatkan item pertama sebagai opsional dan menyelesaikannya sebagai operasi terakhir. Lebih baik lagi - daripada memfilter, cari item yang dapat diatasi pertama dan atasi.

things.filter(Thing::isResolvable)
      .findFirst()
      .flatMap(this::resolve)
      .get();

Aturan praktisnya adalah Anda harus berusaha mengurangi jumlah item dalam aliran sebelum Anda mengubahnya menjadi sesuatu yang lain. YMMV tentu saja.

Roland Tepp
sumber
6
Saya pikir metode tekad OP () mengembalikan Opsional <Other> adalah penggunaan Opsional yang masuk akal. Saya tidak dapat berbicara dengan domain masalah OP, tentu saja, tetapi bisa jadi cara untuk menentukan apakah sesuatu dapat diselesaikan adalah dengan mencoba untuk menyelesaikannya. Jika demikian, Opsional menggabungkan hasil boolean "apakah ini dapat diselesaikan" dengan hasil resolusi, jika berhasil, menjadi satu panggilan API.
Stuart Marks
2
Stuart pada dasarnya benar. Saya memiliki serangkaian istilah pencarian sesuai keinginan, dan saya mencari untuk menemukan hasil yang pertama yang mengembalikan apa pun. Jadi pada dasarnya Optional<Result> searchFor(Term t). Itu tampaknya sesuai dengan niat Opsional. Selain itu, stream () harus dievaluasi dengan malas, jadi tidak ada istilah penyelesaian pekerjaan tambahan yang melewati yang cocok harus ada.
Yona Appletree
Pertanyaannya sangat masuk akal dan menggunakan flatMap dengan Opsional sering dipraktikkan dalam bahasa pemrograman lain yang serupa, seperti Scala.
dzs