将Java 8的Optional与Stream :: flatMap一起使用


240

新的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()方法(或者是a Collection或提供一种将其转换为或将其视为a的方法Collection)。

我能想到的最好的方法是:

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

但这对于一个很常见的案例来说似乎太漫长了。有人有更好的主意吗?


在对您的示例进行了一些编码之后,我实际上发现显式版本比有关的显式版本(如果存在的话)更具可读性.flatMap(Optional::toStream),对于您的版本,您实际上看到了什么。
skiwi 2014年

19
@skiwi好吧,Optional.stream现在存在于JDK 9中。...–
Stuart Marks

我很好奇这是在哪里记录的,以及获取它的过程是什么。还有其他一些似乎确实应该存在的方法,我很好奇正在讨论API更改的地方。
Yona Appletree'3


10
有趣的是,JDK-8050820实际上在其描述中提到了这个问题!
Didier L

Answers:


265

Java 9

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

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

Java 8

是的,这是API中的一个小漏洞,因为将a 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 2014年

5
@Hypher谢谢。.filter()。map()技术还不错,可以避免依赖于辅助方法。“如果有一种更简洁的方法,那就太好了。我将研究如何添加Optional.stream()。
Stuart Marks 2014年

43
我更喜欢:static <T> Stream<T> streamopt(Optional<T> opt) { return opt.map(Stream::of).orElse(Stream.empty()); }
kubek2k 2015年

5
我希望他们能给... 增加一个Optional重载Stream#flatMap,那样你就可以写stream().flatMap(this::resolve)
flakes

4
@flkes是的,我们已经解决了这个想法,但是现在(在JDK 9中)似乎并没有增加太多的价值Optional.stream()
斯图尔特·马克斯

69

我将根据用户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定义如下:

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

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

接下来,对的调用orElseGet(Stream::empty)返回type的值Stream<Other>。如果存在其输入值,则获取该值,即单元素Stream<Other>。否则(如果没有输入值)它将返回一个空值Stream<Other>。因此结果是正确的,与原始条件代码相同。

在关于答案的讨论中,关于被拒绝的评论,我将这种技术描述为“更简洁但也更模糊”。我支持这个。我花了一段时间才弄清楚它在做什么,还花了我一段时间来写下上面关于它在做什么的描述。关键之处在于从Optional<Other>到的转换Optional<Stream<Other>>。一旦您轻描淡写,这是有道理的,但对我而言并不明显。

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

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


16

您不能像已经在做的那样更加简洁。

您声称自己不想要.filter(Optional::isPresent) .map(Optional::get)

@StuartMarks描述的方法已解决了此问题,但是结果是现在将其映射到Optional<T>,因此最后需要使用.flatMap(this::streamopt)and get()

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

所以你有:

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

真正实现你想要什么是最好的办法,那就是你要的结果保存为一个T,而不是一个Optional<T>

我自由创建了一个CustomOptional<T>类,该类包装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>此处返回a ,因为您无法返回T,因为if !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 );

6
我喜欢这个,但值得指出的是,它将评估Stream中的每个项目,而findFirst()只会评估直到找到当前项目。
Duncan McGregor 2014年

1
不幸的是,执行每个解决方案都会破坏交易。但这很聪明。
Yona Appletree

5

由于我以前的回答似乎不太受欢迎,因此我将再说一遍。

一个简短的答案:

您大多处于正确的轨道上。我想出的最短的代码可以达到您想要的输出:

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

每当流发现实际结果时,这样做都具有摆脱double-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();
        }
    }

}

(它的确有很少的额外行可用于调试和验证,仅需要调用的电话数量即可解决...)

在命令行上执行此操作,得到以下结果:

$ 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)一样。当您只需使用一个可选的<optional <?>>进行扁平化时,为什么有人会制作stream <stream <?>>并扁平化
Young Hyun Yoo19年

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的创建者。


3

晚会了,但是那又怎么样

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

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

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

如果您立即从解析函数返回流,则可以节省一行。


3

我想推广用于为功能性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(...)


3

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

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

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

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


当您可以流式传输和收集时为什么这样做?
OneCricketeer

return list.stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList())),就像问题(和您的链接答案)具有...
OneCricketeer

我可能是错的,但是我认为使用isPresent(),然后使用get()并不是一个好习惯。因此,我试图摆脱这种情况。
rastaman

如果你使用.get() 没有 isPresent(),那么你得到的IntelliJ中的一个警告
OneCricketeer

-5

很可能您做错了。

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

在您的情况下,最好先尝试找到一种便宜的方法来过滤掉那些可解析的项目,然后将第一个项目作为可选项目并将其作为最后一个操作来解决,这会更好。更好的是-无需过滤,而是找到第一个可解决的项目并对其进行解决。

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

经验法则是,在将项目转换为其他项目之前,应努力减少其数量。YMMV当然。


6
我认为OP的resolve()方法返回Optional <Other>是对Optional的明智使用。我当然不能谈谈OP的问题领域,但是可能是确定某项是否可解决的方法是尝试解决它。如果是这样,则Optional将布尔值“此可解决”与解析结果(如果成功)融合到单个API调用中。
Stuart Marks 2014年

2
斯图尔特基本上是正确的。我有一组按搜索顺序排列的搜索字词,我正在寻找第一个返回任何内容的结果。所以基本上Optional<Result> searchFor(Term t)。这似乎符合Optional的意图。另外,应该懒惰地评估stream(),因此不会发生超出第一个匹配项的额外工作来解决术语。
Yona Appletree,2014年

这个问题完全明智,并且通常在其他类似的编程语言(例如Scala)中使用带有Optional的flatMap。
2016年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.