Home > Software engineering >  What is the use of Comparator.comparing in respect to Comparator
What is the use of Comparator.comparing in respect to Comparator

Time:03-21

As far as I understand Comparator is a functional interface used to compare 2 objects with int compare(T o1, T o2) as the abstract function that takes two argument. but there is also a function Comparator.comparing(s->s) that can take a lambda function with only one input parameter. for Example to sort a Collection using streams

        List<String> projects=Arrays.asList("abc","def","sss","aaa","bbb");
        projects.stream().sorted((x,y)->y.compareTo(x)).forEach(s->System.out.println(s));
        projects.stream().sorted(Comparator.comparing(s->s)).forEach(s->System.out.println(s));

the sorted method takes a Comparator as a argument. So I am able to understand the first lambda expression but I wonder the use of Comparator.comparing(s->s) i.e. is Comparator.comparing() used for converting a single argument lambda expression to a double argument one or does it has some other use as well. Also please explain the part of the below function declaration.

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor)

CodePudding user response:

Comparator#compare(T o1, T o2) Compare two objects and returns an integer value based on this criteria:

  • A negative value if o1 < o2
  • A positive value if o1 > o2
  • Zero if they are equal.

Comparator.comparing(Function<? super T, ? extends U> key) returns a Comparator that compares by that sort key.

The main difference is that compare method provides a single point of comparison, whereas comparing chained to other functions to provide multiple points of comparison.

Suppose you have a class Person

public class Person implements Comparable {
    private String firstName;
    private String lastName;
    private int age;
    // rest of class omitted
}

if you compare two Person instances p1 vs p2 using compare(p1, p2), the comparison will be executed and the two objects will be sorted based on some natural ordering prescribed by the class. In contrast, if you want to compare the same two instances using comparing(), the comparison will be executed based on whichever criteria you choose to compare based on some attribute of the class. For example: Comparator.comparing(Person::getFirstName).

Because comparing returns a Comparator rather than a value, as I stated before, you can chain multiple comparisons. For instance: Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName);

As for the meaning of the return type <T, U extends Comparable<? super U>> Comparator<T>, you can find the explanation here.

I want to add that, classes must be comparable in order for compare(T o1, T o2) to work. String objects are comparable because they implement this interface. That said, if a class is not Comparable, you can still use comparing method because as I stated, you get to choose which attribute of the class you would like to use for the comparison and those attributes are likely to be comparable (i.e. String in the case of person's name or age in the above example).

CodePudding user response:

is Comparator.comparing() used for converting a single argument lambda expression to a double argument?

Yes, you can sort of think of it like that.

When sorting things, you are supposed to specify "given two things a and b, which of them is greater, or are they equal?" using a Comparator<T>. The a and b is why it has 2 lambda parameters, and you return an integer indicating your answer to that question.

However, a much more convenient way to do this is to specify "given a thing x, what part of x do you want to sort by?". And that is what you can do with the keyExtractor argument of Comparator.comparing.

Compare:

/*
given two people, a and b, the comparison result between a and b is the 
comparison result between a's name and b's name
*/
Comparator<Person> personNameComparator = 
    (a, b) -> a.getName().compareTo(b.getName());

/*
given a person x, compare their name
*/
Comparator<Person> personNameComparator = 
    Comparator.comparing(x -> x.getName()); // or Person::getName

The latter is clearly much more concise and intuitive. We tend to think about what things to sort by, rather than how exactly to compare two things, and the exact number to return depending on the comparison result.

As for the declaration for comparing:

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor)

The <T, U extends Comparable<? super U>> part first declares two generic type parameters - T is what the comparator compares (Person in the above case), and U is the type that you are actually comparing (String in the above case), hence it extends Comparable.

keyExtractor is the parameter you pass in, such as x -> x.getName(), that should answer the question of "when given a T, what is a U that you want to compare by?".

If you are confused by the ? super and ? extends, read What is PECS?.

If you haven't realised already, the implementation of comparing basically boils down to:

return (a, b) -> keyExtractor.apply(a).compareTo(keyExtractor.apply(b));

CodePudding user response:

Imagine you have your custom object:

public class Person {

  private String firstName;
  private String lastName;

  //getters and setters
}

Let's now say you have to compare people by their first names. One option is to write comparator like this:

Comparator<Person> comparator2 = (p1, p2) -> p1.getFirstName().compareTo(p2.getFirstName());

This should be obvious - get first name of one person, and compare it to the first name of other person. Other option is to write this same comparator using Comparator.comparing(Function<? super T,? extends U> keyExtractor). It will look like this:

