Stream :: flatMap ile İsteğe Bağlı Java 8'leri Kullanma


240

Yeni Java 8 akış çerçevesi ve arkadaşları bazı çok özlü java kodu için yapmak, ama kısaca yapmak zor görünen basit bir durumla karşılaştım.

A List<Thing> thingsve yöntemini düşünün Optional<Other> resolve(Thing thing). Ben Things ile eşlemek Optional<Other>ve ilk almak istiyorum Other. Açık çözüm kullanmak olacaktır things.stream().flatMap(this::resolve).findFirst(), ancak flatMapbir akış döndürmenizi gerektirir ve Optionalbir stream()yönteme sahip değildir (veya bir Collectionveya dönüştürmek veya a olarak görüntülemek için bir yöntem mi yoksa Collection).

Ben gelip en iyi şudur:

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

Ama bu çok yaygın bir durum gibi görünen şey için çok uzun soluklu görünüyor. Daha iyi bir fikri olan var mı?


Örneğinizle bir miktar kodlama yaptıktan sonra, açık versiyonu ilgili versiyondan daha okunabilir buluyorum, eğer varsa .flatMap(Optional::toStream), versiyonunuzla gerçekten neler olduğunu görüyorsunuz.
skiwi

19
@skiwi Eh, Optional.streamşimdi JDK 9'da var ....
Stuart Marks

Bunun nerede belgelendiğini ve içeri girme sürecinin ne olduğunu merak ediyorum. Gerçekten olması gerektiği gibi görünen başka yöntemler de var ve API değişiklikleri tartışmasının nerede gerçekleştiğini merak ediyorum.
Yona Appletree


10
Komik olan şey, JDK-8050820'nin aslında bu soruya açıklamasında atıfta bulunmasıdır!
Didier L

Yanıtlar:


265

Java 9

Optional.stream JDK 9'a eklenmiştir. Bu, herhangi bir yardımcı yönteme gerek kalmadan aşağıdakileri yapmanızı sağlar:

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

Java 8

Evet, bu API'da küçük bir delikti, çünkü Optional<T>bir sıfır veya bir uzunluğa dönüştürmek biraz rahatsız edici Stream<T>. Bunu yapabilirsin:

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

İçindeki üçlü operatöre sahip olmak flatMapbiraz hantal, bu yüzden bunu yapmak için küçük bir yardımcı işlev yazmak daha iyi olabilir:

