【java解惑】java中那些反常識的小知識


一、Q:請爲 i == i + 1 ;  提供一個聲明使得條件成立。


    分析:一個數字永遠不會等於它自己加 1對吧!如果這個數字是無窮大的又會怎樣呢?Java 強制要求使用IEEE二進制浮點數算術標準IEEE 754,它可以讓你用一個 double 或 float 來表示無窮大。無窮大加 1 還是無窮大。如果 i 在聲明爲無窮大那麼i == i + 1 就成立。


    A:可以用任何被計算爲無窮大的浮點算術表達式來聲明 i ,例如double i = 1.0 / 0.0; 不過最好是能夠利用標準類庫提供的常量double i = Double.POSITIVE_INFINITY;

    

    總結:事實上不必將 i 聲明爲無窮大也可以使條件成立,任何足夠大的浮點數都可以實現這一目的。例如double i = 1.0e40;這樣做之所以可以起作用是因爲一個浮點數值越大,它和其後繼數值之間的間隔就越大。浮點數的這種分佈是用固定數量的有效位來表示它們的必然結果。對一個足夠大的浮點數加 1 不會改變它的值,因爲 1 不足以“填補它與其後繼者之間的空隙”。浮點數操作返回的是最接近其精確的數學結果的浮點數值。一旦毗鄰的浮點數值之間的距離大於 2,那麼對其中的一個浮點數值加 1 將不會產生任何效果,因爲其結果沒有達到兩個數值之間的一半。對於 float 類型加 1 不會產生任何效果的最小級數是 2^25,即 33,554,432;而對於 double 類型最小級數是 2^54,大約是 1.8*  10^16。毗鄰的浮點數值之間的距離被稱爲一個 ulp,它是“最小單位unit in the last place”的首字母縮寫詞。在5.0版中引入了 Math.ulp 方法來計算 float或 double 數值的 ulp。

    總之用一個 double 或一個 float 數值來表示無窮大是可以的。大多數人在第一次聽到這句話時多少都會有一點吃驚,可能是因爲我們無法用任何整數類型來表示無窮大的原因。

     第二點將一個很小的浮點數加到一個很大的浮點數上時將不會改變大的浮點數的值。這過於違背直覺了,因爲對實際的數字來說這是不成立的。我們應該記住二進制浮點算術只是對實際算術的一種近似。



二、Q:請爲 i!=i  提供一個聲明使得條件成立。


    分析:一個數字總是等於它自己對吧?!但是 IEEE 754 浮點算術保留了一個特殊的值用來表示一個不是數字的數量,這個值就是 NaN(“ 不是一個數字Not a Number” 的縮寫)。對於所有沒有良好的數字定義的浮點計算,例如 0.0/0.0其值都是它。規範中描述到NaN 不等於任何浮點數值包括它自身在內。因此如果 i 在被初始化爲 NaN那麼 i != i 就成立。

    

    A:可以用任何計算結果爲 NaN 的浮點算術表達式來初始化 i, 例如double i = 0.0 / 0.0;同樣爲了表達清晰可以使用標準類庫提供的常量double i = Double.NaN;


   總結: NaN 還有其他的驚人之處。任何浮點操作只要它的一個或多個操作數爲 NaN,那麼其結果爲 NaN。這條規則是非常合理的,但是它卻具有奇怪的結果。 例如下面的程序將打印 false:

class Test {
 public static void main(String[]  args) {
 double i = 0.0 / 0.0;
 System.out.println(i - i == 0);
 }
}

    這條計算 NaN 的規則所基於的原理是,一旦一個計算產生了 NaN,它就被損壞了,沒有任何更進一步的計算可以修復這樣的損壞。NaN 值有意使受損的計算繼續執行下去直到方便處理這種情況的地方爲止。

    總之float 和 double 類型都有一個特殊的 NaN 值用來表示不是數字的數量。對於涉及 NaN 值的計算,其規則很簡單也很明智,但是這些規則的結果可能是違背直覺的。



三、Q:請爲 i!=1+0; 提供一個聲明使得條件成立。但是不能像上題一樣使用浮點數。


    分析:與前一個題一樣這個謎題初看起來是不可能實現的。畢竟一個數字總是等於它自身加上 0,你被禁止使用浮點數,因此不能使用 NaN。而在整數類型中沒有 NaN 的等價物。我們必然可以得出這樣的結論,即 i 的類型必須是非數值類型的,並且這其中存在着解謎方案。唯一對 + 操作符有定義的非數值類型就是 String。+ 操作符被重載了,對於 String 類型它執行的不是加法而是字符串連接。如果在連接中的某個操作數具有非 String 的類型,那麼這個操作數就會在連接之前轉換成字符串。


    A:事實上i 可以被初始化爲任何值,只要它是 String 類型的即可。例如String i="搜索微信公衆號ape_it";


    總結:操作符重載是很容易令人誤解的。在本謎題中的加號看起來是表示一個加法,但是通過爲變量 i 選擇合適的類型即 String我們讓它執行了字符串連接操作。甚至是因爲變量被命名爲 i都使得本題更加容易令人誤解,因爲 i通常被當作整型變量名而被保留的。對於程序的可讀性來說好的變量名、方法名和類名至少與好的註釋同等重要。



四、Q:請提供一個對 i 的聲明,將下面的循環轉變爲一個無限循環:

