简体   繁体   中英

Group a list inside an object by multiple attributes : Java 8

How do I group a list inside an object by two of the object attributes?

I'm using Drools 7.9.0 and Java 8. I have a Result class that is used as return to each matched Drools rule.

import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

@Data
@NoArgsConstructor
public class Result implements Serializable {

    private Integer id;
    private String name;
    private List<Occurrences> occurrences = new ArrayList<>();

    public void addOccurrence(Occurrences occurrence) {
        this.occurrences.add(occurrence);
    }
}

After Drools execution, I end up with a List<Result> . Converted to JSON it looks like this.

[
     {
        name: "Whatever"
        id: 0001,
        occurrences: [{
           (...)
        }]
    },
    {
        name: "Whatever"
        id: 0001,
        occurrences: [{
           (...)
        }]
    },
    {
        name: "Whatever"
        id: 0002,
        occurrences: [{
           (...)
        }]
    },
    {
        name: "Other thing"
        id: 0002,
        occurrences: [{
           (...)
        }]
    }
]

I need to group up my list of Result s so the ocurrences are grouped by id and name , like this.

[
    {
        name: "Whatever"
        id: 0001,
        occurrences: [{
           (...)
        }, {
           (...)
        }]
    },
    {
        name: "Whatever"
        id: 0002,
        occurrences: [{
           (...)
        }]
    },
    {
        name: "Other thing"
        id: 0002,
        occurrences: [{
           (...)
        }]
    }
]

What would be the best way to implement this? I have two options:

  1. Change Drools rules so my List<Result> is already structured the way I need. Maybe create a class with a addResult method that checks the list for id and name and add the occurrence to the right entry. But this is not ideal, because it will increase complexity inside the rules.
  2. Post-process my List<Result> so it groups the occurrences by id and name . But I have no idea how to do this in an optimized and simple way.

What's the best way to do the 2nd option?

You may do it like so,

Map<String, List<Result>> resultsWithSameIdAndName = results.stream()
    .collect(Collectors.groupingBy(r -> r.getId() + ":" + r.getName()));

List<Result> mergedResults = resultsWithSameIdAndName.entrySet().stream()
    .map(e -> new Result(Integer.valueOf(e.getKey().split(":")[0]), 
        e.getKey().split(":")[1],
        e.getValue().stream().flatMap(r -> r.getOccurrences().stream())
            .collect(Collectors.toList())))
        .collect(Collectors.toList());

A simple way of the 2nd option could be:

Map<String, List<Occurences>> resultsMapped = new HashMap<>();
List<Result> collect = results.forEach(r -> resultsMapped.merge(String.format("%s%d", r.name, r.id), 
            r.occurrences, (currentOccurences, newOccurences) -> {
               currentOccurences.addAll(newOccurences);
               return currentOccurences;
            });

Which store in a map your occurences, the key being the name and the id concatenated in a string. This key is not optimized.

You could also use some streams for that. This will group by ID and name, and then create a new Result object for each group, containing all occurrences of the group.

Collection<Result> processed = results.stream().collect(Collectors.groupingBy(r -> r.getId() + r.getName(), 
            Collectors.collectingAndThen(Collectors.toList(),
            l -> new Result(l.get(0).getId(), l.get(0).getName(), 
                    l.stream().flatMap(c -> c.getOccurrences().stream()).collect(Collectors.toList()))
    ))).values();

This requires a AllArgsConstructor to be added to the Result class, as well as some more getters, but you can adjust this as you like. The key used for grouping is not safe in this example.

I would add the following method to Result :

Result addOccurrences (List<Occurrences> occurrences) {
  this.occurrences.addAll (occurrences);
  return this;
}

And then you can use Streams:

List<Result> collect = results.stream ()
  .collect (Collectors.groupingBy (Result::getId, Collectors.groupingBy (Result::getName, Collectors.toList ())))
  .values ()
  .stream ()
  .map (Map::values)
  .flatMap (Collection::stream)
  .map (Collection::stream)
  .map (resultStream -> resultStream.reduce ((r0, r1) -> r0.addOccurrences (r1.getOccurrences ())))
  .filter (Optional::isPresent)
  .map (Optional::get)
  .collect (Collectors.toList ());

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM