递归使用Stream.flatMap()

考虑以下课程:

public class Order { private String id; private List 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 o11Children = new ArrayList(Arrays.asList(o111)); o11.setOrders(o11Children); Order o12 = new Order(); o12.setId("1.2"); List 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 o2Children = new ArrayList(Arrays.asList(o21, o22, o23)); o2.setOrders(o2Children); List 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 flattened = orders.stream() .flatMap(Helper::flatten) .collect(Collectors.toList()); 

这是助手类:

 public final class Helper { private Helper() { } public static Stream 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 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作为filter操作,使用模式flatMap(x -> condition? Stream.of(x): Stream.empty()) 。 它也可以用来复制像这个答案中的元素。

以这种方式使用flatMap确实没问题。 流中的每个中间步骤都是完全独立的(按设计),因此递归中没有风险。 您需要注意的主要事项是在流式传输时可能会改变基础列表的任何内容。 在你的情况下似乎没有风险。

理想情况下,您将使此递归成为Order类本身的一部分:

 class Order { private final List subOrders = new ArrayList<>(); public Stream 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 streamOrders() { return Stream.concat(Stream.of(this), subOrders().flatMap(SafeOrder::streamOrders)); } private Stream subOrders() { return getOrders().stream().map(o -> (SafeOrder)o); } } 

这是一个相当安全的演员,因为您希望用户使用addOrder 。 不是万无一失,因为他们仍然可以调用getOrders并添加一个Order而不是一个SafeOrder 。 如果您有兴趣,还有一些模式可以防止这种情况发生。