Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overriding hashCode() to be consistent with equals() when equals() uses a similarity metric

Let say I have a class Car with fields color and model. I need to store cars in a collection in which I will have no duplicates (no 2 same cars). In the example below I am using a HashMap.

According to the Java documentation, if we have 2 Car objects car1 and car2 such that car1.equals(car2) == true, then it must also hold that car1.hashCode() == car2.hashCode(). So in this example, if I wanted to compare cars just by their color then I would use only the color field in equals() and hashCode(), as I did it in my code, and it works perfectly fine.

public class Car {
String color;
String model;

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((color == null) ? 0 : color.hashCode());
    return result;
}

@Override
public boolean equals(Object obj) {
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    Car other = (Car) obj;
    if (color == null) {
        if (other.color != null)
            return false;
    } else if (!color.equals(other.color))
        return false;
    return true;
}

public Car(String color, String model) {
    super();
    this.color = color;
    this.model = model;
}

@Override
public String toString() {
    return color + "\t" + model;
}

public static void main(String[] args) {
    Map<Car, Car> cars = new HashMap<Car, Car>();
    Car a = new Car("red", "audi");
    Car b = new Car("red", "bmw");
    Car c = new Car("blue", "audi");
    cars.put(a, a);
    cars.put(b, b);
    cars.put(c, c);
    for(Car car : cars.keySet()) {
        System.out.println(cars.get(car));
    }

}

}

The output is:

  • red bmw
  • blue audi

as expected.

So good so far. Now, I am experimenting with other ways for comparing 2 cars. I have provided a function to measure similarity between 2 cars. For the sake of the argument let say I have a method double similarity(Car car1, Car car2) which returns a double value in the interval [0,1]. I consider 2 cars to be equal if their similarity function returns value greater than 0.5. Then, I override the equals method:

@Override
public boolean equals(Object obj) {
    Car other = (Car) obj;
    return similarity(this, other) > 0.5;
}

Now, I don't know how to override the hashCode() to be sure that always will hold the hashCode - equals contract, e.g. 2 equal objects to have always equal hashCodes.

I have been thinking of using TreeMap instead of HashMap, just to avoid overriding the hashCode because I have no idea how to do it properly. But, I don't need any sorting, so I find using TreeMap in this problem not appropriate, and I think it would be more expensive in terms of complexity.

It would be very helpful if you could suggest me: a way of overriding the hashCode or an alternative of a different structure which would be more appropriate for my problem.

Thank you in advance!

like image 470
giliev Avatar asked Feb 09 '15 00:02

giliev


1 Answers

Although sprinter has covered some of the issues with your strategy, there is a more contract-based issue with your method. According to the Javadoc,

[equals] is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true

However, x can be similar to y and y can be similar to z with x being too far from z to be similar, so your equals method doesn't work.

like image 62
k_g Avatar answered Sep 28 '22 11:09

k_g