'阿里最強面試題,8 年 Java 經驗我老淚縱橫'

Java 技術 人生第一份工作 純潔的微笑 2019-09-08
"

前些日子,阿里妹(妹子出題也這麼難)發表了一篇文章《懸賞徵集!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.100000024float 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.100000024Float 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 判斷 值的字符串

來看第三題,代碼如下:

public class SwitchTest { public static void main(String[] args) { String param = ; switch (param) { case "": System.out.println(""); 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 的值為 , 和 “” 肯定是不匹配的,我認為程序應該進入到 default 語句輸出 default。

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

Exception in thread "main" java.lang.PointerException at com.cmower.java_demo.Test.main(Test.java:7)

也就是說,switch 的括號中不允許傳入 。為什麼呢?

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

When the switch statement is executed, first the Expression is evaluated. If the Expression evaluates to , a PointerException 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 表達式,如果表達式的值為 ,就會拋出PointerException異常。

那到底是為什麼呢?

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

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

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.1000000000000000055511151231257827021181583404541015625BigDecimal b = new BigDecimal("0.1");System.out.println(b); // 0.1

這究竟又是怎麼回事呢?

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

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.10000000000000000555111512312578270211815834045410...

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

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。

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.

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

double a = 0.1;System.out.println(new BigDecimal(String.valueOf(a))); // 0.1System.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 塊的代碼。

大家都聽說過,學習設計模式非常的重要,那麼為什麼這麼重要呢,設計模式到底是什麼?打個比喻學編程就像學武功一樣。

武功要練得很牛逼,有兩樣東西不能丟。第一,是內功;第二,是武功祕籍。內功對應到編程就是我們編程基礎能力,那編程的設計模式就可以想象成武術中的武功祕籍。

所以Java要想能編好,設計模式要練好!

年輕人我看你骨骼清秀,是練武奇才

前1000人,50元/年

還有81位優惠名額

點轉發,看看你朋友能屬於3%嗎

"

相關推薦

推薦中...