Comparator : Equals method functionlity

14,106

Solution 1

Implementing the equals() method on a Comparator allows you to indicate that one comparator provides the same ordering as another comparator. It has nothing to do with how your elements are sorted. It is a very advanced and extremely rarely needed functionality. You are highly unlikely to ever encounter a situation where your comparator's equals() method will actually be invoked. I would suggest that you ignore it.

Solution 2

equals() method of the class, Comparator and Comparator require equals consistency because some Java collection classes may behave unpredictably if the compareTo() and equals() methods are not returning consistent results.

Java uses == operator to compare two primitives and/or to check if two variables refer to the same object. Example :

String apple1 = new String("apple");
String apple2 = new String("apple"); 
System.out.println(apple1.equals(apple2)); // true

StringBuilder app1 = new StringBuilder("apple"); 
StringBuilder app2 = new StringBuilder("apple"); 
System.out.println(app1.equals(app2));   // false

As you can see we get different behavior. Why is that? This happens because String class implements an equals() method, which checks that the values are the same. On the other hand, StringBuilder does not implement equals() method, instead it uses the implementation of equals() provided by Object class. And implementation provided(inherited) by Object class does only simply checks if the two referred objects are the same.

So to check if two objects are equivalent Java uses the equals() method and whenever you introduce your own type you must overrides equals() method if you do not want to rely on Object class implementation of equals() method

Let's for example introduce our own type : simple class Apple

public class Apple  {

    private int weight;
    private int cost;
    private String color;

Now how do would you decide if two apples are equal? By color, by weight, by price or something else? This is why you need to supply explicitly your own equals method, so objects of your type can be compared for equality.

Example below compares two Apple object for equality, and says that two object are equal if they belong to the same 'Apple' class and if their weight and cost is the same. Notice we do not compare by color, we assume that color is irrelevant in our case, which means that we accept the fact that apples of different colors, but with the same weight and same cost are considered equals.

