首页 文章

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

提问于
浏览
195

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

但这似乎是一个非常普遍的案例,似乎非常冗长 . 谁有更好的主意?

10 回答

  • 5

    你最有可能做错了 .

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

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

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

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

  • 61

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

  • 3

    我正在根据用户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();
    

    COMMENTARY

    让我们直接比较原始版本和修改版本:

    // 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> . 因此结果是正确的,与原始条件代码相同 .

    在讨论我的回答的评论中,关于被拒绝的编辑,我将此技术描述为"more concise but also more obscure" . 我支持这个 . 我花了一段时间才弄清楚它在做什么,我花了一些时间来写出它正在做的事情的上述描述 . 关键的微妙之处在于从 Optional<Other> 转变为 Optional<Stream<Other>> . 一旦你理解了它,这是有道理的,但对我来说并不明显 .

    不过,我会承认,最初模糊不清的事情随着时间的推移会变得不恰当 . 可能这种技术最终成为实践中的最佳方式,至少直到 Optional.stream 被添加(如果有的话) .

    UPDATE: Optional.stream 已添加到JDK 9中 .

  • 2

    你不能像你已经做的那样更简洁 .

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

    现在将在流操作中抛出 NullPointerException .

    结论

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

  • 1

    使用 reduce 的稍短版本:

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

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

    .reduce(Optional.empty(), Util::firstPresent );
    
  • -5

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

    一个简短的回答:

    你大部分都在正确的轨道上 . 达到你想要的输出的最短代码我可以想出:

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

    这符合您的所有要求:

    • 它会找到第一个解决非空的回复 Optional<Result>

    • 根据需要懒洋洋地调用 this::resolve
      在第一次非空结果后,将不会调用

    • this::resolve

    • 它将返回 Optional<Result>

    更长的答案

    与OP初始版本相比唯一的修改是我在调用 .findFirst() 之前删除了 .map(Optional::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
    
  • 211

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

  • 12

    流提供的My库AbacusUtil支持Null . 这是代码:

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

    迟到了,但是怎么样

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

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

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

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

  • 1

    我想推广 factory methods 来为功能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(...)

相关问题