【轉】java8中謹慎使用實數做爲HashMap的key!

java8中謹慎使用實數做爲HashMap的key!

java8中一個hashCode()函數引起的血案
java8中一個hashCode()函數引起的血案
1.原由
2.實數的hashCode()
3.總結
1.原由
讓我關注到這一點的原由是一道題:牛客網上的max-points-on-a-line (若是連接打不開能夠直接搜索題目哦)java

題目是這麼描述的:函數

Given n points on a 2D plane, find the maximum number of points that lie on the same straight line.測試

大意就是給我一些點的X,Y座標,找到過這些點最多的直線,輸出這條線上的點數量
因而我就敲出瞭如下的代碼:.net

import java.util.HashMap;
import java.util.Map;debug

//class Point {
// int x;
// int y;
// Point(int a, int b) { x = a; y = b; }
//}code

public class Solution {
public int maxPoints(Point[] points) {
if (points.length <= 2) {
return points.length;
}orm

int max = 2;
for (int i = 0; i < points.length - 1; i++) {
Map<Float, Integer> map = new HashMap<>(16);
// 記錄垂直點數; 當前和Points[i]在一條線上的最大點數; 和Points[i]垂直的點數
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);
}blog

max = Math.max(max, cur + dup + 1);
}
return max;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
這段代碼在天真的我看來是沒啥問題的,可就是沒辦法過,通過長久的排查錯誤,我寫了如下代碼加在上面的代碼裏運行ci

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));
}
1
2
3
4
5
6
7
8
9
10
11
12
它輸出的,居然是2
也就是說,它認爲(3-3) / (3-2) 和 (3-3) / (-5-2) 不一樣? 什麼鬼…
通過debug,發現上述結果分別是0.0和-0.0
0.0 難道不等於 -0.0 ?
這時我內心已經一陣臥槽了,不過我仍是寫了驗證代碼:

System.out.println(0.0 == -0.0);
1
結果是True,沒問題啊,我凌亂了……
必定是java底層代碼錯了! 我沒錯……
又是一陣debug,我找到了這條語句:

map.put(d, map.get(d) == null ? 1 : map.get(d) + 1);
1
我以爲map.get()頗有問題, 它的源代碼是這樣的:

public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
1
2
3
4
唔,先得到hash()是吧,那我找到了它的hash函數:

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
1
2
3
4
再來,這裏是要比較h 和key的hashCode是吧,那咱們去看hashCode()函數

public native int hashCode();
1
這是一個本地方法,看不到源碼了,唔,,那我就使用它看看吧,測試一下不就行了嗎,我寫了如下的測試代碼:

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());
}
1
2
3
4
5
結果居然是True和False !!!
這個源頭終於找到了, 0.0 和 -0.0 的hashCode值是不一樣的 !

通過一番修改,我經過了這道題(其實精度也會有問題,應該使用BigDecimal的,不過牛客網要求沒那麼高。後來我想了想只有把直線方程寫成Ax+By+C=0的形式才能徹底避免精度問題)
接下來,探討下實數的hashCode()函數是個啥狀況:

2.實數的hashCode()
在程序執行期間,只要equals方法的比較操做用到的信息沒有被修改,那麼對這同一個對象調用屢次,hashCode方法必須始終如一地返回同一個整數。
若是兩個對象根據equals方法比較是相等的,那麼調用兩個對象的hashCode方法必須返回相同的整數結果。
若是兩個對象根據equals方法比較是不等的,則hashCode方法不必定得返回不一樣的整數。
——《effective java》
那麼咱們來看看,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));
1
2
輸出是True 和 False
好吧,兩者調用equals() 方法不相等,看來是知足了書裏說的邏輯的
那咱們看看Float底層equals函數咋寫的:

public boolean equals(Object obj) {
return (obj instanceof Float)
&& (floatToIntBits(((Float)obj).value) ==
floatToIntBits(value));
}
1
2
3
4
5
哦,原來是把Float轉換成Bits的時候發生了點奇妙的事,因而我找到了一切的源頭:

/**
* 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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
這文檔挺長的,也查了其它資料,看了半天終於搞懂了
就是說Java浮點數的語義通常遵循IEEE 754二進制浮點算術標準。IEEE 754標準提供了浮點無窮,負無窮,負零和NaN(非數字)的定義。在使用Java過程當中,一些特殊的浮點數一般會讓你們很迷惑

詳情請查看這篇文章: java中特殊且複雜的浮點數

裏面提到,當浮點運算產生一個很是接近0的負浮點數時,會產生「-0.0」,而這個浮點數不能正常表示

咱們能夠輸出一波0.0和-0.0的數據:

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));
1
2
3
4
結果:
0
-2147483648
0
-2147483648

就是說,存儲-0.0, 居然用的是0x80000000
這也是咱們熟悉的Integer.MIN_VALUE

3.總結java中浮點數的表示比較複雜,特別是牽涉到-0.0, NaN, 正負無窮這種,因此不適宜用來做爲Map的key, 由於可能跟咱們預想的不一致--------------------- 做者:0x落塵 來源:CSDN 原文:https://blog.csdn.net/qq_30219017/article/details/79689492 版權聲明:本文爲博主原創文章,轉載請附上博文連接!

相關文章
相關標籤/搜索