JiKra
JiKra

Reputation: 1790

How to sort a list/stream using an unknown number of comparators?

I have the following snippet:

List<O> os = new ArrayList<>();
os.add(new O("A", 3, "x"));
os.add(new O("A", 2, "y"));
os.add(new O("B", 1, "z"));

Comparator<O> byA = Comparator.comparing(O::getA);
Comparator<O> byB = Comparator.comparing(O::getB);

// I want to use rather this...    
List<Comparator<O>> comparators = new ArrayList<>();
comparators.add(byA);
comparators.add(byB);

os.stream()
    .sorted(byB.thenComparing(byA))
    .forEach(o -> System.out.println(o.getC()));

As you can see, I sort using explicitly two comparators. But what if I have the unknown number of comparators in some list and I want to sort by them all? Is there any way? Or should rather use the old fashion way comparator with multiple ifs?

Upvotes: 6

Views: 1758

Answers (3)

Grzegorz Piwowarek
Grzegorz Piwowarek

Reputation: 13783

If you have multiple comparators in a list or any other collection, you can replace them with a single one by performing the reduction on a Stream:

List<Comparator<String>> comparators = ...

Comparator<String> combined = comparators.stream()
  .reduce(Comparator::thenComparing)
  .orElse(someDefaultComparator); // empty list case

All instances will be composed together using thenComparing according to their order from the input list.


The same would be achievable using a non-stream approach by utilizing a simple for-loop:

Comparator<String> result = comparators.get(0);
for (int i = 1; i < comparators.size(); i++) {
    result = result.thenComparing(comparators.get(i));
}

Upvotes: 16

Wim De Rammelaere
Wim De Rammelaere

Reputation: 51

If you are allowed to use google guava, you could simply compound your Comparables.

https://google.github.io/guava/releases/snapshot/api/docs/com/google/common/collect/Ordering.html#compound-java.lang.Iterable-

Comparator comparator = Ordering.compound(comparators);

If no comparators are given, the original order will be kept.

Javadoc of guava on this method although proposes for Java 8:

list.stream().sorted(comparators.stream().reduce(defaultComparator, Comparator::thenComparing));

So the solution with 'compound' is recommended for Java 7 (or less).

Upvotes: 0

Szymon Stepniak
Szymon Stepniak

Reputation: 42184

You can reduce stream of comparators to a single comparator by calling .thenComparing() on accumulated comparator and current comparator in the iteration:

Optional<Comparator<O>> comparator = Optional.ofNullable(comparators.stream()
    .reduce(null, (acc, current) -> acc == null ? current : acc.thenComparing(current), (a, b) -> a));

os.stream()
    .sorted(comparator.orElse((a,b) -> 0))
    .forEach(o -> System.out.println(o.getC()));

In this example I use Optional<Comparator<O>> and wrap reduction result with Optional.ofNullable() to handle a case when with empty comparators list. Then you can decide when you pass a result to sorted() method what to do in case of an empty list - you can use (a,b)->0 comparator that does not sort anything.

Then it doesn't matter how many comparators you want to apply. But there is one but - the order of comparators in given collection matters. In given example I apply comparators in ascending order (starting from the first element of the list to the last one). It affects the result of sorting heavily.

For example, in your example you call byB.thenComparing(byA). It produces different result then byA.thenComparing(byB). I can assume that in some cases you want to control the order in which comparators are applied.

Live demo:

https://jdoodle.com/a/4Xz

Upvotes: 6

Related Questions