在Stream :: flatMap中使用Java 8的Optional

时间:2014-03-29 00:42:02

标签: java lambda java-8 java-stream

新的Java 8流框架和朋友们提供了一些非常简洁的Java代码,但我遇到了一个看似简单的情况,这个问题很简洁。

考虑List<Thing> things和方法Optional<Other> resolve(Thing thing)。我想将Thing映射到Optional<Other> s并获取第一个Other。显而易见的解决方案是使用things.stream().flatMap(this::resolve).findFirst(),但flatMap要求您返回一个流,Optional没有stream()方法(或者是{a}} Collection或提供将其转换为Collection的方式或将其视为things.stream() .map(this::resolve) .filter(Optional::isPresent) .map(Optional::get) .findFirst(); )的方法。

我能想出的最好的是:

{{1}}

但对于看似非常普遍的案例而言,这似乎非常啰嗦。任何人都有更好的主意吗?

12 个答案:

答案 0 :(得分:219)

Java 9

Optional.stream已添加到JDK 9.这使您可以执行以下操作,而无需任何帮助方法:

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

Java 8

是的,这是API中的一个小漏洞,因为将Optional变为零或一个长度的流有点不方便。你可以这样做:

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()操作,但这是一个品味问题。

答案 1 :(得分:63)

我正在根据用户srborlonganmy other answer提议的修改添加第二个答案。我认为提出的技术很有意思,但它并不适合作为我的答案的编辑。其他人同意,拟议的编辑被否决。 (我不是选民之一。)但这项技术有其优点。如果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,其定义如下:

  

如果存在值,则将提供的映射函数应用于该值,如果结果为非null,则返回描述结果的Optional。否则返回一个空的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中。

答案 2 :(得分:12)

你不能像现在这样做更简洁。

您声称自己不想要.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();

实际上是实现目标的最佳方式,您希望将结果保存为T,而不是Optional<T>

我冒昧地创建了一个包裹CustomOptional<T>的{​​{1}}类,并提供了一个额外的方法Optional<T>。请注意,您无法扩展flatStream()

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

结论

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

答案 3 :(得分:5)

使用reduce的稍短版本:

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

您还可以将reduce函数移动到静态实用程序方法,然后它变为:

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

答案 4 :(得分:3)

由于我的previous answer似乎不是很受欢迎,我会再给它一次。

一个简短的回答:

你大部分时间都在正确的轨道上。得到你想要的输出的最短代码是:

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

这符合您的所有要求:

  1. 它会找到第一个解析为非空Optional<Result>
  2. 的回复
  3. 根据需要随意调用this::resolve
  4. 首次非空结果后,
  5. this::resolve将不会被调用
  6. 它将返回Optional<Result>
  7. 更长的答案

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

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

    在Java中,你真的不能比这更短。

    使用更传统的for循环技术的替代代码片段将是大约相同数量的代码行,并且您需要执行的操作的顺序和数量大致相同:

    1. 致电this.resolve
    2. 根据Optional.isPresent
    3. 进行过滤
    4. 返回结果和
    5. 处理否定结果的某种方式(当没有找到任何结果时)
    6. 为了证明我的解决方案像宣传的那样有效,我写了一个小测试程序:

      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
      

答案 5 :(得分:3)

如果您坚持使用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();

答案 6 :(得分:2)

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

答案 7 :(得分:2)

流提供的My library AbacusUtil支持Null。这是代码:

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

答案 8 :(得分:2)

我想推广工厂方法,以创建功能性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(...)

答案 9 :(得分:1)

晚会,但

怎么样?
things.stream()
    .map(this::resolve)
    .filter(Optional::isPresent)
    .findFirst().get();

如果您创建一个util方法将可选项转换为手动流,则可以删除最后一个get():

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

如果您立即从解析函数返回流,则再保存一行。

答案 10 :(得分:0)

那怎么办?

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

答案 11 :(得分:-5)

最有可能你做错了。

Java 8 Optional并不意味着以这种方式使用。它通常仅保留给可能会或可能不会返回值的终端流操作,例如查找。

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

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

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