web-dev-qa-db-ja.com

Stream :: flatMapによるJava 8のオプションの使用

新しいJava 8ストリームフレームワークとその仲間たちは、いくつかの非常に簡潔なJavaコードを作り出していますが、私は簡潔にするのが難しいという一見単純な状況に出くわしました。

List<Thing> thingsとメソッドOptional<Other> resolve(Thing thing)を考えます。 ThingsをOptional<Other>sにマップして、最初のOtherを取得したいです。明らかな解決策はthings.stream().flatMap(this::resolve).findFirst()を使うことですが、flatMapはあなたがストリームを返すことを必要とし、そしてOptionalstream()メソッドを持っていません(あるいはCollectionであるか、それをCollectionに変換するかそれを見るメソッドを提供します).

私が思い付くことができる最高のものはこれです:

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

しかし、それは非常に一般的なケースのように思われるもののためにひどく長引いているようです。誰かがより良いアイデアをお持ちですか?

202
Yona Appletree

Java 9

Optional.stream がJDK 9に追加されました。これにより、ヘルパーメソッドを必要とせずに次のことを実行できます。

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

Java 8

はい、これはAPIの小さなホールでした。オプショナルを長さ0または1のストリームに変換するのは少々不便です。あなたはこれをすることができます:

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

ただし、flatMap内に3項演算子を含めるのは少し面倒です。そのためには、少し補助的な関数を作成した方が良い場合があります。

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

ここでは、別個のmap()操作を行う代わりにresolve()の呼び出しをインライン化しましたが、これは好みの問題です。

219
Stuart Marks

私はユーザーによる提案された編集 srborlongan に基づいてこの2番目の答えを 私の他の答えに加えています 私が提案したテクニックは面白かったと思いますが、それは私の答えの編集としてはあまり適していませんでした。他の人も同意し、提案された編集は投票されました。 (私は有権者の一人ではありませんでした。)しかし、そのテクニックにはメリットがあります。 srborlonganが彼/彼女自身の答えを投稿していればそれは最善だったでしょう。これはまだ起きていないし、StackOverflowが編集履歴を拒否したためにこのテクニックを失くしたくなかったので、私はそれを自分自身で別の答えとして表にすることにしました。

基本的に、このテクニックは、三項演算子(? :)またはif/elseステートメントを使用しなくても済むように、いくつかのOptionalメソッドを巧妙に使用することです。

私のインラインの例はこのように書き直されるでしょう:

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

ヘルパーメソッドを使用する私の例はこのように書き直されるでしょう:

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

解説

オリジナルバージョンと修正バージョンを直接比較しましょう。

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

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

オリジナルは、職人的なアプローチであれば単純明快です:私たちはOptional<Other>を得ます。値がある場合はその値を含むストリームを返し、値がない場合は空のストリームを返します。とてもシンプルで説明も簡単です。

変更は巧妙であり、条件付きを避けるという利点があります。 (三項演算子が嫌いな人もいます。それを誤用すると、コードが理解しにくくなることがあります)。変更されたコードもOptional<Other>で始まります。その後、次のように定義されているOptional.mapを呼び出します。

値が存在する場合は、指定されたマッピング関数をそれに適用し、結果がnull以外の場合は、結果を説明するOptionalを返します。それ以外の場合は空の値を返します。

map(Stream::of)呼び出しはOptional<Stream<Other>>を返します。値が入力Optionalに存在した場合、返されたOptionalには、単一のOther結果を含むStreamが含まれます。しかし、値が存在しなかった場合、結果は空になります。

次に、orElseGet(Stream::empty)の呼び出しは、タイプStream<Other>の値を返します。入力値が存在する場合は、その値が取得されます。これは単一要素のStream<Other>です。それ以外の場合(入力値が存在しない場合)、空のStream<Other>が返されます。したがって、元の条件付きコードと同じ、結果は正しいです。

