10

I have a class with a float field. For example:

public class MultipleFields {
  final int   count;
  final float floatValue;

  public MultipleFields(int count, float floatValue) {
    this.count = count;
    this.floatValue = floatValue;
  }

}

I need to be able to compare instances by value. Now how do I properly implement equals & hashCode?

The usual way to implement equals and hashCode is to just consider all fields. E.g. Eclipse will generate the following equals:

  public boolean equals(Object obj) {
    // irrelevant type checks removed
    ....
    MultipleFields other = (MultipleFields) obj;
    if (count != other.count)
      return false;
    if (Float.floatToIntBits(floatValue) != Float.floatToIntBits(other.floatValue))
      return false;
    return true;
  }

(and a similar hashCode, that essentially computes count* 31 + Float.floatToIntBits(floatValue)).

The problem with this is that my FP values are subject to rounding errors (they may come from user input, from a DB, etc.). So I need a "tolerant" comparison.

The common solution is to compare using an epsilon value (see e.g. Comparing IEEE floats and doubles for equality). However, I'm not quite certain how I can implement equals using this method, and still have a hashCode that is consisten with equals.

My idea is to define the number of significant digits for comparison, then always round to that number of digits in both equals and hashCode:

long comparisonFloatValue = Math.round(floatValue* (Math.pow(10, RELEVANT_DIGITS)));

Then if I replace all uses of floatValue with comparisonFloatValue in equals and hashCode, I should get a "tolerant" comparison, which is consistent with hashCode.

  • Will this work?
  • Do you see any problems with this approach?
  • Is there a better way to do this? It seems rather complicated.
Community
  • 1
  • 1
sleske
  • 81,358
  • 34
  • 189
  • 227
  • Have you considered just **not using floating-point** and switching to a fixed-point representation instead? – Karl Knechtel Dec 08 '10 at 11:30
  • @Karl: Yes, good point, and yes, I did. The problem is that the DB holds the value as FP (which is hard to change, as it's not under our control), so I'd have to round when reading from the DB. I don't want to do this, as I feel it is better to consistenly use FP internally and only round on output. – sleske Dec 08 '10 at 11:35
  • Oops, just noted that I used the ^ operator. Of course I meant exponentiation, not the binary operator. Fixed to use `Math.pow`. – sleske Dec 08 '10 at 11:41

1 Answers1

11

The big problem with it is that two float values could still be very close together but still compare unequal. Basically you're dividing the range of floating point values into buckets - and two values could be very close together without being in the same bucket. Imagine you were using two significant digits, applying truncation to obtain the bucket, for example... then 11.999999 and 12.000001 would be unequal, but 12.000001 and 12.9999999 would be equal despite being much further apart from each other.

Unfortunately, if you don't bucket values like this, you can't implement equals appropriately because of transitivity: x and y may be close together, y and z may be close together, but that doesn't mean that x and z are close together.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Sorry, my comparisonFloatValue was calculated incorrectly (I believed Java uses `^` for exponentiation :-( ). Edited my answer. Like this, 0.99999999999999 and 1.0000000001 should compare as equal, unless you set RELEVANT_DIGITS to more than 9 or so. – sleske Dec 08 '10 at 11:42
  • Of course, your point still stands: Two FP values will be considered different in my implementation if they fall into different buckets, and that can happen for FP numbers which are arbitrarily close together. So this does not yield a "tolerant comparison" :-(. – sleske Dec 08 '10 at 11:46
  • @sleske: Java doesn't use ^ for exponentiation - that's XOR. Use Math.pow for exponentiation. My example was probably unfortunate - I'll edit. But yes, you fundamentally need to work out your requirements :) – Jon Skeet Dec 08 '10 at 11:48
  • 1
    Actually, this will effectively be a "tolerant comparison" if the FP values really have less than `RELEVANT_DIGITS` significant digits (i.e. they really are fixed-point numbers with less than `RELEVANT_DIGITS` after the decimal point) - which fortunately is the case in my application. For arbitrary FP values, this will not work (and I believe there is no solution in that case). – sleske Dec 08 '10 at 11:49
  • Actually, my last comment is kind of silly. Of course, a "tolerant" comparison which considers x,y as equal if `abs(x-y) – sleske Dec 28 '10 at 22:10