【问题标题】:Recursive use of Stream.flatMap()递归使用 Stream.flatMap()
【发布时间】:2015-12-15 21:22:17
【问题描述】:

考虑以下类:

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() 是否正确?有没有更好的方法来达到同样的效果?

【问题讨论】:

  • 只是好奇:为什么在创建订单后设置 id 而不是将其作为构造函数的参数?
  • @sprinter 我无法修改 Order 类,因为它是我正在使用的 API 的一部分。
  • 啊,我明白了。然后我的大部分答案都毫无用处。可能值得将这条信息添加到您的问题中。
  • @sprinter 我会编辑它,谢谢。

标签: java java-8 java-stream


【解决方案1】:

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

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

【讨论】:

【解决方案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 方法来允许使用集合字段而不是字段的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。如果您有兴趣,还有一些模式可以防止这种情况发生。

【讨论】:

  • @FedericoPeraltaSchaffner 该问题根本不会影响此解决方案的正确性。这是关于流可以短路的条件 - 在这种情况下,OP 显示在某些条件下流操作应该终止但不会。答案仍然是正确的,只是有时 JRE 执行的处理超出了它的需要。
  • 谢谢。我想检查有关链接问题的方法的正确性,但是您关于如何在这种情况下正确设计的答案也非常有用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-26
  • 2015-08-14
  • 1970-01-01
  • 1970-01-01
  • 2020-12-02
  • 2018-03-26
相关资源
最近更新 更多