【Java面試】Java面試題基礎系列212道(上)

Mr小林發表於2020-11-23

目錄

1.Java面試題

2.問題解析

1、物件導向的特徵有哪些方面?

2、訪問修飾符 public,private,protected,以及不寫(預設)時的區別?

3、String 是最基本的資料型別嗎?

4、float f=3.4;是否正確?

5、short s1 = 1; s1 = s1 + 1;有錯嗎?short s1 = 1; s1 += 1;有錯嗎?

6、Java 有沒有 goto?

7、int 和 Integer 有什麼區別?

8、&和&&的區別?

9、解釋記憶體中的棧(stack)、堆(heap)和方法區(method area)的用法。

10、Math.round(11.5) 等於多少?Math.round(-11.5)等於多少?

11、switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上?

12、用最有效率的方法計算 2 乘以 8?

13、陣列有沒有 length()方法?String 有沒有 length()方法?

14、在 Java 中,如何跳出當前的多重巢狀迴圈?

15、構造器(constructor)是否可被重寫(override)?

16、兩個物件值相同(x.equals(y) == true),但卻可有不同的hash code,這句話對不對?

17、是否可以繼承 String 類?

18、當一個物件被當作引數傳遞到一個方法後,此方法可改變這個物件的屬性,並可返回變化後的結果,那麼這裡到底是值傳遞還是引用傳遞?

19、String 和 StringBuilder、StringBuffer 的區別?

20、過載(Overload)和重寫(Override)的區別。過載的方法能否根據返回型別進行區分?

21、描述一下 JVM 載入 class 檔案的原理機制?

22、char 型變數中能不能存貯一箇中文漢字,為什麼?

23、抽象類(abstract class)和介面(interface)有什麼異同?

24、靜態巢狀類(Static Nested Class)和內部類(Inner Class)的不同?

25、Java 中會存在記憶體洩漏嗎,請簡單描述。

26、抽象的(abstract)方法是否可同時是靜態的(static),是否可同時是本地方法(native),是否可同時被 synchronized修飾?

27、闡述靜態變數和例項變數的區別。

28、是否可以從一個靜態(static)方法內部發出對非靜態(non-static)方法的呼叫 

29、如何實現物件克隆?

30、GC 是什麼?為什麼要有 GC?

31、String s = new String(“xyz”);建立了幾個字串物件?

32、介面是否可繼承(extends)介面?抽象類是否可實現(implements)介面?抽象類是否可繼承具體類(concreteclass)?

33、一個”.java”原始檔中是否可以包含多個類(不是內部類)?有什麼限制? 

34、Anonymous Inner Class(匿名內部類)是否可以繼承其它類?是否可以實現介面?

35、內部類可以引用它的包含類(外部類)的成員嗎?有沒有什麼限制?

36、Java 中的 final 關鍵字有哪些用法?

37、指出下面程式的執行結果

38、資料型別之間的轉換:

39、如何實現字串的反轉及替換?

40、怎樣將 GB2312 編碼的字串轉換為 ISO-8859-1 編碼的字串?

41、日期和時間:

42、列印昨天的當前時刻。

43、比較一下 Java 和 JavaSciprt。

44、什麼時候用斷言(assert)?

45、Error 和 Exception 有什麼區別?

46、try{}裡有一個 return 語句,那麼緊跟在這個 try 後的finally{}裡的程式碼會不會被執行,什麼時候被執行,在 return前還是後?

47、Java 語言如何進行異常處理,關鍵字:throws、throw、try、catch、finally 分別如何使用?

48、執行時異常與受檢異常有何異同?

49、列出一些你常見的執行時異常?

50、闡述 final、finally、finalize 的區別。

51、類 ExampleA 繼承 Exception,類 ExampleB 繼承ExampleA。

52、List、Set、Map 是否繼承自 Collection 介面?

53、闡述 ArrayList、Vector、LinkedList 的儲存效能和特性

54、Collection 和 Collections 的區別?

55、List、Map、Set 三個介面存取元素時,各有什麼特點?

56、TreeMap 和 TreeSet 在排序時如何比較元素?Collections 工具類中的 sort()方法如何比較元素?

57、Thread 類的 sleep()方法和物件的 wait()方法都可以讓執行緒暫停執行,它們有什麼區別?

58、執行緒的 sleep()方法和 yield()方法有什麼區別?

59、當一個執行緒進入一個物件的 synchronized 方法 A 之後,其它執行緒是否可進入此物件的 synchronized 方法 B?

60、請說出與執行緒同步以及執行緒排程相關的方法。

61、編寫多執行緒程式有幾種實現方式?

62、synchronized 關鍵字的用法?

63、舉例說明同步和非同步。

64、啟動一個執行緒是呼叫 run()還是 start()方法?

65、什麼是執行緒池(thread pool)?

66、執行緒的基本狀態以及狀態之間的關係?

67、簡述 synchronized 和 java.util.concurrent.locks.Lock的異同?

68、Java 中如何實現序列化,有什麼意義?

69、Java 中有幾種型別的流?

70、寫一個方法,輸入一個檔名和一個字串,統計這個字串在這個檔案中出現的次數。

71、如何用 Java 程式碼列出一個目錄下所有的檔案?

72、XML 文件定義有幾種形式?它們之間有何本質區別?解析XML 文件有哪幾種方式?

73、你在專案中哪些地方用到了 XML?

74、闡述 JDBC 運算元據庫的步驟。

75、Statement 和 PreparedStatement 有什麼區別?哪個效能更好?

76、使用 JDBC 運算元據庫時,如何提升讀取資料的效能?如何提升更新資料的效能?

77、在進行資料庫程式設計時,連線池有什麼作用? 

78、什麼是 DAO 模式?

79、事務的 ACID 是指什麼?

80、JDBC 中如何進行事務處理?

81、JDBC 能否處理 Blob 和 Clob?

82、簡述正規表示式及其用途。

83、Java 中是如何支援正規表示式操作的?

84、獲得一個類的類物件有哪些方式?

85、如何通過反射建立物件?

86、如何通過反射獲取和設定物件私有欄位的值? 

87、如何通過反射呼叫物件的方法?

88、簡述一下物件導向的”六原則一法則”。

89、簡述一下你瞭解的設計模式。

90、用 Java 寫一個單例類。

91、什麼是 UML? 

92、UML 中有哪些常用的圖?

93、用 Java 寫一個氣泡排序。 

94、用 Java 寫一個折半查詢。


1.Java面試題

1、物件導向的特徵有哪些方面?

2、訪問修飾符 public,private,protected,以及不寫(預設)時的區別?

3、String 是最基本的資料型別嗎?

4、float f=3.4;是否正確?

5、short s1 = 1; s1 = s1 + 1;有錯嗎?short s1 = 1; s1 += 1;有錯嗎?

6、Java 有沒有 goto?

7、int 和 Integer 有什麼區別?

8、&和&&的區別?

9、解釋記憶體中的棧(stack)、堆(heap)和方法區(method area)的用法。

10、Math.round(11.5) 等於多少?Math.round(-11.5)等於多少?

11、switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上?

12、用最有效率的方法計算 2 乘以 8?

13、陣列有沒有 length()方法?String 有沒有 length()方法?

14、在 Java 中,如何跳出當前的多重巢狀迴圈?

15、構造器(constructor)是否可被重寫(override)?

16、兩個物件值相同(x.equals(y) == true),但卻可有不同的 hashcode,這句話對不對?

17、是否可以繼承 String 類?

18、當一個物件被當作引數傳遞到一個方法後,此方法可改變這個物件的屬性,並可返回變化後的結果,那麼這裡到底是值傳遞還是引用傳遞?

19、String 和 StringBuilder、StringBuffer 的區別?

20、過載(Overload)和重寫(Override)的區別。過載的方法能否根據返回型別進行區分?

21、描述一下 JVM 載入 class 檔案的原理機制?

22、char 型變數中能不能存貯一箇中文漢字,為什麼?

23、抽象類(abstract class)和介面(interface)有什麼異同?

24、靜態巢狀類(Static Nested Class)和內部類(Inner Class)的不同?

25、Java 中會存在記憶體洩漏嗎,請簡單描述。

26、抽象的(abstract)方法是否可同時是靜態的(static),是否可同時是本地方法(native),是否可同時被 synchronized 修飾?

27、闡述靜態變數和例項變數的區別。

28、是否可以從一個靜態(static)方法內部發出對非靜態(non-static)方法的呼叫?

29、如何實現物件克隆?

30、GC 是什麼?為什麼要有 GC?

31、String s = new String(“xyz”);建立了幾個字串物件?

32 、 接 口 是 否 可 繼 承 ( extends ) 接 口 ? 抽 象 類 是 否 可 實 現(implements)介面?抽象類是否可繼承具體類(concrete class)?

33、一個”.java”原始檔中是否可以包含多個類(不是內部類)?有什麼限制?

34、Anonymous Inner Class(匿名內部類)是否可以繼承其它類?是否可以實現介面?

35、內部類可以引用它的包含類(外部類)的成員嗎?有沒有什麼限制?

36、Java 中的 final 關鍵字有哪些用法?

37、指出下面程式的執行結果

38、資料型別之間的轉換:

39、如何實現字串的反轉及替換?

40、怎樣將 GB2312 編碼的字串轉換為 ISO-8859-1 編碼的字串?

41、日期和時間:

42、列印昨天的當前時刻。

43、比較一下 Java 和 JavaSciprt。

44、什麼時候用斷言(assert)?

45、Error 和 Exception 有什麼區別?

46、try{}裡有一個 return 語句,那麼緊跟在這個 try 後的 finally{}裡的程式碼會不會被執行,什麼時候被執行,在 return 前還是後?

47、Java 語言如何進行異常處理,關鍵字:throws、throw、try、catch、finally 分別如何使用?

48、執行時異常與受檢異常有何異同?

49、列出一些你常見的執行時異常?

50、闡述 final、finally、finalize 的區別。

51、類 ExampleA 繼承 Exception,類 ExampleB 繼承 ExampleA。

52、List、Set、Map 是否繼承自 Collection 介面?

53、闡述 ArrayList、Vector、LinkedList 的儲存效能和特性。

54、Collection 和 Collections 的區別?

55、List、Map、Set 三個介面存取元素時,各有什麼特點?

56、TreeMap 和 TreeSet 在排序時如何比較元素?Collections 工具類中的 sort()方法如何比較元素?

57、Thread 類的 sleep()方法和物件的 wait()方法都可以讓執行緒暫停執行,它們有什麼區別?

58、執行緒的 sleep()方法和 yield()方法有什麼區別?

59、當一個執行緒進入一個物件的 synchronized 方法 A 之後,其它執行緒是否可進入此物件 synchronized 方法 B?

60、請說出與執行緒同步以及執行緒排程相關的方法。

61、編寫多執行緒程式有幾種實現方式?

62、synchronized 關鍵字的用法?

63、舉例說明同步和非同步。

64、啟動一個執行緒是呼叫 run()還是 start()方法?

65、什麼是執行緒池(thread pool)?

66、執行緒的基本狀態以及狀態之間的關係?

67、簡述 synchronized 和 java.util.concurrent.locks.Lock 的異同?

68、Java 中如何實現序列化,有什麼意義?

69、Java 中有幾種型別的流?

70、寫一個方法,輸入一個檔名和一個字串,統計這個字串在這個檔案中出現的次數。

71、如何用 Java 程式碼列出一個目錄下所有的檔案?

72、用 Java 的套接字程式設計實現一個多執行緒的回顯(echo)伺服器。

73、XML 文件定義有幾種形式?它們之間有何本質區別?解析 XML文件有哪幾種方式?

74、你在專案中哪些地方用到了 XML?

75、闡述 JDBC 運算元據庫的步驟。

76、Statement 和 PreparedStatement 有什麼區別?哪個效能更好?

77、使用 JDBC 運算元據庫時,如何提升讀取資料的效能?如何提升更新資料的效能?

78、在進行資料庫程式設計時,連線池有什麼作用?

79、什麼是 DAO 模式?

80、事務的 ACID 是指什麼?

82、JDBC 能否處理 Blob 和 Clob?

83、簡述正規表示式及其用途。

84、Java 中是如何支援正規表示式操作的?

85、獲得一個類的類物件有哪些方式?

88、如何通過反射呼叫物件的方法?

90、簡述一下你瞭解的設計模式。

91、用 Java 寫一個單例類。

92、什麼是 UML?

93、UML 中有哪些常用的圖?

95、用 Java 寫一個折半查詢。


2.問題解析

1、物件導向的特徵有哪些方面?

物件導向的特徵主要有以下幾個方面:

抽象:抽象是將一類物件的共同特徵總結出來構造類的過程,包括資料抽象和行為抽象兩方面。抽象只關注物件有哪些屬性和行為,並不關注這些行為的細節是什麼。

繼承:繼承是從已有類得到繼承資訊建立新類的過程。提供繼承資訊的類被稱為父類(超類、基類);得到繼承資訊的類被稱為子類(派生類)。繼承讓變化中的軟體系統有了一定的延續性,同時繼承也是封裝程式中可變因素的重要手段(如果不能理解請閱讀閻巨集博士的《Java 與模式》或《設計模式精解》中關於橋樑模式的部分)。

封裝:通常認為封裝是把資料和運算元據的方法繫結起來,對資料的訪問只能通過已定義的介面。物件導向的本質就是將現實世界描繪成一系列完全自治、封閉的物件。我們在類中編寫的方法就是對實現細節的一種封裝;我們編寫一個類就是對資料和資料操作的封裝。可以說,封裝就是隱藏一切可隱藏的東西,只向外界提供最簡單的程式設計介面(可以想想普通洗衣機和全自動洗衣機的差別,明顯全自動洗衣機封裝更好因此操作起來更簡單;我們現在使用的智慧手機也是封裝得足夠好的,因為幾個按鍵就搞定了所有的事情)。

