在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() );
这将符合您的所有要求:
- 它将findparsing为非空的
Optional<Result>
第一个响应 - 它根据需要懒惰地调用
this::resolve
-
this::resolve
在第一个非空结果之后不会被调用 - 它将返回
Optional<Result>
较长的答案
与OP初始版本相比,唯一的修改是在调用.findFirst()
之前删除了.map(Optional::get)
,并添加了.flatMap(o -> o)
作为链中的最后一个调用。
这有一个很好的效果摆脱双可选,每当streamfind一个实际的结果。
在Java中你不可能比这更短。
使用更传统的for
循环技术的替代代码片段的代码行数将大致相同,并且需要执行的操作的顺序和次数或多或less相同:
- 调用
this.resolve
, - 基于
Optional.isPresent
过滤 - 返回结果和
- 处理负面结果的一些方法(当没有发现)
为了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当然。