在程式設計中有時會出現巢狀或瀑布式的流程,就結構來看每一層運算極為類似,只是傳回的型態不同,很難抽取流程重用。舉例來說,如果你的方法可能傳回null
,你可能會設計出某個流程如下:
Customer customer = order.getCustomer();
if(customer != null) {
String address = customer.getAddress();
if(address != null) {
return address;
}
}
return "n.a.";
巢狀的層次可能還會更深,像是 ...
Customer customer = order.getCustomer();
if(customer != null) {
Address address = customer.getAddress();
if(address != null) {
City city = address.getCity();
if(city != null) {
....
}
}
}
return "n.a.";
就 使用
Optional
取代 null
中的說明,null
本身就不建議使用,如果讓getCustomer()
傳回Optional<Customer>
、讓getAddress()
傳回Optional<String>
,那一開始的程式片段可以先改為:String addr = "n.a.";
Optional<Customer> customer = order.getCustomer();
if(customer.isPresent()) {
Optional<String> address = customer.get().getAddress();
if(address.isPresent()) {
addr = address.get();
}
}
return addr;
看來好像沒有高明到哪去,不過至少每一層都是
Optional
型態了,而每一層都是有無的判斷,然後將Optional<T>
轉換為Optional<U>
,如果將Optional<T>
轉換為Optional<U>
的方式可以由外部指定,那你就可以重用有無的判斷了,實際上Optional
有個flatMap()
方法,已經幫你寫好這個邏輯了: public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}
所以,你大可以如下直接使用
Optional
的flatMap()
方法:return order.getCustomer()
.flatMap(Customer::getAddress)
.orElse("n.a.");
如果層次不深,也許看不出使用這個的好處,若層次深比較有益處時,像是一開始第二個程式片段,改寫為以下就清楚多了…
return order.getCustomer()
.flatMap(Customer::getAddress)
.flatMap(Address::getCity)
.orElse("n.a.");
Optional
的flatMap()
這個名稱令人困惑,可從Optional<T>
呼叫flatMap
後會得到Optional<U>
來想像一下,flatMap()
就像是從盒子取出另一盒子置放一旁(flat就是平坦化的意思),過程中依指定之Lambda將前盒的T
映射(map)為U
再放入後盒,因為判斷是否有值的運算情境被隱藏了,使用者因此可明確指定感興趣的特定運算,從而使程式碼意圖顯露出來,又可接暢地接續運算,以避免巢狀或瀑布式的複雜檢查流程。那麼如果你沒辦法修改程式,讓
getCustomer()
、getAddress()
、getCity()
等傳回Optional
型態怎麼辦?Optional
是還有個map()
方法,例如,若參數order
是Order
型態,有null
的可能性,getCustomer()
、getAddress()
、getCity()
等分別的傳回型態是Customer
、Address
、City
,且有可能傳回null
,那麼就可以這麼做:return Optional.ofNullable(order)
.map(Order::getCustomer)
.map(Customer::getAddress)
.map(Address::getCity)
.orElse("n.a.");
與
flatMap()
的差別在於,map()
方法實作中,對mapper.apply(value)
的結果使用了Optional.ofNullable()
方法(flatMap
中使用的是Objects.requireNonNull()
),因此有辦法持續處理null
的情況: public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}
如果之前的
Order
有個getLineItems()
方法,可取得訂單中的產品項目List<LineItem>
,想要取得LineItem
的名稱,可以透過getName
來取得,若你有個List<Order>
,想取得所有的產品項目名稱會怎麼寫?直覺的寫法應該是用迴圈…List<String> itemNames = new ArrayList<>();
for(Order order : orders) {
for(LineItem lineItem : order.getLineItems()) {
itemNames.add(lineItem.getName());
}
}
當然,層次不深時這樣寫很直覺也還好閱讀,不過如果層次深時,例如,想進一步取得
LineItem
的贈品名稱的話,你又得多一層for
迴圈,如果還要繼續取下去呢?...你可以用
List
的stream()
方法取得Stream
之後,使用flatMap()
方法如下改寫:List<String> itemNames = orders.stream()
.flatMap(order -> order.getLineItems().stream())
.map(LineItem::getName)
.collect(toList());
就程式碼閱讀來說,
stream()
方法會傳回Stream<Order>
,把Stream
當成是盒子,stream()
就是將一群Order
物件全部放入盒中,flatMap()
指定的Lambda運算是order.getLineItems().stream()
,意思就是從盒中那群Order
物件逐一取得List<LineItem>
,然後再用一個Stream
將所有LineItem
裝起來,也就是說,Stream<Order>
經由flatMap
方法後映射為Stream<LineItem>
,這類操作一個盒子一個盒子(一個Stream
一個Stream
)接續下去,例如,想進一步取得LineItem
的贈品名稱可以如下:List<String> itemNames = orders.stream()
.flatMap(order -> order.getLineItems().stream())
.flatMap(lineItem -> lineItem.getPremiums().stream())
.map(LineItem::getName)
.collect(toList());
基本上,如果瞭解
Optinal
、Stream
(或其他型態)的flatMap()
方法,在一層一層盒子剝開後做了哪些運算,撰寫與閱讀程式碼時,忽略掉flatMap這個名稱,就能比較清楚程式碼的主要意圖。