IncompleteCoder
IncompleteCoder

Reputation: 153

java 8 stream groupingBy sum of composite variable

I have a class Something which contains an instance variable Anything.

class Anything {
    private final int id;
    private final int noThings;

    public Anything(int id, int noThings) {
        this.id = id;
        this.noThings = noThings;
    }
}

class Something {
    private final int parentId;
    private final List<Anything> anythings;

    private int getParentId() {
        return parentId;
    }

    private List<Anything> getAnythings() {
        return anythings;
    }

    public Something(int parentId, List<Anything> anythings) {
        this.parentId = parentId;
        this.anythings = anythings;
    }
}

Given a list of Somethings

List<Something> mySomethings = Arrays.asList(
    new Something(123, Arrays.asList(new Anything(45, 65),
                                     new Anything(568, 15), 
                                     new Anything(145, 27))),
    new Something(547, Arrays.asList(new Anything(12, 123),
                                     new Anything(678, 76), 
                                     new Anything(98, 81))),
    new Something(685, Arrays.asList(new Anything(23, 57),
                                     new Anything(324, 67), 
                                     new Anything(457, 87))));

I want to sort them such that the Something objects are sorted depending on the total descending sum of the (Anything object) noThings, and then by the descending value of the (Anything object) noThings

123 = 65+15+27 = 107(3rd)
547 = 123+76+81 = 280 (1st)
685 = 57+67+87 = 211 (2nd)

So that I end up with

List<Something> orderedSomethings = Arrays.asList(
    new Something(547, Arrays.asList(new Anything(12, 123),
                                     new Anything(98, 81), 
                                     new Anything(678, 76))),
    new Something(685, Arrays.asList(new Anything(457, 87),
                                     new Anything(324, 67), 
                                     new Anything(23, 57))),
    new Something(123, Arrays.asList(new Anything(45, 65),
                                     new Anything(145, 27), 
                                     new Anything(568, 15))));

I know that I can get the list of Anythings per parent Id

Map<Integer, List<Anythings>> anythings
            = mySomethings.stream()
            .collect(Collectors.toMap(p->p.getParentId(),
                    p->p.getAnythings()))
            ;

But after that I'm a bit stuck.

Upvotes: 2

Views: 922

Answers (3)

IncompleteCoder
IncompleteCoder

Reputation: 153

In the end I added an extra method to the Something class.

public int getTotalNoThings() {
  return anythings.stream().collect(Collectors.summingInt(Anything::getNoThings));
}

then I used this method to sort by total noThings (desc)

somethings = somethings.stream()
            .sorted(Comparator.comparing(Something::getTotalNoThings).reversed())
            .collect(Collectors.toList());

and then I used the code suggested above (thanks!) to sort by the Anything instance noThings

    somethings .stream().map(Something::getAnythings)
            .forEach(as -> as.sort(Comparator.comparing(Anything::getNoThings).reversed()));

Thanks again for help.

Upvotes: 0

tobias_k
tobias_k

Reputation: 82889

Unless I'm mistaken, you can not do both sorts in one go. But since they are independent of each other (the sum of the nothings in the Anythings in a Something is independent of their order), this does not matter much. Just sort one after the other.

To sort the Anytings inside the Somethings by their noThings:

mySomethings.stream().map(Something::getAnythings)
            .forEach(as -> as.sort(Comparator.comparing(Anything::getNoThings)
                                             .reversed()));

To sort the Somethings by the sum of the noThings of their Anythings:

mySomethings.sort(Comparator.comparing((Something s) -> s.getAnythings().stream()
                                                         .mapToInt(Anything::getNoThings).sum())
                            .reversed());

Note that both those sorts will modify the respective lists in-place.


As pointed out by @Tagir, the second sort will calculate the sum of the Anythings again for each pair of Somethings that are compared in the sort. If the lists are long, this can be very wasteful. Instead, you could first calculate the sums in a map and then just look up the value.

Map<Something, Integer> sumsOfThings = mySomethings.stream()
        .collect(Collectors.toMap(s -> s, s -> s.getAnythings().stream()
                                                .mapToInt(Anything::getNoThings).sum()));

mySomethings.sort(Comparator.comparing(sumsOfThings::get).reversed());

Upvotes: 2

Tagir Valeev
Tagir Valeev

Reputation: 100169

The problem of other solutions is that sums are not stored anywhere during sorting, thus when sorting large input, sums will be calculated for every row several times reducing the performance. An alternative solution is to create intermediate pairs of (something, sum), sort by sum, then extract something and forget about sum. Here's how it can be done with Stream API and SimpleImmutableEntry as pair class:

List<Something> orderedSomethings = mySomethings.stream()
        .map(smth -> new AbstractMap.SimpleImmutableEntry<>(smth, smth
                .getAnythings().stream()
                .mapToInt(Anything::getNoThings).sum()))
        .sorted(Entry.<Something, Integer>comparingByValue().reversed())
        .map(Entry::getKey)
        .collect(Collectors.toList());

There's some syntactic sugar available in my free StreamEx library which makes the code a little bit cleaner:

List<Something> orderedSomethings = StreamEx.of(mySomethings)
        .mapToEntry(smth -> smth
                .getAnythings().stream()
                .mapToInt(Anything::getNoThings).sum())
        .reverseSorted(Entry.comparingByValue())
        .keys().toList();

As for sorting the Anything inside something: other solutions are ok.

Upvotes: 0

Related Questions