[英]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.