递归使用Stream.flatMap()

时间:2015-09-18 16:30:45

标签: java java-8 java-stream

考虑以下课程:

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]

到目前为止一切顺利。结果绝对正确。

但是,after reading this question,我对递归方法中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()是否正确?有没有更好的方法来实现同样的目标?

2 个答案:

答案 0 :(得分:19)

好吧,我使用了与通用Tree类相同的模式,并且对它没有错误的感觉。唯一的区别是,Tree类本身提供了children()allDescendants()方法,两者都返回Stream而后者建立在前者上。这与“Should I return a Collection or a Stream?”“Naming java methods that return streams”相关。

Stream的角度来看,flatMap与不同类型的孩子(即穿越财产时)和flatMap与同类型的孩子之间没有区别。如果返回的流再次包含相同的元素也没有问题,因为流的元素之间没有关系。原则上,您可以使用flatMap作为filter操作,使用模式flatMap(x -> condition? Stream.of(x): Stream.empty())。也可以使用它来复制this answer中的元素。

答案 1 :(得分:15)

以这种方式使用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这对我来说比使用帮助类更自然。

感兴趣的是,我倾向于使用这些类型的Order o1 = new Order(); o1.setOrders(Arrays.asList(o1)); o1.streamOrders(); 方法来允许使用集合字段而不是字段的getter。如果方法的用户不需要知道任何有关底层集合的信息,或者需要能够更改它,那么返回流是方便和安全的。

我会注意到您应该注意的数据结构存在一个风险:订单可能是其他几个订单的一部分,甚至可能是其中的一部分。这意味着导致无限递归和堆栈溢出非常简单:

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而不是<%= link_to signup_path do %> <i class="material-icons left"> person </i> <% end %> 。如果你有兴趣,还有一些模式可以防止这种情况。