I have a class Node
public class Node {
private String name;
private List<Integer> list;
// constructor, getters, etc.
}
I want to join Stream<Node>
into a single Node
so that I apply different aggregation for the name
and the list
. In my case, I want to concatenate all names and flatten all lists.
Example
Input:
nodes = [
{ Node(name="a", list=[1,2]) },
{ Node(name="b", list=[3,4]) }
]
Output:
result = { Node(name="ab", list=[1,2,3,4]) }
How can I do this with streams?
List<Node> nodes = getMyNodes();
nodes.stream().????
I know that I can use flatMap()
to flatten lists and that I can use Collectors.joining()
to join strings, but I don't know how to apply two different transformations on the same stream.
CodePudding user response:
Collectors.teeing()
You can achieve this by using Java 12 Collector teeing()
, which expects three arguments: two downstream Collectors and a merger
Function combining the results produced by these Collectors.
As downstream Collectors, we can make use of:
A combination of Collectors
mapping()
andjoining()
to join thename
properties;And to in order to group lists Collector
flatMapping()
in conjunction withtoList()
.
As the merger function, we can a reference to the all-arguments constructor of the Node
.
List<Node> nodes = getMyNodes();
Node aggregatedNode = nodes.stream()
.collect(Collectors.teeing(
Collectors.mapping(Node1::getName, Collectors.joining()),
Collectors.flatMapping(node -> node.getList().stream(), Collectors.toList()),
Node::new // method reference to the all-args constructor
));
Mutable Reduction
An alternative option would be to perform mutable reduction of the stream elements using by making use of collect()
operation and a custom accumulation type.
That's how such accumulation type can be implemented:
public class NodeJoiner implements Consumer<Node> {
private StringBuilder names = new StringBuilder();
private List<Integer> lists = new ArrayList<>();
@Override
public void accept(Node node) {
update(node.getName(), node.getList());
}
public NodeJoiner merge(NodeJoiner other) {
update(other.names, other.lists);
return this;
}
private void update(CharSequence name, List<Integer> list) {
names.append(name);
lists.addAll(list);
}
public Node toNode() {
return new Node(names.toString(), lists);
}
}
And the stream would look like this:
List<Node> nodes = getMyNodes();
Node aggregatedNode = nodes.stream()
.collect(Collector.of(
NodeJoiner::new,
NodeJoiner::accept,
NodeJoiner::merge,
NodeJoiner::toNode
));
Note
There might be a temptation to use reduce()
to perform mutable reduction, but it's not a use-case for reduce()
since this operation is meant to fold a stream by producing a new object at each step of folding (in other words, perform immutable reduction).
And Stream API documentation - Mutable reduction warns against such usage of reduce()
:
A mutable reduction operation accumulates input elements into a mutable result container, such as a
Collection
orStringBuilder
, as it processes the elements in the stream.If we wanted to take a stream of strings and concatenate them into a single long string, we could achieve this with ordinary reduction:
String concatenated = strings.reduce("", String::concat)
We would get the desired result, and it would even work in parallel. However, we might not be happy about the performance! Such an implementation would do a great deal of string copying, and the run time would be O(n^2) in the number of characters. A more performant approach would be to accumulate the results into a
StringBuilder
, which is a mutable container for accumulating strings. We can use the same technique to parallelize mutable reduction as we do with ordinary reduction.The mutable reduction operation is called
collect()
, as it collects together the desired results into a result container such as aCollection
.
The stream in this answer resembles the above code from the documentation, illustrating a not-recommended way of utilizing reduce
.
The answer by @azro is an example of correct usage of reduce
, a new object gets created at each step of folding the stream (but since we can accomplish the same using mutable objects, it would be performance wise to replace reduce()
with collect()
).
CodePudding user response:
You might be looking for reduce operaiton. Check the below code.
List<Node> list = Arrays.asList(new Node("a", Arrays.asList(1, 2)), new Node("b", Arrays.asList(3, 4)));
Node finalNode = new Node("", new ArrayList<>());
Node result = list.stream().reduce(finalNode, (finalResult, currentNode) -> {
finalResult.name = finalResult.name currentNode.name;
finalResult.list.addAll(currentNode.list);
return finalResult;
});
CodePudding user response:
You can merge 2 nodes again and again, until you get one, that is called reducing
List<Node> nodes = Arrays.asList(
new Node("a", Arrays.asList(1, 2)),
new Node("b", Arrays.asList(3, 4)),
new Node("c", Arrays.asList(5, 6))
);
Node result = nodes.stream().reduce(new Node(""), Node::merge);
System.out.println(result);
// Node{name='abc', list=[1, 2, 3, 4, 5, 6]}
class Node {
String name;
List<Integer> list;
public Node() {
this("");
}
public Node(String name) {
this(name, new ArrayList<>());
}
public Node(String name, List<Integer> list) {
this.name = name;
this.list = list;
}
@Override
public String toString() {
return "Node{" "name='" name '\'' ", list=" list '}';
}
public Node merge(Node other) {
Node n = new Node(name other.name);
n.list.addAll(this.list);
n.list.addAll(other.list);
return n;
}
}