Java坑人面試題系列: 包裝類(中級難度)

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語言的理解和認識水平, 有一點小坑,但關鍵在於解答的過程。

包裝類主要提供了三種獲取對象實例的方法:

    1. 每個包裝類都有名爲 valueOf 的靜態工廠方法。
    1. 如果語義很清晰, 在代碼中將原生數據類型賦值給包裝類的變量,則會發生自動裝箱 (autoboxing)。 自動裝箱只是語法上的簡寫,它允許編譯器 (javac) 自動調用valueOf方法, 目的是爲了編碼更簡潔。
    1. 第三種方法是使用構造器, 也就是通過 new 關鍵字來調用構造函數。 實際上,在 Java 9 中已經不推薦使用第三種方法, 而本文的一個目標是解釋爲什麼不贊成使用它。

在Java中,只要使用 new 關鍵字調用構造函數,只會發生兩種情況: 要麼成功創建指定類型的新對象並返回,要麼就拋異常。
這實際上是一個限制,如今一般是推薦使用工廠方法, 因爲工廠方法除了達成構造函數的效果之外, 還會有一些優化。

工廠方法的有些功能是用構造函數實現不了的: 比如返回與請求參數相匹配的已緩存的實例對象。
因爲 Integer 包裝器是不可變的, 表示相同數值的兩個Integer對象一般是可以互換的。
因此,創建多個表示相同值的對象實例會浪費內存。
很多情況下,工廠方法返回的兩個對象允許使用 == 來比較, 而不必每次都寫成 equals(Object o) 這種方式。
對於 Integer 類來說,一般只緩存了 -128 到 +127 範圍內的值。

這種行爲類似於在編碼中直接使用 "XXX" 這種字面量表示方式, 而不是 new String("XXX")

工廠方法更加靈活:

  • 如果有多個工廠方法,則每個方法都可以使用不同的名稱,因爲名稱不同,也就可以使用相同的入參聲明。
  • 對於構造函數而言,因爲必須參數類型不同才能形成重載,也就不可能根據同樣的參數構造不同的對象。

第三個優點是, Java中用 new 調用構造函數只能返回固定類型的對象。
而用工廠方法則可以返回兼容的各種類型對象實例(例如接口的實現類,而且這是一種隱藏實現細節的絕佳方法)。

回到這個問題,最關鍵的地方在於, 我們使用 Boolean.valueOf(...) 方法時, 只會得到兩個常量對象: Boolean.TRUEBoolean.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 關鍵字的任何調用,要麼產生一個新對象, 要麼拋異常。
這意味着 v2v1 引用了不同的對象,== 操作的結果爲 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

發佈了112 篇原創文章 · 獲贊 1704 · 訪問量 530萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章