前些日子,阿里妹(妹子出題也這麼難)發表了一篇文章《懸賞徵集!5 道題徵集代碼界前 3% 的超級王者》——看到這個標題,我心裏很是很是激動,由於終於能夠證實本身技術很牛逼了。java
但遺憾的是,憑藉 8 年的 Java 開發經驗,我發現這五道題本身全解錯了!慘痛的教訓再次證實,我是那被秒殺的 97% 的工程師之一。bash
不過,好歹我這人臉皮特別厚,雖然全都作錯了,但仍是勇於坦然地面對本身。app
第一題是這樣的,代碼以下: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 結尾的浮點值的緣由。
再來看第二題,代碼以下:
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
方法的內部仍然使用了 ==
進行判斷。
來看第三題,代碼以下:
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
了。
來看第四題,代碼以下:
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 鎮樓了。
解釋:使用 double 傳參的時候會產生不可預期的結果,好比說 0.1 實際的值是 0.1000000000000000055511151231257827021181583404541015625,說白了,這仍是精度的問題。(既然如此,爲何不廢棄呢?)
解釋:使用字符串傳參的時候會產生預期的結果,好比說 new BigDecimal("0.1")
的實際結果就是 0.1。
解釋:若是必須將一個 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));
}
複製代碼
最後一題,也就是第五題,代碼以下:
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 方法時,會調用 AbstractQueuedSynchronizer
的 tryRelease
方法,若是當前線程不持有鎖的話,則拋出 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 塊的代碼。
阿里妹出的這五道題仍是蠻有深度的,我相信有很多朋友在實際的項目應用中已經遇到過了。聽說這五道題背後的解決思路,將在《Java開發手冊》華山版中首次披露!
PS:歡迎關注「沉默王二」公衆號,後臺回覆關鍵字「java」便可得到。