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

新的Java 8stream框架和朋友提供了一些非常简洁的java代码,但是我遇到了一个看似简单的情况,这很难做到简洁。

考虑一个List<Thing> things和方法Optional<Other> resolve(Thing thing) 。 我想把这些Thing映射到Optional<Other> s并获得第一个Other 。 明显的解决scheme是使用things.stream().flatMap(this::resolve).findFirst() ,但flatMap需要你返回一个stream,而Optional没有stream()方法(或者它是Collection或提供方法将其转换为或Collection为一个Collection )。

我能想到的最好的是这样的:

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

但是,这似乎是一个非常普遍的情况,似乎非常啰嗦。 任何人有更好的主意?

Java 9

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

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

Java 8

是的,这是API中的一个小漏洞,将一个Optional放入一个零或一个长度的Stream中有点不方便。 你可以这样做:

 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提出的编辑我的其他答案 。 我认为所提出的技术很有趣,但它并不适合作为我的答案的编辑。 其他人同意,并build议编辑被拒绝。 (我不是其中一个选民。)这个技巧有好处。 如果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)) 

原来是一个简单的,如果workmanlike方法:我们得到一个Optional<Other> ; 如果它有一个值,我们返回一个包含该值的stream,如果它没有值,我们返回一个空的stream。 很简单,容易解释。

修改是聪明的,有避免条件的优点。 (我知道有些人不喜欢三元操作符,如果被滥用,确实会使代码难以理解)。然而,有时候事情可能太聪明了。 修改后的代码也以一个Optional<Other>开头。 然后调用Optional.map ,定义如下:

如果存在值,则应用提供的映射函数,如果结果不为空,则返回一个描述结果的可选项。 否则返回一个空的可选。

map(Stream::of)调用返回一个Optional<Stream<Other>> 。 如果在input可选中存在值,则返回的可选包含包含单一其他结果的stream。 但是,如果值不存在,结果是一个空的可选。

接下来,调用orElseGet(Stream::empty)将返回Stream<Other>types的值。 如果其input值存在,则获取该值,即单元素Stream<Other> 。 否则(如果input值不存在)它返回一个空的Stream<Other> 。 所以结果是正确的,就像原来的条件码一样。

在讨论我的答案的评论中,关于被拒绝的编辑,我曾经把这种技术描述为“更简洁但更晦涩”。 我站在这。 我花了一段时间才弄清楚它在做什么,而且还花了我一些时间来写出上面的描述。 关键的细微之处在于从Optional<Other>Optional<Stream<Other>> 。 一旦你这样做,这是有道理的,但对我来说并不明显。

不过,我会承认,最初晦涩难懂的东西随着时间的stream逝会变得习惯用语。 这可能是,这种技术最终成为实践中的最佳方式,至less直到Optional.stream被添加(如果它曾经)。

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

你已经做得不能做得更简洁了。

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

这已经被@StuartMarks描述的方法解决了,但是现在你把它映射到一个Optional<T> ,所以现在你需要使用.flatMap(this::streamopt)和一个get()

所以它仍然由两个语句组成,你现在可以用新的方法得到exception! 因为,如果每个选项都是空的呢? 然后, findFirst()将返回一个空的可选,你的get()将失败!

所以你有什么:

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

实际上是实现你想要的最好的方法,那就是你想把结果保存为T ,而不是作为一个Optional<T>

我冒昧地创build了一个包装Optional<T> flatStream() 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添加到stream,这是不可能的。

举个例子:

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

现在将在stream操作中引发一个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. 它将findparsing为非空的Optional<Result>第一个响应
  2. 它根据需要懒惰地调用this::resolve
  3. this::resolve在第一个非空结果之后不会被调用
  4. 它将返回Optional<Result>

较长的答案

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

这有一个很好的效果摆脱双可选,每当streamfind一个实际的结果。

在Java中你不可能比这更短。

使用更传统的for循环技术的替代代码片段的代码行数将大致相同,并且需要执行的操作的顺序和次数或多或less相同:

  1. 调用this.resolve
  2. 基于Optional.isPresent过滤
  3. 返回结果和
  4. 处理负面结果的一些方法(当没有发现)

为了certificate我的解决scheme能够像广告一样工作,我写了一个小testing程序:

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

(它没有多less额外的线路来debugging和validation,只有尽可能多的电话解决需要…)

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

 $ java StackOferflow ab 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提供的我的库AbacusUtil支持Null。 这是代码:

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

如果你不介意使用第三方库,你可以使用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); 

以下是最初问题示例的解决scheme:

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

如果您创build一个util方法手动将可选的转换为stream,您可以摆脱最后的get()。

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

如果您立即从parsing函数返回stream,则另外保存一行。

很可能你做错了。

Java 8可选并不意味着以这种方式使用。 它通常只保留给可能或不可以返回值的terminalstream操作,例如查找。

在你的情况下,首先尝试find一个便宜的方法来筛选出可parsing的项目,然后将第一个项目作为可选项并将其作为最后一个操作进行parsing可能会更好。 更好的是 – 不是过滤,find第一个可parsing的项目并解决它。

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

经验法则是,您应该努力减lessstream中的项目数量,然后再将其转换为其他项目。 YMMV当然。