多型性:多型性是指允許不同子型別的物件對同一訊息作出不同的響應。簡單的說就是用同樣的物件引用呼叫同樣的方法但是做了不同的事情。多型性分為編譯時的多型性和執行時的多型性。如果將物件的方法視為物件向外界提供的服務,那麼執行時的多型性可以解釋為:當 A 系統訪問 B 系統提供的服務時,B系統有多種提供服務的方式,但一切對 A 系統來說都是透明的(就像電動剃鬚刀是 A 系統,它的供電系統是 B 系統,B 系統可以使用電池供電或者用交流電,甚至還有可能是太陽能,A 系統只會通過 B 類物件呼叫供電的方法,但並不知道供電系統的底層實現是什麼,究竟通過何種方式獲得了動力)。方法過載(overload)實現的是編譯時的多型性(也稱為前繫結),而方法重寫(override)實現的是執行時的多型性(也稱為後繫結)。執行時的多型是物件導向最精髓的東西,要實現多型需要做兩件事:

1). 方法重寫(子類繼承父類並重寫父類中已有的或抽象的方法); 

2). 物件造型(用父型別引用引用子型別物件,這樣同樣的引用呼叫同樣的方法就會根據子類物件的不同而表現出不同的行為)。

 

2、訪問修飾符 public,private,protected,以及不寫(預設)時的區別?

修飾符 當前類 同 包 子 類 其他包

類的成員不寫訪問修飾時預設為 default。預設對於同一個包中的其他類相當於公開(public),對於不是同一個包中的其他類相當於私有(private)。受保護(protected)對子類相當於公開,對不是同一包中的沒有父子關係的類相當於私有。Java 中,外部類的修飾符只能是 public 或預設,類的成員(包括內部類)的修飾符可以是以上四種。

 

3、String 是最基本的資料型別嗎?

不是。Java 中的基本資料型別只有 8 個 :byte、short、int、long、float、double、char、boolean;除了基本型別(primitive type),剩下的都是引用型別(referencetype),Java 5 以後引入的列舉型別也算是一種比較特殊的引用型別。

 

4、float f=3.4;是否正確?

不正確。3.4 是雙精度數,將雙精度型(double)賦值給浮點型(float)屬於下轉型(down-casting,也稱為窄化)會造成精度損失,因此需要強制型別轉換float f =(float)3.4; 或者寫成 float f =3.4F;。

 

5、short s1 = 1; s1 = s1 + 1;有錯嗎?short s1 = 1; s1 += 1;有錯嗎?

