【问题标题】:Using Java 8's Optional with Stream::flatMap将 Java 8 的 Optional 与 Stream::flatMap 一起使用
【发布时间】:2014-05-08 15:57:27
【问题描述】:

新的 Java 8 流框架和朋友们制作了一些非常简洁的 java 代码,但我遇到了一个看似简单但很难简洁的情况。

考虑List<Thing> things 和方法Optional<Other> resolve(Thing thing)。我想将Things 映射到Optional<Other>s 并获得第一个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 好吧,Optional.stream 现在存在于 JDK 9 中......
  • 我很好奇这是在哪里记录的,以及获取它的过程是什么。还有一些其他方法看起来确实应该存在,我很好奇 API 更改的讨论发生在哪里。
  • 好笑的是JDK-8050820在其描述中居然提到了这个问题!

标签: java lambda java-8 java-stream


【解决方案1】:

Java 9

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

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

Java 8

是的,这是 API 中的一个小漏洞,因为将 Optional&lt;T&gt; 转换为零或一长度 Stream&lt;T&gt; 有点不方便。你可以这样做:

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() 操作,但这是个人喜好问题。

【讨论】:

  • 我认为 api 直到现在 Java 9 才能改变。
  • @Hypher 谢谢。 .filter().map() 技术还不错,并且避免了对辅助方法的依赖。 '如果有更简洁的方法,那就太好了。我将调查添加 Optional.stream()。
  • 我更喜欢:static &lt;T&gt; Stream&lt;T&gt; streamopt(Optional&lt;T&gt; opt) { return opt.map(Stream::of).orElse(Stream.empty()); }
  • 我希望他们只是在Stream#flatMap 中添加一个Optional 重载...这样你就可以写stream().flatMap(this::resolve)
  • @flkes 是的,我们已经提出了这个想法,但是现在(在 JDK 9 中)有 Optional.stream(),它似乎并没有增加那么多价值。
【解决方案2】:

我根据用户 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&lt;Other&gt;;如果它有值,我们返回一个包含该值的流,如果它没有值,我们返回一个空流。非常简单易懂。

修改很巧妙,并且具有避免条件的优点。 (我知道有些人不喜欢三元运算符。如果滥用它确实会使代码难以理解。)但是,有时事情可能太聪明了。修改后的代码也以Optional&lt;Other&gt; 开头。然后调用Optional.map,其定义如下:

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

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

接下来,对orElseGet(Stream::empty) 的调用返回一个Stream&lt;Other&gt; 类型的值。如果它的输入值存在,它会获取该值,即单元素Stream&lt;Other&gt;。否则(如果输入值不存在)它返回一个空的Stream&lt;Other&gt;。所以结果是正确的,和原来的条件码一样。

在讨论我的答案的 cmets 中,关于被拒绝的编辑,我将这种技术描述为“更简洁但也更模糊”。我坚持这一点。我花了一段时间才弄清楚它在做什么,我也花了一些时间来写下上面对它在做什么的描述。关键的微妙之处在于从Optional&lt;Other&gt;Optional&lt;Stream&lt;Other&gt;&gt; 的转换。一旦你理解了这一点,这就是有道理的,但对我来说并不明显。

不过,我承认,最初晦涩难懂的事物随着时间的推移会变得惯用语。可能这种技术最终成为实践中的最佳方法,至少在添加 Optional.stream 之前(如果有的话)。

更新:Optional.stream 已添加到 JDK 9。

