Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 336
JiKra Avatar asked Aug 03 '17 15:08

JiKra


People also ask

How do I sort a list in streams?

Sorting a List of Integers with Stream. Found within the Stream interface, the sorted() method has two overloaded variations that we'll be looking into. This methods returns a stream consisting of the elements of the stream, sorted according to natural order - the ordering provided by the JVM.

How do you sort a list with null values?

We are sorting using the nullsFirst() method, after sorting, null values will come in the start and then the sorting list of employees will come sorted by date of birth. To sort on natural order, after ordering null values, use Comparator. nullsFirst( Comparator. naturalOrder() ).


2 Answers

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));
}
like image 83
Grzegorz Piwowarek Avatar answered Sep 28 '22 19:09

Grzegorz Piwowarek


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

like image 35
Szymon Stepniak Avatar answered Sep 28 '22 18:09

Szymon Stepniak