Comparator<Person> comparator1 = Comparator.comparing(p -> p.getFirstName());

Or even better with method reference:

Comparator<Person> comparator1 = Comparator.comparing(Person::getFirstName);

The single argument you provide here is a Function, it accepts a single parameter, this is why your lambda also uses a single parameter. The function here works as a key extractor - it extracts from your object the keys to be compared. It's like saying, get first name from first object and compare it to first name of second object.

Just note, that keys extracted like this must be Comparable. In the example above extracted key is String, which implements comparable.

CodePudding user response:

TL;DR

A keyExtrator is provided to compare the object or any field of the object as long as the field also implements Comparable. What is returned is a Comparator that uses the fields compareTo method.

The rest of the story

Here is the complete method from the Comparator interface

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor)
    {
        Objects.requireNonNull(keyExtractor);
        return (Comparator<T> & Serializable)
            (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
    }

First, Comparable allows an object to provide a natural ordering by implementing the Comparable interface method of int compareTo(ob). So an object can compare itself to another instance of its own class (or perhaps ancestral class).

Comparator is a way to allow comparisons of related objects that don't implement the Comparable interface. It is called by compare(ob1, ob2).

The interface shown above allows a Comparator to be returned that makes use of the Object under comparison's Comparable implementation. But it also allows for a part of that object (e.g. a field) to be obtained via a keyExtractor. Then the Comparator for the extracted key which must also implement Comparable is returned.

What says that this these subsequent fields must also implement Comparable? Look at the signature. The return type if the keyExtractor is U and U extends Comparable<? super U>.

Here are some examples with explanations.

class Bar {
    int val;
    public int getVal(){
        return val;
    }
}

class FooBar implements Comparable<FooBar> {
    String svalue;
    Bar bar;
    int value;
    
    public FooBar(int v, Bar b, String svalue) {
        this.value = v;
        this.bar = b;
        this.svalue = svalue;
    }
    
    public String getSValue() {
        return svalue;
    }
    
    public int getValue() {
        return value;
    }
    
    public Bar getBar() {

        return bar;
    }
    
    public int compareTo(FooBar b) {
        return value < b.value ? -1 : value > b.value ? 1 : 0;
    }
    
    public String toString() {
        return "%s, %s, %s".formatted(value, bar, svalue);
    }
}

List<FooBar> list = new ArrayList<>(
        List.of(new FooBar(1, new Bar(), "11"),
                new FooBar(2, new Bar(), "AA"),
                new FooBar(3, new Bar(), "BA"),
                new FooBar(4, new Bar(), "CC"),
                new FooBar(5, new Bar(), "2A"),
                new FooBar(6, new Bar(), "AA11"),
                new FooBar(7, new Bar(), "11AA"),
                new FooBar(8, new Bar(), "AAG")));

Natural ordering sort of FooBar

list.sort(null);  //null says use natural ordering.
list.forEach(System.out::println);

prints

1, stackOverflow.Bar@681a9515, 11
2, stackOverflow.Bar@3af49f1c, AA
3, stackOverflow.Bar@19469ea2, BA
4, stackOverflow.Bar@13221655, CC
5, stackOverflow.Bar@2f2c9b19, 2A
6, stackOverflow.Bar@31befd9f, AA11
7, stackOverflow.Bar@1c20c684, 11AA
8, stackOverflow.Bar@1fb3ebeb, AAG

Sort on String svalue

Comparator<FooBar> comp = Comparator.comparing(FooBar::getSValue);
list.sort(comp);  // sort on svalue

prints

1, stackOverflow.Bar@33c7353a, 11
7, stackOverflow.Bar@681a9515, 11AA
5, stackOverflow.Bar@3af49f1c, 2A
2, stackOverflow.Bar@19469ea2, AA
6, stackOverflow.Bar@13221655, AA11
8, stackOverflow.Bar@2f2c9b19, AAG
3, stackOverflow.Bar@31befd9f, BA
4, stackOverflow.Bar@1c20c684, CC

Sort on object Bar

Comparator<FooBar> comp = Comparator.comparing(FooBar::getBar); // oops!

This won't work. Can't even define a comparator here because Bar does not implement Comparable as required by the signature. Why was svalue allowed? Because it is a String and the String class implements Comparable

But all is not lost. The following could be done using Integer.compare since Bar's value is an int.

Comparator<FooBar> comp1 = (f1,f2)-> {
          Bar b1 = f1.getBar();
          Bar b2 = f2.getBar();
          return Integer.compare(b1.getVal(),b2.getVal());
};

list.sort(comp1);
  • Related