while (i != 0) {
 i >>>= 1;
}

   

     分析:>>>=是對應於無符號右移操作符的賦值操作符。0 被從左移入到由移位操作而空出來的位上,即使被移位的負數也是如此。

    爲了使移位合法,i 必須是一個整數類型byte、char、short、int 或 long。無符號右移操作符把 0 從左邊移入。因此看起來這個循環執行迭代的次數與最大的整數類型所佔據的位數相同即 64 次。怎樣才能將它轉變爲一個無限循環呢?解決本謎題的關鍵在於,>>>=是一個複合賦值操作符。複合賦值操作符包括*=、/=、%=、+=、-=、 <<=、 >>=、 >>>=、&=、 ^=和| =。有關混合操作符的一個不幸的事實是,它們可能會自動地執行窄化原始類型轉換。這種轉換把一種數字類型轉換成了另一種更缺乏表示能力的類型。窄化原始類型轉換可能會丟失級數的信息或者是數值的精度。


    A:假設你在循環的前面放置了下面的聲明 short i = -1; 因爲 i 的初始值(short)0xffff是非 0 的,所以循環體會被執行。在執行移位操作時,第一步是將 i 提升爲 int 類型。所有算數操作都會對short、byte和 char 類型的操作數執行這樣的提升。這種提升是一個拓寬原始類型轉換,因此沒有任何信息會丟失。這種提升執行的是符號擴展,因此所產生的 int 數值是0xffffffff。然後這個數值右移 1 位,但不使用符號擴展,因此產生了 int數值 0x7fffffff。最後這個數值被存回到 i 中。爲了將 int 數值存入 short變量,Java 執行的是可怕的窄化原始類型轉換,它直接將高 16 位截掉。這樣就只剩下(short)0xffff 了,我們又回到了開始處。循環的第二次以及後續的迭代行爲都是一樣的,因此循環將永遠不會終止。


    總結:如果你將 i 聲明爲一個 short 或 byte 變量,並且初始化爲任何負數,那麼這種行爲也會發生。如果你聲明 i 爲一個 char,那麼你將無法得到無限循環,因爲char 是無符號的,所以發生在移位之前的拓寬原始類型轉換不會執行符號擴展。總之,不要在 short、byte 或 char 類型的變量之上使用複合賦值操作符。因爲這樣的表達式執行的是混合類型算術運算,它容易造成混亂。更糟的是,它們執行將隱式地執行會丟失信息的窄化轉型,其結果是災難性的。



五、Q:請爲 i <= j && j <= i && i != j ;提供一個聲明使得條件成立。

   

    分析:如果 i <= j 並且 j <= i,i 不是肯定等於 j 嗎?這一屬性對實數肯定有效。 事實上,它是如此地重要,以至於它有這樣的定義: 實數上的<=關係是反對稱的。Java 的<=操作符在 5.0 版之前是反對稱的,但是這從 5.0 版之後就不再是了 。

    在5.0 版之前,Java 的數字比較操作符(<、 <=、 >和>=)要求它們的兩個操作數都是原始數字類型的(byte、char、short、int、long、float 和 double)。但是在 5.0 版中,規範作出了修改,新規範描述道:每一個操作數的類型必須可以轉換成原始數字類型。

    在 5.0 版中,自動包裝(auto-boxing)和自動反包裝(auto-unboxing)被添加到了 Java 語言中。<=操作符在原始數字類型集上仍然是反對稱的,但是現在它還被應用到了被包裝的數字類型上。(被包裝的數字類型有: Byte、 Character、 Short、Integer、 Long、 Float 和 Double。)<=操作符在這些類型的操作數上不是反對稱的,因爲 Java 的判等操作符(==和!=)在作用於對象引用時,執行的是引用的比較,而不是值的比較。


    A:下面的聲明賦予表達式(i <= j && j <= i && i != j)的值爲 true:

Integer i = new Integer(0);
Integer j = new Integer(0);

    前兩個子表達式(i <= j 和 j <= i)在 i 和 j 上執行解包轉換,並且在數字上比較所產生的 int 數值。i 和 j 都表示 0,所以這兩個子表達式都被計算爲 true。第三個子表達式(i != j)在對象引 用 i 和 j 上執行標識比較,因爲它們都初始化爲一個新的 Integer 實例,因此,第三個子表達式同樣也被計算爲 true。 

    

    總結:當兩個操作數都是被包裝的數字類型時,數值比較操作符和判等操作符的行爲存在着根本的差異:數值比較操作符執行的是值比較,而判等操作符執行的是引用標識的比較。




注:本【java解惑】系列均是博主閱讀《java解惑》原書後將原書上的講解和例子部分改編然後寫成博文進行發佈的。所有例子均親自測試通過並共享在github上。通過這些例子激勵自己惠及他人。同時本系列所有博文會同步發佈在博主個人微信公衆號搜索“愛題猿”或者“ape_it”方便大家閱讀。如果文中有任何侵犯原作者權利的內容請及時告知博主以便及時刪除如果讀者對文中的內容有異議或者問題歡迎通過博客留言或者微信公衆號留言等方式共同探討。

源代碼地址https://github.com/rocwinger/java-disabuse


本文出自 “winger” 博客,謝絕轉載!

發佈了48 篇原創文章 · 獲贊 4 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章