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

沉默王二發表於2019-07-16

前些日子,阿里妹(妹子出題也這麼難)發表了一篇文章《懸賞徵集!5 道題徵集程式碼界前 3% 的超級王者》——看到這個標題,我內心非常非常激動,因為終於可以證明自己技術很牛逼了。

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

不過,好歹我這人臉皮特別厚,雖然全都做錯了,但還是敢於坦然地面對自己。

01、原始型別的 float

第一題是這樣的,程式碼如下:

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 啊。

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

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

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

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

但浮點運算很少是精確的。雖然一些數字可以精確地表示為二進位制小數,比如說 0.5,它等於 2-1;但有些數字則不能精確的表示,比如說 0.1。因此,浮點運算可能會導致舍入誤差,產生的結果接近但並不等於我們希望的結果。

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

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

02、包裝器型別 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 方法的內部仍然使用了 == 進行判斷。

03、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 了。

04、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));
}

05、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 塊的程式碼。

 

 

相關文章