謎題87:緊張的關系
在數學中,等號(=)定義了一種真實的數之間的等價關系(equivalence relation)。這種等價關系將一個集合分成許多等價類(equivalence class),每個等價類由所有相互相等的值組成。其他的等價關系包括有所有三角形集合上的“全等”關系和所有書的集合上的“有相同頁數”的關系等。事實上,關系 ~ 是一種等價關系,當且僅當它是自反的、傳遞的和對稱的。這些性質定義如下:
1,自反性:對於所有x,x ~ x。也就是說,每個值與其自身存在關系 ~ 。
2,傳遞性:如果x ~ y 並且y ~ z,那麽x ~ z。也就是說,如果第一個值與第二個值存在關系 ~,並且第二個值與第三個值存在關系 ~ ,那麽第一個值與第三個值也存在關系 ~ 。
3,對稱性:如果x ~ y,那麽y ~ x。也就是說,如果第一個值和第二個值存在關系 ~ ,那麽第二個值與第一個值也存在關系 ~ 。
如果你看了謎題29,便可以知道操作符 == 不是自反的,因為表達式( Double.NaN == Double.NaN )值為false,表達式( Float.NaN == Float.NaN )也是如此。但是操作符 == 是否還違反了對稱性和傳遞性呢?事實上它並不違反對稱性:對於所有x和y的值,( x == y )意味著( y == x )。 傳遞性則完全是另一回事。 謎題35為操作符 == 作用於原始類型的數值時不符合傳遞性的原因提供了線索。當比較兩個原始類型數值時,操作符 == 首先進行二進制數據類型提升(binary numeric promotion)[JLS 5.6.2]。這會導致這兩個數值中有一個會進行拓寬原始類型轉換(widening primitive conversion)。大部分拓寬原始類型轉換是不會有問題的,但有三個值得註意的異常情況:將int或long值轉換成float值,或long值轉換成double值時,均會導致精度丟失。這種精度丟失可以證明 == 操作符的不可傳遞性。
實現這種不可傳遞性的竅門就是利用上述三種數值比較中的兩種去丟失精度,然後就可以得到與事實相反的結果。可以這樣構造例子:選擇兩個較大的但不相同的long型數值賦給x和z,將一個與前面兩個long型數值相近的double型數值賦給y。下面的程序就是其代碼,它打印的結果是true true false,這顯然證明了操作符 == 作用於原始類型時具有不可傳遞性。
public class Transitive {
public static void main(String[] args) throws Exception {
long x = Long.MAX_VALUE;
double y = (double) Long.MAX_VALUE;
long z = Long.MAX_VALUE - 1;
System.out.print((x == y) + “ “); // Imprecise!
System.out.print((y == z) + “ “); // Imprecise!
System.out.println(x == z); // Precise!
}
}
本謎題的教訓是:要警惕到float和double類型的拓寬原始類型轉換所造成的損失。它們是悄無聲息的,但卻是致命的。它們會違反你的直覺,並且可以造成非常微妙的錯誤(見謎題34)。更一般地說,要警惕那些混合類型的運算(謎題5、8、24和31)。本謎題給語言設計者的教訓和謎題34一樣:悄無聲息的精度損失把程序員們搞糊塗了。
謎題87:緊張的關系