[英]Using Java 8's Optional with Stream::flatMap
新的 Java 8 流框架和朋友們制作了一些非常簡潔的 Java 代碼,但我遇到了一個看似簡單但很難做到簡潔的情況。
考慮一個List<Thing> things
和方法Optional<Other> resolve(Thing thing)
。 我想將Thing
映射到Optional<Other>
並獲得第一個Other
。
顯而易見的解決方案是使用things.stream().flatMap(this::resolve).findFirst()
,但flatMap
要求您返回一個流,而Optional
沒有stream()
方法(或者它是Collection
或提供將其轉換為 Collection 或將其視為Collection
的方法)。
我能想到的最好的是:
things.stream()
.map(this::resolve)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();
但對於一個非常常見的情況來說,這似乎是非常冗長的。
有人有更好的主意嗎?
Optional.stream
已添加到 JDK 9。這使您能夠執行以下操作,而無需任何輔助方法:
Optional<Other> result =
things.stream()
.map(this::resolve)
.flatMap(Optional::stream)
.findFirst();
是的,這是 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提出的編輯將第二個答案添加到我的另一個答案中。 我認為提出的技術很有趣,但它並不適合作為我的答案的編輯。 其他人同意,提議的編輯被否決。 (我不是選民之一。)不過,這項技術有其優點。 如果 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 包含一個包含單個 Other 結果的 Stream。 但如果該值不存在,則結果為空 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() );
這將滿足您的所有要求:
Optional<Result>
的第一個響應this::resolve
this::resolve
在第一個非空結果后不會被調用Optional<Result>
與 OP 初始版本相比,唯一的修改是我在調用.findFirst()
) 之前刪除了.map(Optional::get)
並添加了.flatMap(o -> o)
作為鏈中的最后一個調用。
每當流找到實際結果時,這對擺脫雙重可選具有很好的效果。
在 Java 中,你真的不能比這更短。
使用更傳統for
循環技術的替代代碼片段將具有大約相同數量的代碼行,並且具有或多或少相同的順序和您需要執行的操作數量:
this.resolve
,Optional.isPresent
的過濾為了證明我的解決方案像宣傳的那樣有效,我編寫了一個小測試程序:
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。 它也有自己的 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 的創建者。
Null 由提供的 Stream 支持 My library AbacusUtil 。 這是代碼:
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;
}
很可能你做錯了。
Java 8 Optional 並不意味着以這種方式使用。 它通常只保留用於可能返回值或不返回值的終端流操作,例如 find。
在您的情況下,最好先嘗試找到一種廉價的方法來過濾掉那些可解析的項目,然后將第一個項目作為可選項目並將其作為最后一個操作來解決。 更好的是 - 不是過濾,而是找到第一個可解析的項目並解決它。
things.filter(Thing::isResolvable)
.findFirst()
.flatMap(this::resolve)
.get();
經驗法則是,您應該努力減少流中的項目數量,然后再將它們轉換為其他內容。 當然是 YMMV。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.