Java 代碼界 3% 的王者?看我是如何解錯這 5 道題的

前些日子,阿里妹(妹子出題也這麼難)發表了一篇文章《懸賞徵集!5 道題徵集代碼界前 3% 的超級王者》——看到這個標題,我心裏很是很是激動,由於終於能夠證實本身技術很牛逼了。java

但遺憾的是,憑藉 8 年的 Java 開發經驗,我發現這五道題本身全解錯了!慘痛的教訓再次證實,我是那被秒殺的 97% 的工程師之一。bash

不過,好歹我這人臉皮特別厚,雖然全都作錯了,但仍是勇於坦然地面對本身。app

0一、原始類型的 float

第一題是這樣的,代碼以下:ide

public class FloatPrimitiveTest {
    public static void main(String[] args) {
        float a = 1.0f - 0.9f;
        float b = 0.9f - 0.8f;
        if (a == b) {
            System.out.println("true");
        } else {
            System.out.println("false");
        }
    }
}
複製代碼

乍一看,這道題也太簡單了吧?性能

1.0f - 0.9f 的結果爲 0.1f,0.9f - 0.8f 的結果爲 0.1f,那天然 a == b 啊。優化

但實際的結果居然不是這樣的,太傷自尊了。ui

float a = 1.0f - 0.9f;
System.out.println(a); // 0.100000024
float b = 0.9f - 0.8f;
System.out.println(b); // 0.099999964
複製代碼

加上兩條打印語句後,我明白了,原來發生了精度問題。this

Java 語言支持兩種基本的浮點類型: float 和 double ,以及與它們對應的包裝類 Float 和 Double 。它們都依據 IEEE 754 標準,該標準用科學記數法以底數爲 2 的小數來表示浮點數。lua

但浮點運算不多是精確的。雖然一些數字能夠精確地表示爲二進制小數,好比說 0.5,它等於 2-1;但有些數字則不能精確的表示,好比說 0.1。所以,浮點運算可能會致使舍入偏差,產生的結果接近但並不等於咱們但願的結果。spa

因此,咱們看到了 0.1 的兩個相近的浮點值,一個是比 0.1 略微大了一點點的 0.100000024,一個是比 0.1 略微小了一點點的 0.099999964。

Java 對於任意一個浮點字面量,最終都舍入到所能表示的最靠近的那個浮點值,遇到該值離左右兩個能表示的浮點值距離相等時,默認採用偶數優先的原則——這就是爲何咱們會看到兩個都以 4 結尾的浮點值的緣由。

0二、包裝器類型 Float

再來看第二題,代碼以下:

public class FloatWrapperTest {
    public static void main(String[] args) {
        Float a = Float.valueOf(1.0f - 0.9f);
        Float b = Float.valueOf(0.9f - 0.8f);
        if (a.equals(b)) {
            System.out.println("true");
        } else {
            System.out.println("false");
        }
    }
}
複製代碼

乍一看,這道題也不難,對吧?無非是把原始類型的 float 轉成了包裝器類型 Float,而且使用 equals 替代 == 進行判斷。

這一次,我覺得包裝器會解決掉精度的問題,因此我猜測輸出結果爲 true。但結果再次打臉——雖然我臉皮厚,但仍然能感受到臉有些微微的紅了起來。

Float a = Float.valueOf(1.0f - 0.9f);
System.out.println(a); // 0.100000024
Float b = Float.valueOf(0.9f - 0.8f);
System.out.println(b); // 0.099999964
複製代碼

加上兩條打印語句後,我明白了,原來包裝器並不會解決精度的問題。

private final float value;
public Float(float value) {
    this.value = value;
}
public static Float valueOf(float f) {
    return new Float(f);
}
public boolean equals(Object obj) {
    return (obj instanceof Float)
           && (floatToIntBits(((Float)obj).value) == floatToIntBits(value));
}
複製代碼

從源碼能夠看得出來,包裝器 Float 的確沒有對精度作任何處理,何況 equals 方法的內部仍然使用了 == 進行判斷。

0三、switch 判斷 null 值的字符串

來看第三題,代碼以下:

