Home > front end >  Apply different transformations for each element in the stream
Apply different transformations for each element in the stream

Time:12-17

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() and joining() to join the name properties;

  • And to in order to group lists Collector flatMapping() in conjunction with toList().

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 or StringBuilder, 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 a Collection.

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;
    }
}
  • Related