轉載:http://www.importnew.com/2217.html
常見Java面試題 – 第一部分:非可變性(Immutability)和對象引用(Object reference)
英文原文: Java Success,編譯:ImportNew - 鄭雯
ImportNew注: 本文是ImportNew編譯整理的Java面試題系列文章之一。請看此係列相關面試題。你可以從這裏查看全部的Java面試系列。
一些比較核心的Java問題經常會用來考驗面試者的Java基本知識功底。這篇文章列出來了一些在我的書裏面沒有的面試題,通過這些面試題讀者也可以梳理一下Java基礎知識點。
Q1.下面的代碼片段會輸出什麼?
1
2
3
4
|
String
s = "
Hello " ; s
+= "
World " ; s.trim(
); System.out.println(s); |
A1.正確輸出是” Hello World “。
由於字符串前後都有空格,因爲有些人可能會認爲結果是”Hello World”。那麼,這個題目想要考察的是什麼呢?
1. 字符串對象(String Object)是非可變的(immutable),這個題目容易迷惑人的地方在s.trim( )這一行。
2. 理解對象引用和不可達對象會由垃圾回收器收集。
順着這個題目,你覺得還可以考察哪些概念呢?
1. 例如,上面的代碼中一共會生成幾個字符串對象,什麼時候這些對象會變成不可達對象從而被垃圾回收器回收。
2. 又比如,上面的代碼的效率如何?
最好的解釋方式是通過一個圖表來說明,如下:
如果需要輸出前後沒有空格的”Hello World”,那麼應該將s.trim( )再賦值給”s”。這個賦值操作可以讓s指向新創建出來的字符串對象。
上面的代碼也可以改寫成如下方式:
1
2
3
|
StringBuilder
sb = new StringBuilder( "
Hello " ); sb.append( "
World " ); System.out.println(sb.toString().trim(
)); |
StringBuilder不是一個線程安全的類,因此僅僅用作本地變量是沒有問題的。如果你希望用作實例變量,那麼可以選擇線程安全的StringBuffer類。想知道字符串操作背後的原理嗎?可以點擊這裏:String concatenation。
常見Java面試題 – 第二部分:equals與==
英文原文: Java Success,編譯:ImportNew - 鄭雯
ImportNew注: 本文是ImportNew編譯整理的Java面試題系列文章之一。你可以從這裏查看全部的Java面試系列。
Q2.下面的代碼片段的輸出是什麼?
1
2
3
4
5
6
7
8
|
Object
s1 = new
String( "Hello" ); Object
s2 = new
String( "Hello" ); if (s1
== s2) { System.out.println( "s1
and s2 are ==" ); } else
if
(s1.equals(s2)) { System.out.println( "s1
and s2 are equals()" ); } |
1
|
|
A2.輸出結果是:
s1 and s2 are equals()
可以用下面這個圖來解釋:
因此,上面的問題考察了面試者對”==” 和 “equals( )”在Java對象上如何應用的理解是否正確。前者比較引用,後者則比較對象中真正的值。
接着還可以有下面的問題:
Q.下面代碼片段的輸出是什麼?
1
2
3
4
5
6
7
8
|
Object
s1 = "Hello" ; Object
s2 = "Hello" ; if
(s1 == s2) { System.out.println( "s1
and s2 are ==" ); }
else
if
(s1.equals(s2)) { System.out.println( "s1
and s2 are equals()" ); } |
A.答案是:
s1 and s2 are ==
看上去這個答案和對前面一個問題所做的解釋似乎有所違背。事實上,這個例子(或者說,規則)比較特殊,是一個典型的flyweight 模式在字符串對象創建中的應用。這個模式通過減少對象的創建來節約內存。String對象會創建一個字符串池(a pool of string),如果當前準備新創建的字符串對象的值在這個池子中已經存在,那麼就不會生成新對象,而是複用池中已有的字符串對象。flyweight 模式的精髓就是對象複用。不過,只有採用Object s = “Hello”方式(而非用”new“關鍵字)聲明String對象的時候這個規則纔會被應用。
這是一個非常經常被採用的Java面試問題。
常見Java面試題 – 第三部分:重載(overloading)與重寫(overriding)
英文原文: Java Success,編譯:ImportNew - 鄭雯
ImportNew注: 本文是ImportNew編譯整理的Java面試題系列文章之一。你可以從這裏查看全部的Java面試系列。
這篇文章介紹的常見面試題是關於重載(overloading)方法和重寫(overriding)方法的。
Q.下面代碼片段的輸出結果是什麼?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public
class
MethodOverrideVsOverload { public
boolean
equals( MethodOverrideVsOverload other ) { System.out.println( "MethodOverrideVsOverload
equals method reached"
); return
true ; } public
static
void
main(String[] args) { Object
o1 = new
MethodOverrideVsOverload(); Object
o2 = new
MethodOverrideVsOverload(); MethodOverrideVsOverload
o3 = new
MethodOverrideVsOverload(); MethodOverrideVsOverload
o4 = new
MethodOverrideVsOverload(); if (o1.equals(o2)){ System.out.println( "objects
o1 and o2 are equal" ); } if (o3.equals(o4)){ System.out.println( "objects
o3 and o4 are equal" ); } } } |
A.輸出結果是:
MethodOverrideVsOverload equals method reached
objects o3 and o4 are equal
這個問題考察了哪些概念呢?
- Java語言中,一個類只能從一個類中繼承出來(也就是,單繼承結構),如果沒有顯式的標明所繼承自的類,那麼自動繼承自Object對象。
- 大多數的非final對象類方法都會被子類重寫(overridden):
public boolean equals(Object obj); // make note of this method
public int hashCode();
public String toString();
- 重載方法在編譯時起作用(例如,靜態綁定),重寫方法在運行時起作用(例如,動態綁定)。靜態綁定意味着JVM在編譯時決定調用的類或方法。而動態綁定時,JVM是在運行時決定調用的類或方法。動態綁定設計是多態的基礎。更多瞭解編譯時和運行時.
- 子類中重寫父類的對應方法必須遵循下面的規則:
參數 | 不可變(譯者注:包括參數類型和個數)。 |
返回類型 | 不可變,除了協變返回類型或其子類型(covariant (subtype) returns)。 |
異常 | 子類中可以拋出更少的異常,但絕對不能拋出父類中沒有定義的已檢查異常。 |
訪問權限 | 比父類中對應方法更寬鬆。 |
調用 | 運行時(也就是動態綁定),根據對象類型來決定調用的具體方法。 |
現在,再回頭看上面的代碼,MethodOverrideVsOverload 類中的”equals(MethodOverrideVsOverload other)” 方法並沒有重寫Object類中的”public boolean equals(Object obj)” 方法。這是因爲其違背了參數規則,其中一個是MethodOverrideVsOverload 類型,而另一個是Object類型。因此,這兩個方法是重載關係(發生在編譯時),而不是重寫關係。
因此,當調用o1.equals(o2)時,實際上調用了object類中的public boolean equals(Object obj)方法。這是因爲在編譯時,o1和o2都是Object類型,而Object類的equals( … )方法是比較內存地址(例如,Object@235f56和Object@653af32)的,因此會返回false。
當調用o3.equals(o4)時,實際上調用了MethodOverrideVsOverload 類中的equals( MethodOverrideVsOverload other )方法。這是因爲在編譯時,o3和o4都是MethodOverrideVsOverload類型的,因此得到上述結果。
接下來還可以怎麼提問呢?
Q.那怎麼解決上面的那個問題呢?
A.在Java5中,新增了註解,其中包括很好用的編譯時註解(compile time annotations)@override,來保證方法正確的重寫了父類方法。如果在上面的代碼中添加了註解,那麼JVM會拋出一個編譯錯誤。
因此,解決的方法就是給MethodOverrideVsOverload 類的boolean equals( MethodOverrideVsOverload other )方法添加@override註解。這樣的話編譯時就會有錯誤拋出來提示開發者某個方法沒有正確的重寫父類方法。之後,還需要修改方法的參數,將其從MethodOverrideVsOverload變成Object,具體如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
public
class
MethodOverrideVsOverload { @Override public
boolean
equals( Object other ) { System.out.println( "MethodOverrideVsOverload
equals method reached"
); return
true ; } public
static
void
main(String[] args) { Object
o1 = new
MethodOverrideVsOverload(); //during
compile time o1 is of type Object //during
runtime o1 is of type MethodOverrideVsOverload Object
o2 = new
MethodOverrideVsOverload(); //during
compile time o2 is of type Object //during
runtime o2 is of type MethodOverrideVsOverload MethodOverrideVsOverload
o3 = new
MethodOverrideVsOverload(); //o3
is of type MethodOverrideVsOverload //
during both compile time and runtime MethodOverrideVsOverload
o4 = new
MethodOverrideVsOverload(); //o4
is of type MethodOverrideVsOverload //
during both compile time and runtime if (o1.equals(o2)){ System.out.println( "objects
o1 and o2 are equal" ); } if (o3.equals(o4)){ System.out.println( "objects
o3 and o4 are equal" ); } } } |
輸出爲:
MethodOverrideVsOverload equals method reached
objects o1 and o2 are equal
MethodOverrideVsOverload equals method reached
objects o3 and o4 are equal
上面的代碼中,運行時equals方法正確的重寫了Object中的相應方法。這是一個比較容易混淆的問題,面試的時候需要很詳盡的解釋相關的概念。
常見Java面試題 – 第四部分:迭代(iteration)和遞歸(recursion)
英文原文: Java Success,編譯:ImportNew - 鄭雯
ImportNew注: 本文是ImportNew編譯整理的Java面試題系列文章之一。你可以從這裏查看全部的Java面試系列。
Q.請寫一段代碼來計算給定文本內字符“A”的個數。分別用迭代和遞歸兩種方式。
A.假設給定文本爲”AAA rating”。迭代方式就很直觀,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public
class
Iteration { public
int
countA(String input) { if
(input == null
|| input.length( ) == 0 )
{ return
0 ; } int
count = 0 ; for
( int
i = 0 ;
i < input.length( ); i++) { if (input.substring(i,
i+ 1 ).equals( "A" )){ count++; } } return
count; } public
static
void
main(String[ ] args) { System.out.println( new
Iteration( ).countA( "AAA
rating" ));
//
Ans.3 } } |
接下來,遞歸方式的代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public
class
RecursiveCall { public
int
countA(String input) { //
exit condition – recursive calls must have an exit condition if
(input == null
|| input.length( ) == 0 )
{ return
0 ; } int
count = 0 ; //check
first character of the input if
(input.substring( 0 ,
1 ).equals( "A" ))
{ count
= 1 ; } //recursive
call to evaluate rest of the input //(i.e.
2nd character onwards) return
count + countA(input.substring( 1 )); } public
static
void
main(String[ ] args) { System.out.println( new
RecursiveCall( ).countA( "AAA
rating" ));
//
Ans. 3 } } |
遞歸比較難以理解,我們用下面的圖來進行說明。
Q.理解遞歸需要了解哪些概念?
A. 可重入方法(re-entrant method)是可以安全進入的方法,即使同一個方法正在被執行,深入到同一個線程的調用棧裏面也不會影響此次執行的安全性。一個非可重入方法則不是可以安全進入的。例如,加入寫文件或者向文件中寫入日誌的方法不是可重入方法時,有可能會毀壞那個文件。
如果一個方法調用了其自身的話,我們稱之爲遞歸調用。假定棧空間足夠的話,儘管遞歸調用比較難以調試,在Java語言中實現遞歸調用也是完全可行的。遞歸方法是衆多算法中替代循環的一個不錯選擇。所有的遞歸方法都是可重入的,但是不是所有可重入的方法都是遞歸的。
棧遵守LIFO(Last In First Out)規則,因此遞歸調用方法能夠記住“調用者”並且知道此輪執行結束之返回至當初的被調用位置。遞歸利用系統棧來存儲方法調用的返回地址。 Java是一種基於棧設計的編程語言。
順着這個思路還有那些問題可以用來面試?
Q.什麼情況下應該採用遞歸?
A. 上面的例子中其實不必採用遞歸,循環的方式可以達到目的,但是在某些情況下采用遞歸方式則代碼會更加簡短易讀。遞歸方法在循環樹結構以及避免醜陋的嵌套循環的情況下是非常好用的。
Q.什麼是尾遞歸,爲什麼需要尾遞歸?上面的代碼用尾遞歸方式如何重寫?
A. 常規遞歸方法(亦稱,頭遞歸)在上面演示了,這種方式會增加調用棧的大小。每次遞歸,其入口需要被記錄在棧中。方法返回之前需要給countA(input.substring(1)的結果加一個count。假定count大於1,那麼返回結果就是count + countA(input.substring(1)),當然事先要算出來countA(input.substring(1))纔行。同時,這也意味着直到countA(input.substring(1)計算出來才能得到最終的結果。因此,最後需要做的事其實是加法運算,而非遞歸本身。
尾遞歸的好處是什麼?
在尾遞歸中,最後要做的是遞歸,加法運算在之前就已經完成了。一輪遞歸調用完畢後就沒有其他事情了(除了加法運算),因此調用時生成的信息也就沒什麼用了。這些無用信息可以丟棄,然後用一組新的參數來調用一次遞歸方法來產生一個新的結果。這也就是說,棧調用減少帶來了內存消耗減少並且程序的性能更好。
尾遞歸重寫的代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
public
class
TailRecursiveCall { public
int
countA(String input) { //
exit condition – recursive calls must have an exit condition if
(input == null
|| input.length() == 0 )
{ return
0 ; } return
countA(input, 0 )
; } public
int
countA(String input, int
count) { if
(input.length() == 0 )
{ return
count; } //
check first character of the input if
(input.substring( 0 ,
1 ).equals( "A" ))
{ count
= count + 1 ; } //
recursive call is the last call as the count is cumulative return
countA(input.substring( 1 ),
count); } public
static
void
main(String[] args) { System.out.println( new
TailRecursiveCall().countA( "AAA
rating" )); } } |