public class SwitchTest {
    public static void main(String[] args) {
        String param = null;
        switch (param) {
            case "null":
                System.out.println("null");
                break;
            default:
                System.out.println("default");
        }
    }
}
複製代碼

這道題就有點令我霧裏看花了。

咱們都知道,switch 是一種高效的判斷語句,比起 if/else 真的是爽快多了。尤爲是 JDK 1.7 以後,switch 的 case 條件能夠是 char, byte, short, int, Character, Byte, Short, Integer, String, 或者 enum 類型。

本題中,param 類型爲 String,那麼我認爲是能夠做爲 switch 的 case 條件的,但 param 的值爲 null,null 和 "null" 確定是不匹配的,我認爲程序應該進入到 default 語句輸出 default。

但結果再次打臉!程序拋出了異常:

Exception in thread "main" java.lang.NullPointerException
	at com.cmower.java_demo.Test.main(Test.java:7)
複製代碼

也就是說,switch () 的括號中不容許傳入 null。爲何呢?

我翻了翻 JDK 的官方文檔,看到其中有這樣一句描述,我直接搬過來你們看一眼就明白了。

When the switch statement is executed, first the Expression is evaluated. If the Expression evaluates to null, a NullPointerException is thrown and the entire switch statement completes abruptly for that reason. Otherwise, if the result is of a reference type, it is subject to unboxing conversion.

大體的意思就是說,switch 語句執行的時候,會先執行 switch () 表達式,若是表達式的值爲 null,就會拋出 NullPointerException 異常。

那究竟是爲何呢?

public static void main(String args[]) {
    String param = null;
    String s;
    switch((s = param).hashCode())
    {
    case 3392903: 
        if(s.equals("null"))
        {
            System.out.println("null");
            break;
        }
        // fall through

    default:
        System.out.println("default");
        break;
    }
}
複製代碼

藉助 jad,咱們來反編譯一下 switch 的字節碼,結果如上所示。原來 switch () 表達式內部執行的居然是 (s = param).hashCode(),當 param 爲 null 的時候,s 也爲 null,調用 hashCode() 方法的時候天然會拋出 NullPointerException 了。

0四、BigDecimal 的賦值方式

來看第四題,代碼以下:

public class BigDecimalTest {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal(0.1);
        System.out.println(a);
        BigDecimal b = new BigDecimal("0.1");
        System.out.println(b);
    }
}
複製代碼

這道題真不難,a 和 b 的惟一區別就在於 a 在調用 BigDecimal 構造方法賦值的時候傳入了浮點數,而 b 傳入了字符串,a 和 b 的結果應該都爲 0.1,因此我認爲這兩種賦值方式是同樣的。

但實際上,輸出結果徹底出乎個人意料:

BigDecimal a = new BigDecimal(0.1);
System.out.println(a); // 0.1000000000000000055511151231257827021181583404541015625
BigDecimal b = new BigDecimal("0.1");
System.out.println(b); // 0.1
複製代碼

這究竟又是怎麼回事呢?

這就必須看官方文檔了,是時候搬出 BigDecimal(double val) 的 JavaDoc 鎮樓了。

  1. The results of this constructor can be somewhat unpredictable. One might assume that writing new BigDecimal(0.1) in Java creates a BigDecimal which is exactly equal to 0.1 (an unscaled value of 1, with a scale of 1), but it is actually equal to 0.1000000000000000055511151231257827021181583404541015625. This is because 0.1 cannot be represented exactly as a double (or, for that matter, as a binary fraction of any finite length). Thus, the value that is being passed in to the constructor is not exactly equal to 0.1, appearances notwithstanding.

解釋:使用 double 傳參的時候會產生不可預期的結果,好比說 0.1 實際的值是 0.1000000000000000055511151231257827021181583404541015625,說白了,這仍是精度的問題。(既然如此,爲何不廢棄呢?)

  1. The String constructor, on the other hand, is perfectly predictable: writing new BigDecimal("0.1") creates a BigDecimal which is exactly equal to 0.1, as one would expect. Therefore, it is generally recommended that the String constructor be used in preference to this one.