却下された編集に関して、私の答えについて議論しているコメントの中で、私はこのテクニックを「もっと簡潔だが、もっとあいまいにする」と説明していた。私はこれを待っています。それが何をしているのかを理解するのにしばらく時間がかかりました、そしてそれが何をしていたのかについての上記の説明を書くのにも少し時間がかかりました。重要なのはOptional<Other>からOptional<Stream<Other>>への変換です。これを調べてしまえば意味がありますが、私には明白ではありませんでした。

しかし、最初はあいまいであったことが時間の経過とともに慣用句になる可能性があることを認めます。少なくともOptional.streamが追加されるまでは、このテクニックが実際には最良の方法であることになるかもしれません。

更新:Optional.streamがJDK 9に追加されました。

63
Stuart Marks

reduceを使った少し短いバージョン:

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

Reduce関数を静的なユーティリティメソッドに移動すると、次のようになります。

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

私の 前の答え はあまり一般的ではないように見えたので、私はこれをもう一回やります。

簡単な答え:

あなたは主に正しい方向に向かっています。私が思い付くことができるあなたの望ましい出力を得るための最も短いコードはこれです:

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

これはあなたのすべての要求に合うでしょう:

  1. 空でないOptional<Result>に解決される最初の応答が見つかります。
  2. 必要に応じてthis::resolveを遅延呼び出しします。
  3. 空でない最初の結果の後にthis::resolveは呼び出されません
  4. Optional<Result>が返されます

長い答え

OP初期バージョンと比較した唯一の変更は、.map(Optional::get)を呼び出す前に.findFirst()を削除し、チェーンの最後の呼び出しとして.flatMap(o -> o)を追加したことです。

Streamが実際の結果を見つけたときはいつでも、これはdouble-Optionalを取り除くのにいい効果があります。

Javaではこれ以上短くすることはできません。

より一般的なforループ手法を使用したコードの代替スニペットは、ほぼ同じ行数のコードを使用し、実行する必要がある操作の順序と数はほぼ同じです。

  1. this.resolveを呼び出す、
  2. Optional.isPresentに基づくフィルタリング
  3. 結果を返す
  4. 否定的な結果に対処するための何らかの方法(何も見つからなかったとき)

私のソリューションが宣伝どおりに機能することを証明するために、小さなテストプログラムを書きました。

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

}

(デバッグのための余分な行がほとんどなく、必要に応じて解決する呼び出しがあることを確認しています...)

これをコマンドラインで実行すると、次の結果が得られました。

$ 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
4
Roland Tepp

機能的なAPIのためのヘルパーを作成するためのファクトリメソッドを促進したいです。

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

工場メソッド:

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

推論:

  • 一般的なメソッド参照と同様に、ラムダ式と比較して、アクセス可能なスコープから変数を誤って取得することはできません。

    t -> streamopt(resolve(o))

  • それは構成可能です、あなたはすることができます例えばファクトリメソッドの結果に対してFunction::andThenを呼び出します。

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

    ラムダの場合は、最初にキャストする必要があります。

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

2
charlie

Nullは、Streamが提供するMy library AbacusUtil でサポートされています。これがコードです:

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

サードパーティのライブラリを使用しても構わない場合は、 Javaslang を使用できます。 Scalaに似ていますが、Javaで実装されています。

それはScalaから知られているものと非常によく似た完全な不変のコレクションライブラリが付属しています。これらのコレクションは、JavaのコレクションおよびJava 8のStreamに代わるものです。また、独自のOptionの実装もあります。

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

これは最初の質問の例に対する解決策です:

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

免責事項:私はJavaslangの作成者です。

2
Daniel Dietrich

パーティーに遅刻しますが、どうですか?

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

オプションのストリームを手動でストリームに変換するutilメソッドを作成すると、最後のget()を削除できます。

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

あなたがすぐにあなたの解決関数から流れを返すならば、あなたはもう1行を保存します。

1
Ljubopytnov

Java 8にこだわっているが、Guava 21.0以降にアクセスできる場合は、 Streams.stream を使用してオプションをストリームに変換できます。

したがって、与えられた

import com.google.common.collect.Streams;

あなたは書ける

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