對於 short s1 = 1; s1 = s1 + 1;由於 1 是 int 型別,因此 s1+1 運算結果也是 int型,需要強制轉換型別才能賦值給 short 型。而 short s1 = 1; s1 += 1;可以正確編譯,因為 s1+= 1;相當於 s1 = (short(s1 + 1);其中有隱含的強制型別轉換。

 

6、Java 有沒有 goto?

goto 是 Java 中的保留字,在目前版本的 Java 中沒有使用。(根據 James Gosling(Java 之父)編寫的《The Java Programming Language》一書的附錄中給出了一個 Java 關鍵字列表,其中有goto 和 const,但是這兩個是目前無法使用的關鍵字,因此有些地方將其稱之為保留字,其實保留字這個詞應該有更廣泛的意義,因為熟悉 C 語言的程式設計師都知道,在系統類庫中使用過的有特殊意義的但詞或單詞的組合都被視為保留字)

 

7、int 和 Integer 有什麼區別?

Java 是一個近乎純潔的物件導向程式語言,但是為了程式設計的方便還是引入了基本資料型別,但是為了能夠將這些基本資料型別當成物件操作,Java 為每一個基本資料型別都引入了對應的包裝型別(wrapper class),int 的包裝類就是 Integer,從 Java 5 開始引入了自動裝箱/拆箱機制,使得二者可以相互轉換。

Java 為每個原始型別提供了包裝型別:

原始型別: boolean,char,byte,short,int,long,float,double

包裝型別:Boolean,Character,Byte,Short,Integer,Long,Float,Double

 
class AutoUnboxingTest {
	public static void main(String[] args) {
		Integer a = new Integer(3);
		Integer b = 3;
		// 將 3 自動裝箱成 Integer 型別
		int c = 3;
		System.out.println(a == b);
		// false 兩個引用沒有引用同一對
		象
		System.out.println(a == c);
		// true a 自動拆箱成 int 型別再和 c比較
	}
}

最近還遇到一個面試題,也是和自動裝箱和拆箱有點關係的,程式碼如下所示:

public class Test03 {
	public static void main(String[] args) {
		Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;
		System.out.println(f1 == f2);
		System.out.println(f3 == f4);
	}
}

如果不明就裡很容易認為兩個輸出要麼都是 true 要麼都是 false。首先需要注意的是 f1、f2、f3、f4 四個變數都是 Integer 物件引用,所以下面的==運算比較的不是值而是引用。裝箱的本質是什麼呢?當我們給一個 Integer 物件賦一個 int 值的時候,會呼叫 Integer 類的靜態方法 valueOf,如果看 valueOf 的原始碼就知道發生了什麼。

public static Integer valueOf(int i) {
	if (i >= IntegerCache.low && i <= IntegerCache.high)
	return IntegerCache.cache[i + (-IntegerCache.low)];
	return new Integer(i);
}

IntegerCache 是 Integer 的內部類,其程式碼如下所示:
/**
* Cache to support the object identity semantics of autoboxing for
values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>}
option.
* During VM initialization, java.lang.Integer.IntegerCache.high
property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
private static class IntegerCache {
	static final int low = -128;
	static final int high;
	static final Integer cache[];
	static {
		// high value may be configured by property
		int h = 127;
		String integerCacheHighPropValue =
		sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
		if (integerCacheHighPropValue != null) {
			try {
				int i = parseint(integerCacheHighPropValue);
				i = Math.max(i, 127);
				// Maximum array size is Integer.MAX_VALUE
				h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
			}
			catch( NumberFormatException nfe) {
				// If the property cannot be parsed into an int,
				ignore it.
			}
		}
		high = h;
		cache = new Integer[(high - low) + 1];
		int j = low;
		for (int k = 0; k < cache.length; k++)
		cache[k] = new Integer(j++);
		// range [-128, 127] must be interned (JLS7 5.1.7)
		assert IntegerCache.high >= 127;
	}
	private IntegerCache() {
	}
}
簡單的說,如果整型字面量的值在-128 到 127 之間,那麼不會 new 新的 Integer物件,而是直接引用常量池中的 Integer 物件,所以上面的面試題中 f1==f2為true,f3==f4 的結果是 false。

 

8、&和&&的區別?

&運算子有兩種用法:(1)按位與;(2)邏輯與。&&運算子是短路與運算。邏輯與跟短路與的差別是非常巨大的,雖然二者都要求運算子左右兩端的布林值都是true 整個表示式的值才是 true。&&之所以稱為短路運算是因為,如果&&左邊的表示式的值是 false,右邊的表示式會被直接短路掉,不會進行運算。很多時候我們可能都需要用&&而不是&,例如在驗證使用者登入時判定使用者名稱不是 null 而且不是空字串,應當寫為:username != null &&!username.equals(“”),二者的順序不能交換,更不能用&運算子,因為第一個條件如果不成立,根本不能進行字串的 equals 比較,否則會生 NullPointerException 異常。注意:邏輯或運算子(|)和短路或運算子(||)的差別也是如此。

 

9、解釋記憶體中的棧(stack)、堆(heap)和方法區(method area)的用法。

通常我們定義一個基本資料型別的變數,一個物件的引用,還有就是函式呼叫的現場儲存都使用 JVM 中的棧空間;而通過 new 關鍵字和構造器建立的物件則放在堆空間,堆是垃圾收集器管理的主要區域,由於現在的垃圾收集器都採用分代收集演算法,所以堆空間還可以細分為新生代和老生代,再具體一點可以分為 Eden、Survivor(又可分為 From Survivor 和 To Survivor)、Tenured;方法區和堆都是各個執行緒共享的記憶體區域,用於儲存已經被 JVM 載入的類資訊、常量、靜態變數、JIT 編譯器編譯後的程式碼等資料;程式中的字面量(literal)如直接書寫的 100、”hello”和常量都是放在常量池中,常量池是方法區的一部分,。棧空間操作起來最快但是棧很小,通常大量的物件都是放在堆空間,棧和堆的大小都可以通過 JVM的啟動引數來進行調整,棧空間用光了會引發 StackOverflowError,而堆和常量池空間不足則會引發 OutOfMemoryError。

String str = new String("hello");

上面的語句中變數 str 放在棧上,用 new 建立出來的字串物件放在堆上,而”hello”這個字面量是放在方法區的。

補充 1:較新版本的 Java(從 Java 6 的某個更新開始)中,由於 JIT 編譯器的發展和”逃逸分析”技術的逐漸成熟,棧上分配、標量替換等優化技術使得物件一定分配在堆上這件事情已經變得不那麼絕對了。

補充 2:執行時常量池相當於 Class 檔案常量池具有動態性,Java 語言並不要求常量一定只有編譯期間才能產生,執行期間也可以將新的常量放入池中,String類的 intern()方法就是這樣的。

看看下面程式碼的執行結果是什麼並且比較一下 Java 7 以前和以後的執行結果是否一致。

String s1 = new StringBuilder("go")
.append("od").toString();
System.out.println(s1.intern() == s1);
String s2 = new StringBuilder("ja")
.append("va").toString();
System.out.println(s2.intern() == s2);

10、Math.round(11.5) 等於多少?Math.round(-11.5)等於多少?

Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。四捨五入的原理是在引數上加 0.5 然後進行下取整。

 

11、switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上?

在 Java 5 以前,switch(expr)中,expr 只能是 byte、short、char、int。從 Java5 開始,Java 中引入了列舉型別,expr 也可以是 enum 型別,從 Java 7 開始,expr 還可以是字串(String),但是長整型(long)在目前所有的版本中都是不可以的。

 

12、用最有效率的方法計算 2 乘以 8?

2 << 3(左移 3 位相當於乘以 2 的 3 次方,右移 3 位相當於除以 2 的 3 次方)。

補充:我們為編寫的類重寫 hashCode 方法時,可能會看到如下所示的程式碼,其實我們不太理解為什麼要使用這樣的乘法運算來產生雜湊碼(雜湊碼),而且為什麼這個數是個素數,為什麼通常選擇 31 這個數?前兩個問題的答案你可以自己百度一下,選擇 31 是因為可以用移位和減法運算來代替乘法,從而得到更好的效能。說到這裡你可能已經想到了:31 * num 等價於(num << 5) - num,左移 5位相當於乘以 2 的 5 次方再減去自身就相當於乘以 31,現在的 VM 都能自動完成這個優化。

public class PhoneNumber {
	private int areaCode;
	private String prefix;
	private String lineNumber;
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + areaCode;
		result = prime * result
		+ ((lineNumber == null) ? 0 : lineNumber.hashCode());
		result = prime * result + ((prefix == null) ? 0 : prefix.hashCode());
		return result;
	}
	@Override
	public Boolean equals(Object obj) {
		if (this == obj)
		return true;
		if (obj == null)
		return false;
		if (getClass() != obj.getClass())
		return false;
		PhoneNumber other = (PhoneNumber) obj;
		if (areaCode != other.areaCode)
		return false;
		if (lineNumber == null) {
			if (other.lineNumber != null)
			return false;
		} else if (!lineNumber.equals(other.lineNumber))
		return false;
		if (prefix == null) {
			if (other.prefix != null)
			return false;
		} else if (!prefix.equals(other.prefix))
		return false;
		return true;
	}
}

 

13、陣列有沒有 length()方法?String 有沒有 length()方法?

陣列沒有 length()方法 ,有 length 的屬性。String 有 length()方法。JavaScript中,獲得字串的長度是通過 length 屬性得到的,這一點容易和 Java 混淆。

 

14、在 Java 中,如何跳出當前的多重巢狀迴圈?

在最外層迴圈前加一個標記如 A,然後用 break A;可以跳出多重迴圈。(Java 中支援帶標籤的 break 和 continue 語句,作用有點類似於 C 和 C++中的 goto 語句,但是就像要避免使用 goto 一樣,應該避免使用帶標籤的 break 和 continue,因為它不會讓你的程式變得更優雅,很多時候甚至有相反的作用,所以這種語法其實不知道更好)

 

15、構造器(constructor)是否可被重寫(override)?

構造器不能被繼承,因此不能被重寫,但可以被過載。

 

16、兩個物件值相同(x.equals(y) == true),但卻可有不同的hash code,這句話對不對?

不對,如果兩個物件 x 和 y 滿足 x.equals(y) == true,它們的雜湊碼(hash code)應當相同。Java 對於 eqauls 方法和 hashCode 方法是這樣規定的:

(1)如果兩個物件相同(equals 方法返回 true),那麼它們的 hashCode 值一定要相同;

(2)如果兩個物件的 hashCode 相同,它們並不一定相同。當然,你未必要按照要求去做,但是如果你違背了上述原則就會發現在使用容器時,相同的物件可以出現在 Set 集合中,同時增加新元素的效率會大大下降(對於使用雜湊儲存的系統,如果雜湊碼頻繁的衝突將會造成存取效能急劇下降)。

補充:equals 方法的:首先 equals 方法必須滿足自反性(x.equals(x)必須返回 true)、對稱性(x.equals(y)返回 true 時,y.equals(x)也必須返回 true)、傳遞性(x.equals(y)和 y.equals(z)都返回 true 時,x.equals(z)也必須返回 true)和一致性(當 x 和 y 引用的物件資訊沒有被修改時,多次呼叫 x.equals(y)應該得到同樣的返回值),而且對於任何非 null 值的引用 x,x.equals(null)必須返回 false。

實現高質量的 equals 方法的訣竅包括:

(1) 使用==操作符檢查”引數是否為這個物件的引用”;

(2) 使用 instanceof 操作符檢查”引數是否為正確的型別”;

(3) 對於類中的關鍵屬性,檢查引數傳入物件的屬性是否與之相匹配;

(4) 編寫完 equals方法後,問自己它是否滿足對稱性、傳遞性、一致性;

(5) 重寫 equals 時總是要重寫 hashCode;

(6) 不要將 equals 方法引數中的 Object 物件替換為其他的型別,在重寫時不要忘掉@Override 註解。

 

17、是否可以繼承 String 類?

String 類是 final 類,不可以被繼承。

補充:繼承 String 本身就是一個錯誤的行為,對 String 型別最好的重用方式是關聯關係(Has-A)和依賴關係(Use-A)而不是繼承關係(Is-A)。

 

18、當一個物件被當作引數傳遞到一個方法後,此方法可改變這個物件的屬性,並可返回變化後的結果,那麼這裡到底是值傳遞還是引用傳遞?

是值傳遞。Java 語言的方法呼叫只支援引數的值傳遞。當一個物件例項作為一個引數被傳遞到方法中時,引數的值就是對該物件的引用。物件的屬性可以在被呼叫過程中被改變,但對物件引用的改變是不會影響到呼叫者的。C++和 C#中可以通過傳引用或傳輸出引數來改變傳入的引數的值。在 C#中可以編寫如下所示的程式碼,但是在 Java 中卻做不到。

using System;
namespace CS01 {
	class Program {
		public static void swap(ref int x, ref int y) {
			int temp = x;
			x = y;
			y = temp;
		}
		public static void Main (string[] args) {
			int a = 5, b = 10;
			swap (ref a, ref b);
			// a = 10, b = 5;
			Console.WriteLine ("a = {0}, b = {1}", a, b);
		}
	}
}

說明:Java 中沒有傳引用實在是非常的不方便,這一點在 Java 8 中仍然沒有得到改進,正是如此在 Java 編寫的程式碼中才會出現大量的 Wrapper 類(將需要通過方法呼叫修改的引用置於一個 Wrapper 類中,再將 Wrapper 物件傳入方法),這樣的做法只會讓程式碼變得臃腫,尤其是讓從 C 和 C++轉型為 Java 程式設計師的開發者無法容忍。

 

19、String 和 StringBuilder、StringBuffer 的區別?

Java 平臺提供了兩種型別的字串:String 和 StringBuffer/StringBuilder,它們可以儲存和操作字串。其中 String 是隻讀字串,也就意味著 String 引用的字串內容是不能被改變的。而 StringBuffer/StringBuilder 類表示的字串物件可以直接進行修改。StringBuilder 是 Java 5 中引入的,它和 StringBuffer 的方法完全相同,區別在於它是在單執行緒環境下使用的,因為它的所有方面都沒有被synchronized 修飾,因此它的效率也比 StringBuffer 要高。

面試題 1 - 什麼情況下用+運算子進行字串連線比呼叫

StringBuffer/StringBuilder 物件的 append 方法連線字串效能更好?

面試題 2 - 請說出下面程式的輸出。

class StringEqualTest {
	public static void main(String[] args) {
		String s1 = "Programming";
		String s2 = new String("Programming");
		String s3 = "Program";
		String s4 = "ming";
		String s5 = "Program" + "ming";
		String s6 = s3 + s4;
		System.out.println(s1 == s2);
		System.out.println(s1 == s5);
		System.out.println(s1 == s6);
		System.out.println(s1 == s6.intern());
		System.out.println(s2 == s2.intern());
	}
}

補充:解答上面的面試題需要清楚兩點:

(1)String 物件的 intern 方法會得到字串物件在常量池中對應的版本的引用(如果常量池中有一個字串與 String 物件的 equals 結果是 true),如果常量池中沒有對應的字串,則該字串將被新增到常量池中,然後返回常量池中字串的引用;

(2)字串的+操作其本質是建立了 StringBuilder 物件進行 append 操作,然後將拼接後的 StringBuilder 物件用toString 方法處理成 String 物件,這一點可以用 javap -c StringEqualTest.class命令獲得 class 檔案對應的 JVM 位元組碼指令就可以看出來。

 

20、過載(Overload)和重寫(Override)的區別。過載的方法能否根據返回型別進行區分?

方法的過載和重寫都是實現多型的方式,區別在於前者實現的是編譯時的多型性,而後者實現的是執行時的多型性。過載發生在一個類中,同名的方法如果有不同的引數列表(引數型別不同、引數個數不同或者二者都不同)則視為過載;重寫發生在子類與父類之間,重寫要求子類被重寫方法與父類被重寫方法有相同的返回型別,比父類被重寫方法更好訪問,不能比父類被重寫方法宣告更多的異常(里氏代換原則)。過載對返回型別沒有特殊的要求。

 

21、描述一下 JVM 載入 class 檔案的原理機制?

JVM 中類的裝載是由類載入器(ClassLoader)和它的子類來實現的,Java 中的類載入器是一個重要的 Java 執行時系統元件,它負責在執行時查詢和裝入類檔案中的類。

由於 Java 的跨平臺性,經過編譯的 Java 源程式並不是一個可執行程式,而是一個或多個類檔案。當 Java 程式需要使用某個類時,JVM 會確保這個類已經被載入、連線(驗證、準備和解析)和初始化。類的載入是指把類的.class 檔案中的資料讀入到記憶體中,通常是建立一個位元組陣列讀入.class 檔案,然後產生與所載入類對應的 Class 物件。載入完成後,Class 物件還不完整,所以此時的類還不可用。當類被載入後就進入連線階段,這一階段包括驗證、準備(為靜態變數分配記憶體並設定預設的初始值)和解析(將符號引用替換為直接引用)三個步驟。最後 JVM 對類進行初始化,包括:1)如果類存在直接的父類並且這個類還沒有被初始化,那麼就先初始化父類;2)如果類中存在初始化語句,就依次執行這些初始化語句。

類的載入是由類載入器完成的,類載入器包括:根載入器(BootStrap)、擴充套件載入器(Extension)、系統載入器(System)和使用者自定義類載入器(java.lang.ClassLoader 的子類)。從 Java 2(JDK 1.2)開始,從 Java 2(JDK 1.2)開始,類載入過程採取了父親委託機制(PDM)。PDM 更好的保證了 Java 平臺的安全性,在該機制中,JVM 自帶的 Bootstrap 是根載入器,其他的載入器都有且僅有一個父類載入器。類的載入首先請求父類載入器載入,父類載入器無能為力時才由其子類載入器自行載入。JVM 不會向 Java 程式提供對 Bootstrap 的引用。下面是關於幾個類載入器的說明:

(1) Bootstrap:一般用原生程式碼實現,負責載入 JVM 基礎核心類庫(rt.jar);

(2) Extension:從 java.ext.dirs 系統屬性所指定的目錄中載入類庫,它的父載入器是 Bootstrap;

(3) System:又叫應用類載入器,其父類是 Extension。它是應用最廣泛的類載入器。它從環境變數 classpath 或者系統屬性 java.class.path 所指定的目錄中記載類,是使用者自定義載入器的預設父載入器。

 

22、char 型變數中能不能存貯一箇中文漢字,為什麼?

char 型別可以儲存一箇中文漢字,因為 Java 中使用的編碼是 Unicode(不選擇任何特定的編碼,直接使用字元在字符集中的編號,這是統一的唯一方法),一個 char 型別佔 2 個位元組(16 位元),所以放一箇中文是沒問題的。

補充:使用 Unicode 意味著字元在 JVM 內部和外部有不同的表現形式,在 JVM內部都是 Unicode,當這個字元被從 JVM 內部轉移到外部時(例如存入檔案系統中),需要進行編碼轉換。所以 Java 中有位元組流和字元流,以及在字元流和位元組流之間進行轉換的轉換流,如 InputStreamReader 和 OutputStreamReader,這兩個類是位元組流和字元流之間的介面卡類,承擔了編碼轉換的任務;對於 C 程式設計師來說,要完成這樣的編碼轉換恐怕要依賴於 union(聯合體/共用體)共享記憶體的特徵來實現了。

 

23、抽象類(abstract class)和介面(interface)有什麼異同?

抽象類和介面都不能夠例項化,但可以定義抽象類和介面型別的引用。一個類如果繼承了某個抽象類或者實現了某個介面都需要對其中的抽象方法全部進行實現,否則該類仍然需要被宣告為抽象類。介面比抽象類更加抽象,因為抽象類中可以定義構造器,可以有抽象方法和具體方法,而介面中不能定義構造器而且其中的方法全部都是抽象方法。抽象類中的成員可以是 private、預設、protected、public 的,而介面中的成員全都是 public 的。抽象類中可以定義成員變數,而介面中定義的成員變數實際上都是常量。有抽象方法的類必須被宣告為抽象類,而抽象類未必要有抽象方法。

 

24、靜態巢狀類(Static Nested Class)和內部類(Inner Class)的不同?

Static Nested Class 是被宣告為靜態(static)的內部類,它可以不依賴於外部類例項被例項化。而通常的內部類需要在外部類例項化後才能例項化,其語法看起來挺詭異的,如下所示。

/**
* 撲克類(一副撲克)
* @author 駱昊
*
*/
public class Poker {
	private static String[] suites = {"黑桃", "紅桃", "草花", "方塊"};
	private static int[] faces = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
	private Card[] cards;
	/**
* 構造器
*
*/
	public Poker() {
		cards = new Card[52];
		for (int i = 0; i < suites.length; i++) {
			for (int j = 0; j < faces.length; j++) {
				cards[i * 13 + j] = new Card(suites[i], faces[j]);
			}
		}
	}
	/**
* 洗牌 (隨機亂序)
*
*/
	public void shuffle() {
		for (int i = 0, len = cards.length; i < len; i++) {
			int index = (int) (Math.random() * len);
			Card temp = cards[index];
			cards[index] = cards[i];
			cards[i] = temp;
		}
	}
	/**
* 發牌
* @param index 發牌的位置
*
*/
	public Card deal(int index) {
		return cards[index];
	}
	/**
* 卡片類(一張撲克)
* [內部類]
* @author 駱昊
*
*/
	public class Card {
		private String suite;
		// 花色
		private int face;
		// 點數
		public Card(String suite, int face) {
			this.suite = suite;
			this.face = face;
		}
		@Override
		public String toString() {
			String faceStr = "";
			switch(face) {
				case 1: faceStr = "A";
				break;
				case 11: faceStr = "J";
				break;
				case 12: faceStr = "Q";
				break;
				case 13: faceStr = "K";
				break;
				default: faceStr = String.valueOf(face);
			}
			return suite + faceStr;
		}
	}
}
測試程式碼:
class PokerTest {
	public static void main(String[] args) {
		Poker poker = new Poker();
		poker.shuffle();
		// 洗牌
		Poker.Card c1 = poker.deal(0);
		// 發第一張牌
		// 對於非靜態內部類 Card
		// 只有通過其外部類 Poker 物件才能建立 Card 物件
		Poker.Card c2 = poker.new Card("紅心", 1);
		// 自己建立一張牌
		System.out.println(c1);
		// 洗牌後的第一張
		System.out.println(c2);
		// 列印: 紅心 A
	}
}
面試題 - 下面的程式碼哪些地方會產生編譯錯誤?
 
class Outer {
	class Inner {
	}
	public static void foo() {
		new Inner();
	}
	public void bar() {
		new Inner();
	}
	public static void main(String[] args) {
		new Inner();
	}
}

注意:Java 中非靜態內部類物件的建立要依賴其外部類物件,上面的面試題中 foo和 main 方法都是靜態方法,靜態方法中沒有 this,也就是說沒有所謂的外部類物件,因此無法建立內部類物件,如果要在靜態方法中建立內部類物件,可以這樣做:

new Outer().new Inner();

 

25、Java 中會存在記憶體洩漏嗎,請簡單描述。

理論上 Java 因為有垃圾回收機制(GC)不會存在記憶體洩露問題(這也是 Java 被廣泛使用於伺服器端程式設計的一個重要原因);然而在實際開發中,可能會存在無用但可達的物件,這些物件不能被 GC 回收,因此也會導致記憶體洩露的發生。例如Hibernate 的 Session(一級快取)中的物件屬於持久態,垃圾回收器是不會回收這些物件的,然而這些物件中可能存在無用的垃圾物件,如果不及時關閉(close)或清空(flush)一級快取就可能導致記憶體洩露。下面例子中的程式碼也會導致記憶體洩露。

import java.util.Arrays;
import java.util.EmptyStackException;
public class MyStack<T> {
	private T[] elements;
	private int size = 0;
	private static final int INIT_CAPACITY = 16;
	public MyStack() {
		elements = (T[]) new Object[INIT_CAPACITY];
	}
	public void push(T elem) {
		ensureCapacity();
		elements[size++] = elem;
	}
	public T pop() {
		if(size == 0)
		throw new EmptyStackException();
		return elements[--size];
	}
	private void ensureCapacity() {
		if(elements.length == size) {
			elements = Arrays.copyOf(elements, 2 * size + 1);
		}
	}
}

上面的程式碼實現了一個棧(先進後出(FILO))結構,乍看之下似乎沒有什麼明顯的問題,它甚至可以通過你編寫的各種單元測試。然而其中的 pop 方法卻存在記憶體洩露的問題,當我們用 pop 方法彈出棧中的物件時,該物件不會被當作垃圾回收,即使使用棧的程式不再引用這些物件,因為棧內部維護著對這些物件的過期引 用(obsolete reference)。在支援垃圾回收的語言中,記憶體洩露是很隱蔽的,這種記憶體洩露其實就是無意識的物件保持。如果一個物件引用被無意識的保留起來了,那麼垃圾回收器不會處理這個物件,也不會處理該物件引用的其他物件,即使這樣的物件只有少數幾個,也可能會導致很多的物件被排除在垃圾回收之外,從而對效能造成重大影響,極端情況下會引發 Disk Paging(實體記憶體與硬碟的虛擬記憶體交換資料),甚至造成 OutOfMemoryError。

 

26、抽象的(abstract)方法是否可同時是靜態的(static),是否可同時是本地方法(native),是否可同時被 synchronized修飾?

都不能。抽象方法需要子類重寫,而靜態的方法是無法被重寫的,因此二者是矛盾的。本地方法是由原生程式碼(如 C 程式碼)實現的方法,而抽象方法是沒有實現的,也是矛盾的。synchronized 和方法的實現細節有關,抽象方法不涉及實現細節,因此也是相互矛盾的。

 

27、闡述靜態變數和例項變數的區別。

靜態變數是被 static 修飾符修飾的變數,也稱為類變數,它屬於類,不屬於類的任何一個物件,一個類不管建立多少個物件,靜態變數在記憶體中有且僅有一個拷貝;例項變數必須依存於某一例項,需要先建立物件然後通過物件才能訪問到它。靜態變數可以實現讓多個物件共享記憶體。

補充:在 Java 開發中,上下文類和工具類中通常會有大量的靜態成員。

 

28、是否可以從一個靜態(static)方法內部發出對非靜態(non-static)方法的呼叫?

不可以,靜態方法只能訪問靜態成員,因為非靜態方法的呼叫要先建立物件,在呼叫靜態方法時可能物件並沒有被初始化。

 

29、如何實現物件克隆?

有兩種方式:

1). 實現 Cloneable 介面並重寫 Object 類中的 clone()方法;