解釋:使用字符串傳參的時候會產生預期的結果,好比說 new BigDecimal("0.1") 的實際結果就是 0.1。

  1. When a double must be used as a source for a BigDecimal, note that this constructor provides an exact conversion; it does not give the same result as converting the double to a String using the Double.toString(double) method and then using the BigDecimal(String) constructor. To get that result, use the static valueOf(double) method.

解釋:若是必須將一個 double 做爲參數傳遞給 BigDecimal 的話,建議傳遞該 double 值匹配的字符串值。方式有兩種:

double a = 0.1;
System.out.println(new BigDecimal(String.valueOf(a))); // 0.1
System.out.println(BigDecimal.valueOf(a)); // 0.1
複製代碼

第一種,使用 String.valueOf() 把 double 轉爲字符串。

第二種,使用 valueOf() 方法,該方法內部會調用 Double.toString() 將 double 轉爲字符串,源碼以下:

public static BigDecimal valueOf(double val) {
    // Reminder: a zero double returns '0.0', so we cannot fastpath
    // to use the constant ZERO. This might be important enough to
    // justify a factory approach, a cache, or a few private
    // constants, later.
    return new BigDecimal(Double.toString(val));
}
複製代碼

0五、ReentrantLock

最後一題,也就是第五題,代碼以下:

public class LockTest {
    private final static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        try {
            lock.tryLock();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
複製代碼

問題以下:

A: lock 是非公平鎖 B: finally 代碼塊不會拋出異常 C: tryLock 獲取鎖失敗則直接往下執行

很慚愧,我不知道 ReentrantLock 是否是公平鎖;也不知道 finally 代碼塊會不會拋出異常;更不知道 tryLock 獲取鎖失敗的時候會不會直接往下執行。無法做答了。

連續五道題解不出來,雖然我臉皮很是厚,但也以爲臉上火辣辣的,就像被人狠狠地抽了一個耳光。

容我研究研究吧。

1)lock 是非公平鎖

ReentrantLock 是一個使用頻率很是高的鎖,支持重入性,可以對共享資源重複加鎖,即當前線程獲取該鎖後再次獲取時不會被阻塞。

ReentrantLock 既是公平鎖又是非公平鎖。調用無參構造方法時是非公平鎖,源碼以下:

public ReentrantLock() {
    sync = new NonfairSync();
}
複製代碼

因此本題中的 lock 是非公平鎖,A 選項是正確的。

ReentrantLock 還提供了另一種構造方法,源碼以下:

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
複製代碼

當傳入 true 的時候爲公平鎖,false 的時候爲非公平鎖。

那公平鎖和非公平鎖到底有什麼區別呢?

公平鎖能夠保證請求資源在時間上的絕對順序,而非公平鎖有可能致使其餘線程永遠沒法獲取到鎖,形成「飢餓」的現象。

公平鎖爲了保證時間上的絕對順序,須要頻繁的上下文切換,而非公平鎖會減小一些上下文切換,性能開銷相對較小,能夠保證系統更大的吞吐量。

2)finally 代碼塊不會拋出異常

Lock 對象在調用 unlock 方法時,會調用 AbstractQueuedSynchronizertryRelease 方法,若是當前線程不持有鎖的話,則拋出 IllegalMonitorStateException 異常。

因此建議本題的示例代碼優化爲如下形式(進入業務代碼塊以前,先判斷當前線程是否持有鎖):

boolean isLocked = lock.tryLock();
if (isLocked) {
    try {
        // doSomething();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        lock.unlock();
    }
}
複製代碼

3)tryLock 獲取鎖失敗則直接往下執行

tryLock() 方法的 Javadoc 以下:

Acquires the lock if it is available and returns immediately with the value true. If the lock is not available then this method will return immediately with the value false.

中文意思是若是鎖能夠用,則獲取該鎖,並當即返回 true,若是鎖不可用,則當即返回 false。

針對本題的話, 在 tryLock 獲取鎖失敗的時候,程序會執行 finally 塊的代碼。

0六、最後

阿里妹出的這五道題仍是蠻有深度的,我相信有很多朋友在實際的項目應用中已經遇到過了。聽說這五道題背後的解決思路,將在《Java開發手冊》華山版中首次披露!

PS:歡迎關注「沉默王二」公衆號,後臺回覆關鍵字「java」便可得到。

相關文章
相關標籤/搜索