/**
 * 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();

Burada, resolve()ayrı bir map()operasyon yerine çağrı çağrısında bulundum , ama bu bir zevk meselesi.


2
Api'nin Java 9'a kadar değişebileceğini sanmıyorum.
2014

5
@ Teşekkürler Teşekkürler. .Filter (). Map () tekniği çok kötü değil ve yardımcı yöntemlere bağımlılıkları önlüyor. `` Ama daha özlü bir yol olsaydı iyi olurdu. Optional.stream () eklenmesini araştıracağım.
Stuart Marks

43
Tercih ederim:static <T> Stream<T> streamopt(Optional<T> opt) { return opt.map(Stream::of).orElse(Stream.empty()); }
kubek2k

5
Keşke sadece bir Optionalaşırı yük eklemek isterdim Stream#flatMap... bu şekilde yazabilirsinizstream().flatMap(this::resolve)
pullar

4
@flkes Evet, bu fikri tekmeledik, ama şimdi (JDK 9'da) olduğu kadar çok değer katmıyor gibi görünüyor Optional.stream().
Stuart Marks

69

Ben kullanıcı tarafından önerilmiş düzenleme dayalı bu ikinci cevabı ekliyorum srborlongan için benim diğer cevap . Önerilen tekniğin ilginç olduğunu düşünüyorum, ancak cevabımda bir düzenleme olarak gerçekten uygun değildi. Diğerleri de kabul etti ve önerilen düzenleme oylandı. (Ben seçmenlerden biri değildim.) Yine de tekniğin değeri var. Srborlongan'ın kendi cevabını göndermesi iyi olurdu. Bu henüz gerçekleşmedi ve StackOverflow'un düzenleme geçmişinin sislerinde kaybolmasını istemedim, bu yüzden bunu ayrı bir cevap olarak ortaya çıkarmaya karar verdim.

Temel olarak teknik, Optionalüçlü bir işleç ( ? :) veya bir if / else deyimini kullanmaktan kaçınmak için yöntemlerin bazılarını akıllıca kullanmaktır .

Satır içi örneğim şu şekilde yeniden yazılabilir:

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

Bir yardımcı yöntem kullanan bir örneğim şu şekilde yeniden yazılabilir:

/**
 * 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();

YORUM

Orijinal ve değiştirilmiş sürümleri doğrudan karşılaştıralım:

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

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

Orijinal, eğer işçi benzeri bir yaklaşımsa basittir: bir Optional<Other>; bir değeri varsa, bu değeri içeren bir akış döndürürüz ve değeri yoksa boş bir akış döndürürüz. Oldukça basit ve açıklanması kolay.

Modifikasyon zekidir ve şartlardan kaçınma avantajına sahiptir. (Bazı kişilerin üçlü operatörden hoşlanmadığını biliyorum. Yanlış kullanılırsa kodun anlaşılmasını zorlaştırabilir.) Ancak, bazen işler çok akıllı olabilir. Değiştirilen kod ayrıca bir ile başlar Optional<Other>. Sonra Optional.mapaşağıdaki gibi tanımlanan çağırır :

Bir değer varsa, sağlanan eşleme işlevini ona uygulayın ve sonuç boş değilse, sonucu açıklayan bir İsteğe Bağlı döndürün. Aksi takdirde boş bir İsteğe bağlı döndürün.

map(Stream::of)Çağrısı döndürür Optional<Stream<Other>>. İsteğe bağlı girişinde bir değer varsa, döndürülen İsteğe Bağlı tek bir Diğer sonucu içeren bir Akış içerir. Ancak değer yoksa, sonuç boş bir İsteğe bağlı olur.

Ardından, çağrı orElseGet(Stream::empty)türünde bir değer döndürür Stream<Other>. Girdi değeri varsa, tek eleman olan değeri alır Stream<Other>. Aksi takdirde (giriş değeri yoksa) boş döner Stream<Other>. Böylece sonuç doğrudur, orijinal koşullu kodla aynıdır.

Cevabımın tartışıldığı yorumlarda, reddedilen düzenleme ile ilgili olarak, bu tekniği "daha özlü ama aynı zamanda daha belirsiz" olarak tanımlamıştım. Ben bunun yanındayım. Ne yaptığını anlamak biraz zaman aldı ve ayrıca ne yaptığının yukarıdaki açıklamasını yazmak da biraz zamanımı aldı. Anahtar incelik gelen dönüşümü Optional<Other>için Optional<Stream<Other>>. Bunu yaptıktan sonra bu mantıklı, ama benim için açık değildi.

Bununla birlikte, başlangıçta belirsiz olan şeylerin zamanla deyimsel hale gelebileceğini kabul edeceğim. Bu teknik, en azından eklenene kadar Optional.stream(eğer varsa) pratikte en iyi yol olabilir.

GÜNCELLEME: Optional.stream JDK 9'a eklendi.


16

Zaten yaptığınız gibi daha özlü bir şekilde yapamazsınız.

İstemediğini iddia ediyorsun .filter(Optional::isPresent) ve .map(Optional::get) .

Bu @StuartMarks'ın açıkladığı yöntemle çözülmüştür, ancak sonuç olarak şimdi bir ile eşleştiriyorsunuz Optional<T>, bu yüzden şimdi ve sonunda .flatMap(this::streamopt)bir kullanmanız gerekiyor get().

Bu yüzden hala iki ifadeden oluşuyor ve artık yeni yöntemle istisnalar elde edebilirsiniz! Çünkü ya her isteğe bağlı boşsa? Sonra findFirst()boş bir isteğe bağlı dönecek ve get()başarısız olacak!

Yani sahip olduğunuz şeyler:

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

olduğu istediğini başarmak için en iyi yolu aslında ve bu bir şekilde bir sonuç kaydetmek istediğiniz olduğu Tdeğil gibi Optional<T>.

Ek bir yöntem CustomOptional<T>saran Optional<T>ve sağlayan bir sınıf yaratma özgürlüğünü aldım flatStream(). Genişletemeyeceğinizi unutmayın 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();
    }
}

flatStream()Burada eklediğimi göreceksiniz :

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

Olarak kullanılır:

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

Sen hala bir dönüş gerekecektir Stream<T>geri dönemezsin olarak, burada Tçünkü eğer, !optional.isPresent()o zaman, T == nullbunu böyle beyan, ama sonra eğer senin .flatMap(CustomOptional::flatStream)eklemek girişiminde nullbir akışa ve bu mümkün değildir.

Örnek olarak:

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

Olarak kullanılır:

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

Şimdi NullPointerExceptionakış işlemlerinin içine bir atacak .

Sonuç

Kullandığınız yöntem aslında en iyi yöntemdir.


6

Kullanarak biraz daha kısa bir sürüm reduce:

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

Ayrıca, azaltma işlevini statik bir yardımcı program yöntemine taşıyabilirsiniz ve sonra olur:

  .reduce(Optional.empty(), Util::firstPresent );

6
Bunu beğendim, ancak akıştaki her öğeyi değerlendireceğine dikkat çekerken, findFirst () yalnızca mevcut bir öğeyi bulana kadar değerlendirecektir.
Duncan McGregor

1
Ne yazık ki, her bir çözümü uygulamak bir anlaşma kırıcıdır. Ama akıllı.
Yona Appletree

5

Benim gibi önceki cevabı çok popüler olmamaya göründü, bu başka gitmek verecektir.

Kısa bir cevap:

Çoğunlukla doğru yoldasınız. Ben gelebilir istediğiniz çıktı almak için en kısa kodu şudur:

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

Bu, tüm gereksinimlerinize uyacaktır:

  1. İşsizliğe karar veren ilk yanıtı bulacak Optional<Result>
  2. this::resolveGerektiğinde tembelce çağırır
  3. this::resolve ilk boş olmayan sonuçtan sonra çağrılmayacak
  4. Geri dönecek Optional<Result>

Daha uzun cevap

OP başlangıç ​​sürümüne kıyasla tek değişiklik, .map(Optional::get)çağrıdan önce kaldırıldım ve zincirdeki son çağrı olarak .findFirst()eklediğim oldu .flatMap(o -> o).

Bu, akış gerçek bir sonuç bulduğunda çift Opsiyonel'den kurtulmanın güzel bir etkisi vardır.

Java'da bundan daha kısa süremezsiniz.

Daha geleneksel fordöngü tekniğini kullanan alternatif kod snippet'i, aynı sayıda kod satırı olacak ve gerçekleştirmeniz gereken az çok aynı sıra ve işlem sayısına sahip olacak:

  1. Arama this.resolve,
  2. dayalı filtreleme Optional.isPresent
  3. Sonucu döndürmek ve
  4. olumsuz sonuçla başa çıkmanın bir yolu (hiçbir şey bulunmadığında)

Çözümümün ilan edildiği gibi çalıştığını kanıtlamak için küçük bir test programı yazdım:

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

}

(Hata ayıklama ve doğrulama için yalnızca gerektiğinde çözülmesi gereken çağrı sayısının fazlalığı yoktur ...)

Bunu bir komut satırında yürütürken, aşağıdaki sonuçları aldım:

$ 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 ile aynı düşünüyorum. Neden mü birisi yapmak akışı <akışı <>> ve düz zaman yapabilirsiniz sadece düz biri opsiyonel <opsiyonel <>> ile?
Genç Hyun Yoo

3

Üçüncü taraf bir kütüphane kullanmak sakıncası yoksa Javaslang kullanabilirsiniz . Scala gibidir, ancak Java ile uygulanır.

Scala'nın bildiği kütüphaneye çok benzeyen eksiksiz bir değişmez koleksiyon kütüphanesi ile birlikte gelir. Bu koleksiyonlar Java'nın koleksiyonlarının ve Java 8'in Akışının yerini alır. Ayrıca kendi Seçenek seçeneği vardır.

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

İlk soru örneği için bir çözüm:

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 + ")"; }
}

Feragatname: Javaslang'ın yaratıcısıyım.


3

Partiye geç, ama ne olacak

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

İsteğe bağlı akışı el ile dönüştürmek için bir util yöntemi oluşturursanız, son get () yönteminden kurtulabilirsiniz:

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

Akışı çözümleme işlevinizden hemen döndürürseniz, bir satır daha kaydedersiniz.


3

İşlevsel API'lar için yardımcılar oluşturmak için fabrika yöntemlerini tanıtmak istiyorum :

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

Fabrika yöntemi:

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

Akıl Yürütme:

  • Genel olarak yöntem referanslarında olduğu gibi, lambda ifadelerine kıyasla, yanlışlıkla erişilebilir kapsamdan bir değişkeni yakalayamazsınız, örneğin:

    t -> streamopt(resolve(o))

  • Oluşturulabilir, örneğin Function::andThenfabrika yöntemi sonucunu arayabilirsiniz :

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

    Oysa bir lambda durumunda, önce onu atmanız gerekir:

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


3

Boş kütüphanem AbacusUtil tarafından sağlanan Akış tarafından desteklenir . İşte kod:

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

3

Java 8 ile sıkışıp kalıyorsanız, ancak Guava 21.0 veya daha yenisine erişiminiz varsa, Streams.streamisteğe bağlı bir akışı akışa dönüştürmek için kullanabilirsiniz .

Böylece, verilen

import com.google.common.collect.Streams;

Yazabilirsin

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

0

Buna ne dersin?

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


Akış ve toplama yapabildiğinizde neden bunu yapıyorsunuz?
OneCricketeer

return list.stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList())), tıpkı soru (ve bağlantılı cevabınız) gibi ...
OneCricketeer

Yanlış olabilirim, ama isPresent () kullanmayı düşünürüm ve sonra get () iyi bir uygulama değildir. Bu yüzden ondan uzaklaşmaya çalışıyorum.
rastaman

.get() Olmadan kullanırsanız isPresent(), IntelliJ
OneCricketeer'de

-5

Büyük olasılıkla yanlış yapıyorsunuz.

Java 8 İsteğe Bağlı'nın bu şekilde kullanılması amaçlanmamıştır. Genellikle yalnızca find gibi bir değer döndürebilen veya döndürmeyen terminal akışı işlemleri için ayrılmıştır.

Sizin durumunuzda, çözülebilir olan öğeleri filtrelemek için önce ucuz bir yol bulmaya çalışın ve ardından ilk öğeyi isteğe bağlı olarak alın ve son işlem olarak çözün. Daha da iyisi - filtrelemek yerine, ilk çözülebilir öğeyi bulun ve çözün.

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

Temel kural, akıştaki öğelerin sayısını başka bir şeye dönüştürmeden önce azaltmaya çalışmanızdır. Tabii ki YMMV.


6
Ben isteğe bağlı <Diğer> döndüren OP'nin fix () yöntemi isteğe bağlı mükemmel mantıklı bir kullanım olduğunu düşünüyorum. OP'nin problem alanıyla elbette konuşamıyorum, ancak bir şeyin çözülebilir olup olmadığını belirlemenin yolu onu çözmeye çalışmak olabilir. Öyleyse, İsteğe Bağlı "boolean" bir boole sonucunu, başarılı olması durumunda çözümün sonucuyla tek bir API çağrısına birleştirir.
Stuart Marks

2
Stuart temelde doğrudur. İstenmesi sırasına göre bir dizi arama terimi var ve bir şey döndüren ilkinin sonucunu bulmak istiyorum. Temel olarak Optional<Result> searchFor(Term t). Bu, İsteğe bağlı niyeti uygun görünüyor. Ayrıca, stream () leri tembel olarak değerlendirilmelidir, bu nedenle ilk eşleşmeyi geçen terimleri çözen fazladan bir iş olmamalıdır.
Yona Appletree

Soru tamamen mantıklı ve flatMap'i İsteğe Bağlı ile kullanmak genellikle Scala gibi benzer programlama dillerinde uygulanmaktadır.
dzs
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.