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



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

我能想到的最好的是:

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

但对于一个似乎非常常见的案例来说,这似乎非常冗长。

有人有更好的主意吗?

Java 9

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

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

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

我根据用户 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。

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

在讨论我的答案的评论中,关于被拒绝的编辑,我将这种技术描述为"更简洁但也更晦涩"。我支持这一点。我花了一段时间才弄清楚它在做什么,我也花了一段时间才写出上面关于它在做什么的描述。关键的微妙之处是从Optional<Other>Optional<Stream<Other>>的转变。一旦你摸索到这一点,这是有道理的,但对我来说并不明显。

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

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

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

声称你不想要.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>类来包装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();

现在将在流操作中抛出NullPointerException

结论

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

使用reduce的稍短版本:

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

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

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

由于我之前的回答似乎不是很受欢迎,我会再试一次。

简短的回答:

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

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)添加为链中的最后一次调用。

这在流找到实际结果时,可以摆脱双可选

在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

晚了,但是呢

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

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

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

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

我想

推广为函数式 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(...)

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

如果您不介意使用第三方库,则可以使用 Javaslang。它类似于Scala,但用Java实现。

它带有一个完整的不可变集合库,与 Scala 中已知的集合库非常相似。这些集合取代了Java的集合和Java 8的Stream。它还有自己的选项实现。

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

Null 由提供的流支持我的库算盘通用。这是代码:

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

那怎么办?

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

很可能

你做错了。

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

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

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

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

最新更新