5

After researching I still can't find the specific solution for my problem. I have an "approximately equals" method that uses an epsilon, while my hashCode method uses the exact values. This breaks the precondition of HashSet when I compare the values.

@Override
public boolean equals(Object o) {
    if (o == this)
        return true;
    if (!(o instanceof EPoint)) {
        return false;
    }
    EPoint ePoint = (EPoint) o;
    return Math.abs(Math.abs(ePoint.lat) - Math.abs(lat)) < EPSILON && Math.abs(Math.abs(ePoint.lon) - Math.abs(lon)) < EPSILON;
}

@Override
public int hashCode() {
    return Objects.hash(lat, lon);
}

I can't find a way to make the hasCode() consistent with my equals method.

Alex Blasco
  • 793
  • 11
  • 22
  • !(o instanceof EPoint) you should compare with getClass, not instanceOf, otherwise, you risk problems with possible subclasses. And what on earth does this have to do with an equals: return Math.abs(Math.abs(ePoint.lat) - Math.abs(lat)) < EPSILON && Math.abs(Math.abs(ePoint.lon) - Math.abs(lon)) < EPSILON; – Stultuske Oct 30 '17 at 13:20
  • 9
    Don't use equals for approximate equality. Create a differently named method. – Kayaman Oct 30 '17 at 13:21
  • Why don't you just hash the difference regarding the delta instead of hashing both exact values? Wouldn't that give you the same hash for same deltas? Like you did in your `equals` method. – Zabuzard Oct 30 '17 at 13:22
  • 4
    @Zabuza that's not possible, because hashcode doesn't know of any delta. It's a single point. This is just abusing equals/hashCode, and should not be attempted at all. Make equals/hashCode consistent (or leave them out completely), and create your own mechanisms instead of attempting to rely on the ones provided by collections (which do not play well with something like approximate equality). – Kayaman Oct 30 '17 at 13:24
  • Ah I see. The `delta` is between two points. With this definition it doesn't make any sense to define such a `hash` for a **single** point. What are you trying to reach on a higher level? Maybe its enough to define the `difference` to another **pre-known location**. – Zabuzard Oct 30 '17 at 13:28
  • 4
    Like I said, it's really not a good idea to abuse hashCode/equals for something it wasn't meant for. His previous question was about `HashSets`, but this isn't the solution. The `equals()` method already violates the contract that states `if(x.equals(y))` and `y.equals(z)` then `x.equals(z)` (transitivity). – Kayaman Oct 30 '17 at 13:34
  • 4
    It is not possible to do what you are asking. Consider three instances `A < B < C` where `A` and `B` are within epsilon of each other and `B` and `C` are also within epsilon, but `A` and `C` are not. Individually, `A == B` and `B == C` but NOT `A == C`. This breaks your model and cannot be resolved. – Jim Garrison Oct 30 '17 at 13:35
  • 1
    The real problem is that you can't use a `HashSet` for this. You need another data structure that is capable of dealing with spatial coordinates, eg an [R-tree](https://en.wikipedia.org/wiki/R-tree) – fps Oct 30 '17 at 13:46
  • While I am creating a differently named method for approximate equality, I found as a temporary solution, return the hashCode rounded. `return Objects.hash((double)Math.round(lat * 10000d) / 10000d, (double)Math.round(lon * 10000d) / 10000d);` Is that an ugly practice? – Alex Blasco Oct 30 '17 at 14:19
  • 3
    It's not ugly, it's wrong. I recommend you **don't** implement those methods, and solve your problems without attempting to use `HashSets`. – Kayaman Oct 30 '17 at 14:34
  • @Kayaman I need to use HashSets because I don't want to check repeated items from two EList when I am comparing them. – Alex Blasco Oct 30 '17 at 14:46
  • 2
    No, you don't **need** to use HashSets. Plenty of code is written without using HashSets. If you had a consistent equals and hashCode, you could use HashSets, but now you can't. I saw your last question, I know what you're doing. Now you'll have to find another way. – Kayaman Oct 30 '17 at 14:52
  • 2
    @Alex The problem is that `HashSet` (which internally uses `HashMap`) is based on equality. And I mean, mathematical equality, as defined in the `Object.equals` method docs. If you want any map and list from the Java collections to work, then you need to stick to `Object.equals`' contract, there's no way out of it. What you are needing is a data structure that doesn't rely on equality, but on *proximity*. You might say 'Oh, but the difference is just a very small epsilon value'... However, all the algorithms and the general approach change dramatically because of this... – fps Oct 30 '17 at 14:56
  • 1
    If you want to check whether a spatial value (a geo point) lies within i.e. a rectangle (an area), then the approach cannot be solved with equality. Think about it: you don't need to check if `(1.11, 2.22) == (1.11, 2.22)`. Instead, you want to check if `(1.11, 2.22)` lies within some bounded area, i.e. (1.00, 1.00+epsilon, 2.00, 2.00+epsilon)`. – fps Oct 30 '17 at 15:03

2 Answers2

6

Your equals itself breaks the contract even before you get to hashCode because it isn't transitive.

This also immediately leads to the only consistent hashCode implementation being to return a constant, because for any two points there is a (very long) chain of intermediate points so that

  1. every two neighbors are equal, therefore

  2. every two neighbors must have the same hashCode, therefore

  3. beginning and end must have the same hashCode.

Now, this is a consistent implementation, but quite obviously a useless one.

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
2

I agree with Kayaman: The way your equals methos is implemented, you can have three EPoints (pointA,pointB,and pointC) with:

pointA.equals(pointB) //true
pointA.equals(pointC) //true
pointB.equals(pointC) //false

And this is not allowed. Creating a method with another name might be a solution.

If, however, you need your "almost Equal" objects to have the same hashcode, you could try a different approach:
Map every EPoint to an EPoint out of a grid. If, e.g. your EPoint's lat and lon where floats, you could map each EPoint to an EPoint with the rounded int-values.
If you need higher precision, you could extend on that and go into first, second,...decimal place).

If you do the equals() and the hashcode() method against the "mapped" Point, this should satisfy all requirements:

@Override
public boolean equals(Object o) {
    if (o == this)
        return true;
    if (!(o instanceof EPoint)) {
        return false;
    }
    EPoint ePoint = (EPoint) o;
    return this.gridLon() == ePoint.gridLon() && ePoint.gridLat() == this.gridLat();
}

@Override
public int hashCode() {
    return Objects.hash(this.gridLon(), this.gridLat());
}
  • The grid system wouldn't work for what he's looking for (and it would be a really bad idea even if did work). – Kayaman Oct 30 '17 at 13:51