    @Override
    public boolean equals(Object obj) {
        if ( !(obj instanceof Apple)) return false;    
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Apple other = (Apple) obj;
        if (cost != other.cost and weight != other.weight )
            return false;
        return true;
    }

You can implement any logic you like for your equals() method. Java equals() method is very important and it provides contract or important rules developer shall follow. I will not list them, you get them from here, they are logic and pretty straight forward.

There is another contract of equals() - whenever you override equals(), you are also expected to override hashCode() method. The reason behind that is because hashcode is used internally by some of Java collections, when object is stored as a key in the map.

Hashcode is a number that categorizes object into categories. Imagine you are given various sorts of apples(red, green, yellow) and asked to give them back when asked for particular sort. It would be much efficient and faster if you categorize them, put each in a different buckets accordingly and whenever asked for particular sort (for simplicity let's say red apple), and as you have already sorted and categorized them, you can retrieve them much faster. Hope it is clear now why do you need to implement hashcode(). hashcode() has it's own contract or rules same as equals() method. They pretty much make common sense :

First, result of hashcode() within same program shall not change. What it means that in your hascode() calculations you should not include variable that my change during execution of a program. For example if cost of Apple's is mutable, i.e. it may change, it is not good idea to include them into hashcode() calculation, otherwise results would be inconsistent.

Second rule says, that when called with two objects, if equals() returns true then calling hashCode() on each of those objects must retrieve the same result. But if equals() returns false when called with two objects, calling hashCode() on each of those objects does not necessary have to return a different result. Confusing? Why? Because hashCode() results are not required to be unique when called on unequal objects - you may put two unequal object in one bucket.

Now regarding Comparator - it is easier to understand it's logic when you introduce your own type, instead of using built-in types.

For example let's say we have class Apple with two attributes : weight and price and want to put out object this type into sorted collection TreeSet.

public class Apple  {

    private int weight;
    private int cost;

Now how do you want this apples to be sorted inside collection - do you want to sort them by weight or by price? How shall compiler decide?

Supplying appropriate Comparatoror Comparable allows you to pass your intent. Let's try to add object to collection.

  public class Apple {

    private int weight;
    private int cost;

  public static void main(String[] args) {
    Apple redApple = new Apple();
    redApple.setCost(10);
    redApple.setWeight(2);

    Apple greenApple = new Apple();
    greenApple.setCost(12);
    greenApple.setWeight(3);

    Set<Apple> apples = new TreeSet<>();
    apples.add(redApple);
    apples.add(greenApple);

    System.out.println(apples);
   } 

    public int getWeight() {
       return weight;
    }

   public void setWeight(int weight) {
       this.weight = weight;
    }

   public int getCost() {
       return cost;
   }

   public void setCost(int cost) {
     this.cost = cost;
    }

   @Override
   public String toString() {
       return "Apple [weight=" + weight + ", cost=" + cost + "]";
    }
}

If you run above code you will get RuntimeError : Apple cannot be cast to java.lang.Comparable, because compiler has to way to figure out how do you want to compare your apples. So let's fix it : Let's implement Comparable interface

  public class Apple implements Comparable<Apple> {

and override compareTo method

 @Override
    public int compareTo(Object obj) {

        int cost = ((Apple) obj).getCost();
        return this.getCost() - cost; // sorting in ascending order.

        // change to this to sort in Descending order
        // return cost - this.getCost();
    }

Now with this changes, let's run our code :

[Apple [weight=2, cost=10], Apple [weight=3, cost=12]]

Our collection is sorted by cost in ascending order.

Now what if you don't have access to Apple class and you can't change source code to implement Comparable.

This is where Comparator helps.

Remove implements Comparable, as we assume we can't modify this class

public class Apple {

and remove method

@Override
public String toString() {
    return "Apple [weight=" + weight + ", cost=" + cost + "]";
}

Add comparator implementation :

  public class AppleComparator implements Comparator<Apple> {

        @Override
        public int compare(Apple app1, Apple app2) {
            return app1.getCost() - app2.getCost();
        }

}

Now I can just supply Comparator to collection to express my intend

Set<Apple> apples = new TreeSet<>(new AppleComparator());

Collection again will be sorted by cost, accordingly to supplied Comparator. So we need to supply either Comparator or Comparable in order to get them stored in the collection, particularly in TreeSet.

Now regarding your question : There is no connection between Comparator and equals() method . There is only required consistency between Comparable (compareTo() method ) and equals() method.

Example - in above mentioned Apple class, in version that implements Comparable, we introduce new logic for determining equality. The compareTo() method returns 0 if two objects are equal, while your equals() method returns true if two objects are equal.

A natural ordering that uses compareTo() required to be consistent with equals iff x.equals(y) is true whenever x.compareTo(y) equals 0. So you have to make your Comparable class consistent with equals because some Java collection classes may behave unpredictably if the compareTo() and equals() methods are not returning consistent results.

The following example shows compareTo() method that is not consistent with equals:

public class Apple implements Comparable<Apple> { 

private int weight;
private int cost;
private String color;

public boolean equals(Object obj) {
   if(!(obj instanceof Apple)) { 
      return false;
  }
   Apple other = (Apple) obj; 
    return this.weight == other.weight;
  }

public int compareTo(Apple obj) {
    return this.cost.compareTo(obj.cost); }
 }

If we want to sort Apple objects by cost, but cost may not be unique. There could two objects with the same cost. Therefore, the return value of compareTo() might not be 0 when comparing two equal Apple objects, which mean that this compareTo() method is not consistent with equals().

Solution 3

The equals() method that you have override, is in the MyComparator class so it will be used if you need to compare 2 instance of Mycomparator ;)

And that's not what you're doing, you are comparing int so yes the compare() method is usefull because it's the one which will be used for sorting the Treeset but the equals method here won't be used

In 99% of the time, you don't need to override equals method in classes that implements Comparator, because they are here just to compare values not themself to another one, and in fact because most of the time they have to attributes, you won't have comparator equals.

Solution 4

I don't understand why you are linking the Comparator and the equals() method. The interfaces like Comparable and Comparatorare used for giving a comparison mechanism, like if you are putting data into a collection which is designed for sorting purpose, then it should have a comparison mechanism. Here is an example

package snippet;

import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;

class Person {

    private int id;
    private String name;
    private String code;
    private double salary;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getCode() {
        return code;
    }
    public void setCode(String code) {
        this.code = code;
    }
    public double getSalary() {
        return salary;
    }
    public void setSalary(double salary) {
        this.salary = salary;
    }
    public boolean equals(Object obj) {         
        if(obj != null && (obj instanceof Person)) {
            Person other = (Person) obj;
            return this.code.equals(other.getCode());
        }
        return false;
    }
    public Person(int id, String name, String code, double salary) {
        super();
        this.id = id;
        this.name = name;
        this.code = code;
        this.salary = salary;
    }
}

public class EqualsMethodImpl
{
    public static void main(String[] args) {
        Set<Person> set = new TreeSet<Person>();
        Person p1 = new Person(1, "Sam", "M-1-SAM-50", 50000.00);
        Person p2 = new Person(2, "Diaz", "M-1-SAM-35", 35000.00);
        Person p3 = new Person(3, "Remy", "M-1-SAM-100", 100000.00);
        Person p4 = new Person(4, "Cesar", "M-1-SAM-80", 80000.00);
        Person p5 = new Person(5, "Rino", "M-1-SAM-5", 5000.00);

        set.add(p1);
        set.add(p2);
        set.add(p3);
        set.add(p4);
        set.add(p5);

        printPersons(set);
    }

    private static void printPersons(Set<Person> set) {
        System.out.println("Id\tName\tCode\t\tSalary");
        Iterator<Person> perItr = set.iterator();       
        while(perItr.hasNext()) {
            Person p = perItr.next();
            System.out.println(p.getId()+"\t"+p.getName()+"\t"+p.getCode()+"\t"+p.getSalary());
        }
    }
}

While you running this code you will get ClasCastException at line set.add(p1);, why because the collection TreeSet is confused about on which attribute of Peron class should it do the sorting. That's where the interfaces like Comparable and Comparator have their significance.

Add this code to your Person class

public int compareTo(Person other) {
    Double person1Salary = this.getSalary();
    Double person2Salary = other.getSalary();
    return person1Salary.compareTo(person2Salary);
}

by making your class Comparable type like Person implements Comparable<Person>

So by this code you are explaining the collection on which attribute we need to do the sorting. So when you print the content of Set it will be ordered on the base salary (ascending by default).

The Wrapper classes like Byte, Short, Integer, Long, Float, Double and other predefied classes like String all are implementing Comaparable interface and that's why you can simply pass them to a Sortable collection.

Now coming to equals method, it is used to check the equality of two objects, the traditional equals method inherited from Object class will check equality based on address locations. So take a case in Person class where I haven't overrided equals method and I'm writing a code like this

Person p1 = new Person(1, "Sam", "M-1-SAM-50", 50000.00);
Person p2 = new Person(1, "Sam", "M-1-SAM-50", 50000.00);
System.out.println(p1.equals(p2));

It will return false because both are two different objects placed in two different address locations, but we know that the objects are having data of the same person. That's the place where we override equals() method like I did in my Person class based on Person code. In that case System.out.println(p1.equals(p2)); will print true because the codes are equal. equals() method helps us in finding the duplicates. I hope this helps.

Share:
14,106
Admin
Author by

Admin

Updated on June 04, 2022

Comments

  • Admin
    Admin about 2 years

    Actually i am going through one of the tutorial in which it mentioned that when we need to implement the comparator interface we can override equals method(However it is not necessary to override).

    So just to understand better

    i override the method as below

    Test.java

     import java.util.TreeSet;
    
    public class Test
    {
        public static void main(String[] args)
        {
            TreeSet t = new TreeSet(new MyComparator());
            t.add(1);
            t.add(1);
            t.add(2);
            System.out.println(t);
        }
    }
    

    MyComparator.java

    import java.util.Comparator;

    public class MyComparator
        implements Comparator
    {
        @Override
        public int compare(Object o1, Object o2)
        {
            Integer i1 = (Integer) o1;
            Integer i2 = (Integer) o2;
            return i1.compareTo(i2);
        }
    
        @Override
        public boolean equals(Object o1)
        {
            return false;
        }
    }
    

    for other scenario

    import java.util.Comparator;
    
    public class MyComparator
        implements Comparator
    {
        @Override
        public int compare(Object o1, Object o2)
        {
            Integer i1 = (Integer) o1;
            Integer i2 = (Integer) o2;
            return i1.compareTo(i2);
        }
    
        @Override
        public boolean equals(Object o1)
        {
            return true;
        }
    }
    

    Now no matter what i return from equals method either true or false .. it is returning the same treeset value. if anyone can clear the concept of functionlity of equals method please