同事把實數(shù)作為 HashMap 的key,領導發(fā)飆了...
1.起因
讓我關注到這一點的起因是一道題:??途W(wǎng)上的max-points-on-a-line
題目是這么描述的:
Given n points on a 2D plane, find the maximum number of points that lie on the same straight line.
大意就是給我一些點的X,Y坐標,找到過這些點最多的直線,輸出這條線上的點數(shù)量
import java.util.HashMap;
import java.util.Map;
//class Point {
// int x;
// int y;
// Point(int a, int b) { x = a; y = b; }
//}
public class Solution {
public int maxPoints(Point[] points) {
if (points.length <= 2) {
return points.length;
}
int max = 2;
for (int i = 0; i < points.length - 1; i++) {
Map<Float, Integer> map = new HashMap<>(16);
// 記錄垂直點數(shù); 當前和Points[i]在一條線上的最大點數(shù); 和Points[i]垂直的點數(shù)
int ver = 0, cur, dup = 0;
for (int j = i + 1; j < points.length; j++) {
if (points[j].x == points[i].x) {
if (points[j].y != points[i].y) {
ver++;
} else {
dup++;
}
} else {
float d = (float)((points[j].y - points[i].y) / (double) (points[j].x - points[i].x));
map.put(d, map.get(d) == null ? 1 : map.get(d) + 1);
}
}
cur = ver;
for (int v : map.values()) {
cur = Math.max(v, cur);
}
max = Math.max(max, cur + dup + 1);
}
return max;
}
}這段代碼在天真的我看來是沒啥問題的,可就是沒辦法過,經過長久的排查錯誤,我寫了以下代碼加在上面的代碼里運行,搜索公眾號互聯(lián)網(wǎng)架構師復“2T”,送你一份驚喜禮包。 public static void main(String[] args) {
int[][] vals = {{2,3},{3,3},{-5,3}};
Point[] points = new Point[3];
for (int i=0; i<vals.length; i++){
points[i] = new Point(vals[i][0], vals[i][1]);
}
Solution solution = new Solution();
System.out.println(solution.maxPoints(points));
}它輸出的,竟然是2
也就是說,它認為(3-3) / (3-2) 和 (3-3) / (-5-2) 不同? 什么鬼… 經過debug,發(fā)現(xiàn)上述結果分別是0.0和-0.0 0.0 難道不等于 -0.0 ? 這時我心里已經一陣臥槽了,不過我還是寫了驗證代碼:
System.out.println(0.0 == -0.0);結果是True,沒問題啊,我凌亂了…… 又是一陣debug,我找到了這條語句: map.put(d, map.get(d) == null ? 1 : map.get(d) + 1);我覺得
map.get()很有問題, 它的源代碼是這樣的:public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}唔,先獲得
hash()是吧,那我找到了它的hash函數(shù):static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}再來,這里是要比較h 和key的hashCode是吧,那我們去看
hashCode()函數(shù)public native int hashCode();這是一個本地方法,看不到源碼了,唔,,那我就使用它看看吧,測試一下不就好了嗎,我寫了以下的測試代碼:搜索公眾號互聯(lián)網(wǎng)架構師復“2T”,送你一份驚喜禮包。
public static void main(String[] args) {
System.out.println(0.0 == -0.0);
System.out.println(new Float(0.0).hashCode() ==
new Float(-0.0).hashCode());
}結果竟然是True和False !!!
這個源頭終于找到了, 0.0 和 -0.0 的hashCode值是不同的 ! 經過一番修改,我通過了這道題(其實精度也會有問題,應該使用BigDecimal的,不過??途W(wǎng)要求沒那么高。后來我想了想只有把直線方程寫成Ax+By+C=0的形式才能完全避免精度問題) 接下來,探討下實數(shù)的hashCode()函數(shù)是個啥情況: 2.實數(shù)的hashCode()
在程序執(zhí)行期間,只要equals方法的比較操作用到的信息沒有被修改,那么對這同一個對象調用多次,hashCode方法必須始終如一地返回同一個整數(shù)。 如果兩個對象根據(jù)equals方法比較是相等的,那么調用兩個對象的hashCode方法必須返回相同的整數(shù)結果。 如果兩個對象根據(jù)equals方法比較是不等的,則hashCode方法不一定得返回不同的整數(shù)?!秂ffective java》 相關閱讀:2T架構師學習資料干貨分享 那么我們來看看,0.0和-0.0調用equals方法是否相等: System.out.println(new Float(0.0).equals(0.0f));
System.out.println(new Float(0.0).equals((float) -0.0));好吧,二者調用
equals()方法不相等,看來是滿足了書里說的邏輯的那我們看看Float底層equals函數(shù)咋寫的:
public boolean equals(Object obj) {
return (obj instanceof Float)
&& (floatToIntBits(((Float)obj).value) ==
floatToIntBits(value));
}哦,原來是把Float轉換成Bits的時候發(fā)生了點奇妙的事,于是我找到了一切的源頭:搜索公眾號互聯(lián)網(wǎng)架構師復“2T”,送你一份驚喜禮包。
/**
* Returns a representation of the specified floating-point value
* according to the IEEE 754 floating-point "single format" bit
* layout.
*
* <p>Bit 31 (the bit that is selected by the mask
* {@code 0x80000000}) represents the sign of the floating-point
* number.
* Bits 30-23 (the bits that are selected by the mask
* {@code 0x7f800000}) represent the exponent.
* Bits 22-0 (the bits that are selected by the mask
* {@code 0x007fffff}) represent the significand (sometimes called
* the mantissa) of the floating-point number.
*
* <p>If the argument is positive infinity, the result is
* {@code 0x7f800000}.
*
* <p>If the argument is negative infinity, the result is
* {@code 0xff800000}.
*
* <p>If the argument is NaN, the result is {@code 0x7fc00000}.
*
* <p>In all cases, the result is an integer that, when given to the
* {@link #intBitsToFloat(int)} method, will produce a floating-point
* value the same as the argument to {@code floatToIntBits}
* (except all NaN values are collapsed to a single
* "canonical" NaN value).
*
* @param value a floating-point number.
* @return the bits that represent the floating-point number.
*/
public static int floatToIntBits(float value) {
int result = floatToRawIntBits(value);
// Check for NaN based on values of bit fields, maximum
// exponent and nonzero significand.
if (((result & FloatConsts.EXP_BIT_MASK) ==
FloatConsts.EXP_BIT_MASK) &&
(result & FloatConsts.SIGNIF_BIT_MASK) != 0)
result = 0x7fc00000;
return result;
}這文檔挺長的,也查了其它資料,看了半天終于搞懂了
就是說Java浮點數(shù)的語義一般遵循IEEE 754二進制浮點算術標準。IEEE 754標準提供了浮點無窮,負無窮,負零和NaN(非數(shù)字)的定義。在使用Java過程中,一些特殊的浮點數(shù)通常會讓大家很迷惑 我們可以輸出一波0.0和-0.0的數(shù)據(jù):
System.out.println(Float.floatToIntBits((float) 0.0));
System.out.println(Float.floatToIntBits((float) -0.0));
System.out.println(Float.floatToRawIntBits(0.0f));
System.out.println(Float.floatToRawIntBits((float)-0.0));結果:
0
-2147483648
0
-2147483648就是說,存儲-0.0, 竟然用的是
0x80000000這也是我們熟悉的
Integer.MIN_VALUE3.總結
java中浮點數(shù)的表示比較復雜,特別是牽涉到
-0.0,NaN, 正負無窮這種,所以不適宜用來作為Map的key, 因為可能跟我們預想的不一致