2). 實現 Serializable 介面,通過物件的序列化和反序列化實現克隆,可以實現真正的深度克隆,程式碼如下。

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class MyUtil {
	private MyUtil() {
		throw new AssertionError();
	}
	@SuppressWarnings("unchecked")
	public static <T extends Serializable> T clone(T obj) throws
	Exception {
		ByteArrayOutputStream bout = new ByteArrayOutputStream();
		ObjectOutputStream oos = new ObjectOutputStream(bout);
		oos.writeObject(obj);
		ByteArrayInputStream bin = new
		ByteArrayInputStream(bout.toByteArray());
		ObjectInputStream ois = new ObjectInputStream(bin);
		return (T) ois.readObject();
		// 說明:呼叫 ByteArrayInputStream 或 ByteArrayOutputStream物件的 close 方法沒有任何意義
		// 這兩個基於記憶體的流只要垃圾回收器清理物件就能夠釋放資源,這一點不同於對外部資源(如檔案 流)的釋放
	}
}

下面是測試程式碼:

import java.io.Serializable;
/**
* 人類
*
*/
class Person implements Serializable {
	private static final long serialVersionUID = -9102017020286042305L;
	private String name;
	// 姓名
	private int age;
	// 年齡
	private Car car;
	// 座駕
	public Person(String name, int age, Car car) {
		this.name = name;
		this.age = age;
		this.car = car;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	public Car getCar() {
		return car;
	}
	public void setCar(Car car) {
		this.car = car;
	}
	@Override
	public String toString() {
		return "Person [name=" + name + ", age=" + age + ", car=" +
		car + "]";
	}
}
/**
* 小汽車類
*
*/
class Car implements Serializable {
	private static final long serialVersionUID = -5713945027627603702L;
	private String brand;
	// 品牌
	private int maxSpeed;
	// 最高時速
	public Car(String brand, int maxSpeed) {
		this.brand = brand;
		this.maxSpeed = maxSpeed;
	}
	public String getBrand() {
		return brand;
	}
	public void setBrand(String brand) {
		this.brand = brand;
	}
	public int getMaxSpeed() {
		return maxSpeed;
	}
	public void setMaxSpeed(int maxSpeed) {
		this.maxSpeed = maxSpeed;
	}
	@Override
	public String toString() {
		return "Car [brand=" + brand + ", maxSpeed=" + maxSpeed +
		"]";
	}
}
class CloneTest {
	public static void main(String[] args) {
		try {
			Person p1 = new Person("Hao LUO", 33, new Car("Benz",
			300));
			Person p2 = MyUtil.clone(p1);
			// 深度克隆
			p2.getCar().setBrand("BYD");
			// 修改克隆的 Person 物件 p2 關聯的汽車物件的品牌屬性
			// 原來的 Person 物件 p1 關聯的汽車不會受到任何影響
			// 因為在克隆 Person 物件時其關聯的汽車物件也被克隆了
			System.out.println(p1);
		}
		catch (Exception e) {
			e.printStackTrace();
		}
	}
}

注意:基於序列化和反序列化實現的克隆不僅僅是深度克隆,更重要的是通過泛型限定,可以檢查出要克隆的物件是否支援序列化,這項檢查是編譯器完成的,不是在執行時丟擲異常,這種是方案明顯優於使用 Object 類的 clone 方法克隆物件。讓問題在編譯的時候暴露出來總是好過把問題留到執行時。

 

30、GC 是什麼?為什麼要有 GC?

GC 是垃圾收集的意思,記憶體處理是程式設計人員容易出現問題的地方,忘記或者錯誤的記憶體回收會導致程式或系統的不穩定甚至崩潰,Java 提供的 GC 功能可以自動監測物件是否超過作用域從而達到自動回收記憶體的目的,Java 語言沒有提供釋放已分配記憶體的顯示操作方法。Java 程式設計師不用擔心記憶體管理,因為垃圾收集器會自動進行管理。要請求垃圾收集,可以呼叫下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但 JVM 可以遮蔽掉顯示的垃圾回收呼叫。垃圾回收可以有效的防止記憶體洩露,有效的使用可以使用的記憶體。垃圾回收器通常是作為一個單獨的低優先順序的執行緒執行,不可預知的情況下對記憶體堆中已經死亡的或者長時間沒有使用的物件進行清除和回收,程式設計師不能實時的呼叫垃圾回收器對某個物件或所有物件進行垃圾回收。在 Java 誕生初期,垃圾回收是 Java最大的亮點之一,因為伺服器端的程式設計需要有效的防止記憶體洩露問題,然而時過境遷,如今 Java 的垃圾回收機制已經成為被詬病的東西。移動智慧終端使用者通常覺得 iOS 的系統比 Android 系統有更好的使用者體驗,其中一個深層次的原因就在於 Android 系統中垃圾回收的不可預知性。

補充:垃圾回收機制有很多種,包括:分代複製垃圾回收、標記垃圾回收、增量垃圾回收等方式。標準的 Java 程式既有棧又有堆。棧儲存了原始型區域性變數,堆儲存了要建立的物件。Java 平臺對堆記憶體回收和再利用的基本演算法被稱為標記和清除,但是 Java 對其進行了改進,採用“分代式垃圾收集”。這種方法會跟 Java物件的生命週期將堆記憶體劃分為不同的區域,在垃圾收集過程中,可能會將物件移動到不同區域:

(1)伊甸園(Eden):這是物件最初誕生的區域,並且對大多數物件來說,這裡是它們唯一存在過的區域。

(2)倖存者樂園(Survivor):從伊甸園倖存下來的物件會被挪到這裡。

(3)終身頤養園(Tenured):這是足夠老的倖存物件的歸宿。年輕代收集(Minor-GC)過程是不會觸及這個地方的。當年輕代收集不能把物件放進終身頤養園時,就會觸發一次完全收集(Major-GC),這裡可能還會牽扯到壓縮,以便為大物件騰出足夠的空間。

與垃圾回收相關的 JVM 引數:

 
  1. -Xms / -Xmx — 堆的初始大小 / 堆的最大大小

  2. -Xmn — 堆中年輕代的大小

  3. -XX:-DisableExplicitGC — 讓 System.gc()不產生任何作用

  4. -XX:+PrintGCDetails — 列印 GC 的細節

  5. -XX:+PrintGCDateStamps — 列印 GC 操作的時間戳

  6. -XX:NewSize / XX:MaxNewSize — 設定新生代大小/新生代最大大小

  7. -XX:NewRatio — 可以設定老生代和新生代的比例

  8. -XX:PrintTenuringDistribution — 設定每次新生代 GC 後輸出倖存者

  9. 樂園中物件年齡的分佈

  10. -XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:設定老

  11. 年代閥值的初始值和最大值

