新的Java 8流框架及其朋友可以编写一些非常简洁的Java代码,但我遇到过一个看似简单的情况,但要做到简洁却很棘手。

考虑一个列表<Thing> things and method Optional<Other> resolve(Thing Thing)。我想映射的东西可选<其他>s,并获得第一个其他。

显而易见的解决方案是使用things.stream().flatMap(this::resolve). findfirst(),但flatMap要求您返回一个流,而Optional没有stream()方法(或者它是一个集合或提供一个方法将其转换为或将其视为一个集合)。

我能想到的最好的是:

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

但对于一个很普通的例子来说,这似乎太啰嗦了。

有人有更好的主意吗?


当前回答

I'm adding this second answer based on a proposed edit by user srborlongan to my other answer. I think the technique proposed was interesting, but it wasn't really suitable as an edit to my answer. Others agreed and the proposed edit was voted down. (I wasn't one of the voters.) The technique has merit, though. It would have been best if srborlongan had posted his/her own answer. This hasn't happened yet, and I didn't want the technique to be lost in the mists of the StackOverflow rejected edit history, so I decided to surface it as a separate answer myself.

基本上,这种技术是以一种巧妙的方式使用一些可选方法来避免必须使用三元操作符(?:)或if/else语句。

我的内联示例可以这样重写:

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

我的例子中使用了一个helper方法,可以这样重写:

/**
 * 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。映射,定义如下:

如果值存在,则对其应用提供的映射函数,如果结果是非空,则返回描述结果的Optional。否则返回空的Optional。

map(Stream::of)调用返回一个Optional<Stream<Other>>。如果输入Optional中存在一个值,则返回的Optional包含一个包含单个Other结果的流。但如果该值不存在,则结果为空的Optional。

接下来,调用orElseGet(Stream::empty)返回类型为Stream<Other>的值。如果它的输入值是存在的,它得到的值是单个元素Stream<Other>。否则(如果输入值不存在)它返回一个空的Stream<Other>。因此,结果是正确的,与原始条件代码相同。

在对我的回答的评论中,关于被拒绝的编辑,我把这种方法描述为“更简洁,但也更模糊”。我坚持这一点。我花了一段时间才弄清楚它在做什么,也花了一段时间写了上面关于它在做什么的描述。关键的微妙之处在于从Optional<Other>到Optional<Stream<Other>>的转换。一旦你明白了这一点,它就说得通了,但对我来说并不明显。

不过,我承认,随着时间的推移,最初晦涩的东西可能会变成惯用用语。这种技术最终可能成为实践中最好的方法,至少在可选之前是这样。流被添加(如果曾经添加过的话)。

更新:可选的。流已添加到JDK 9。

其他回答

使用reduce的略短版本:

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

你也可以将reduce函数移动到一个静态实用程序方法,然后它变成:

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

那个怎么样?

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

由于我之前的答案似乎不太受欢迎,我将再试一次。

简短的回答:

你基本上是在正确的轨道上。得到你想要的输出的最短代码是这样的:

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

这将符合您的所有要求:

它将找到解析为非空可选<Result>的第一个响应 它会根据需要惰性地调用::resolve 这个::resolve不会在第一个非空结果之后被调用 它将返回Optional<Result>

再回答

与OP初始版本相比,唯一的修改是我在调用. findfirst()之前删除了.map(可选::get),并添加了. flatmap (o -> o)作为链中的最后一个调用。

当流找到一个实际的结果时,这有一个很好的效果,可以消除double-Optional。

在Java中没有比这个更短的了。

使用更传统的for循环技术的替代代码片段将有相同的代码行数,并且有或多或少相同的顺序和你需要执行的操作数量:

调用this.resolve, 基于Optional.isPresent进行过滤 返回结果和 处理消极结果的方法(当什么都没有发现时)

为了证明我的解决方案像宣传的那样有效,我写了一个小测试程序:

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

如果你不介意使用第三方库,你可以使用Javaslang。它类似于Scala,但用Java实现。

它带有一个完整的不可变集合库,与Scala中的集合库非常相似。这些集合取代了Java的集合和Java 8的流。它也有自己的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的创造者。

I'm adding this second answer based on a proposed edit by user srborlongan to my other answer. I think the technique proposed was interesting, but it wasn't really suitable as an edit to my answer. Others agreed and the proposed edit was voted down. (I wasn't one of the voters.) The technique has merit, though. It would have been best if srborlongan had posted his/her own answer. This hasn't happened yet, and I didn't want the technique to be lost in the mists of the StackOverflow rejected edit history, so I decided to surface it as a separate answer myself.

基本上,这种技术是以一种巧妙的方式使用一些可选方法来避免必须使用三元操作符(?:)或if/else语句。

我的内联示例可以这样重写:

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

我的例子中使用了一个helper方法,可以这样重写:

/**
 * 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。映射,定义如下:

如果值存在,则对其应用提供的映射函数,如果结果是非空,则返回描述结果的Optional。否则返回空的Optional。

map(Stream::of)调用返回一个Optional<Stream<Other>>。如果输入Optional中存在一个值,则返回的Optional包含一个包含单个Other结果的流。但如果该值不存在,则结果为空的Optional。

接下来,调用orElseGet(Stream::empty)返回类型为Stream<Other>的值。如果它的输入值是存在的,它得到的值是单个元素Stream<Other>。否则(如果输入值不存在)它返回一个空的Stream<Other>。因此,结果是正确的,与原始条件代码相同。

在对我的回答的评论中,关于被拒绝的编辑,我把这种方法描述为“更简洁,但也更模糊”。我坚持这一点。我花了一段时间才弄清楚它在做什么,也花了一段时间写了上面关于它在做什么的描述。关键的微妙之处在于从Optional<Other>到Optional<Stream<Other>>的转换。一旦你明白了这一点,它就说得通了,但对我来说并不明显。

不过,我承认,随着时间的推移,最初晦涩的东西可能会变成惯用用语。这种技术最终可能成为实践中最好的方法,至少在可选之前是这样。流被添加(如果曾经添加过的话)。

更新:可选的。流已添加到JDK 9。