【讨论】:

    【解决方案3】:

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

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

    这已通过@StuartMarks 描述的方法解决,但结果您现在将其映射到Optional&lt;T&gt;,所以现在您需要使用.flatMap(this::streamopt) 和最后的get()

    所以它仍然由两个语句组成,您现在可以使用新方法获取异常!因为,如果每个可选项都是空的怎么办?然后findFirst() 将返回一个空的可选项,您的get() 将失败!

    那么你有什么:

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

    实际上是实现您想要的最佳方式,即您希望将结果保存为T,而不是Optional&lt;T&gt;

    我冒昧地创建了一个 CustomOptional&lt;T&gt; 类,它包装了 Optional&lt;T&gt; 并提供了一个额外的方法 flatStream()。请注意,您不能扩展Optional&lt;T&gt;

    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&lt;T&gt;,因为你不能返回T,因为如果!optional.isPresent(),那么T == null如果你这样声明,那么你的@ 987654343@ 会尝试将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

    结论

    你用的方法,其实是最好的方法。

    【讨论】:

      【解决方案4】:

      使用reduce的略短版本:

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

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

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

      【讨论】:

      • 我喜欢这个,但值得指出的是,这将评估 Stream 中的每个项目,而 findFirst() 只会在找到当前项目之前进行评估。
      • 不幸的是,执行每个解析都会破坏交易。但它很聪明。
      【解决方案5】:

      由于我的previous answer 似乎不太受欢迎,我会再试一次。

      简短回答:

      你基本上是在正确的轨道上。我能想出的获得所需输出的最短代码是这样的:

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

      这将满足您的所有要求:

      1. 它将找到解析为非空Optional&lt;Result&gt; 的第一个响应
      2. 它会根据需要懒惰地调用this::resolve
      3. this::resolve 在第一个非空结果后不会被调用
      4. 它将返回Optional&lt;Result&gt;

      更长的答案

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

      每当流找到实际结果时,这对摆脱双可选具有很好的效果。

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

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

      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
      

      【讨论】:

      • 我觉得和罗兰特普一样。当您可以使用一个可选的>进行平坦时,为什么有人会制作stream>和平坦
      【解决方案6】:

      聚会迟到了,但是呢

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

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

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

      如果您立即从您的解析函数返回流,您会多保存一行。

      【讨论】:

        【解决方案7】:

        我想推广工厂方法来为函数式 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 -&gt; streamopt(resolve(o))

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

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

          而在 lambda 的情况下,您需要先转换它:

          ((Function&lt;T, Stream&lt;R&gt;&gt;) t -&gt; streamopt(resolve(t))).andThen(...)

        【讨论】:

          【解决方案8】:

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

          【讨论】:

            【解决方案9】:

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

            【讨论】:

              【解决方案10】:

              我的库 AbacusUtil 提供的 Stream 支持 Null。这是代码:

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

              【讨论】:

                【解决方案11】:

                那又怎样?

                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

                【讨论】:

                • 当您可以流式传输和收集时为什么要这样做?
                • return list.stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList())),就像问题(和您的链接答案)有......
                • 我可能是错的,但我认为使用 isPresent() 然后 get() 不是一个好习惯。所以我试图摆脱它。
                • 如果你使用.get() 没有 isPresent(),那么你会在 IntelliJ 中收到警告
                【解决方案12】:

                很可能你做错了。

                Java 8 Optional 并不意味着以这种方式使用。它通常只保留给终端流操作,可能返回也可能不返回值,例如 find。

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

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

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

                【讨论】:

                • 我认为返回 Optional 的 OP 的 resolve() 方法是对 Optional 的完全明智的使用。当然,我不能谈论 OP 的问题域,但确定某事是否可解决的方法可能是尝试解决它。如果是这样,Optional 会将“是否可解析”的布尔结果与解析结果(如果成功)融合到单个 API 调用中。
                • Stuart 基本正确。我有一组按需求顺序排列的搜索词,我正在寻找第一个返回任何内容的结果。所以基本上Optional&lt;Result&gt; searchFor(Term t)。这似乎符合 Optional 的意图。此外,stream()s 应该被延迟评估,因此不应该发生额外的工作来解决超过第一个匹配项的术语。
                • 这个问题非常明智,并且在其他类似的编程语言(例如 Scala)中经常使用 flatMap 和 Optional。
                猜你喜欢
                • 2014-06-15
                • 2014-12-02
                • 1970-01-01
                • 1970-01-01
                • 2021-06-16
                • 1970-01-01
                • 1970-01-01
                • 2023-04-08
                • 1970-01-01
                相关资源
                最近更新 更多