使用Java 8's可选的Stream::flatMap

新的Java 8流框架及其朋友可以编写一些非常简洁的Java代码,但我遇到过一个看似简单的情况,但要做到简洁却很棘手。

考虑List<Thing> things和方法Optional<Other> resolve(Thing thing)。我想把__abc2映射到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();

但对于一个很普通的例子来说,这似乎太啰嗦了。

有人有更好的主意吗?

226172 次浏览

很可能你做错了。

Java 8 Optional不是这样使用的。它通常只保留给终端流操作,这些操作可能返回值,也可能不返回值,例如find。

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

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

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

Java 9

Optional.stream已添加到JDK 9中。这使你能够做以下事情,而不需要任何帮助方法:

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

Java 8

是的,这是API中的一个小漏洞,因为将Optional<T>转换为0或1长度的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()操作,但这是一个品味问题。

你不能做得更简洁,因为你已经在做了。

你声称你不想要.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

结论

你用的方法,实际上是最好的方法。

我将根据用户srborlongan我的另一个答案的建议编辑添加第二个答案。我认为提出的技巧很有趣,但它并不适合作为我的答案的编辑。其他人也表示同意,修改提议被否决了。(我不是选民之一。)不过,这项技术也有优点。如果srborlongan把他/她自己的答案贴出来就更好了。这种情况还没有发生,我不想让这项技术迷失在StackOverflow拒绝编辑历史的迷雾中,所以我决定自己把它作为一个单独的答案。

基本上,这个技巧是以一种巧妙的方式使用一些Optional方法,以避免必须使用三元操作符(? :)或if/else语句。

我的内联示例可以这样重写:

Optional<Other> result =
things.stream()
.map(this::resolve)
.flatMap(o -> o.map(Stream::of).orElseGet(Stream::empty))
.findFirst();

我的例子中使用了一个helper方法,可以这样重写:

/**
* 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,定义如下:

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

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

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

在对我的回答的评论中,关于被拒绝的编辑,我把这种方法描述为“更简洁,但也更模糊”。我坚持这一点。我花了一段时间才弄清楚它在做什么,也花了一段时间写了上面关于它在做什么的描述。关键的微妙之处在于从Optional<Other>Optional<Stream<Other>>的转换。一旦你明白了这一点,它就说得通了,但对我来说并不明显。

不过,我承认,随着时间的推移,最初晦涩的东西可能会变成惯用用语。这种技术最终可能是实践中最好的方法,至少在Optional.stream被添加之前(如果它被添加的话)。

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

使用reduce的略短的版本:

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

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

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

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

它带有一个完整的不可变集合库,与Scala中的集合库非常相似。这些集合取代了Java的集合和Java 8的流。它也有自己的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的创造者。

由于我的以前的回答似乎不太受欢迎,我将再给这个。

简短的回答:

你基本上是在正确的轨道上。得到你想要的输出的最短代码是这样的:

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)作为链中的最后一个调用。

当流找到一个实际的结果时,这有一个很好的效果,可以消除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

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

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

来晚了,但是

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

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

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

如果你从resolve函数中立即返回stream,你就多保存一行。

我想推广工厂方法来为函数式api创建helper:

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)) < / p >

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

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

    而在lambda的情况下,你需要先强制转换:

    ((Function<T, Stream<R>>) t -> streamopt(resolve(t))).andThen(...) < / p >

如果你被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();

那个怎么样?

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 < a href = " https://stackoverflow.com/a/58281000/3477539 " > < / >