As others have stated, your latest implementation of .equals() violates his contract. You simply cannot implement it that way. And if you stop thinking about it, it makes sense, since your .equals() implementation is not intended to return true when two objects are actually equal, but when they are quite similar. But it looks pretty much the same as Java, or anywhere else.
Check the .equals() javadocs and you will see that any object that implements it must adhere to its contract:
The equals method implements an equivalence relation for non-zero object references:
This is reflective: For any nonzero reference x, x.equals (x) should return true
It is symmetrical: for any non-empty reference values x and y x.equals (y) should return true if and only if y.equals (x) returns true.
This is transitive: for any non-zero 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.
This is consistent: for any non-empty reference values x and y, several calls to x.equals (y) successively return true or successively return false if the information used in equal comparisons with objects does not change.
For any non-zero reference x, x.equals (NULL) should return false.
Your implementation of .equals() does not fulfill this contract:
- Depending on your implementation of
double similarity(Car car1, Car car2) it may not be symmetrical - This is clearly not transitive (well explained in previous answers)
- This can be consistent:
Consider an example slightly different from the one you gave in the comment:
'cobalt' will be equal to blue, while red will be different from blue
If you used some external source to calculate the similarity, for example, a dictionary, and if one day “cobalt” was not found as a record, you can return the similarity about 0.0, so the cars will not be equal. However, the next day you will understand that “cobalt” is a special kind of “blue”, so you add it to the dictionary, and this time when you compare the same cars, the similarities are very large (or around 1.0), so they are equal. It will be inconsistency. I don’t know how your affinity function works, but if it depends on something other than the data contained in the two objects you are comparing, you can violate the .equals() sequence restriction.
Regarding the use of TreeMap<Car, Whatever> , I don’t see how this could help. From TreeMap javadocs :
... the map interface is defined in terms of the equality operation, but the sorted map performs all key mappings using the compareTo (or comparison) method, so the two keys that are considered equal to this method are, from the point of view of an ordered mapping, equal.
In other words, in TreeMap<Car, Whatever> map , map.containsKey(car1) will return true iff car1.compareTo(car2) , returning exactly 0 for some car2 belonging to map . However, if the comparison did not return 0 , map.containsKey(car1) could return false , even though car1 and car2 were very similar in terms of your similarity function. This is because .compareTo() intended for ordering, not similarity.
So, the key point here is that you cannot only use map for your use , because this is just the wrong structure. In fact, you cannot use any Java structure that relies on .hashCode() and .equals() because you can never find an object that matches your key.
Now, if you want to find the car that most closely resembles a given car using your similarity() function, I suggest you use Guava HashBasedTable to create a table of similarity coefficients (or any other favorite name) between each car of your set.
For this approach, Car will need to implement .hashCode() and .equals() as usual (i.e. do not check only by color and, of course, without calling your similarity() function). For example, you can check the new attribute number Car .
The idea is to have a table that stores the similarities between each car, with its diagonal purity, since we already know that the car is similar to itself (in fact, it is equal to itself). For example, for the following vehicles:
Car a = new Car("red", "audi", "plate1"); Car b = new Car("red", "bmw", "plate2"); Car c = new Car("light red", "audi", "plate3");
the table will look like this:
abc a ---- 0.60 0.95 b 0.60 ---- 0.45 c 0.95 0.45 ----
As for the similarity values, I assume that cars of the same brand and the same color family are more similar than cars of the same color, but different brands, and that cars of different brands and more than one color are even less similar.
You may have noticed that the table is symmetrical. We could only store half the cells if space optimization were needed. However, according to the docs, the HashBasedTable optimized for accessing the row key, so let it simplify and allow further optimization as an exercise.
The search algorithm for the car that is most similar to this car can be described as follows:
- Get the given row of the car
- Return the car that is most similar to this car in the returned row, i.e. highest similarity series car
Here is some code that shows common ideas:
public class SimilarityTest { Table<Car, Car, Double> table; void initialize(Car... cars) { int size = cars.length - 1; // implicit null check this.table = HashBasedTable.create(size, size); for (Car rowCar : cars) { for (Car columnCar : cars) { if (!rowCar.equals(columnCar)) { // add only different cars double similarity = this.similarity(rowCar, columnCar); this.table.put(rowCar, columnCar, similarity); } } } } double similarity(Car car1, Car car2) { // Place your similarity calculation here } Car mostSimilar(Car car) { Map<Car, Double> row = this.table.row(car); Map.Entry mostSimilar = Maps.immutableEntry(car, Double.MIN_VALUE); for (Map.Entry<Car, Double> entry : row.entrySet()) { double mostSimilarCoefficient = mostSimilar.getValue(); double currentCoefficient = entry.getValue(); if (currentCoefficient > mostSimilarCoefficient) { mostSimilar = entry; } } return mostSimilar.getKey(); } public static void main(String... args) { SimilarityTest test = new SimilarityTest(); Car a = new Car("red", "audi", "plate1"); Car b = new Car("red", "bmw", "plate2"); Car c = new Car("light red", "audi", "plate3"); test.initialize(a, b, c); Car mostSimilarToA = test.mostSimilar(a); System.out.println(mostSimilarToA); // should be c Car mostSimilarToB = test.mostSimilar(b); System.out.println(mostSimilarToB); // should be a Car mostSimilarToC = test.mostSimilar(c); System.out.println(mostSimilarToC); // should be a } }
Regarding complexity ... Initializing a table takes O (n2), while finding the most similar car takes O (n). I am sure that this can be improved, that is, why put cars in the table, which, as you know, are not similar to each other? (we could only put cars with a similarity coefficient above a given threshold), or instead of finding a car with the highest similarity coefficient, we could stop searching when we find a car whose similarity coefficient is above another given threshold, etc.