[英]Recursive use of Stream.flatMap()
考慮以下課程:
public class Order {
private String id;
private List<Order> orders = new ArrayList<>();
@Override
public String toString() {
return this.id;
}
// getters & setters
}
注意: 重要的是要注意我無法修改此類 ,因為我正在從外部API中使用它。
還要考慮以下訂單層次結構:
Order o1 = new Order();
o1.setId("1");
Order o11 = new Order();
o11.setId("1.1");
Order o111 = new Order();
o111.setId("1.1.1");
List<Order> o11Children = new ArrayList<>(Arrays.asList(o111));
o11.setOrders(o11Children);
Order o12 = new Order();
o12.setId("1.2");
List<Order> o1Children = new ArrayList<>(Arrays.asList(o11, o12));
o1.setOrders(o1Children);
Order o2 = new Order();
o2.setId("2");
Order o21 = new Order();
o21.setId("2.1");
Order o22 = new Order();
o22.setId("2.2");
Order o23 = new Order();
o23.setId("2.3");
List<Order> o2Children = new ArrayList<>(Arrays.asList(o21, o22, o23));
o2.setOrders(o2Children);
List<Order> orders = new ArrayList<>(Arrays.asList(o1, o2));
這可以用這種方式直觀地表示:
1
1.1
1.1.1
1.2
2
2.1
2.2
2.3
現在,我想將這個訂單層次結構扁平化為List
,以便我得到以下內容:
[1, 1.1, 1.1.1, 1.2, 2, 2.1, 2.2, 2.3]
我設法通過遞歸使用flatMap()
(以及輔助類)來完成它,如下所示:
List<Order> flattened = orders.stream()
.flatMap(Helper::flatten)
.collect(Collectors.toList());
這是助手類:
public final class Helper {
private Helper() {
}
public static Stream<Order> flatten(Order order) {
return Stream.concat(
Stream.of(order),
order.getOrders().stream().flatMap(Helper::flatten)); // recursion here
}
}
以下行:
System.out.println(flattened);
產生以下輸出:
[1, 1.1, 1.1.1, 1.2, 2, 2.1, 2.2, 2.3]
到現在為止還挺好。 結果絕對正確。
但是, 在閱讀完這個問題后 ,我對一個遞歸方法中flatMap()
的使用有些擔憂。 特別是,我想知道如何擴展流(如果是這個術語)。 所以我修改了Helper
類並使用peek(System.out::println)
來檢查:
public static final class Helper {
private Helper() {
}
public static Stream<Order> flatten(Order order) {
return Stream.concat(
Stream.of(order),
order.getOrders().stream().flatMap(Helper::flatten))
.peek(System.out::println);
}
}
輸出是:
1
1.1
1.1
1.1.1
1.1.1
1.1.1
1.2
1.2
2
2.1
2.1
2.2
2.2
2.3
2.3
我不確定這是否應該打印輸出。
所以,我想知道讓中間流包含重復元素是否可行。 此外,這種方法的優點和缺點是什么? 畢竟,這樣使用flatMap()
是否正確? 有沒有更好的方法來實現同樣的目標?
好吧,我使用了與通用Tree
類相同的模式,並且沒有錯誤的感覺。 唯一的區別是, Tree
類本身提供了一個children()
和allDescendants()
方法,它們都返回Stream
,而后者則返回前者。 這與“我應該返回集合還是流?”和“命名返回流的java方法”有關 。
從Stream
的角度來看, flatMap
與不同類型的子flatMap
(即遍歷屬性時)和flatMap
與相同類型的子flatMap
之間沒有區別。 如果返回的流再次包含相同的元素也沒有問題,因為流的元素之間沒有關系。 原則上,您可以使用flatMap
作為filter
操作,使用模式flatMap(x -> condition? Stream.of(x): Stream.empty())
。 它也可以用來復制像這個答案中的元素。
以這種方式使用flatMap
確實沒問題。 流中的每個中間步驟都是完全獨立的(按設計),因此遞歸沒有風險。 您需要注意的主要事項是在流式傳輸時可能會改變基礎列表的任何內容。 在你的情況下似乎沒有風險。
理想情況下,您將使此遞歸成為Order
類本身的一部分:
class Order {
private final List<Order> subOrders = new ArrayList<>();
public Stream<Order> streamOrders() {
return Stream.concat(
Stream.of(this),
subOrders.stream().flatMap(Order::streamOrders));
}
}
然后你可以使用orders.stream().flatMap(Order::streamOrders)
,這對我來說比使用幫助類更自然。
感興趣的是,我傾向於使用這些類型的stream
方法來允許使用集合字段而不是字段的getter。 如果方法的用戶不需要知道有關底層集合的任何信息或需要能夠更改它,那么返回流是方便和安全的。
我會注意到您應該注意的數據結構存在一個風險:訂單可能是其他幾個訂單的一部分,甚至可能是其中的一部分。 這意味着導致無限遞歸和堆棧溢出非常簡單:
Order o1 = new Order();
o1.setOrders(Arrays.asList(o1));
o1.streamOrders();
有很多好的模式可以避免這些問題所以請詢問您是否需要在該領域提供一些幫助。
你指出你不能改變Order
類。 在這種情況下,我建議你擴展它以創建自己更安全的版本:
class SafeOrder extends Order {
public SafeOrder(String id) {
setId(id);
}
public void addOrder(SafeOrder subOrder) {
getOrders().add(subOrder);
}
public Stream<SafeOrder> streamOrders() {
return Stream.concat(Stream.of(this), subOrders().flatMap(SafeOrder::streamOrders));
}
private Stream<SafeOrder> subOrders() {
return getOrders().stream().map(o -> (SafeOrder)o);
}
}
這是一個相當安全的演員,因為您希望用戶使用addOrder
。 不是萬無一失,因為他們仍然可以調用getOrders
並添加一個Order
而不是一個SafeOrder
。 如果您有興趣,還有一些模式可以防止這種情況。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.