新的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();

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

有人有更好的主意吗?


当前回答

如果你不介意使用第三方库,你可以使用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的创造者。

其他回答

Java 9

可选的。流已添加到JDK 9。这使你能够做以下事情,而不需要任何帮助方法:

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

Java 8

是的,这是API中的一个小洞,因为将可选的<T>转换为0或1长度的流<T>有点不方便。你可以这样做:

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

不过,在flatMap中使用三元操作符有点麻烦,所以最好写一个小的帮助函数来做这件事:

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

在这里,我内联了对resolve()的调用,而不是使用单独的map()操作,但这只是个人喜好的问题。

很可能你做错了。

Java 8 Optional不是这样使用的。它通常只保留给终端流操作,这些操作可能返回值,也可能不返回值,例如find。

在您的情况下,最好先尝试找到一种廉价的方法来过滤那些可解析的项,然后将第一个项作为可选项,并将其作为最后一个操作进行解析。更好的方法是,找到第一个可解决的项目并解决它,而不是过滤。

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

经验法则是,在将流中的项转换为其他内容之前,应该努力减少它们的数量。当然是YMMV。

我想推广用于为函数式api创建helper的工厂方法:

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

推理:

与一般的方法引用相比,与lambda表达式相比,你不能意外地从可访问范围中捕获变量,例如: T -> streamopt(resolve(o)) 它是可组合的,你可以调用Function::and然后在工厂方法result上: streamopt(::解决).andThen(…) 而在lambda的情况下,你需要先强制转换: ((T <函数,流< R > >) T - > streamopt(解决(T))) .andThen(…)

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。

如果你只能使用Java 8,但可以访问Guava 21.0或更新版本,你可以使用Streams。流将可选项转换为流。

因此,鉴于

import com.google.common.collect.Streams;

你可以写

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