java Stream.flatMap() 的递归使用
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/32656888/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Recursive use of Stream.flatMap()
提问by Federico Peralta Schaffner
Consider the following class:
考虑以下类:
public class Order {
private String id;
private List<Order> orders = new ArrayList<>();
@Override
public String toString() {
return this.id;
}
// getters & setters
}
NOTE: It is important to note that I cannot modifythis class, because I'm consuming it from an external API.
注意:请务必注意,我无法修改此类,因为我是从外部 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
Now, I want to flatten this hierarchy of orders into a List
, so that I get the following:
现在,我想将此订单层次结构展平为 a List
,以便获得以下内容:
[1, 1.1, 1.1.1, 1.2, 2, 2.1, 2.2, 2.3]
I've managed to do it by recursively using flatMap()
(along with a helper class), as follows:
我已经设法通过递归使用flatMap()
(以及一个帮助类)来做到这一点,如下所示:
List<Order> flattened = orders.stream()
.flatMap(Helper::flatten)
.collect(Collectors.toList());
This is the helper class:
这是助手类:
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
}
}
The following line:
以下行:
System.out.println(flattened);
Produces the following output:
产生以下输出:
[1, 1.1, 1.1.1, 1.2, 2, 2.1, 2.2, 2.3]
So far so good. The result is absolutely correct.
到现在为止还挺好。结果是完全正确的。
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:
但是,在阅读了这个问题之后,我对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);
}
}
And the output was:
输出是:
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.
我不确定这是否是应该打印的输出。
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?
所以,我想知道让中间流包含重复元素是否可以。此外,这种方法的优缺点是什么?毕竟,这样使用是否正确flatMap()
?有没有更好的方法来实现相同的目标?
采纳答案by Holger
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”.
好吧,我在泛型Tree
类中使用了相同的模式,并且对它没有任何错误的感觉。唯一的区别是,Tree
类本身提供了 achildren()
和allDescendants()
方法,都返回 aStream
和后者建立在前者的基础上。这与“我应该返回集合还是流?”有关。和“命名返回流的 java 方法”。
From a Stream
s 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.
从Stream
s 的角度来看,flatMap
不同类型的to children(即遍历属性时)和flatMap
相同类型的to children之间没有区别。如果返回的流再次包含相同的元素也没有问题,因为流的元素之间没有关系。原则上,您可以使用模式flatMap
作为filter
操作使用flatMap(x -> condition? Stream.of(x): Stream.empty())
。也可以使用它来复制这个答案中的元素。
回答by sprinter
There is really no problem with you using flatMap
in this way. Each of the intermediate steps in a stream are quite independent (by design) so there's no risk in your recursion. The main thing you need to watch out for is anything that might alter the underlying list while you are streaming it. In your case that does not seem to be a risk.
你这样使用确实没有问题flatMap
。流中的每个中间步骤都非常独立(按设计),因此您的递归没有风险。您需要注意的主要事情是在您流式传输时可能改变底层列表的任何事情。在您的情况下,这似乎没有风险。
Ideally you would make this recursion part of the Order
class itself:
理想情况下,您可以将此递归作为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));
}
}
Then you can use orders.stream().flatMap(Order::streamOrders)
which seems a bit more natural to me than using a helper class.
然后你可以使用orders.stream().flatMap(Order::streamOrders)
这对我来说似乎比使用帮助类更自然。
As a matter of interest, I tend to use these types of stream
methods for allowing use of collection fields rather than a getter for the field. If the user of the method does not need to know anything about the underlying collection or need to be able to change it then returning a stream is convenient and safe.
stream
出于兴趣,我倾向于使用这些类型的方法来允许使用集合字段而不是该字段的 getter。如果该方法的用户不需要了解有关底层集合的任何信息或需要能够更改它,则返回流是方便且安全的。
I will note that there is one risk in your data structure you should be aware of: an order could be part of several other orders and can even be part of itself. This means that it's pretty trivial to cause infinite recursion and a stack overflow:
我将指出,您应该注意数据结构中的一个风险:一个订单可能是其他几个订单的一部分,甚至可能是其自身的一部分。这意味着导致无限递归和堆栈溢出非常简单:
Order o1 = new Order();
o1.setOrders(Arrays.asList(o1));
o1.streamOrders();
There are lots of good patterns available to avoid these sorts of issues so please ask if you want some help in that area.
有很多好的模式可以避免这些问题,所以请询问您是否需要这方面的帮助。
You point out that you can't alter the Order
class. In that case I suggest you extend it to create your own safer version:
你指出你不能改变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);
}
}
This is a fairly safe cast because you expect users to use addOrder
. Not foolproof as they could still call getOrders
and add an Order
rather than a SafeOrder
. Again there are patterns to prevent that if you are interested.
这是一个相当安全的转换,因为您希望用户使用addOrder
. 并非万无一失,因为他们仍然可以调用getOrders
并添加一个Order
而不是SafeOrder
. 如果您有兴趣,还有一些模式可以防止这种情况发生。