Search code examples
javajava-8java-streamcollectors

Group by two fields using Collectors


I have a java object Record:

public Record(ZonedDateTime day, int ptid, String name, String category, int amount) {
        this.day= day;
        this.id= id;
        this.name = name;
        this.category = category;
        this.amount = amount;
}

I'm grouping a list of Records by their day, and then creating a new Record which combines the amount field and returns a map:

Map<ZonedDateTime, Record> map = tempList.stream().collect(Collectors.groupingBy(Record::getDay,
                Collectors.collectingAndThen(
                        Collectors.reducing((r1, r2) -> new Record(r1.getDay(),Integer.toString(r1.getId),r1.getName(),
                                r1.getCategory(),r1.getAmount() + r2.getAmount())),
                        Optional::get)));

I want to group the list by day AND category. So if day and category are the same, I would like to combine the amount field in a new Record like I am already doing. I need to add in another Collectors.groupingBy clause but the syntax just hasn't been working. I believe the return type would be Map<ZonedDateTime, Map<String, List<Record>>>. I also then need to convert the returned map into a List.

I've been trying to go off of this example Group by multiple field names in java 8


Solution

  • You could simplify the whole construct by using Collectors.toMap:

    Map<List<Object>, Record> map = tempList.stream()
            .collect(Collectors.toMap(
                                r -> List.of(r.getDay(), r.getCategory()), // or Arrays.asList
                                Record::new,
                                Record::merge));
    

    The trick is to group by a composite key. In this case, we're using a List<Object> with both Record.day and Record.category. (List implements Object.hashCode and Object.equals as required, so it can be safely used as the key of any Map).

    For the reduction to work, we need a copy constructor and a merge method:

    public Record(Record r) {
        this(r.day, r.name, r.name, r.category, r.amount);
    }
    
    public Record merge(Record r) {
        this.amount += r.amount;
        return this;
    }
    

    Finally, to return the list of records, there's no need to do anything fancier than this:

    List<Record> result = new ArrayList<>(map.values());