使用Java 8的Optional和Stream::flatMap。

309

新的Java 8流框架以及相关工具使得编写Java代码更加简洁,但我遇到了一个看似简单却很难变得简洁的情况。

考虑一个List<Thing> things和方法Optional<Other> resolve(Thing thing)。我想将Thing映射为Optional<Other>并获取第一个Other

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

我能想到的最好的解决方案是:

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

但是这似乎对于一个非常常见的情况来说太冗长了。

有没有更好的想法?


在使用你的示例进行一些编码后,我实际上发现显式版本比那个假设存在 .flatMap(Optional::toStream) 的版本更易读。使用你的版本,你可以看到正在发生什么。 - skiwi
21
嗨,@skiwi,现在JDK 9中存在Optional.stream方法了。 - Stuart Marks
3
这是一个Java编程语言Bug的跟踪记录,在Oracle公司的Bug管理系统中。该Bug编号为JDK-8050820。 - Christoffer Hammarström
12
有趣的是,JDK-8050820 实际上在其描述中提到了这个问题! - Didier L
从纯性能的角度来看,我认为你所做的是可以的。我不确定Java 8如何实现flatMap,但这通常会引入合并操作,而一些简单的.filter().map()则不会像合并操作那样具有良好的可扩展性。 - Crystark
显示剩余2条评论
12个回答

358

Java 9

Optional.stream已添加到JDK 9中。这使您能够在不需要任何辅助方法的情况下执行以下操作:

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

Java 8

是的,这是 API 中的一个小漏洞,因为将 Optional<T> 转换为零或一长度的 Stream<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()操作,但这只是一种口味问题。


2
我认为在Java 9之前,API不会发生变化。 - assylias
6
谢谢。使用 .filter().map() 技术还不错,避免了对辅助方法的依赖。但如果有更简洁的方式就更好了。我会调查添加 Optional.stream() 的方法。 - Stuart Marks
49
我偏爱以下代码:static Stream streamopt(Optional opt) { return opt.map(Stream::of).orElse(Stream.empty()); } - kubek2k
6
希望他们只是在Stream#flatMap中添加一个Optional重载...这样你就可以直接写stream().flatMap(this::resolve) - flakes
4
是的,我们讨论过这个想法,但现在似乎没有太多附加价值了(在JDK 9中)因为有Optional.stream() - Stuart Marks
显示剩余9条评论

71
我正在根据用户srborlongan我的另一个答案的建议编辑添加第二个答案。我认为所提出的技术很有趣,但不适合作为我的答案的修改。其他人也同意并投票反对了这个修改。(我不是投票者之一。)然而,这种技术确实有价值。最好的方法是让srborlongan自己发表自己的答案。但现在仍未发生,我不想让这种技术在StackOverflow拒绝编辑历史的迷雾中消失,所以决定自己将其作为单独的答案呈现。

基本上,这种技术是通过巧妙地使用一些Optional方法来避免使用三元运算符(? :)或if / else语句。

我的内联示例将重写为:

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,其定义如下:
如果存在值,则将提供的映射函数应用于该值,如果结果非空,则返回描述结果的Optional。否则返回一个空Optional。 map(Stream::of)调用返回一个Optional<Stream<Other>>。如果输入Optional中存在值,则返回的Optional包含包含单个Other结果的Stream。但如果没有值,则结果是一个空Optional。
接下来,对orElseGet(Stream::empty)的调用返回类型为Stream<Other>。如果它的输入值存在,则获取该值,即单元素Stream<Other>。否则(如果输入值不存在),它将返回一个空的Stream<Other>。因此,结果是正确的,与原有条件代码相同。
在评论中讨论我的答案时,关于被拒绝的编辑,我曾将这种技术描述为“更简洁但也更晦涩”。我坚持这个观点。我花了一段时间才弄清楚它在做什么,编写上述描述也花了一段时间。关键的微妙之处在于从Optional<Other>Optional<Stream<Other>>的转换。一旦你理解了这一点,它就有意义了,但对我来说并不明显。
不过,我承认,最初晦涩的东西随着时间的推移可能会成为惯用语。直到Optional.stream被添加(如果有的话),这种技术可能成为实践中最好的方法。

