递归使用 Stream.flatMap()

2022-01-22 00:00:00 java-8 java java-stream

考虑以下类:

public class Order {

    private String id;

    private List<Order> orders = new ArrayList<>();

    @Override
    public String toString() {
        return this.id;
    }

    // getters & setters
}

注意:请务必注意,我无法修改此类,因为我从外部 API 使用它.

NOTE: It is important to note that I cannot modify this class, because I'm consuming it from an external API.

还要考虑以下订单层次结构:

Also consider the following hierarchy of orders:

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

可以用这种方式直观地表示:

Which could be visually represented this way:

1
1.1
1.1.1
1.2
2
2.1
2.2
2.3

现在,我想将这个订单层次结构扁平化为一个List,这样我就可以得到以下信息:

Now, I want to flatten this hierarchy of orders into a List, so that I get the following:

[1, 1.1, 1.1.1, 1.2, 2, 2.1, 2.2, 2.3]

我已经设法通过递归地使用 flatMap() (以及一个辅助类)来做到这一点,如下所示:

I've managed to do it by recursively using flatMap() (along with a helper class), as follows:

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]

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

So far so good. The result is absolutely correct.

但是,阅读后这个问题,我对在递归方法中使用 flatMap() 有一些担忧.特别是,我想知道流是如何扩展的(如果这是术语).所以我修改了 Helper 类并使用 peek(System.out::println) 来检查这个:

However, after reading this question, I had some concerns regarding the usage of flatMap() within a recursive method. Particularly, I wanted to know how the stream was being expanded (if that's the term). So I modified the Helper class and used peek(System.out::println) to check this:

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

我不确定这是否是应该打印的输出.

I'm not sure if this is the output that should be printed.

所以,我想知道是否可以让中间流包含重复的元素.此外,这种方法的优缺点是什么?毕竟,以这种方式使用 flatMap() 是否正确?有没有更好的方法来达到同样的效果?

So, I wonder if it's OK to let intermediate streams contain repeated elements. Furthermore, what are the pros and cons of this approach? Is it correct, after all, to use flatMap() this way? Is there a better way to achieve the same?

推荐答案

嗯,我在一个通用的 Tree 类中使用了相同的模式,并且对它没有错误的感觉.唯一的区别是,Tree 类本身提供了一个 children()allDescendants() 方法,它们都返回一个 Stream 和后者建立在前者之上.这与 我应该返回集合还是流?" 和 命名返回流的java方法".

Well, I used the same pattern with a generic Tree class and didn’t have a wrong feeling with it. The only difference is, that the Tree class itself offered a children() and allDescendants() methods, both returning a Stream and the latter building on the former. This is related to "Should I return a Collection or a Stream?" and "Naming java methods that return streams".

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

From a Streams perspective, there is no difference between a flatMap to children of a different type (i.e. when traversing a property) and a flatMap to children of the same type. There is also no problem if the returned stream contains the same element again, as there is no relationship between the elements of the streams. In principle, you can use flatMap as a filter operation, using the pattern flatMap(x -> condition? Stream.of(x): Stream.empty()). It’s also possible to use it to duplicate elements like in this answer.

相关文章