  12. -XX:TargetSurvivorRatio:設定倖存區的目標使用率

 

31、String s = new String(“xyz”);建立了幾個字串物件?

兩個物件,一個是靜態區的”xyz”,一個是用 new 建立在堆上的物件。

 

32、介面是否可繼承(extends)介面?抽象類是否可實現(implements)介面?抽象類是否可繼承具體類(concreteclass)?

介面可以繼承介面,而且支援多重繼承。抽象類可以實現(implements)介面,抽象類可繼承具體類也可以繼承抽象類。

 

33、一個”.java”原始檔中是否可以包含多個類(不是內部類)?有什麼限制?

可以,但一個原始檔中最多隻能有一個公開類(public class)而且檔名必須和公開類的類名完全保持一致。

 

34、Anonymous Inner Class(匿名內部類)是否可以繼承其它類?是否可以實現介面?

可以繼承其他類或實現其他介面,在 Swing 程式設計和 Android 開發中常用此方式來實現事件監聽和回撥。

 

35、內部類可以引用它的包含類(外部類)的成員嗎?有沒有什麼限制?

一個內部類物件可以訪問建立它的外部類物件的成員,包括私有成員。

 

36、Java 中的 final 關鍵字有哪些用法?

(1)修飾類:表示該類不能被繼承;

(2)修飾方法:表示方法不能被重寫; 

(3)修飾變數:表示變數只能一次賦值以後值不能被修改(常量)。

 

37、指出下面程式的執行結果

 
class A {
	static {
		System.out.print("1");
	}
	public A() {
		System.out.print("2");
	}
}
class B extends A{
	static {
		System.out.print("a");
	}
	public B() {
		System.out.print("b");
	}
}
public class Hello {
	public static void main(String[] args) {
		A ab = new B();
		ab = new B();
	}
}

執行結果:1a2b2b。建立物件時構造器的呼叫順序是:先初始化靜態成員,然後呼叫父類構造器,再初始化非靜態成員,最後呼叫自身構造器。

提示:如果不能給出此題的正確答案,說明之前第 21 題 Java 類載入機制還沒有完全理解,趕緊再看看吧。

 

38、資料型別之間的轉換:

(1) 如何將字串轉換為基本資料型別?

(2) 如何將基本資料型別轉換為字串?

答:

(1)呼叫基本資料型別對應的包裝類中的方法 parseXXX(String)或valueOf(String)即可返回相應基本型別;

(2)一種方法是將基本資料型別與空字串(”“)連線(+)即可獲得其所對應的字串;另一種方法是呼叫 String 類中的 valueOf()方法返回相應字串

 

39、如何實現字串的反轉及替換?

方法很多,可以自己寫實現也可以使用 String 或 StringBuffer/StringBuilder 中的方法。有一道很常見的面試題是用遞迴實現字串反轉,程式碼如下所示:

 
public static String reverse(String originStr) {
	if(originStr == null || originStr.length() <= 1)
	return originStr;
	return reverse(originStr.substring(1)) + originStr.charAt(0);
}

40、怎樣將 GB2312 編碼的字串轉換為 ISO-8859-1 編碼的字串?

程式碼如下所示:

String s1 = "你好";
String s2 = new String(s1.getBytes("GB2312"), "ISO-8859-1");

 

41、日期和時間:

(1)如何取得年月日、小時分鐘秒?

(2) 如何取得從 1970 年 1 月 1 日 0 時 0 分 0 秒到現在的毫秒數?

(3) 如何取得某月的最後一天?

(4)如何格式化日期?

答:

問題 1:建立 java.util.Calendar 例項,呼叫其 get()方法傳入不同的引數即可獲得引數所對應的值。Java 8 中可以使用 java.time.LocalDateTimel 來獲取,程式碼如下所示。

public class DateTimeTest {
	public static void main(String[] args) {
		Calendar cal = Calendar.getInstance();
		System.out.println(cal.get(Calendar.YEAR));
		System.out.println(cal.get(Calendar.MONTH));
		// 0 - 11
		System.out.println(cal.get(Calendar.DATE));
		System.out.println(cal.get(Calendar.HOUR_OF_DAY));
		System.out.println(cal.get(Calendar.MINUTE));
		System.out.println(cal.get(Calendar.SECOND));
		// Java 8
		LocalDateTime dt = LocalDateTime.now();
		System.out.println(dt.getYear());
		System.out.println(dt.getMonthValue());
		// 1 - 12
		System.out.println(dt.getDayOfMonth());
		System.out.println(dt.getHour());
		System.out.println(dt.getMinute());
		System.out.println(dt.getSecond());
	}
}

問題 2:以下方法均可獲得該毫秒數。

Calendar.getInstance().getTimeInMillis();
System.currentTimeMillis();
Clock.systemDefaultZone().millis();

問題 3:程式碼如下所示。
Calendar time = Calendar.getInstance();
time.getActualMaximum(Calendar.DAY_OF_MONTH
問題 4:利用 java.text.DataFormat 的子類(如 SimpleDateFormat 類)中的format(Date)方法可將日期格式化。Java 8 中可以用java.time.format.DateTimeFormatter 來格式化時間日期,程式碼如下所示。
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Date;
class DateFormatTest {
	public static void main(String[] args) {
		SimpleDateFormat oldFormatter = new
		SimpleDateFormat("yyyy/MM/dd");
		Date date1 = new Date();
		System.out.println(oldFormatter.format(date1));
		// Java 8
		DateTimeFormatter newFormatter =
		DateTimeFormatter.ofPattern("yyyy/MM/dd");
		LocalDate date2 = LocalDate.now();
		System.out.println(date2.format(newFormatter));
	}
}

補充:Java 的時間日期 API 一直以來都是被詬病的東西,為了解決這一問題,Java8 中引入了新的時間日期 API,其中包括 LocalDate、LocalTime、LocalDateTime、Clock、Instant 等類,這些的類的設計都使用了不變模式,因此是執行緒安全的設計。

 

42、列印昨天的當前時刻。

import java.util.Calendar;
class YesterdayCurrent {
	public static void main(String[] args){
		Calendar cal = Calendar.getInstance();
		cal.add(Calendar.DATE, -1);
		System.out.println(cal.getTime());
	}
}

在 Java 8 中,可以用下面的程式碼實現相同的功能。

 

import java.time.LocalDateTime;
class YesterdayCurrent {
	public static void main(String[] args) {
		LocalDateTime today = LocalDateTime.now();
		LocalDateTime yesterday = today.minusDays(1);
		System.out.println(yesterday);
	}
}

43、比較一下 Java 和 JavaSciprt。

JavaScript 與 Java 是兩個公司開發的不同的兩個產品。Java 是原 SunMicrosystems 公司推出的物件導向的程式設計語言,特別適合於網際網路應用程式開發;而 JavaScript 是 Netscape 公司的產品,為了擴充套件 Netscape 瀏覽器的功能而開發的一種可以嵌入 Web 頁面中執行的基於物件和事件驅動的解釋性語言。JavaScript 的前身是 LiveScript;而 Java 的前身是 Oak 語言。

下面對兩種語言間的異同作如下比較:

(1)基於物件和麵向物件:Java 是一種真正的物件導向的語言,即使是開發簡單的程式,必須設計物件;JavaScript 是種指令碼語言,它可以用來製作與網路無關的,與使用者互動作用的複雜軟體。它是一種基於物件(Object-Based)和事件驅動(Event-Driven)的程式語言,因而它本身提供了非常豐富的內部物件供設計人員使用。

(2)解釋和編譯:Java 的原始碼在執行之前,必須經過編譯。JavaScript 是一種解釋性程式語言,其原始碼不需經過編譯,由瀏覽器解釋執行。(目前的瀏覽器幾乎都使用了 JIT(即時編譯)技術來提升 JavaScript 的執行效率)

(3)強型別變數和型別弱變數:Java 採用強型別變數檢查,即所有變數在編譯之前必須作宣告;JavaScript 中變數是弱型別的,甚至在使用變數前可以不作宣告,JavaScript 的直譯器在執行時檢查推斷其資料型別。

(4)程式碼格式不一樣。

補充:上面列出的四點是網上流傳的所謂的標準答案。其實 Java 和 JavaScript最重要的區別是一個是靜態語言,一個是動態語言。目前的程式語言的發展趨勢是函式式語言和動態語言。在 Java 中類(class)是一等公民,而 JavaScript 中函式(function)是一等公民,因此 JavaScript 支援函數語言程式設計,可以使用 Lambda函式和閉包(closure),當然 Java 8 也開始支援函數語言程式設計,提供了對 Lambda表示式以及函式式介面的支援。對於這類問題,在面試的時候最好還是用自己的語言回答會更加靠譜,不要背網上所謂的標準答案。

 

44、什麼時候用斷言(assert)?

斷言在軟體開發中是一種常用的除錯方式,很多開發語言中都支援這種機制。一般來說,斷言用於保證程式最基本、關鍵的正確性。斷言檢查通常在開發和測試時開啟。為了保證程式的執行效率,在軟體釋出後斷言檢查通常是關閉的。斷言是一個包含布林表示式的語句,在執行這個語句時假定該表示式為 true;如果表示式的值為 false,那麼系統會報告一個 AssertionError。斷言的使用如下面的程式碼所示:

assert(a > 0); // throws an AssertionError if a <= 0

斷言可以有兩種形式:

assert Expression1;

assert Expression1 : Expression2 ;

Expression1 應該總是產生一個布林值。

Expression2 可以是得出一個值的任意表示式;這個值用於生成顯示更多除錯資訊的字串訊息。

要在執行時啟用斷言,可以在啟動 JVM 時使用-enableassertions 或者-ea 標記。要在執行時選擇禁用斷言,可以在啟動 JVM 時使用-da 或者-disableassertions標記。要在系統類中啟用或禁用斷言,可使用-esa 或-dsa 標記。還可以在包的基礎上啟用或者禁用斷言。

注意:斷言不應該以任何方式改變程式的狀態。簡單的說,如果希望在不滿足某些條件時阻止程式碼的執行,就可以考慮用斷言來阻止它。

 

45、Error 和 Exception 有什麼區別?

Error 表示系統級的錯誤和程式不必處理的異常,是恢復不是不可能但很困難的情況下的一種嚴重問題;比如記憶體溢位,不可能指望程式能處理這樣的情況;

Exception 表示需要捕捉或者需要程式進行處理的異常,是一種設計或實現問題;也就是說,它表示如果程式執行正常,從不會發生的情況。

 

46、try{}裡有一個 return 語句,那麼緊跟在這個 try 後的finally{}裡的程式碼會不會被執行,什麼時候被執行,在 return前還是後?

會執行,在方法返回撥用者前執行。

注意:在 finally 中改變返回值的做法是不好的,因為如果存在 finally 程式碼塊,try中的 return 語句不會立馬返回撥用者,而是記錄下返回值待 finally 程式碼塊執行完畢之後再向呼叫者返回其值,然後如果在 finally 中修改了返回值,就會返回修改後的值。顯然,在 finally 中返回或者修改返回值會對程式造成很大的困擾,C#中直接用編譯錯誤的方式來阻止程式設計師幹這種齷齪的事情,Java 中也可以通過提升編譯器的語法檢查級別來產生警告或錯誤,Eclipse 中可以在如圖所示的地方進行設定,強烈建議將此項設定為編譯錯誤。

 

47、Java 語言如何進行異常處理,關鍵字:throws、throw、try、catch、finally 分別如何使用?

Java 通過物件導向的方法進行異常處理,把各種不同的異常進行分類,並提供了良好的介面。在 Java 中,每個異常都是一個物件,它是 Throwable 類或其子類的例項。當一個方法出現異常後便丟擲一個異常物件,該物件中包含有異常資訊,呼叫這個物件的方法可以捕獲到這個異常並可以對其進行處理。Java 的異常處理是通過 5 個關鍵詞來實現的:try、catch、throw、throws 和 finally。一般情況下是用 try 來執行一段程式,如果系統會丟擲(throw)一個異常物件,可以通過它的型別來捕獲(catch)它,或通過總是執行程式碼塊(finally)來處理;try 用來指定一塊預防所有異常的程式;catch 子句緊跟在 try 塊後面,用來指定你想要捕獲的異常的型別;throw 語句用來明確地丟擲一個異常;throws 用來宣告一個方法可能丟擲的各種異常(當然宣告異常時允許無病呻吟);finally 為確保一段程式碼不管發生什麼異常狀況都要被執行;try 語句可以巢狀,每當遇到一個 try 語句,異常的結構就會被放入異常棧中,直到所有的 try 語句都完成。如果下一級的try 語句沒有對某種異常進行處理,異常棧就會執行出棧操作,直到遇到有處理這種異常的 try 語句或者最終將異常拋給 JVM。

 

48、執行時異常與受檢異常有何異同?

異常表示程式執行過程中可能出現的非正常狀態,執行時異常表示虛擬機器的通常操作中可能遇到的異常,是一種常見執行錯誤,只要程式設計得沒有問題通常就不會發生。受檢異常跟程式執行的上下文環境有關,即使程式設計無誤,仍然可能因使用的問題而引發。Java 編譯器要求方法必須宣告丟擲可能發生的受檢異常,但是並不要求必須宣告丟擲未被捕獲的執行時異常。異常和繼承一樣,是物件導向程式設計中經常被濫用的東西,在 Effective Java 中對異常的使用給出了以下指導原則:

(1)不要將異常處理用於正常的控制流(設計良好的 API 不應該強迫它的呼叫者為了正常的控制流而使用異常)

(2)對可以恢復的情況使用受檢異常,對程式設計錯誤使用執行時異常

(3)避免不必要的使用受檢異常(可以通過一些狀態檢測手段來避免異常的發生)

(4)優先使用標準的異常

(5)每個方法丟擲的異常都要有文件

(6)保持異常的原子性

(7)不要在 catch 中忽略掉捕獲到的異常

 

49、列出一些你常見的執行時異常?

(1)ArithmeticException(算術異常)

(2) ClassCastException (類轉換異常)

(3) IllegalArgumentException (非法引數異常)

(4) IndexOutOfBoundsException (下標越界異常)

(5) NullPointerException (空指標異常)

(6) SecurityException (安全異常)

 

50、闡述 final、finally、finalize 的區別。

(1) final:修飾符(關鍵字)有三種用法:如果一個類被宣告為 final,意味著它不能再派生出新的子類,即不能被繼承,因此它和 abstract 是反義詞。將變數宣告為 final,可以保證它們在使用中不被改變,被宣告為 final 的變數必須在宣告時給定初值,而在以後的引用中只能讀取不可修改。被宣告為 final 的方法也同樣只能使用,不能在子類中被重寫。

(2)finally:通常放在 try…catch…的後面構造總是執行程式碼塊,這就意味著程式無論正常執行還是發生異常,這裡的程式碼只要 JVM 不關閉都能執行,可以將釋放外部資源的程式碼寫在 finally 塊中.

(3)finalize:Object 類中定義的方法,Java 中允許使用 finalize()方法在垃圾收集器將物件從記憶體中清除出去之前做必要的清理工作。這個方法是由垃圾收集器在銷燬物件時呼叫的,通過重寫 finalize()方法可以整理系統資源或者執行其他清理工作。

 

51、類 ExampleA 繼承 Exception,類 ExampleB 繼承ExampleA。

有如下程式碼片斷:

try {
	throw new ExampleB("b")
}
catch(ExampleA e){
	System.out.println("ExampleA");
}
catch(Exception e){
	System.out.println("Exception");
}

**請問執行此段程式碼的輸出是什麼?

答:

輸出:ExampleA。(根據里氏代換原則[能使用父型別的地方一定能使用子型別],抓取 ExampleA 型別異常的 catch 塊能夠抓住 try 塊中丟擲的 ExampleB 型別的異常)

面試題 - 說出下面程式碼的執行結果。(此題的出處是《Java 程式設計思想》一書)

class Annoyance extends Exception {
}
class Sneeze extends Annoyance {
}
class Human {
	public static void main(String[] args)
	throws Exception {
		try {
			try {
				throw new Sneeze();
			}
			catch ( Annoyance a ) {
				System.out.println("Caught Annoyance");
				throw a;
			}
		}
		catch ( Sneeze s ) {
			System.out.println("Caught Sneeze");
			return ;
		}
		finally {
			System.out.println("Hello World!");
		}
	}

 

52、List、Set、Map 是否繼承自 Collection 介面?

List、Set 是 ,Map 不是。Map 是鍵值對對映容器,與 List 和 Set 有明顯的區別,而 Set 儲存的零散的元素且不允許有重複元素(數學中的集合也是如此),List是線性結構的容器,適用於按數值索引訪問元素的情形。

 

53、闡述 ArrayList、Vector、LinkedList 的儲存效能和特性。

ArrayList 和 Vector 都是使用陣列方式儲存資料,此陣列元素數大於實際儲存的資料以便增加和插入元素,它們都允許直接按序號索引元素,但是插入元素要涉及陣列元素移動等記憶體操作,所以索引資料快而插入資料慢,Vector 中的方法由於新增了 synchronized 修飾,因此 Vector 是執行緒安全的容器,但效能上較ArrayList 差,因此已經是 Java 中的遺留容器。LinkedList 使用雙向連結串列實現儲存(將記憶體中零散的記憶體單元通過附加的引用關聯起來,形成一個可以按序號索引的線性結構,這種鏈式儲存方式與陣列的連續儲存方式相比,記憶體的利用率更高),按序號索引資料需要進行前向或後向遍歷,但是插入資料時只需要記錄本項的前後項即可,所以插入速度較快。Vector 屬於遺留容器(Java 早期的版本中提供的容器,除此之外,Hashtable、Dictionary、BitSet、Stack、Properties都是遺留容器),已經不推薦使用,但是由於 ArrayList 和 LinkedListed 都是非執行緒安全的,如果遇到多個執行緒操作同一個容器的場景,則可以通過工具類Collections 中的 synchronizedList 方法將其轉換成執行緒安全的容器後再使用(這是對裝潢模式的應用,將已有物件傳入另一個類的構造器中建立新的物件來增強實現)。

補充:遺留容器中的 Properties 類和 Stack 類在設計上有嚴重的問題,Properties是一個鍵和值都是字串的特殊的鍵值對對映,在設計上應該是關聯一個Hashtable 並將其兩個泛型引數設定為 String 型別,但是 Java API 中的Properties 直接繼承了 Hashtable,這很明顯是對繼承的濫用。這裡複用程式碼的方式應該是 Has-A 關係而不是 Is-A 關係,另一方面容器都屬於工具類,繼承工具類本身就是一個錯誤的做法,使用工具類最好的方式是 Has-A 關係(關聯)或Use-A 關係(依賴)。同理,Stack 類繼承 Vector 也是不正確的。Sun 公司的工程師們也會犯這種低階錯誤,讓人唏噓不已。

 

54、Collection 和 Collections 的區別?

Collection 是一個介面,它是 Set、List 等容器的父介面;Collections 是個一個工具類,提供了一系列的靜態方法來輔助容器操作,這些方法包括對容器的搜尋、排序、執行緒安全化等等。

 

55、List、Map、Set 三個介面存取元素時,各有什麼特點?

List 以特定索引來存取元素,可以有重複元素。Set 不能存放重複元素(用物件的equals()方法來區分元素是否重複)。Map 儲存鍵值對(key-value pair)對映,對映關係可以是一對一或多對一。Set 和 Map 容器都有基於雜湊儲存和排序樹的兩種實現版本,基於雜湊儲存的版本理論存取時間複雜度為 O(1),而基於排序樹版本的實現在插入或刪除元素時會按照元素或元素的鍵(key)構成排序樹從而達到排序和去重的效果。

 

56、TreeMap 和 TreeSet 在排序時如何比較元素?Collections 工具類中的 sort()方法如何比較元素?

TreeSet 要求存放的物件所屬的類必須實現 Comparable 介面,該介面提供了比較元素的 compareTo()方法,當插入元素時會回撥該方法比較元素的大小。TreeMap 要求存放的鍵值對對映的鍵必須實現 Comparable 介面從而根據鍵對元素進 行排 序。Collections 工具類的 sort 方法有兩種過載的形式,第一種要求傳入的待排序容器中存放的物件比較實現 Comparable 介面以實現元素的比較;第二種不強制性的要求容器中的元素必須可比較,但是要求傳入第二個引數,引數是Comparator 介面的子型別(需要重寫 compare 方法實現元素的比較),相當於一個臨時定義的排序規則,其實就是通過介面注入比較元素大小的演算法,也是對回撥模式的應用(Java 中對函數語言程式設計的支援)。

 

57、Thread 類的 sleep()方法和物件的 wait()方法都可以讓執行緒暫停執行,它們有什麼區別?

sleep()方法(休眠)是執行緒類(Thread)的靜態方法,呼叫此方法會讓當前執行緒暫停執行指定的時間,將執行機會(CPU)讓給其他執行緒,但是物件的鎖依然保持,因此休眠時間結束後會自動恢復(執行緒回到就緒狀態,請參考第 66 題中的執行緒狀態轉換圖)。wait()是 Object 類的方法,呼叫物件的 wait()方法導致當前執行緒放棄物件的鎖(執行緒暫停執行),進入物件的等待池(wait pool),只有呼叫物件的 notify()方法(或 notifyAll()方法)時才能喚醒等待池中的執行緒進入等鎖池(lock pool),如果執行緒重新獲得物件的鎖就可以進入就緒狀態。

補充:可能不少人對什麼是程式,什麼是執行緒還比較模糊,對於為什麼需要多執行緒程式設計也不是特別理解。簡單的說:程式是具有一定獨立功能的程式關於某個資料集合上的一次執行活動,是作業系統進行資源分配和排程的一個獨立單位;執行緒是程式的一個實體,是 CPU 排程和分派的基本單位,是比程式更小的能獨立執行的基本單位。執行緒的劃分尺度小於程式,這使得多執行緒程式的併發性高;程式在執行時通常擁有獨立的記憶體單元,而執行緒之間可以共享記憶體。使用多執行緒的程式設計通常能夠帶來更好的效能和使用者體驗,但是多執行緒的程式對於其他程式是不友好的,因為它可能佔用了更多的 CPU 資源。當然,也不是執行緒越多,程式的效能就越好,因為執行緒之間的排程和切換也會浪費 CPU 時間。時下很時髦的 Node.js就採用了單執行緒非同步 I/O 的工作模式。

 

58、執行緒的 sleep()方法和 yield()方法有什麼區別?

(1) sleep()方法給其他執行緒執行機會時不考慮執行緒的優先順序,因此會給低優先順序的執行緒以執行的機會;yield()方法只會給相同優先順序或更高優先順序的執行緒以執行的機會;

(2) 執行緒執行 sleep()方法後轉入阻塞(blocked)狀態,而執行 yield()方法後轉入就緒(ready)狀態;

(3)sleep()方法宣告丟擲 InterruptedException,而 yield()方法沒有宣告任何異常;

(4)sleep()方法比 yield()方法(跟作業系統 CPU 排程相關)具有更好的可移植性。

 

59、當一個執行緒進入一個物件的 synchronized 方法 A 之後,其它執行緒是否可進入此物件的 synchronized 方法 B?

不能。其它執行緒只能訪問該物件的非同步方法,同步方法則不能進入。因為非靜態方法上的 synchronized 修飾符要求執行方法時要獲得物件的鎖,如果已經進入A 方法說明物件鎖已經被取走,那麼試圖進入 B 方法的執行緒就只能在等鎖池(注意不是等待池哦)中等待物件的鎖。

 

60、請說出與執行緒同步以及執行緒排程相關的方法。

(1) wait():使一個執行緒處於等待(阻塞)狀態,並且釋放所持有的物件的鎖;

(2)sleep():使一個正在執行的執行緒處於睡眠狀態,是一個靜態方法,呼叫此方法要處理 InterruptedException 異常;

(3)notify():喚醒一個處於等待狀態的執行緒,當然在呼叫此方法的時候,並不能確切的喚醒某一個等待狀態的執行緒,而是由 JVM 確定喚醒哪個執行緒,而且與優先順序無關;

(4)notityAll():喚醒所有處於等待狀態的執行緒,該方法並不是將物件的鎖給所有執行緒,而是讓它們競爭,只有獲得鎖的執行緒才能進入就緒狀態;

補充:Java 5 通過 Lock 介面提供了顯式的鎖機制(explicit lock),增強了靈活性以及對執行緒的協調。Lock 介面中定義了加鎖(lock())和解鎖(unlock())的方法,同時還提供了 newCondition()方法來產生用於執行緒之間通訊的 Condition 物件;此外,Java 5 還提供了訊號量機制(semaphore),訊號量可以用來限制對某個共享資源進行訪問的執行緒的數量。在對資源進行訪問之前,執行緒必須得到訊號量的許可(呼叫 Semaphore 物件的 acquire()方法);在完成對資源的訪問後,執行緒必須向訊號量歸還許可(呼叫 Semaphore 物件的 release()方法)。

 

61、編寫多執行緒程式有幾種實現方式?

Java 5 以前實現多執行緒有兩種實現方法:一種是繼承 Thread 類;另一種是實現Runnable 介面。兩種方式都要通過重寫 run()方法來定義執行緒的行為,推薦使用後者,因為 Java 中的繼承是單繼承,一個類有一個父類,如果繼承了 Thread 類就無法再繼承其他類了,顯然使用 Runnable 介面更為靈活。

補充:Java 5 以後建立執行緒還有第三種方式:實現 Callable 介面,該介面中的 call方法可以線上程執行結束時產生一個返回值。

 

62、synchronized 關鍵字的用法?

synchronized 關鍵字可以將物件或者方法標記為同步,以實現對物件和方法的互斥訪問,可以用 synchronized(物件) { … }定義同步程式碼塊,或者在宣告方法時將 synchronized 作為方法的修飾符。在第 60 題的例子中已經展示了synchronized 關鍵字的用法。

 

63、舉例說明同步和非同步。

如果系統中存在臨界資源(資源數量少於競爭資源的執行緒數量的資源),例如正在寫的資料以後可能被另一個執行緒讀到,或者正在讀的資料可能已經被另一個執行緒寫過了,那麼這些資料就必須進行同步存取(資料庫操作中的排他鎖就是最好的例子)。當應用程式在物件上呼叫了一個需要花費很長時間來執行的方法,並且不希望讓程式等待方法的返回時,就應該使用非同步程式設計,在很多情況下采用非同步途徑往往更有效率。事實上,所謂的同步就是指阻塞式操作,而非同步就是非阻塞式操作。

 

64、啟動一個執行緒是呼叫 run()還是 start()方法?

啟動一個執行緒是呼叫 start()方法,使執行緒所代表的虛擬處理機處於可執行狀態,這意味著它可以由 JVM 排程並執行,這並不意味著執行緒就會立即執行。run()方法是執行緒啟動後要進行回撥(callback)的方法。

 

65、什麼是執行緒池(thread pool)?

在物件導向程式設計中,建立和銷燬物件是很費時間的,因為建立一個物件要獲取記憶體資源或者其它更多資源。在 Java 中更是如此,虛擬機器將試圖跟蹤每一個物件,以便能夠在物件銷燬後進行垃圾回收。所以提高服務程式效率的一個手段就是儘可能減少建立和銷燬物件的次數,特別是一些很耗資源的物件建立和銷燬,這就是”池化資源”技術產生的原因。執行緒池顧名思義就是事先建立若干個可執行的執行緒放入一個池(容器)中,需要的時候從池中獲取執行緒不用自行建立,使用完畢不需要銷燬執行緒而是放回池中,從而減少建立和銷燬執行緒物件的開銷。Java 5+中的 Executor 介面定義一個執行執行緒的工具。它的子型別即執行緒池介面是 ExecutorService。要配置一個執行緒池是比較複雜的,尤其是對於執行緒池的原理不是很清楚的情況下,因此在工具類 Executors 面提供了一些靜態工廠方法,生成一些常用的執行緒池,如下所示:

(1)newSingleThreadExecutor:建立一個單執行緒的執行緒池。這個執行緒池只有一個執行緒在工作,也就是相當於單執行緒序列執行所有任務。如果這個唯一的執行緒因為異常結束,那麼會有一個新的執行緒來替代它。此執行緒池保證所有任務的執行順序按照任務的提交順序執行。

(2)newFixedThreadPool:建立固定大小的執行緒池。每次提交一個任務就建立一個執行緒,直到執行緒達到執行緒池的最大大小。執行緒池的大小一旦達到最大值就會保持不變,如果某個執行緒因為執行異常而結束,那麼執行緒池會補充一個新執行緒。

(3) newCachedThreadPool:建立一個可快取的執行緒池。如果執行緒池的大小超過了處理任務所需要的執行緒,那麼就會回收部分空閒(60 秒不執行任務)的執行緒,當任務數增加時,此執行緒池又可以智慧的新增新執行緒來處理任務。此執行緒池不會對執行緒池大小做限制,執行緒池大小完全依賴於作業系統(或者說 JVM)能夠建立的最大執行緒大小。

(4)newScheduledThreadPool:建立一個大小無限的執行緒池。此執行緒池支援定時以及週期性執行任務的需求。

(5)newSingleThreadExecutor:建立一個單執行緒的執行緒池。此執行緒池支援定時以及週期性執行任務的需求。

第 60 題的例子中演示了通過 Executors 工具類建立執行緒池並使用執行緒池執行執行緒的程式碼。如果希望在伺服器上使用執行緒池,強烈建議使用 newFixedThreadPool方法來建立執行緒池,這樣能獲得更好的效能。

 

66、執行緒的基本狀態以及狀態之間的關係?

 

說明:其中 Running 表示執行狀態,Runnable 表示就緒狀態(萬事俱備,只欠CPU),Blocked 表示阻塞狀態,阻塞狀態又有多種情況,可能是因為呼叫 wait()方法進入等待池,也可能是執行同步方法或同步程式碼塊進入等鎖池,或者是呼叫了 sleep()方法或 join()方法等待休眠或其他執行緒結束,或是因為發生了 I/O 中斷。

 

67、簡述 synchronized 和 java.util.concurrent.locks.Lock的異同?

Lock 是 Java 5 以後引入的新的 API,和關鍵字 synchronized 相比主要相同點:Lock 能完成 synchronized 所實現的所有功能;主要不同點:Lock 有比synchronized 更精確的執行緒語義和更好的效能,而且不強制性的要求一定要獲得鎖。synchronized 會自動釋放鎖,而 Lock 一定要求程式設計師手工釋放,並且最好在 finally 塊中釋放(這是釋放外部資源的最好的地方)。

 

68、Java 中如何實現序列化,有什麼意義?

序列化就是一種用來處理物件流的機制,所謂物件流也就是將物件的內容進行流化。可以對流化後的物件進行讀寫操作,也可將流化後的物件傳輸於網路之間。序列化是為了解決物件流讀寫操作時可能引發的問題(如果不進行序列化可能會存在資料亂序的問題)。要實現序列化,需要讓一個類實現 Serializable 介面,該介面是一個標識性介面,標註該類物件是可被序列化的,然後使用一個輸出流來構造一個物件輸出流並通過 writeObject(Object)方法就可以將實現物件寫出(即儲存其狀態);如果需要反序列化則可以用一個輸入流建立物件輸入流,然後通過 readObject 方法從流中讀取物件。序列化除了能夠實現物件的持久化之外,還能夠用於物件的深度克隆(可以參考第 29 題)。

 

69、Java 中有幾種型別的流?

位元組流和字元流。位元組流繼承於 InputStream、OutputStream,字元流繼承於Reader、Writer。在 java.io 包中還有許多其他的流,主要是為了提高效能和使用方便。關於 Java 的 I/O 需要注意的有兩點:一是兩種對稱性(輸入和輸出的對稱性,位元組和字元的對稱性);二是兩種設計模式(介面卡模式和裝潢模式)。另外 Java 中的流不同於 C#的是它只有一個維度一個方向。

 

70、寫一個方法,輸入一個檔名和一個字串,統計這個字串在這個檔案中出現的次數。

程式碼如下:

 

import java.io.BufferedReader;
import java.io.FileReader;
public final class MyUtil {
	// 工具類中的方法都是靜態方式訪問的因此將構造器私有不允許建立物件(絕對好習慣)
	private MyUtil() {
		throw new AssertionError();
	}
 
   /**
    * 統計給定檔案中給定字串的出現次數
    *
    * @param filename 檔名
    * @param word 字串
    * @return 字串在檔案中出現的次數
   */
	public static int countWordInFile(String filename, String word) {
		int counter = 0;
		try (FileReader fr = new FileReader(filename)) {
			try (BufferedReader br = new BufferedReader(fr)) {
				String line = null;
				while ((line = br.readLine()) != null) {
					int index = -1;
					while (line.length() >= word.length() && (index =
					line.indexOf(word)) >= 0) {
						counter++;
						line = line.substring(index + word.length());
					}
				}
			}
		}
		catch (Exception ex) {
			ex.printStackTrace();
		}
		return counter;
	}
}

71、如何用 Java 程式碼列出一個目錄下所有的檔案?

如果只要求列出當前資料夾下的檔案,程式碼如下所示:

 
import java.io.File;
class Test12 {
	public static void main(String[] args) {
		File f = new File("/Users/Hao/Downloads");
		for (File temp : f.listFiles()) {
			if(temp.isFile()) {
				System.out.println(temp.getName());
			}
		}
	}
}

如果需要對資料夾繼續展開,程式碼如下所示:

import java.io.File;
class Test12 {
	public static void main(String[] args) {
		showDirectory(new File("/Users/Hao/Downloads"));
	}
	public static void showDirectory(File f) {
		_walkDirectory(f, 0);
	}
	private static void _walkDirectory(File f, int level) {
		if(f.isDirectory()) {
			for (File temp : f.listFiles()) {
				_walkDirectory(temp, level + 1);
			}
		} else {
			for (int i = 0; i < level - 1; i++) {
				System.out.print("t");
            }
         System.out.println(f.getName());
        }
    }
}
在 Java 7 中可以使用 NIO.2 的 API 來做同樣的事情,程式碼如下所示:
class ShowFileTest {
	public static void main(String[] args) throws IOException {
		Path initPath = Paths.get("/Users/Hao/Downloads");
		Files.walkFileTree(initPath, new SimpleFileVisitor<Path>() {
			@Override
			public FileVisitResult visitFile(Path file, BasicFileAttributes
			attrs)
			throws IOException {
				System.out.println(file.getFileName().toString());
				return FileVisitResult.CONTINUE;
			}
		}
		);
	}
}

 

72、XML 文件定義有幾種形式?它們之間有何本質區別?解析XML 文件有哪幾種方式?

XML 文件定義分為 DTD 和 Schema 兩種形式,二者都是對 XML 語法的約束,其本質區別在於 Schema 本身也是一個 XML 檔案,可以被 XML 解析器解析,而且可以為 XML 承載的資料定義型別,約束能力較之 DTD 更強大。對 XML 的解析主要有 DOM(文件物件模型,Document Object Model)、SAX(Simple API forXML)和 StAX(Java 6 中引入的新的解析 XML 的方式,Streaming API for XML),其中 DOM 處理大型檔案時其效能下降的非常厲害,這個問題是由 DOM 樹結構佔用的記憶體較多造成的,而且 DOM 解析方式必須在解析檔案之前把整個文件裝入記憶體,適合對 XML 的隨機訪問(典型的用空間換取時間的策略);SAX 是事件驅動型的 XML 解析方式,它順序讀取 XML 檔案,不需要一次全部裝載整個檔案。當遇到像檔案開頭,文件結束,或者標籤開頭與標籤結束時,它會觸發一個事件,使用者通過事件回撥程式碼來處理 XML 檔案,適合對 XML 的順序訪問;顧名思義,StAX 把重點放在流上,實際上 StAX 與其他解析方式的本質區別就在於應用程式能夠把 XML 作為一個事件流來處理。將 XML 作為一組事件來處理的想法並不新穎( SAX 就是這樣做的),但不同之處在於 StAX 允許應用程式程式碼把這些事件逐個拉出來,而不用提供在解析器方便時從解析器中接收事件的處理程式。

 

73、你在專案中哪些地方用到了 XML?

XML 的主要作用有兩個方面:資料交換和資訊配置。在做資料交換時,XML 將資料用標籤組裝成起來,然後壓縮打包加密後通過網路傳送給接收者,接收解密與解壓縮後再從 XML 檔案中還原相關資訊進行處理,XML 曾經是異構系統間交換資料的事實標準,但此項功能幾乎已經被被JSON(JavaScript Object Notation)取而代之。當然,目前很多軟體仍然使用 XML 來儲存配置資訊,我們在很多專案中通常也會將作為配置資訊的硬程式碼寫在 XML 檔案中,Java 的很多框架也是這麼做的,而且這些框架都選擇了 dom4j 作為處理 XML 的工具,因為 Sun 公司的官方API 實在不怎麼好用。

補充:現在有很多時髦的軟體(如 Sublime)已經開始將配置檔案書寫成 JSON格式,我們已經強烈的感受到 XML 的另一項功能也將逐漸被業界拋棄。

 

74、闡述 JDBC 運算元據庫的步驟。

下面的程式碼以連線本機的 Oracle 資料庫為例,演示 JDBC 運算元據庫的步驟。

(1) 載入驅動。

Class.forName("oracle.jdbc.driver.OracleDriver");

(2) 建立連線。

 
Connection con =
DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:orcl",
"scott", "tiger");

(3) 建立語句。

PreparedStatement ps = con.prepareStatement("select * from empwhere sal between ? and ?");
ps.setint(1, 1000);
ps.setint(2, 3000);

(4)執行語句。

ResultSet rs = ps.executeQuery();

(5)處理結果。

while(rs.next()) {
	System.out.println(rs.getint("empno") + " - " +
	rs.getString("ename"));
}

(6) 關閉資源。

 
finally {
	if(con != null) {
		try {
			con.close();
		}
		catch (SQLException e) {
			e.printStackTrace();
		}
	}
}

提示:關閉外部資源的順序應該和開啟的順序相反,也就是說先關閉 ResultSet、再關閉 Statement、在關閉 Connection。上面的程式碼只關閉了 Connection(連線),雖然通常情況下在關閉連線時,連線上建立的語句和開啟的遊標也會關閉,但不能保證總是如此,因此應該按照剛才說的順序分別關閉。此外,第一步載入驅動在 JDBC 4.0 中是可以省略的(自動從類路徑中載入驅動),但是我們建議保留。

 

75、Statement 和 PreparedStatement 有什麼區別?哪個效能更好?

與 Statement 相比,①PreparedStatement 介面代表預編譯的語句,它主要的優勢在於可以減少 SQL 的編譯錯誤並增加 SQL 的安全性(減少 SQL 注射攻擊的可能性);②PreparedStatement 中的 SQL 語句是可以帶引數的,避免了用字串連線拼接 SQL 語句的麻煩和不安全;③當批量處理 SQL 或頻繁執行相同的查詢時,PreparedStatement 有明顯的效能上的優勢,由於資料庫可以將編譯優化後的SQL 語句快取起來,下次執行相同結構的語句時就會很快(不用再次編譯和生成執行計劃)。

補充:為了提供對儲存過程的呼叫,JDBC API 中還提供了 CallableStatement 介面。儲存過程(Stored Procedure)是資料庫中一組為了完成特定功能的 SQL 語句的集合,經編譯後儲存在資料庫中,使用者通過指定儲存過程的名字並給出引數(如果該儲存過程帶有引數)來執行它。雖然呼叫儲存過程會在網路開銷、安全性、效能上獲得很多好處,但是存在如果底層資料庫發生遷移時就會有很多麻煩,因為每種資料庫的儲存過程在書寫上存在不少的差別。

 

76、使用 JDBC 運算元據庫時,如何提升讀取資料的效能?如何提升更新資料的效能?

要提升讀取資料的效能,可以指定通過結果集(ResultSet)物件的 setFetchSize()方法指定每次抓取的記錄數(典型的空間換時間策略);要提升更新資料的效能可以使用 PreparedStatement 語句構建批處理,將若干 SQL 語句置於一個批處理中執行。

 

77、在進行資料庫程式設計時,連線池有什麼作用?

由於建立連線和釋放連線都有很大的開銷(尤其是資料庫伺服器不在本地時,每次建立連線都需要進行 TCP 的三次握手,釋放連線需要進行 TCP 四次握手,造成的開銷是不可忽視的),為了提升系統訪問資料庫的效能,可以事先建立若干連線置於連線池中,需要時直接從連線池獲取,使用結束時歸還連線池而不必關閉連線,從而避免頻繁建立和釋放連線所造成的開銷,這是典型的用空間換取時間的策略(浪費了空間儲存連線,但節省了建立和釋放連線的時間)。池化技術在Java 開發中是很常見的,在使用執行緒時建立執行緒池的道理與此相同。基於 Java 的開源資料庫連線池主要有:C3P0、Proxool、DBCP、BoneCP、Druid 等。

補充:在計算機系統中時間和空間是不可調和的矛盾,理解這一點對設計滿足效能要求的演算法是至關重要的。大型網站效能優化的一個關鍵就是使用快取,而快取跟上面講的連線池道理非常類似,也是使用空間換時間的策略。可以將熱點資料置於快取中,當使用者查詢這些資料時可以直接從快取中得到,這無論如何也快過去資料庫中查詢。當然,快取的置換策略等也會對系統效能產生重要影響,對於這個問題的討論已經超出了這裡要闡述的範圍。

 

78、什麼是 DAO 模式?

DAO(Data Access Object)顧名思義是一個為資料庫或其他持久化機制提供了抽象介面的物件,在不暴露底層持久化方案實現細節的前提下提供了各種資料訪問操作。在實際的開發中,應該將所有對資料來源的訪問操作進行抽象化後封裝在一個公共 API 中。用程式設計語言來說,就是建立一個介面,介面中定義了此應用程式中將會用到的所有事務方法。在這個應用程式中,當需要和資料來源進行互動的時候則使用這個介面,並且編寫一個單獨的類來實現這個介面,在邏輯上該類對應一個特定的資料儲存。DAO 模式實際上包含了兩個模式,一是 DataAccessor(資料訪問器),二是 Data Object(資料物件),前者要解決如何訪問資料的問題,而後者要解決的是如何用物件封裝資料。

 

79、事務的 ACID 是指什麼?

(1)原子性(Atomic):事務中各項操作,要麼全做要麼全不做,任何一項操作的失敗都會導致整個事務的失敗;

(2)一致性(Consistent):事務結束後系統狀態是一致的;

(3)隔離性(Isolated):併發執行的事務彼此無法看到對方的中間狀態;

(4)永續性(Durable):事務完成後所做的改動都會被持久化,即使發生災難性的失敗。通過日誌和同步備份可以在故障發生後重建資料。

補充:關於事務,在面試中被問到的概率是很高的,可以問的問題也是很多的。首先需要知道的是,只有存在併發資料訪問時才需要事務。當多個事務訪問同一資料時,可能會存在 5 類問題,包括 3 類資料讀取問題(髒讀、不可重複讀和幻讀)和 2 類資料更新問題(第 1 類丟失更新和第 2 類丟失更新)。

髒讀(Dirty Read):A 事務讀取 B 事務尚未提交的資料並在此基礎上操作,而 B事務執行回滾,那麼 A 讀取到的資料就是髒資料。

不可重複讀(Unrepeatable Read):事務 A 重新讀取前面讀取過的資料,發現該資料已經被另一個已提交的事務 B 修改過了。

幻讀(Phantom Read):事務 A 重新執行一個查詢,返回一系列符合查詢條件的行,發現其中插入了被事務 B 提交的行。

第 1 類丟失更新:事務 A 撤銷時,把已經提交的事務 B 的更新資料覆蓋了。

第 2 類丟失更新:事務 A 覆蓋事務 B 已經提交的資料,造成事務 B 所做的操作丟失。

資料併發訪問所產生的問題,在有些場景下可能是允許的,但是有些場景下可能就是致命的,資料庫通常會通過鎖機制來解決資料併發訪問問題,按鎖定物件不同可以分為表級鎖和行級鎖;按併發事務鎖定關係可以分為共享鎖和獨佔鎖,具體的內容大家可以自行查閱資料進行了解。直接使用鎖是非常麻煩的,為此資料庫為使用者提供了自動鎖機制,只要使用者指定會話的事務隔離級別,資料庫就會通過分析 SQL 語句然後為事務訪問的資源加上合適的鎖,此外,資料庫還會維護這些鎖通過各種手段提高系統的效能,這些對使用者來說都是透明的(就是說你不用理解,事實上我確實也不知道)。ANSI/ISOSQL 92 標準定義了 4 個等級的事務隔離級別,如下表所示:

需要說明的是,事務隔離級別和資料訪問的併發性是對立的,事務隔離級別越高併發性就越差。所以要根據具體的應用來確定合適的事務隔離級別,這個地方沒有萬能的原則。

 

80、JDBC 中如何進行事務處理?

Connection 提供了事務處理的方法,通過呼叫 setAutoCommit(false)可以設定手動提交事務;當事務完成後用 commit()顯式提交事務;如果在事務處理過程中發生異常則通過 rollback()進行事務回滾。除此之外,從 JDBC 3.0 中還引入了Savepoint(儲存點)的概念,允許通過程式碼設定儲存點並讓事務回滾到指定的儲存點。

 

81、JDBC 能否處理 Blob 和 Clob?

Blob 是指二進位制大物件(Binary Large Object),而 Clob 是指大字元物件(Character Large Objec),因此其中 Blob 是為儲存大的二進位制資料而設計的,而 Clob 是為儲存大的文字資料而設計的。JDBC 的 PreparedStatement 和ResultSet 都提供了相應的方法來支援 Blob 和 Clob 操作。

 

82、簡述正規表示式及其用途。

在編寫處理字串的程式時,經常會有查詢符合某些複雜規則的字串的需要。正規表示式就是用於描述這些規則的工具。換句話說,正規表示式就是記錄文字規則的程式碼。

說明:計算機誕生初期處理的資訊幾乎都是數值,但是時過境遷,今天我們使用計算機處理的資訊更多的時候不是數值而是字串,正規表示式就是在進行字串匹配和處理的時候最為強大的工具,絕大多數語言都提供了對正規表示式的支援。

 

83、Java 中是如何支援正規表示式操作的?

Java 中的 String 類提供了支援正規表示式操作的方法,包括:matches()、replaceAll()、replaceFirst()、split()。此外,Java 中可以用 Pattern 類表示正規表示式物件,它提供了豐富的 API 進行各種正規表示式操作。

面試題: - 如果要從字串中擷取第一個英文左括號之前的字串,例如:北京市(朝陽區)(西城區)(海淀區),擷取結果為:北京市,那麼正規表示式怎麼寫?

 
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class RegExpTest {
	public static void main(String[] args) {
		String str = "北京市(朝陽區)(西城區)(海淀區)";
		Pattern p = Pattern.compile(".*?(?=\()");
		Matcher m = p.matcher(str);
		if(m.find()) {
			System.out.println(m.group());
		}
	}
}

 

84、獲得一個類的類物件有哪些方式?

(1)方法 1:型別.class,例如:String.class

(2)方法 2:物件.getClass(),例如:”hello”.getClass()

(3)方法 3:Class.forName(),例如:Class.forName(“java.lang.String”)

 

85、如何通過反射建立物件?

方法 1:通過類物件呼叫 newInstance()方法,例如:String.class.newInstance()

方法 2:通過類物件的 getConstructor()或 getDeclaredConstructor()方法獲得構造器(Constructor)物件並呼叫其 newInstance()方法建立物件,例如:String.class.getConstructor(String.class).newInstance(“Hello”);

 

86、如何通過反射獲取和設定物件私有欄位的值?

可以通過類物件的 getDeclaredField()方法欄位(Field)物件,然後再通過欄位物件的 setAccessible(true)將其設定為可以訪問,接下來就可以通過 get/set 方法來獲取/設定欄位的值了。下面的程式碼實現了一個反射的工具類,其中的兩個靜態方法分別用於獲取和設定私有欄位的值,欄位可以是基本型別也可以是物件型別且支援多級物件操作。

 

87、如何通過反射呼叫物件的方法?

請看下面的程式碼:

import java.lang.reflect.Method;
class MethodInvokeTest {
	public static void main(String[] args) throws Exception {
		String str = "hello";
		Method m = str.getClass().getMethod("toUpperCase");
		System.out.println(m.invoke(str));
		// HELLO
	}
}
 

88、簡述一下物件導向的”六原則一法則”。

(1)單一職責原則:一個類只做它該做的事情。(單一職責原則想表達的就是”高內聚”,寫程式碼最終極的原則只有六個字”高內聚、低耦合”,就如同葵花寶典或辟邪劍譜的中心思想就八個字”欲練此功必先自宮”,所謂的高內聚就是一個程式碼模組只完成一項功能,在物件導向中,如果只讓一個類完成它該做的事,而不涉及與它無關的領域就是踐行了高內聚的原則,這個類就只有單一職責。我們都知道一句話叫”因為專注,所以專業”,一個物件如果承擔太多的職責,那麼註定它什麼都做不好。這個世界上任何好的東西都有兩個特徵,一個是功能單一,好的相機絕對不是電視購物裡面賣的那種一個機器有一百多種功能的,它基本上只能照相;另一個是模組化,好的自行車是組裝車,從減震叉、剎車到變速器,所有的部件都是可以拆卸和重新組裝的,好的乒乓球拍也不是成品拍,一定是底板和膠皮可以拆分和自行組裝的,一個好的軟體系統,它裡面的每個功能模組也應該是可以輕易的拿到其他系統中使用的,這樣才能實現軟體複用的目標。)

(2)開閉原則:軟體實體應當對擴充套件開放,對修改關閉。(在理想的狀態下,當我們需要為一個軟體系統增加新功能時,只需要從原來的系統派生出一些新類就可以,不需要修改原來的任何一行程式碼。要做到開閉有兩個要點:①抽象是關鍵,一個系統中如果沒有抽象類或介面系統就沒有擴充套件點;②封裝可變性,將系統中的各種可變因素封裝到一個繼承結構中,如果多個可變因素混雜在一起,系統將變得複雜而換亂,如果不清楚如何封裝可變性,可以參考《設計模式精解》一書中對橋樑模式的講解的章節。)

(3)依賴倒轉原則:面向介面程式設計。(該原則說得直白和具體一些就是宣告方法的引數型別、方法的返回型別、變數的引用型別時,儘可能使用抽象型別而不用具體型別,因為抽象型別可以被它的任何一個子型別所替代,請參考下面的里氏替換原則。)

(4)里氏替換原則:任何時候都可以用子型別替換掉父型別。(關於里氏替換原則的描述,Barbara Liskov 女士的描述比這個要複雜得多,但簡單的說就是能用父型別的地方就一定能使用子型別。里氏替換原則可以檢查繼承關係是否合理,如果一個繼承關係違背了里氏替換原則,那麼這個繼承關係一定是錯誤的,需要對程式碼進行重構。例如讓貓繼承狗,或者狗繼承貓,又或者讓正方形繼承長方形都是錯誤的繼承關係,因為你很容易找到違反里氏替換原則的場景。需要注意的是:子類一定是增加父類的能力而不是減少父類的能力,因為子類比父類的能力更多,把能力多的物件當成能力少的物件來用當然沒有任何問題。)

(5)介面隔離原則:介面要小而專,絕不能大而全。(臃腫的介面是對介面的汙染,既然介面表示能力,那麼一個介面只應該描述一種能力,介面也應該是高度內聚的。例如,琴棋書畫就應該分別設計為四個介面,而不應設計成一個介面中的四個方法,因為如果設計成一個介面中的四個方法,那麼這個介面很難用,畢竟琴棋書畫四樣都精通的人還是少數,而如果設計成四個介面,會幾項就實現幾個介面,這樣的話每個介面被複用的可能性是很高的。Java 中的介面代表能力、代表約定、代表角色,能否正確的使用介面一定是程式設計水平高低的重要標識。)

(6)合成聚合複用原則:優先使用聚合或合成關係複用程式碼。(通過繼承來複用程式碼是物件導向程式設計中被濫用得最多的東西,因為所有的教科書都無一例外的對繼承進行了鼓吹從而誤導了初學者,類與類之間簡單的說有三種關係,Is-A 關係、Has-A 關係、Use-A 關係,分別代表繼承、關聯和依賴。其中,關聯關係根據其關聯的強度又可以進一步劃分為關聯、聚合和合成,但說白了都是Has-A 關係,合成聚合複用原則想表達的是優先考慮 Has-A 關係而不是 Is-A 關係複用程式碼,原因嘛可以自己從百度上找到一萬個理由,需要說明的是,即使在Java 的 API 中也有不少濫用繼承的例子,例如 Properties 類繼承了 Hashtable類,Stack 類繼承了 Vector 類,這些繼承明顯就是錯誤的,更好的做法是在Properties 類中放置一個 Hashtable 型別的成員並且將其鍵和值都設定為字串來儲存資料,而 Stack 類的設計也應該是在 Stack 類中放一個 Vector 物件來儲存資料。記住:任何時候都不要繼承工具類,工具是可以擁有並可以使用的,而不是拿來繼承的。)

(7)迪米特法則:迪米特法則又叫最少知識原則,一個物件應當對其他物件有儘可能少的瞭解。(迪米特法則簡單的說就是如何做到”低耦合”,門面模式和調停者模式就是對迪米特法則的踐行。對於門面模式可以舉一個簡單的例子,你去一家公司洽談業務,你不需要了解這個公司內部是如何運作的,你甚至可以對這個公司一無所知,去的時候只需要找到公司入口處的前臺美女,告訴她們你要做什麼,她們會找到合適的人跟你接洽,前臺的美女就是公司這個系統的門面。再複雜的系統都可以為使用者提供一個簡單的門面,Java Web 開發中作為前端控制器的 Servlet 或 Filter 不就是一個門面嗎,瀏覽器對伺服器的運作方式一無所知,但是通過前端控制器就能夠根據你的請求得到相應的服務。調停者模式也可以舉一個簡單的例子來說明,例如一臺計算機,CPU、記憶體、硬碟、顯示卡、音效卡各種裝置需要相互配合才能很好的工作,但是如果這些東西都直接連線到一起,計算機的佈線將異常複雜,在這種情況下,主機板作為一個調停者的身份出現,它將各個裝置連線在一起而不需要每個裝置之間直接交換資料,這樣就減小了系統的耦合度和複雜度,如下圖所示。迪米特法則用通俗的話來將就是不要和陌生人打交道,如果真的需要,找一個自己的朋友,讓他替你和陌生人打交道。)

 

 

89、簡述一下你瞭解的設計模式。

所謂設計模式,就是一套被反覆使用的程式碼設計經驗的總結(情境中一個問題經過證實的一個解決方案)。使用設計模式是為了可重用程式碼、讓程式碼更容易被他人理解、保證程式碼可靠性。設計模式使人們可以更加簡單方便的複用成功的設計和體系結構。將已證實的技術表述成設計模式也會使新系統開發者更加容易理解其設計思路。

在 GoF 的《Design Patterns: Elements of Reusable Object-OrientedSoftware》中給出了三類(建立型[對類的例項化過程的抽象化]、結構型[描述如何將類或物件結合在一起形成更大的結構]、行為型[對在不同的物件之間劃分責任和演算法的抽象化])共 23 種設計模式,包括:Abstract Factory(抽象工廠模式),Builder(建造者模式),Factory Method(工廠方法模式),Prototype(原始模型模式),Singleton(單例模式);Facade(門面模式),Adapter(介面卡模式),Bridge(橋樑模式),Composite(合成模式),Decorator(裝飾模式),Flyweight(享元模式),Proxy(代理模式);Command(命令模式),Interpreter(直譯器模式),Visitor(訪問者模式),Iterator(迭代子模式),Mediator(調停者模式),Memento(備忘錄模式),Observer(觀察者模式),State(狀態 模式 ),Strategy(策略 模式 ),Template Method(模板方法模式),Chain Of Responsibility(責任鏈模式)。

面試被問到關於設計模式的知識時,可以揀最常用的作答,例如:

(1)工廠模式:工廠類可以根據條件生成不同的子類例項,這些子類有一個公共的抽象父類並且實現了相同的方法,但是這些方法針對不同的資料進行了不同的操作(多型方法)。當得到子類的例項後,開發人員可以呼叫基類中的方法而不必考慮到底返回的是哪一個子類的例項。

(2)代理模式:給一個物件提供一個代理物件,並由代理物件控制原物件的引用。實際開發中,按照使用目的的不同,代理可以分為:遠端代理、虛擬代理、保護代理、Cache 代理、防火牆代理、同步化代理、智慧引用代理。

(3)介面卡模式:把一個類的介面變換成客戶端所期待的另一種介面,從而使原本因介面不匹配而無法在一起使用的類能夠一起工作。

(4)模板方法模式:提供一個抽象類,將部分邏輯以具體方法或構造器的形式實現,然後宣告一些抽象方法來迫使子類實現剩餘的邏輯。不同的子類可以以不同的方式實現這些抽象方法(多型實現),從而實現不同的業務邏輯。除此之外,還可以講講上面提到的門面模式、橋樑模式、單例模式、裝潢模式(Collections 工具類和 I/O 系統中都使用裝潢模式)等,反正基本原則就是揀自己最熟悉的、用得最多的作答,以免言多必失。

 

90、用 Java 寫一個單例類。

(1)餓漢式單例

public class Singleton {
	private Singleton(){
	}
	private static Singleton instance = new Singleton();
	public static Singleton getInstance(){
		return instance;
	}
}

(2)懶漢式單例

public class Singleton {
	private static Singleton instance = null;
	private Singleton() {
	}
	public static synchronized Singleton getInstance(){
		if (instance == null) instance = new Singleton();
		return instance;
	}
}

注意:實現一個單例有兩點注意事項,①將構造器私有,不允許外界通過構造器建立物件;②通過公開的靜態方法向外界返回類的唯一例項。這裡有一個問題可以思考:Spring 的 IoC 容器可以為普通的類建立單例,它是怎麼做到的呢?

 

91、什麼是 UML?

UML 是統一建模語言(Unified Modeling Language)的縮寫,它發表於 1997年,綜合了當時已經存在的物件導向的建模語言、方法和過程,是一個支援模型化和軟體系統開發的圖形化語言,為軟體開發的所有階段提供模型化和視覺化支援。使用 UML 可以幫助溝通與交流,輔助應用設計和文件的生成,還能夠闡釋系統的結構和行為。

 

92、UML 中有哪些常用的圖?

UML 定義了多種圖形化的符號來描述軟體系統部分或全部的靜態結構和動態結構,包括:用例圖(use case diagram)、類圖(class diagram)、時序圖(sequencediagram)、協作圖(collaboration diagram)、狀態圖(statechart diagram)、活動圖(activity diagram)、構件圖(component diagram)、部署圖(deploymentdiagram)等。在這些圖形化符號中,有三種圖最為重要,分別是:用例圖(用來捕獲需求,描述系統的功能,通過該圖可以迅速的瞭解系統的功能模組及其關係)、類圖(描述類以及類與類之間的關係,通過該圖可以快速瞭解系統)、時序圖(描述執行特定任務時物件之間的互動關係以及執行順序,通過該圖可以瞭解物件能接收的訊息也就是說物件能夠向外界提供的服務)。用例圖:

類圖:

時序圖:

 

 

93、用 Java 寫一個氣泡排序。

氣泡排序幾乎是個程式設計師都寫得出來,但是面試的時候如何寫一個逼格高的氣泡排序卻不是每個人都能做到,下面提供一個參考程式碼:

 
import java.util.Comparator;
/**
* 排序器介面(策略模式: 將演算法封裝到具有共同介面的獨立的類中使得它們可
以相互替換)
*
*/
public interface Sorter {
	/**
* 排序
* @param list 待排序的陣列
*/
	public <T extends Comparable<T>> void sort(T[] list);
	/**
* 排序
* @param list 待排序的陣列
* @param comp 比較兩個物件的比較器
*/
	public <T> void sort(T[] list, Comparator<T> comp);
}
import java.util.Comparator;
/**
* 氣泡排序
*
*/
public class BubbleSorter implements Sorter {
	@Override
	public <T extends Comparable<T>> void sort(T[] list) {
		Boolean swapped = true;
		for (int i = 1, len = list.length; i < len && swapped; ++i) {
			swapped = false;
			for (int j = 0; j < len - i; ++j) {
				if (list[j].compareTo(list[j + 1]) > 0) {
					T temp = list[j];
					list[j] = list[j + 1];
					list[j + 1] = temp;
					swapped = true;
				}
			}
		}
	}
	@Override
	public <T> void sort(T[] list, Comparator<T> comp) {
		Boolean swapped = true;
		for (int i = 1, len = list.length; i < len && swapped; ++i) {
			swapped = false;
			for (int j = 0; j < len - i; ++j) {
				if (comp.compare(list[j], list[j + 1]) > 0) {
					T temp = list[j];
					list[j] = list[j + 1];
					list[j + 1] = temp;
					swapped = true;
				}
			}
		}
	}
}

 

94、用 Java 寫一個折半查詢。

折半查詢,也稱二分查詢、二分搜尋,是一種在有序陣列中查詢某一特定元素的搜尋演算法。搜素過程從陣列的中間元素開始,如果中間元素正好是要查詢的元素,則搜素過程結束;如果某一特定元素大於或者小於中間元素,則在陣列大於或小於中間元素的那一半中查詢,而且跟開始一樣從中間元素開始比較。如果在某一步驟陣列已經為空,則表示找不到指定的元素。這種搜尋演算法每一次比較都使搜尋範圍縮小一半,其時間複雜度是 O(logN)。

 
import java.util.Comparator;
public class MyUtil {
	public static <T extends Comparable<T>> int binarySearch(T[] x, T
	key) {
		return binarySearch(x, 0, x.length- 1, key);
	}
	// 使用迴圈實現的二分查詢
	public static <T> int binarySearch(T[] x, T key, Comparator<T> comp)
	{
		int low = 0;
		int high = x.length - 1;
		while (low <= high) {
			int mid = (low + high) >>> 1;
			int cmp = comp.compare(x[mid], key);
			if (cmp < 0) {
				low= mid + 1;
			} else if (cmp > 0) {
				high= mid - 1;
			} else {
				return mid;
			}
		}
		return -1;
	}
	// 使用遞迴實現的二分查詢
	private static<T extends Comparable<T>> int binarySearch(T[] x, int
	low, int high, T key) {
		if(low <= high) {
			int mid = low + ((high -low) >> 1);
			if(key.compareTo(x[mid])== 0) {
				return mid;
			} else if(key.compareTo(x[mid])< 0) {
				return binarySearch(x,low, mid - 1, key);
			} else {
				return binarySearch(x,mid + 1, high, key);
			}
		}
		return -1;
	}
}

說明:上面的程式碼中給出了折半查詢的兩個版本,一個用遞迴實現,一個用迴圈實現。需要注意的是計算中間位置時不應該使用(high+ low) / 2 的方式,因為加法運算可能導致整數越界,這裡應該使用以下三種方式之一:low + (high - low)/ 2 或 low + (high – low) >> 1 或(low + high) >>> 1(>>>是邏輯右移,是不帶符號位的右移)

相關文章