Java Magazine上面有一個專門坑人的面試題系列: https://blogs.oracle.com/javamagazine/quiz-2。
這些問題的設計宗旨,主要是測試面試者對Java語言的瞭解程度,而不是爲了用彎彎繞繞的手段把面試者搞蒙。
如果你看過往期的問題,就會發現每一個都不簡單。
這些試題模擬了認證考試中的一些難題。 而 “中級(intermediate)” 和 “高級(advanced)” 指的是試題難度,而不是說這些知識本身很深。 一般來說,“高級”問題會稍微難一點。
先思考一個簡單的問題: 兩個
Integer
包裝類對象。 怎樣比較它們的值是否相等,有哪些方法?
問題(中級難度)
在開發中我們經常會使用包裝類(例如 Boolean
, Double
, 以及 Integer
等等)。
請看下面的代碼片段:
String one = "1";
Boolean b1 = Boolean.valueOf(one); // line n1
Integer i1 = new Integer(one);
Integer i2 = 1;
if (b1) {
System.out.print(i1 == i2);
}
執行結果是什麼, 請選擇:
- A、 拋出運行時異常
- B、 true
- C、 false
- D、 無任何輸出
答案和解析
這個問題考察原生數據的包裝類(primitive wrapper),主要是 Boolean
類比較生僻的 valueOf
工廠方法。
在認證考試和麪試中,這個問題可能不太容易碰到,因爲主要還是靠死記硬背, 大部分考試都會避免此類問題。
但是,這個問題從多個方面綜合考察了面試者對Java語言的理解和認識水平, 有一點小坑,但關鍵在於解答的過程。
包裝類主要提供了三種獲取對象實例的方法:
-
- 每個包裝類都有名爲
valueOf
的靜態工廠方法。
- 每個包裝類都有名爲
-
- 如果語義很清晰, 在代碼中將原生數據類型賦值給包裝類的變量,則會發生自動裝箱 (autoboxing)。 自動裝箱只是語法上的簡寫,它允許編譯器 (
javac
) 自動調用valueOf
方法, 目的是爲了編碼更簡潔。
- 如果語義很清晰, 在代碼中將原生數據類型賦值給包裝類的變量,則會發生自動裝箱 (autoboxing)。 自動裝箱只是語法上的簡寫,它允許編譯器 (
-
- 第三種方法是使用構造器, 也就是通過
new
關鍵字來調用構造函數。 實際上,在 Java 9 中已經不推薦使用第三種方法, 而本文的一個目標是解釋爲什麼不贊成使用它。
- 第三種方法是使用構造器, 也就是通過
在Java中,只要使用 new
關鍵字調用構造函數,只會發生兩種情況: 要麼成功創建指定類型的新對象並返回,要麼就拋異常。
這實際上是一個限制,如今一般是推薦使用工廠方法, 因爲工廠方法除了達成構造函數的效果之外, 還會有一些優化。
工廠方法的有些功能是用構造函數實現不了的: 比如返回與請求參數相匹配的已緩存的實例對象。
因爲 Integer
包裝器是不可變的, 表示相同數值的兩個Integer
對象一般是可以互換的。
因此,創建多個表示相同值的對象實例會浪費內存。
很多情況下,工廠方法返回的兩個對象允許使用 ==
來比較, 而不必每次都寫成 equals(Object o)
這種方式。
對於 Integer
類來說,一般只緩存了 -128 到 +127
範圍內的值。
這種行爲類似於在編碼中直接使用
"XXX"
這種字面量表示方式, 而不是new String("XXX")
。
工廠方法更加靈活:
- 如果有多個工廠方法,則每個方法都可以使用不同的名稱,因爲名稱不同,也就可以使用相同的入參聲明。
- 對於構造函數而言,因爲必須參數類型不同才能形成重載,也就不可能根據同樣的參數構造不同的對象。
第三個優點是, Java中用 new
調用構造函數只能返回固定類型的對象。
而用工廠方法則可以返回兼容的各種類型對象實例(例如接口的實現類,而且這是一種隱藏實現細節的絕佳方法)。
回到這個問題,最關鍵的地方在於, 我們使用 Boolean.valueOf(...)
方法時, 只會得到兩個常量對象: Boolean.TRUE
和 Boolean.FALSE
。
這兩個對象可以被重複利用,不會浪費多餘的內存。 如果使用 new
調用顯然是不可能的。
大部分包裝類的工廠方法, 如果傳入了 null
參數, 或者字符串參數不符合目標值的表現形式就會拋出異常,例如,Integer.valueOf("six")
就會拋異常。
但 java.lang.Boolean
類的工廠方法是個特例, 內部實現判斷的是非空(null
)並且等於 “true
”(忽略大小寫)。
內部實現如下所示:
public static boolean parseBoolean(String s) {
return ((s != null) && s.equalsIgnoreCase("true"));
}
如果滿足這兩個條件則返回 Boolean.TRUE
。
否則直接返回 Boolean.FALSE
。
這意味着: 如果傳入 null
或者無意義的字符串, 則會返回 Boolean.FALSE
,並不會拋出異常。
基於這點,我們可以確定 n1 行那裏不會拋出異常,而是返回 Boolean.FALSE
, 被賦值給變量 b1
。
因此,可以確定 選項A不正確
。
然後我們看一下 if
語句和裏面的比較代碼。
一般來說 if
語句小括號中的表達式必須是 boolean
類型。
顯然,這裏會自動將 Boolean
對象進行拆箱操作, 變爲 boolean
類型。
這算是Java的基礎知識,當然,如果在 Java 5 之前的版本這樣寫, 代碼確實會無法編譯。
即使有這樣的擔憂,但因爲沒有【編譯錯誤】的選項,所以我們不關注這個問題。
在這種情況下,我們已經確定 b1
所引用的對象值相當於 false
。 因此,if
判斷不通過,裏面的代碼不會被執行。
所以我們可以確定 選項D是正確的
。
雖然我們已經確定 if
語句內部的代碼沒有執行,但是面試過程中可能會問到: 如果執行了呢,又是什麼結果。
Java語言中有兩種形式的相等比較。
- 第一種是
==
運算符,是Java語法的一部分。 - 第二種是
equals(Object o)
方法,本質上是一個API。
每個對象都可以使用 equals(Object o)
方法,因爲這個方法是在 java.lang.Object
類中定義的。
除非某個類覆寫了equals方法,否則這個方法一般不定返回 true
。
下面我們主要討論 ==
運算符,如果對 equals 方法的實現感興趣, 請參考: Java中hashCode與equals方法的約定及重寫原則。
==
運算符比較兩個表達式的值。
聽起來很簡單,但是表達式的值可能有兩種不同的類型。這兩種類型使用 ==
的結果可能會不同。
順便說一下,這裏故意使用術語“表達式
”, 而變量是一種簡單的表達式。
表達式主要有兩種類型:
- 原生數據類型/基本數據類型 (
primitive
, 共8種:boolean
,byte
,short
,char
,int
,long
,float
,double
) - 引用類型(
reference
)。 引用類似於指針, 表示內存中某個對象的地址值(可以認爲是一個偏移量數值)。
如果表達式是原生數據類型,則表達式的值很直觀。 例如,如果 int
表達式的值爲 32
,則該表達式的值就是32
的二進制表示形式。
但問題是,如果變量是引用類型呢(例如,Integer
類型), 它所引用對象內部的值爲32
,那麼這個引用的值 並不是32
。
而是一個神祕的數字(引用地址),通過這個引用地址,JVM可以找到對應的 Integer
對象。
也就是說,對於引用類型(即除了8種原生數據類型之外的所有類型), ==
表達式判斷的是這兩個引用的內存地址值是否相等,即判斷它們是否引用了同一個對象。
最重要的是,即使兩個 Integer
對象裏面的值都是 32
,但如果它們是不同的對象, 那麼它們的引用地址也就不同,使用==
比較會返回 false
。
這一點應該很好理解,再看下面這樣的代碼:
Integer v1 = new Integer("1");
Integer v2 = new Integer("1");
System.out.print(v1 == v2);
這裏的輸出肯定是 false
。
前面提到過,new
關鍵字的任何調用,要麼產生一個新對象, 要麼拋異常。
這意味着 v2
和 v1
引用了不同的對象,==
操作的結果爲 false
。
換一種方式,如果有以下代碼:
Integer v1 = new Integer("1");
Integer v2 = 1;
System.out.print(v1 == v2);
這與面試題中的代碼很像,一個使用構造函數, 一個使用自動裝箱,可以肯定這也會輸出 false
。
構造函數創建的對象必定是唯一的新對象,因此,不可能 ==
自動裝箱爲工廠方法返回的對象。
不可變對象的工廠方法一般都會有特殊處理,只要在一個範圍內,並且參數相等,就返回同一個(緩存的)對象。
Integer
類的API文檔中,對 valueOf(int)
方法有如下說明:
“此方法將始終緩存
[-128 ~ 127]
範圍內的值, 可能還會緩存這個範圍之外的其他值。”
Integer v1 = Integer.valueOf(1);
Integer v2 = Integer.valueOf(1);
System.out.print(v1 == v2);
也就是說,上面這段代碼肯定會輸出 true
。
雖然只在 valueOf(int)
和 valueOf(String)
方法的文檔說明中提到了這個緩存保證。
但在實際的實現中, 其他包裝類也表現出相同的緩存行爲。
當然,這裏討論了兩個 Integer
對象: 一個是使用構造函數創建,另一個是使用自動裝箱創建(Integer.valueOf(int)
方法)。
假如我們稍微改變一下面試題中 if
語句,則輸出內容將爲 false
。
總結: 本文開始提到的面試題, 選項D是正確答案。 這裏只是附帶的討論。
相關鏈接
原文鏈接: https://blogs.oracle.com/javamagazine/quiz-intermediate-wrapper-classes