Stream.flatMap() 的递归使用

2022-09-01 01:39:03

请考虑以下类:

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()Helperpeek(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()


答案 1

好吧,我对泛型类使用了相同的模式,并且没有错误的感觉。唯一的区别是,类本身提供了 a 和 方法,既返回 a,又返回后者在前者之上。这与“我应该返回集合还是流?”“命名返回流的java方法”有关。TreeTreechildren()allDescendants()Stream

从 s 的角度来看,a 到不同类型的子项(即遍历属性时)和 a 到相同类型的子项之间没有区别。如果返回的流再次包含相同的元素,也没有问题,因为流的元素之间没有关系。原则上,您可以使用 模式 作为操作使用。也可以使用它来复制元素,如这个答案所示。StreamflatMapflatMapflatMapfilterflatMap(x -> condition? Stream.of(x): Stream.empty())


答案 2

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

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

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);
    }
}

这是一个相当安全的强制转换,因为您希望用户使用 。不是万无一失的,因为他们仍然可以调用并添加一个而不是一个.同样,如果您有兴趣,有一些模式可以防止这种情况。addOrdergetOrdersOrderSafeOrder


推荐