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語言的理解和認識水平, 有一點小坑,但關鍵在於解答的過程。
包裝類主要提供了三種獲取物件例項的方法:
-
- 每個包裝類都有名為
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
相關文章
- Java中的包裝類Java
- Java 包裝類Java
- 【Java】基本資料型別包裝類面試題之一Java資料型別面試題
- PHP 個人面試題總結PHP面試題
- 【Java】基本型別包裝類Java型別
- educoder上的實訓題目(學習-Java包裝類之Byte類)Java
- Java中的基本型別包裝類 Integer 類該怎麼使用?Java型別
- Java 包裝類和基本型別Java型別
- java學習筆記(包裝類)Java筆記
- Java面試系列第2篇-Object類中的方法Java面試Object
- 01揹包面試題系列(一)面試題
- [JAVA] Java物件導向之包裝類,拆箱、裝箱Java物件
- 14.Java-Arrays(類)、基本型別包裝類、Integer(類)Java型別
- 【Java面試題系列】:Java中final finally finalize的區別Java面試題
- Java面試中,遇到這類面試題最吃虧!Java面試題
- 經歷過有難度的面試題面試題
- Java 異常 隨機數 包裝類Java隨機
- Java面試系列:Java面試題基礎系列228道(上)Java面試題
- Java常用類——包裝類 小白版個人推薦Java
- 包裝類
- Java面試鍊金系列 (1) | 關於String類的常見面試題剖析Java面試題
- 好程式設計師Java教程分享Java之包裝類與常用類程式設計師Java
- java初級面試題(二)Java面試題
- 七,包裝類
- Integer包裝類
- 包裝類(Wrapper)APP
- 物件,包裝類物件
- 冒險島中的BOSS難度評級及分析。
- JavaScript中揹包問題(面試題)JavaScript面試題
- PAT甲級考試題庫題目分類
- Java中atomic包中的原子操作類總結Java
- iOS 中級面試題iOS面試題
- iOS面試題 --- 中級iOS面試題
- Java-API-基本資料型別包裝類JavaAPI資料型別
- 【Java面試】Java面試題基礎系列212道(上)Java面試題
- JAVA集合類簡要筆記 - 內部類 包裝類 Object類 String類 BigDecimal類 system類Java筆記ObjectDecimal
- Java面試題講解,Java面試中最容易踩的坑請注意Java面試題
- Java填坑系列之LinkedListJava