更新:JDK 9中已添加Optional.stream


19

你已经做得很简洁了,无法更加简洁。

你声称不想使用.filter(Optional::isPresent).map(Optional::get)

这个问题已经被@StuartMarks描述的方法解决了,但是作为结果,现在你需要将其映射到一个Optional<T>,所以现在你需要使用.flatMap(this::streamopt)并最终使用get()

因此,它仍然由两个语句组成,并且新方法可能会出现异常!因为,如果每个可选项都为空怎么办?那么findFirst()将返回一个空的可选项,而你的get()将失败!

所以你现在有:

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

is实际上是实现你想要的最佳方式,而这就是你想将结果保存为T而不是Optional<T>

我冒昧创建了一个包装Optional<T>并提供额外方法flatStream()CustomOptional<T>类。请注意,您无法扩展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(),如下所示:
public Stream<T> flatStream() {
    if (!optional.isPresent()) {
        return Stream.empty();
    }
    return Stream.of(get());
}

用途:

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

在这里,您仍然需要返回一个Stream<T>,因为您不能返回T。如果!optional.isPresent(),那么如果您这样声明,T == null,但是您的.flatMap(CustomOptional::flatStream)将尝试将null添加到流中,这是不可能的。

例如:

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

用途:

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

现在,在流操作中将抛出一个NullPointerException

结论

你使用的方法实际上是最好的方法。


6

使用reduce的稍微简短的版本:

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

您也可以将reduce函数移动到静态实用程序方法中,然后它变成:
  .reduce(Optional.empty(), Util::firstPresent );

7
我喜欢这个,但值得指出的是,这将评估流中的每个项目,而findFirst()只会在找到一个存在的项目时停止评估。 - Duncan McGregor
1
不幸的是,执行每个解析都是一个破坏者。但它很聪明。 - Yona Appletree

6
作为我的 之前的回答 似乎不太受欢迎,我会再试一次。

简短回答:

你大致上是正确的。我能想到的最短的代码来得到你想要的输出是这个:

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 初始版本相比,唯一的修改是我在调用 .findFirst() 之前删除了 .map(Optional::get) 并添加了 .flatMap(o -> o) 作为最后一个调用。

这有一个很好的效果,在流找到实际结果时消除了双重 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();
        }
    }

}

这段代码有一些额外的调试和验证行,仅在需要时才调用resolve函数...

在命令行上执行此代码,我得到了以下结果:

$ 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一样。为什么有人会创建stream<stream<?>>和flat,而不是只用一个optional<optional<?>>就可以了呢? - Young Hyun Yoo

5
我希望推广使用工厂方法来创建函数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);
}

原因:

  • 与一般的方法引用相比,与lambda表达式不同,您不能意外地从可访问范围内捕获变量,例如:

    t -> streamopt(resolve(o))

  • 它是可组合的,例如可以在工厂方法结果上调用Function::andThen

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

    而在lambda的情况下,您需要首先进行转换:

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


5
晚来的问候,不过关于IT技术方面的问题,您需要了解的是:
things.stream()
    .map(this::resolve)
    .filter(Optional::isPresent)
    .findFirst().get();

如果您手动创建一个工具方法将Optional转换为Stream,则可以省略最后一个get():
things.stream()
    .map(this::resolve)
    .flatMap(Util::optionalToStream)
    .findFirst();

如果你的resolve函数立即返回流,那么你可以少写一行代码。

4
如果您使用的是Java 8,但可以访问Guava 21.0或更新版本,则可以使用Streams.stream将Optional转换为流。
因此,假设有以下代码:
import com.google.common.collect.Streams;

您可以编写

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

3

如果您不介意使用第三方库,可以使用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

流式操作支持Null,我的库abacus-common提供了支持。以下是代码示例:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接