递归使用Stream.flatMap()

Fed*_*ner 30 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
}
Run Code Online (Sandbox Code Playgroud)

注意:重要的是要注意我无法修改此类,因为我正在从外部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));
Run Code Online (Sandbox Code Playgroud)

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

1
1.1
1.1.1
1.2
2
2.1
2.2
2.3
Run Code Online (Sandbox Code Playgroud)

现在,我想将这个订单层次变为a List,以便我得到以下内容:

[1, 1.1, 1.1.1, 1.2, 2, 2.1, 2.2, 2.3]
Run Code Online (Sandbox Code Playgroud)

我设法通过递归使用flatMap()(以及辅助类)来完成它,如下所示:

List<Order> flattened = orders.stream()
    .flatMap(Helper::flatten)
    .collect(Collectors.toList());
Run Code Online (Sandbox Code Playgroud)

这是助手类:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

以下行:

System.out.println(flattened);
Run Code Online (Sandbox Code Playgroud)

产生以下输出:

[1, 1.1, 1.1.1, 1.2, 2, 2.1, 2.2, 2.3]
Run Code Online (Sandbox Code Playgroud)

到现在为止还挺好.结果绝对正确.

但是,在阅读完这个问题后,我对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);
    }
}
Run Code Online (Sandbox Code Playgroud)

输出是:

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
Run Code Online (Sandbox Code Playgroud)

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

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

Hol*_*ger 19

好吧,我使用相同的模式与泛型Tree类,并没有错误的感觉.唯一的区别是,Tree班级本身提供了一个children()allDescendants()方法,两者都返回Stream前者和后者.这与"我应该返回集合还是流?""命名返回流的java方法"有关.

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

  • 这是实现中的一个弱点(很多人认为它是一个错误),但它不会使您的解决方案无效.你的方法没有错.有关短路操作的相关性能问题应由JRE维护人员确定. (2认同)
  • 有什么办法可以让流变得懒惰?也看到这个问题 - http://stackoverflow.com/q/32749148 (2认同)
  • @bayou.io:是的,我决定为此制定一个通用解决方案(又名解决方法),请参阅 http://stackoverflow.com/a/32767282/2711488 (2认同)

spr*_*ter 16

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));
    }
}
Run Code Online (Sandbox Code Playgroud)

然后你可以使用orders.stream().flatMap(Order::streamOrders)对我来说比使用帮助类更自然.

令人感兴趣的是,我倾向于使用这些类型的stream方法来允许使用集合字段而不是字段的getter.如果方法的用户不需要知道有关底层集合的任何信息或需要能够更改它,那么返回流是方便和安全的.

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

Order o1 = new Order();
o1.setOrders(Arrays.asList(o1));
o1.streamOrders();
Run Code Online (Sandbox Code Playgroud)

有很多好的模式可以避免这些问题所以请询问您是否需要在该领域提供一些帮助.

你指出你不能改变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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这是一个相当安全的演员,因为您希望用户使用addOrder.不是万无一失,因为他们仍然可以调用getOrders并添加一个Order而不是一个SafeOrder.如果您有兴趣,还有一些模式可以防止这种情况.

  • @FedericoPeraltaSchaffner该问题根本不会影响此解决方案的正确性.它是关于流可以被短路的条件 - 在这种情况下OP表明在某些条件下流操作应该终止但不是.答案仍然是正确的,有时候JRE会进行比需要更多的处理. (3认同)