2020年的最新的Java面試題

我冷漠發表於2020-02-23

1、物件導向的特徵有哪些方面?
答:物件導向的特徵主要有以下幾個方面:
1)抽象:
抽象是將一類物件的共同特徵總結出來構造類的過程,包括資料抽象和行為抽象兩方面。抽象只關注物件有哪些屬性和行為,並不關注這些行為的細節是什麼。

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

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

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

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

作用域    當前類  同包   子類   其他
public     √      √     √      √
protected  √      √     √      ×
default    √      √     ×      ×
private    √      ×     ×      ×

類的成員不寫訪問修飾時預設為default。預設對於同一個包中的其他類相當於公開(public),對於不是同一個包中的其他類相當於私有(private)。受保護(protected)對子類相當於公開,對不是同一包中的沒有父子關係的類相當於私有。

3、String 是最基本的資料型別嗎?
答:不是,String是引用資料型別。Java中的基本資料型別只有8個:byte、short、int、long、float、double、char、boolean;除了基本型別(primitive type)和列舉型別(enumeration type),剩下的都是引用型別(reference type)。

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,從JDK 1.5開始引入了自動裝箱/拆箱機制,使得二者可以相互轉換。
Java 為每個原始型別提供了包裝型別:

原始型別: boolean,char,byte,short,int,long,float,double
包裝型別:Boolean,Character,Byte,Short,Integer,Long,Float,Double

package com.lovo;
public 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的內部類,其程式碼如下所示:

    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物件,所以上面的面試題中f1f2的結果是true,而f3f4的結果是false。越是貌似簡單的面試題其中的玄機就越多,需要面試者有相當深厚的功力。

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

9、解釋記憶體中的棧(stack)、堆(heap)和靜態儲存區的用法。
答:通常我們定義一個基本資料型別的變數,一個物件的引用,還有就是函式呼叫的現場儲存都使用記憶體中的棧空間;而通過new關鍵字和構造器建立的物件放在堆空間;程式中的字面量(literal)如直接書寫的100、“hello”和常量都是放在靜態儲存區中。棧空間操作最快但是也很小,通常大量的物件都是放在堆空間,整個記憶體包括硬碟上的虛擬記憶體都可以被當成堆空間來使用。
String str = new String(“hello”);
上面的語句中str放在棧上,用new建立出來的字串物件放在堆上,而“hello”這個字面量放在靜態儲存區。
補充:較新版本的Java中使用了一項叫“逃逸分析“的技術,可以將一些區域性物件放在棧上以提升物件的操作效能。

10、Math.round(11.5) 等於多少? Math.round(-11.5)等於多少?
答:Math.round(11.5)的返回值是12,Math.round(-11.5)的返回值是-11。四捨五入的原理是在引數上加0.5然後進行下取整。

11、swtich 是否能作用在byte 上,是否能作用在long 上,是否能作用在String上?
答:早期的JDK中,switch(expr)中,expr可以是byte、short、char、int。從1.5版開始,Java中引入了列舉型別(enum),expr也可以是列舉,從JDK 1.7版開始,還可以是字串(String)。長整型(long)是不可以的。

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

package com.loonstudio;
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和hashCode方法,很多Java程式都知道,但很多人也就是僅僅知道而已,在Joshua Bloch的大作《Effective Java》(很多軟體公司,《Effective Java》、《Java程式設計思想》以及《重構:改善既有程式碼質量》是Java程式設計師必看書籍,如果你還沒看過,那就趕緊去亞馬遜買一本吧)中是這樣介紹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)而不是繼承(IS-A)。

18、當一個物件被當作引數傳遞到一個方法後,此方法可改變這個物件的屬性,並可返回變化後的結果,那麼這裡到底是值傳遞還是引用傳遞?
答:是值傳遞。Java 程式語言只有值傳遞引數。當一個物件例項作為一個引數被傳遞到方法中時,引數的值就是對該物件的引用。物件的屬性可以在被呼叫過程中被改變,但物件的引用是永遠不會改變的。C++和C#中可以通過傳引用或傳輸出引數來改變傳入的引數的值。
補充:Java中沒有傳引用實在是非常的不方便,這一點在Java 8中仍然沒有得到改進,正是如此在Java編寫的程式碼中才會出現大量的Wrapper類(將需要通過方法呼叫修改的引用置於一個Wrapper類中,再將Wrapper物件傳入方法),這樣的做法只會讓程式碼變得臃腫,尤其是讓從C和C++轉型為Java程式設計師的開發者無法容忍。

19、String 和StringBuilder、StringBuffer 的區別?
答:Java 平臺提供了兩種型別的字串:String和StringBuffer / StringBuilder,它們可以儲存和操作字串。其中String是隻讀字串,也就意味著String引用的字串內容是不能被改變的。而StringBuffer和StringBuilder類表示的字串物件可以直接進行修改。StringBuilder是JDK 1.5中引入的,它和StringBuffer的方法完全相同,區別在於它是在單執行緒環境下使用的,因為它的所有方面都沒有被synchronized修飾,因此它的效率也比StringBuffer略高。
補充1:有一個面試題問:有沒有哪種情況用+做字串連線比呼叫StringBuffer / StringBuilder物件的append方法效能更好?如果連線後得到的字串在靜態儲存區中是早已存在的,那麼用+做字串連線是優於StringBuffer / StringBuilder的append方法的。
補充2:下面也是一個面試題,問程式的輸出,看看自己能不能說出正確答案。


package com.lovo;

 

public class StringEqualTest {

 

	public static void main(String[] args) {

		String a = "Programming";

		String b = new String("Programming");

		String c = "Program" + "ming";

		

		System.out.println(a == b);

		System.out.println(a == c);

		System.out.println(a.equals(b));

		System.out.println(a.equals(c));

		System.out.println(a.intern() == b.intern());

	}

}

20、過載(Overload)和重寫(Override)的區別。過載的方法能否根據返回型別進行區分?
答:方法的過載和重寫都是實現多型的方式,區別在於前者實現的是編譯時的多型性,而後者實現的是執行時的多型性。過載發生在一個類中,同名的方法如果有不同的引數列表(引數型別不同、引數個數不同或者二者都不同)則視為過載;重寫發生在子類與父類之間,重寫要求子類被重寫方法與父類被重寫方法有相同的返回型別,比父類被重寫方法更好訪問,不能比父類被重寫方法宣告更多的異常(里氏代換原則)。過載對返回型別沒有特殊的要求。
補充:華為的面試題中曾經問過這樣一個問題:為什麼不能根據返回型別來區分過載,說出你的答案吧!

21、描述一下JVM 載入class檔案的原理機制?
答:JVM 中類的裝載是由類載入器(ClassLoader) 和它的子類來實現的,Java中的類載入器是一個重要的Java 執行時系統元件,它負責在執行時查詢和裝入類檔案中的類。

補充:
1.由於Java的跨平臺性,經過編譯的Java源程式並不是一個可執行程式,而是一個或多個類檔案。當Java程式需要使用某個類時,JVM會確保這個類已經被載入、連線(驗證、準備和解析)和初始化。類的載入是指把類的.class檔案中的資料讀入到記憶體中,通常是建立一個位元組陣列讀入.class檔案,然後產生與所載入類對應的Class物件。載入完成後,Class物件還不完整,所以此時的類還不可用。當類被載入後就進入連線階段,這一階段包括驗證、準備(為靜態變數分配記憶體並設定預設的初始值)和解析(將符號引用替換為直接引用)三個步驟。最後JVM對類進行初始化,包括:1如果類存在直接的父類並且這個類還沒有被初始化,那麼就先初始化父類;2如果類中存在初始化語句,就依次執行這些初始化語句。
2.類的載入是由類載入器完成的,類載入器包括:根載入器(BootStrap)、擴充套件載入器(Extension)、系統載入器(System)和使用者自定義類載入器(java.lang.ClassLoader的子類)。從JDK 1.2開始,類載入過程採取了父親委託機制(PDM)。PDM更好的保證了Java平臺的安全性,在該機制中,JVM自帶的Bootstrap是根載入器,其他的載入器都有且僅有一個父類載入器。類的載入首先請求父類載入器載入,父類載入器無能為力時才由其子類載入器自行載入。JVM不會向Java程式提供對Bootstrap的引用。下面是關於幾個類載入器的說明:
a)Bootstrap:一般用原生程式碼實現,負責載入JVM基礎核心類庫(rt.jar);
b)Extension:從java.ext.dirs系統屬性所指定的目錄中載入類庫,它的父載入器是Bootstrap;
c)System:又叫應用類載入器,其父類是Extension。它是應用最廣泛的類載入器。它從環境變數classpath或者系統屬性java.class.path所指定的目錄中記載類,是使用者自定義載入器的預設父載入器。

22、char 型變數中能不能存貯一箇中文漢字?為什麼?
答:char型別可以儲存一箇中文漢字,因為Java中使用的編碼是Unicode(不選擇任何特定的編碼,直接使用字元在字符集中的編號,這是統一的唯一方法),一個char型別佔2個位元組(16bit),所以放一箇中文是沒問題的。
補充:使用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)的內部類,它可以不依賴於外部類例項被例項化。而通常的內部類需要在外部類例項化後才能例項化,其語法看起來挺詭異的,如下所示。


package com.lovo;

 

/**

 * 撲克類(一副撲克)

 * @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;

		}

	}

}

測試類:


package com.lovo;

 

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

	}

}

25、Java 中會存在記憶體洩漏嗎,請簡單描述。
答:理論上Java因為有垃圾回收機制(GC)不會存在記憶體洩露問題(這也是Java被廣泛使用於伺服器端程式設計的一個重要原因);然而在實際開發中,可能會存在無用但可達的物件,這些物件不能被GC回收也會發生記憶體洩露。一個例子就是Hibernate的Session(一級快取)中的物件屬於持久態,垃圾回收器是不會回收這些物件的,然而這些物件中可能存在無用的垃圾物件。下面的例子也展示了Java中發生記憶體洩露的情況:


package com.lovo;

 

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介面,通過物件的序列化和反序列化實現克隆,可以實現真正的深度克隆,程式碼如下。


package com.lovo;

 

import java.io.ByteArrayInputStream;

import java.io.ByteArrayOutputStream;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;

 

public class MyUtil {

 

	private MyUtil() {

		throw new AssertionError();

	}

 

	public static <T> 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方法沒有任何意義

		// 這兩個基於記憶體的流只要垃圾回收器清理物件就能夠釋放資源

	}

}

下面是測試程式碼:


package com.lovo;

 

import java.io.Serializable;

 

/**

 * 人類

 * @author 駱昊

 *

 */

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 + "]";

	}

 

}

 

/**

 * 小汽車類

 * @author 駱昊

 *

 */

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物件的生命週期將堆記憶體劃分為不同的區域,在垃圾收集過程中,可能會將物件移動到不同區域:

伊甸園(Eden):這是物件最初誕生的區域,並且對大多數物件來說,這裡是它們唯一存在過的區域。倖存者樂園(Survivor):從伊甸園倖存下來的物件會被挪到這裡。終身頤養園(Tenured):這是足夠老的倖存物件的歸宿。年輕代收集(Minor-GC)過程是不會觸及這個地方的。當年輕代收集不能把物件放進終身頤養園時,就會觸發一次完全收集(Major-GC),這裡可能還會牽扯到壓縮,以便為大物件騰出足夠的空間。
與垃圾回收相關的JVM引數:

-Xms / -Xmx — 堆的初始大小 / 堆的最大大小-Xmn — 堆中年輕代的大小-XX:-DisableExplicitGC — 讓System.gc()不產生任何作用-XX:+PrintGCDetail — 列印GC的細節-XX:+PrintGCDateStamps — 列印GC操作的時間戳

31、String s=new String(“xyz”);建立了幾個字串物件?
答:兩個物件,一個是靜態儲存區的"xyz",一個是用new建立在堆上的物件。

32、介面是否可繼承(extends)介面? 抽象類是否可實現(implements)介面? 抽象類是否可繼承具體類(concrete class)?
答:介面可以繼承介面。抽象類可以實現(implements)介面,抽象類可繼承具體類,但前提是具體類必須有明確的建構函式。

33、一個“.java”原始檔中是否可以包含多個類(不是內部類)?有什麼限制?
答:可以,但一個原始檔中最多隻能有一個公開類(public class)而且檔名必須和公開類的類名完全保持一致。

34、Anonymous Inner Class(匿名內部類)是否可以繼承其它類?是否可以實現介面?
答:可以繼承其他類或實現其他介面,在Swing程式設計中常用此方式來實現事件監聽和回撥。

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。建立物件時構造器的呼叫順序是:先初始化靜態成員,然後呼叫父類構造器,再初始化非靜態成員,最後呼叫自身構造器。

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 = newString(s1.getBytes(“GB2312”), “ISO-8859-1”);

41、日期和時間:
1)如何取得年月日、小時分鐘秒?
2)如何取得從1970年1月1日0時0分0秒到現在的毫秒數?
3)如何取得某月的最後一天?
4)如何格式化日期?
答:操作方法如下所示:
1)建立java.util.Calendar 例項,呼叫其get()方法傳入不同的引數即可獲得引數所對應的值
2)以下方法均可獲得該毫秒數:

Calendar.getInstance().getTimeInMillis();
System.currentTimeMillis();

3)示例程式碼如下:

Calendar time = Calendar.getInstance();
time.getActualMaximum(Calendar.DAY_OF_MONTH);

4)利用java.text.DataFormat 的子類(如SimpleDateFormat類)中的format(Date)方法可將日期格式化。

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

public class YesterdayCurrent {

    public static void main(String[] args){

        Calendar cal = Calendar.getInstance();

        cal.add(Calendar.DATE, -1);

        System.out.println(cal.getTime());

    }

}

43、try{}裡有一個return語句,那麼緊跟在這個try後的finally{}裡的code會不會被執行,什麼時候被執行,在return前還是後?
答:會執行,在方法返回撥用者前執行。Java允許在finally中改變返回值的做法是不好的,因為如果存在finally程式碼塊,try中的return語句不會立馬返回撥用者,而是記錄下返回值待finally程式碼塊執行完畢之後再向呼叫者返回其值,然後如果在finally中修改了返回值,這會對程式造成很大的困擾,C#中就從語法上規定不能做這樣的事。

44、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語句沒有對某種“異常”進行處理,棧就會展開,直到遇到有處理這種“異常”的try 語句。

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

不要將異常處理用於正常的控制流(設計良好的API不應該強迫它的呼叫者為了正常的控制流而使用異常)對可以恢復的情況使用受檢異常,對程式設計錯誤使用執行時異常避免不必要的使用受檢異常(可以通過一些狀態檢測手段來避免異常的發生)優先使用標準的異常每個方法丟擲的異常都要有文件保持異常的原子性不要在catch中忽略掉捕獲到的異常

46、列出一些你常見的執行時異常?
答:
ArithmeticException(算術異常)
ClassCastException (類轉換異常)
IllegalArgumentException (非法引數異常)
IndexOutOfBoundsException (下表越界異常)
NullPointerException (空指標異常)
SecurityException (安全異常)

47、final, finally, finalize 的區別?
答:final:修飾符(關鍵字)有三種用法:如果一個類被宣告為final,意味著它不能再派生出新的子類,即不能被繼承,因此它和abstract是反義詞。將變數宣告為final,可以保證它們在使用中不被改變,被宣告為final 的變數必須在宣告時給定初值,而在以後的引用中只能讀取不可修改。被宣告為final 的方法也同樣只能使用,不能在子類中被重寫。finally:通常放在try…catch的後面構造總是執行程式碼塊,這就意味著程式無論正常執行還是發生異常,這裡的程式碼只要JVM不關閉都能執行,可以將釋放外部資源的程式碼寫在finally塊中。finalize:Object類中定義的方法,Java中允許使用finalize()
方法在垃圾收集器將物件從記憶體中清除出去之前做必要的清理工作。這個方法是由垃圾收

48、類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型別的異常).

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

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

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

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


public class Student implements Comparable<Student> {
    private String name;        // 姓名
    private int age;            // 年齡

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student [name=" + name + ", age=" + age + "]";
    }

    @Override
    public int compareTo(Student o) {
        return this.age - o.age; // 比較年齡(年齡的升序)
    }

}

==============================================================
import java.util.Set;
import java.util.TreeSet;

class Test01 {

    public static void main(String[] args) {
        Set<Student> set = new TreeSet<>();     // Java 7的鑽石語法(構造器後面的尖括號中不需要寫型別)
        set.add(new Student("Hao LUO", 33));
        set.add(new Student("XJ WANG", 32));
        set.add(new Student("Bruce LEE", 60));
        set.add(new Student("Bob YANG", 22));

        for(Student stu : set) {
            System.out.println(stu);
        }
//      輸出結果: 
//      Student [name=Bob YANG, age=22]
//      Student [name=XJ WANG, age=32]
//      Student [name=Hao LUO, age=33]
//      Student [name=Bruce LEE, age=60]
    }
}

==============================================================================
import java.util.Set;
import java.util.TreeSet;

class Test01 {

    public static void main(String[] args) {
        Set<Student> set = new TreeSet<>();     // Java 7的鑽石語法(構造器後面的尖括號中不需要寫型別)
        set.add(new Student("Hao LUO", 33));
        set.add(new Student("XJ WANG", 32));
        set.add(new Student("Bruce LEE", 60));
        set.add(new Student("Bob YANG", 22));

        for(Student stu : set) {
            System.out.println(stu);
        }
//      輸出結果: 
//      Student [name=Bob YANG, age=22]
//      Student [name=XJ WANG, age=32]
//      Student [name=Hao LUO, age=33]
//      Student [name=Bruce LEE, age=60]
    }
}

例子2:

public class Student {
    private String name;    // 姓名
    private int age;        // 年齡

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    /**
     * 獲取學生姓名
     */
    public String getName() {
        return name;
    }

    /**
     * 獲取學生年齡
     */
    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Student [name=" + name + ", age=" + age + "]";
    }

}

==================================================================
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

class Test02 {

    public static void main(String[] args) {
        List<Student> list = new ArrayList<>();     // Java 7的鑽石語法(構造器後面的尖括號中不需要寫型別)
        list.add(new Student("Hao LUO", 33));
        list.add(new Student("XJ WANG", 32));
        list.add(new Student("Bruce LEE", 60));
        list.add(new Student("Bob YANG", 22));

        // 通過sort方法的第二個引數傳入一個Comparator介面物件
        // 相當於是傳入一個比較物件大小的演算法到sort方法中
        // 由於Java中沒有函式指標、仿函式、委託這樣的概念
        // 因此要將一個演算法傳入一個方法中唯一的選擇就是通過介面回撥
        Collections.sort(list, new Comparator<Student> () {

            @Override
            public int compare(Student o1, Student o2) {
                return o1.getName().compareTo(o2.getName());    // 比較學生姓名
            }
        });

        for(Student stu : list) {
            System.out.println(stu);
        }
//      輸出結果: 
//      Student [name=Bob YANG, age=22]
//      Student [name=Bruce LEE, age=60]
//      Student [name=Hao LUO, age=33]
//      Student [name=XJ WANG, age=32]
    }
}

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

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

54、執行緒的sleep()方法和yield()方法有什麼區別?
答:
① sleep()方法給其他執行緒執行機會時不考慮執行緒的優先順序,因此會給低優先順序的執行緒以執行的機會;yield()方法只會給相同優先順序或更高優先順序的執行緒以執行的機會;
② 執行緒執行sleep()方法後轉入阻塞(blocked)狀態,而執行yield()方法後轉入就緒(ready)狀態;
③ sleep()方法宣告丟擲InterruptedException,而yield()方法沒有宣告任何異常;
④ sleep()方法比yield()方法(跟作業系統CPU排程相關)具有更好的可移植性。

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

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

  • wait():使一個執行緒處於等待(阻塞)狀態,並且釋放所持有的物件的鎖;
  • sleep():使一個正在執行的執行緒處於睡眠狀態,是一個靜態方法,呼叫此方法要處理InterruptedException異常;
  • notify():喚醒一個處於等待狀態的執行緒,當然在呼叫此方法的時候,並不能確切的喚醒某一個等待狀態的執行緒,而是由JVM確定喚醒哪個執行緒,而且與優先順序無關;
  • notityAll():喚醒所有處於等待狀態的執行緒,該方法並不是將物件的鎖給所有執行緒,而是讓它們競爭,只有獲得鎖的執行緒才能進入就緒狀態;
    補充: Java 5通過Lock介面提供了顯式的鎖機制(explicit lock),增強了靈活性以及對執行緒的協調。Lock介面中定義了加鎖(lock())和解鎖(unlock())的方法,同時還提供了newCondition()方法來產生用於執行緒之間通訊的Condition物件;此外,Java 5還提供了訊號量機制(semaphore),訊號量可以用來限制對某個共享資源進行訪問的執行緒的數量。在對資源進行訪問之前,執行緒必須得到訊號量的許可(呼叫Semaphore物件的acquire()方法);在完成對資源的訪問後,執行緒必須向訊號量歸還許可(呼叫Semaphore物件的release()方法)。

下面的例子演示了100個執行緒同時向一個銀行賬戶中存入1元錢,在沒有使用同步機制和使用同步機制情況下的執行情況。

  • 銀行賬戶類:
/**
 * 銀行賬戶
 * @author 駱昊
 *
 */
public class Account {
    private double balance;     // 賬戶餘額

    /**
     * 存款
     * @param money 存入金額
     */
    public void deposit(double money) {
        double newBalance = balance + money;
        try {
            Thread.sleep(10);   // 模擬此業務需要一段處理時間
        }
        catch(InterruptedException ex) {
            ex.printStackTrace();
        }
        balance = newBalance;
    }

    /**
     * 獲得賬戶餘額
     */
    public double getBalance() {
        return balance;
    }
}

  • 存錢執行緒類:
/**
 * 存錢執行緒
 * @author 駱昊
 *
 */
public class AddMoneyThread implements Runnable {
    private Account account;    // 存入賬戶
    private double money;       // 存入金額

    public AddMoneyThread(Account account, double money) {
        this.account = account;
        this.money = money;
    }

    @Override
    public void run() {
        account.deposit(money);
    }

}

  • 測試類:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test01 {

    public static void main(String[] args) {
        Account account = new Account();
        ExecutorService service = Executors.newFixedThreadPool(100);

        for(int i = 1; i <= 100; i++) {
            service.execute(new AddMoneyThread(account, 1));
        }

        service.shutdown();

        while(!service.isTerminated()) {}

        System.out.println("賬戶餘額: " + account.getBalance());
    }
}

在沒有同步的情況下,執行結果通常是顯示賬戶餘額在10元以下,出現這種狀況的原因是,當一個執行緒A試圖存入1元的時候,另外一個執行緒B也能夠進入存款的方法中,執行緒B讀取到的賬戶餘額仍然是執行緒A存入1元錢之前的賬戶餘額,因此也是在原來的餘額0上面做了加1元的操作,同理執行緒C也會做類似的事情,所以最後100個執行緒執行結束時,本來期望賬戶餘額為100元,但實際得到的通常在10元以下(很可能是1元哦)。解決這個問題的辦法就是同步,當一個執行緒對銀行賬戶存錢時,需要將此賬戶鎖定,待其操作完成後才允許其他的執行緒進行操作,程式碼有如下幾種調整方案:

  • 在銀行賬戶的存款(deposit)方法上同步(synchronized)關鍵字
/**
 * 銀行賬戶
 * @author 駱昊
 *
 */
public class Account {
    private double balance;     // 賬戶餘額

    /**
     * 存款
     * @param money 存入金額
     */
    public synchronized void deposit(double money) {
        double newBalance = balance + money;
        try {
            Thread.sleep(10);   // 模擬此業務需要一段處理時間
        }
        catch(InterruptedException ex) {
            ex.printStackTrace();
        }
        balance = newBalance;
    }

    /**
     * 獲得賬戶餘額
     */
    public double getBalance() {
        return balance;
    }
}

  • 線上程呼叫存款方法時對銀行賬戶進行同步
/**
 * 存錢執行緒
 * @author 駱昊
 *
 */
public class AddMoneyThread implements Runnable {
    private Account account;    // 存入賬戶
    private double money;       // 存入金額

    public AddMoneyThread(Account account, double money) {
        this.account = account;
        this.money = money;
    }

    @Override
    public void run() {
        synchronized (account) {
            account.deposit(money); 
        }
    }

}

  • 通過Java 5顯示的鎖機制,為每個銀行賬戶建立一個鎖物件,在存款操作進行加鎖和解鎖的操作:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 銀行賬戶
 * 
 * @author 駱昊
 *
 */
public class Account {
    private Lock accountLock = new ReentrantLock();
    private double balance; // 賬戶餘額

    /**
     * 存款
     * 
     * @param money
     *            存入金額
     */
    public void deposit(double money) {
        accountLock.lock();
        try {
            double newBalance = balance + money;
            try {
                Thread.sleep(10); // 模擬此業務需要一段處理時間
            }
            catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            balance = newBalance;
        }
        finally {
            accountLock.unlock();
        }
    }

    /**
     * 獲得賬戶餘額
     */
    public double getBalance() {
        return balance;
    }
}

按照上述三種方式對程式碼進行修改後,重寫執行測試程式碼Test01,將看到最終的賬戶餘額為100元。當然也可以使用Semaphore或CountdownLatch來實現同步。

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

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

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;


class MyTask implements Callable<Integer> {
    private int upperBounds;

    public MyTask(int upperBounds) {
        this.upperBounds = upperBounds;
    }

    @Override
    public Integer call() throws Exception {
        int sum = 0; 
        for(int i = 1; i <= upperBounds; i++) {
            sum += i;
        }
        return sum;
    }

}

class Test {

    public static void main(String[] args) throws Exception {
        List<Future<Integer>> list = new ArrayList<>();
        ExecutorService service = Executors.newFixedThreadPool(10);
        for(int i = 0; i < 10; i++) {
            list.add(service.submit(new MyTask((int) (Math.random() * 100))));
        }

        int sum = 0;
        for(Future<Integer> future : list) {
            // while(!future.isDone()) ;
            sum += future.get();
        }

        System.out.println(sum);
    }
}

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

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

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

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

  • newSingleThreadExecutor:建立一個單執行緒的執行緒池。這個執行緒池只有一個執行緒在工作,也就是相當於單執行緒序列執行所有任務。如果這個唯一的執行緒因為異常結束,那麼會有一個新的執行緒來替代它。此執行緒池保證所有任務的執行順序按照任務的提交順序執行。
  • newFixedThreadPool:建立固定大小的執行緒池。每次提交一個任務就建立一個執行緒,直到執行緒達到執行緒池的最大大小。執行緒池的大小一旦達到最大值就會保持不變,如果某個執行緒因為執行異常而結束,那麼執行緒池會補充一個新執行緒。
  • newCachedThreadPool:建立一個可快取的執行緒池。如果執行緒池的大小超過了處理任務所需要的執行緒,那麼就會回收部分空閒(60秒不執行任務)的執行緒,當任務數增加時,此執行緒池又可以智慧的新增新執行緒來處理任務。此執行緒池不會對執行緒池大小做限制,執行緒池大小完全依賴於作業系統(或者說JVM)能夠建立的最大執行緒大小。
  • newScheduledThreadPool:建立一個大小無限的執行緒池。此執行緒池支援定時以及週期性執行任務的需求。
  • newSingleThreadExecutor:建立一個單執行緒的執行緒池。此執行緒池支援定時以及週期性執行任務的需求。

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

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

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

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

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

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

66、面試題 - 程式設計實現檔案拷貝。(這個題目在筆試的時候經常出現,下面的程式碼給出了兩種實現方案)

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public final class MyUtil {

    private MyUtil() {
        throw new AssertionError();
    }

    public static void fileCopy(String source, String target) throws IOException {
        try (InputStream in = new FileInputStream(source)) {
            try (OutputStream out = new FileOutputStream(target)) {
                byte[] buffer = new byte[4096];
                int bytesToRead;
                while((bytesToRead = in.read(buffer)) != -1) {
                    out.write(buffer, 0, bytesToRead);
                }
            }
        }
    }

    public static void fileCopyNIO(String source, String target) throws IOException {
        try (FileInputStream in = new FileInputStream(source)) {
            try (FileOutputStream out = new FileOutputStream(target)) {
                FileChannel inChannel = in.getChannel();
                FileChannel outChannel = out.getChannel();
                ByteBuffer buffer = ByteBuffer.allocate(4096);
                while(inChannel.read(buffer) != -1) {
                    buffer.flip();
                    outChannel.write(buffer);
                    buffer.clear();
                }
            }
        }
    }
}

注意: 上面用到Java 7的TWR,使用TWR後可以不用在finally中釋放外部資源 ,從而讓程式碼更加優雅。

67、寫一個方法,輸入一個檔名和一個字串,統計這個字串在這個檔案中出現的次數。
答:程式碼如下:

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;
    }

}

68、如何用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;
            }

        });
    }
}

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

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class EchoServer {

    private static final int ECHO_SERVER_PORT = 6789;

    public static void main(String[] args) {        
        try(ServerSocket server = new ServerSocket(ECHO_SERVER_PORT)) {
            System.out.println("伺服器已經啟動...");
            while(true) {
                Socket client = server.accept();
                new Thread(new ClientHandler(client)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static class ClientHandler implements Runnable {
        private Socket client;

        public ClientHandler(Socket client) {
            this.client = client;
        }

        @Override
        public void run() {
            try(BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));
                    PrintWriter pw = new PrintWriter(client.getOutputStream())) {
                String msg = br.readLine();
                System.out.println("收到" + client.getInetAddress() + "傳送的: " + msg);
                pw.println(msg);
                pw.flush();
            } catch(Exception ex) {
                ex.printStackTrace();
            } finally {
                try {
                    client.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

注意: 上面的程式碼使用了Java 7的TWR語法,由於很多外部資源類都間接的實現了AutoCloseable介面(單方法回撥介面),因此可以利用TWR語法在try結束的時候通過回撥的方式自動呼叫外部資源類的close()方法,避免書寫冗長的finally程式碼塊。此外,上面的程式碼用一個靜態內部類實現執行緒的功能,使用多執行緒可以避免一個使用者I/O操作所產生的中斷影響其他使用者對伺服器的訪問,簡單的說就是一個使用者的輸入操作不會造成其他使用者的阻塞。當然,上面的程式碼使用執行緒池可以獲得更好的效能,因為頻繁的建立和銷燬執行緒所造成的開銷也是不可忽視的。

下面是一段回顯客戶端測試程式碼:

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class EchoClient {

    public static void main(String[] args) throws Exception {
        Socket client = new Socket("localhost", 6789);
        Scanner sc = new Scanner(System.in);
        System.out.print("請輸入內容: ");
        String msg = sc.nextLine();
        sc.close();
        PrintWriter pw = new PrintWriter(client.getOutputStream());
        pw.println(msg);
        pw.flush();
        BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));
        System.out.println(br.readLine());
        client.close();
    }
}

如果希望用NIO的多路複用套接字實現伺服器,程式碼如下所示。NIO的操作雖然帶來了更好的效能,但是有些操作是比較底層的,對於初學者來說還是有些難於理解。

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class EchoServerNIO {

    private static final int ECHO_SERVER_PORT = 6789;
    private static final int ECHO_SERVER_TIMEOUT = 5000;
    private static final int BUFFER_SIZE = 1024;

    private static ServerSocketChannel serverChannel = null;
    private static Selector selector = null;    // 多路複用選擇器
    private static ByteBuffer buffer = null;    // 緩衝區

    public static void main(String[] args) {
        init();
        listen();
    }

    private static void init() {
        try {
            serverChannel = ServerSocketChannel.open();
            buffer = ByteBuffer.allocate(BUFFER_SIZE);
            serverChannel.socket().bind(new InetSocketAddress(ECHO_SERVER_PORT));
            serverChannel.configureBlocking(false);
            selector = Selector.open();
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static void listen() {
        while (true) {
            try {
                if (selector.select(ECHO_SERVER_TIMEOUT) != 0) {
                    Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                    while (it.hasNext()) {
                        SelectionKey key = it.next();
                        it.remove();
                        handleKey(key);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private static void handleKey(SelectionKey key) throws IOException {
        SocketChannel channel = null;

        try {
            if (key.isAcceptable()) {
                ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                channel = serverChannel.accept();
                channel.configureBlocking(false);
                channel.register(selector, SelectionKey.OP_READ);
            } else if (key.isReadable()) {
                channel = (SocketChannel) key.channel();
                buffer.clear();
                if (channel.read(buffer) > 0) {
                    buffer.flip();
                    CharBuffer charBuffer = CharsetHelper.decode(buffer);
                    String msg = charBuffer.toString();
                    System.out.println("收到" + channel.getRemoteAddress() + "的訊息:" + msg);
                    channel.write(CharsetHelper.encode(CharBuffer.wrap(msg)));
                } else {
                    channel.close();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            if (channel != null) {
                channel.close();
            }
        }
    }

}

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;

public final class CharsetHelper {
    private static final String UTF_8 = "UTF-8";
    private static CharsetEncoder encoder = Charset.forName(UTF_8).newEncoder();
    private static CharsetDecoder decoder = Charset.forName(UTF_8).newDecoder();

    private CharsetHelper() {
    }

    public static ByteBuffer encode(CharBuffer in) throws CharacterCodingException{
        return encoder.encode(in);
    }

    public static CharBuffer decode(ByteBuffer in) throws CharacterCodingException{
        return decoder.decode(in);
    }
}

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

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

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

  • 載入驅動。

Class.forName(“oracle.jdbc.driver.OracleDriver”);

  • 建立連線。

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

  • 建立語句。

PreparedStatement ps = con.prepareStatement(“select * from emp where sal between ? and ?”);
ps.setInt(1, 1000);
ps.setInt(2, 3000);

  • 執行語句。

ResultSet rs = ps.executeQuery();

  • 處理結果。

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

  • 關閉資源。

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

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

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

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

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

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

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

77、事務的ACID是指什麼?
答:

  • 原子性(Atomic):事務中各項操作,要麼全做要麼全不做,任何一項操作的失敗都會導致整個事務的失敗;
  • 一致性(Consistent):事務結束後系統狀態是一致的;
  • 隔離性(Isolated):併發執行的事務彼此無法看到對方的中間狀態;
  • 永續性(Durable):事務完成後所做的改動都會被持久化,即使發生災難性的失敗。通過日誌和同步備份可以在故障發生後重建資料。

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

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

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

79、JDBC能否處理Blob和Clob?
答: Blob是指二進位制大物件(Binary Large Object),而Clob是指大字元物件(Character Large Objec),因此其中Blob是為儲存大的二進位制資料而設計的,而Clob是為儲存大的文字資料而設計的。JDBC的PreparedStatement和ResultSet都提供了相應的方法來支援Blob和Clob操作。下面的程式碼展示瞭如何使用JDBC操作LOB:
下面以MySQL資料庫為例,建立一個張有三個欄位的使用者表,包括編號(id)、姓名(name)和照片(photo),建表語句如下:

create table tb_user
(
id int primary key auto_increment,
name varchar(20) unique not null,
photo longblob
);

下面的Java程式碼向資料庫中插入一條記錄:

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

class JdbcLobTest {

    public static void main(String[] args) {
        Connection con = null;
        try {
            // 1. 載入驅動(Java6以上版本可以省略)
            Class.forName("com.mysql.jdbc.Driver");
            // 2. 建立連線
            con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
            // 3. 建立語句物件
            PreparedStatement ps = con.prepareStatement("insert into tb_user values (default, ?, ?)");
            ps.setString(1, "駱昊");              // 將SQL語句中第一個佔位符換成字串
            try (InputStream in = new FileInputStream("test.jpg")) {    // Java 7的TWR
                ps.setBinaryStream(2, in);      // 將SQL語句中第二個佔位符換成二進位制流
                // 4. 發出SQL語句獲得受影響行數
                System.out.println(ps.executeUpdate() == 1 ? "插入成功" : "插入失敗");
            } catch(IOException e) {
                System.out.println("讀取照片失敗!");
            }
        } catch (ClassNotFoundException | SQLException e) {     // Java 7的多異常捕獲
            e.printStackTrace();
        } finally { // 釋放外部資源的程式碼都應當放在finally中保證其能夠得到執行
            try {
                if(con != null && !con.isClosed()) {
                    con.close();    // 5. 釋放資料庫連線 
                    con = null;     // 指示垃圾回收器可以回收該物件
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

80、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());
        }
    }
}

81、獲得一個類的類物件有哪些方式?
答:

  • 方法1:型別.class,例如:String.class
  • 方法2:物件.getClass(),例如:“hello”.getClass()
  • 方法3:Class.forName(),例如:Class.forName(“java.lang.String”)

82、如何通過反射建立物件?
答:

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

83、如何通過反射獲取和設定物件私有欄位的值?
答:可以通過類物件的getDeclaredField()方法欄位(Field)物件,然後再通過欄位物件的setAccessible(true)將其設定為可以訪問,接下來就可以通過get/set方法來獲取/設定欄位的值了。下面的程式碼實現了一個反射的工具類,其中的兩個靜態方法分別用於獲取和設定私有欄位的值,欄位可以是基本型別也可以是物件型別且支援多級物件操作,例如ReflectionUtil.get(dog, “owner.car.engine.id”);可以獲得dog物件的主人的汽車的引擎的ID號。

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;

/**
 * 反射工具類
 * @author 駱昊
 *
 */
public class ReflectionUtil {

    private ReflectionUtil() {
        throw new AssertionError();
    }

    /**
     * 通過反射取物件指定欄位(屬性)的值
     * @param target 目標物件
     * @param fieldName 欄位的名字
     * @throws 如果取不到物件指定欄位的值則丟擲異常
     * @return 欄位的值
     */
    public static Object getValue(Object target, String fieldName) {
        Class<?> clazz = target.getClass();
        String[] fs = fieldName.split("\\.");

        try {
            for(int i = 0; i < fs.length - 1; i++) {
                Field f = clazz.getDeclaredField(fs[i]);
                f.setAccessible(true);
                target = f.get(target);
                clazz = target.getClass();
            }

            Field f = clazz.getDeclaredField(fs[fs.length - 1]);
            f.setAccessible(true);
            return f.get(target);
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 通過反射給物件的指定欄位賦值
     * @param target 目標物件
     * @param fieldName 欄位的名稱
     * @param value 值
     */
    public static void setValue(Object target, String fieldName, Object value) {
        Class<?> clazz = target.getClass();
        String[] fs = fieldName.split("\\.");
        try {
            for(int i = 0; i < fs.length - 1; i++) {
                Field f = clazz.getDeclaredField(fs[i]);
                f.setAccessible(true);
                Object val = f.get(target);
                if(val == null) {
                    Constructor<?> c = f.getType().getDeclaredConstructor();
                    c.setAccessible(true);
                    val = c.newInstance();
                    f.set(target, val);
                }
                target = val;
                clazz = target.getClass();
            }

            Field f = clazz.getDeclaredField(fs[fs.length - 1]);
            f.setAccessible(true);
            f.set(target, value);
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

84、如何通過反射呼叫物件的方法?
答:請看下面的程式碼:

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
    }
}

85、簡述一下物件導向的"六原則一法則"。
答:

  • 單一職責原則: 一個類只做它該做的事情。(單一職責原則想表達的就是"高內聚",寫程式碼最終極的原則只有六個字"高內聚、低耦合",就如同葵花寶典或辟邪劍譜的中心思想就八個字"欲練此功必先自宮",所謂的高內聚就是一個程式碼模組只完成一項功能,在物件導向中,如果只讓一個類完成它該做的事,而不涉及與它無關的領域就是踐行了高內聚的原則,這個類就只有單一職責。我們都知道一句話叫"因為專注,所以專業",一個物件如果承擔太多的職責,那麼註定它什麼都做不好。這個世界上任何好的東西都有兩個特徵,一個是功能單一,好的相機絕對不是電視購物裡面賣的那種一個機器有一百多種功能的,它基本上只能照相;另一個是模組化,好的自行車是組裝車,從減震叉、剎車到變速器,所有的部件都是可以拆卸和重新組裝的,好的乒乓球拍也不是成品拍,一定是底板和膠皮可以拆分和自行組裝的,一個好的軟體系統,它裡面的每個功能模組也應該是可以輕易的拿到其他系統中使用的,這樣才能實現軟體複用的目標。)
  • 開閉原則: 軟體實體應當對擴充套件開放,對修改關閉。(在理想的狀態下,當我們需要為一個軟體系統增加新功能時,只需要從原來的系統派生出一些新類就可以,不需要修改原來的任何一行程式碼。要做到開閉有兩個要點:①抽象是關鍵,一個系統中如果沒有抽象類或介面系統就沒有擴充套件點;②封裝可變性,將系統中的各種可變因素封裝到一個繼承結構中,如果多個可變因素混雜在一起,系統將變得複雜而換亂,如果不清楚如何封裝可變性,可以參考《設計模式精解》一書中對橋樑模式的講解的章節。)
  • 依賴倒轉原則: 面向介面程式設計。(該原則說得直白和具體一些就是宣告方法的引數型別、方法的返回型別、變數的引用型別時,儘可能使用抽象型別而不用具體型別,因為抽象型別可以被它的任何一個子型別所替代,請參考下面的里氏替換原則。)
    里氏替換原則:任何時候都可以用子型別替換掉父型別。(關於里氏替換原則的描述,Barbara Liskov女士的描述比這個要複雜得多,但簡單的說就是能用父型別的地方就一定能使用子型別。里氏替換原則可以檢查繼承關係是否合理,如果一個繼承關係違背了里氏替換原則,那麼這個繼承關係一定是錯誤的,需要對程式碼進行重構。例如讓貓繼承狗,或者狗繼承貓,又或者讓正方形繼承長方形都是錯誤的繼承關係,因為你很容易找到違反里氏替換原則的場景。需要注意的是:子類一定是增加父類的能力而不是減少父類的能力,因為子類比父類的能力更多,把能力多的物件當成能力少的物件來用當然沒有任何問題。)
  • 介面隔離原則: 介面要小而專,絕不能大而全。(臃腫的介面是對介面的汙染,既然介面表示能力,那麼一個介面只應該描述一種能力,介面也應該是高度內聚的。例如,琴棋書畫就應該分別設計為四個介面,而不應設計成一個介面中的四個方法,因為如果設計成一個介面中的四個方法,那麼這個介面很難用,畢竟琴棋書畫四樣都精通的人還是少數,而如果設計成四個介面,會幾項就實現幾個介面,這樣的話每個介面被複用的可能性是很高的。Java中的介面代表能力、代表約定、代表角色,能否正確的使用介面一定是程式設計水平高低的重要標識。)
  • 合成聚合複用原則: 優先使用聚合或合成關係複用程式碼。(通過繼承來複用程式碼是物件導向程式設計中被濫用得最多的東西,因為所有的教科書都無一例外的對繼承進行了鼓吹從而誤導了初學者,類與類之間簡單的說有三種關係,Is-A關係、Has-A關係、Use-A關係,分別代表繼承、關聯和依賴。其中,關聯關係根據其關聯的強度又可以進一步劃分為關聯、聚合和合成,但說白了都是Has-A關係,合成聚合複用原則想表達的是優先考慮Has-A關係而不是Is-A關係複用程式碼,原因嘛可以自己從百度上找到一萬個理由,需要說明的是,即使在Java的API中也有不少濫用繼承的例子,例如Properties類繼承了Hashtable類,Stack類繼承了Vector類,這些繼承明顯就是錯誤的,更好的做法是在Properties類中放置一個Hashtable型別的成員並且將其鍵和值都設定為字串來儲存資料,而Stack類的設計也應該是在Stack類中放一個Vector物件來儲存資料。記住:任何時候都不要繼承工具類,工具是可以擁有並可以使用的,而不是拿來繼承的。)
  • 迪米特法則: 迪米特法則又叫最少知識原則,一個物件應當對其他物件有儘可能少的瞭解。(迪米特法則簡單的說就是如何做到"低耦合",門面模式和調停者模式就是對迪米特法則的踐行。對於門面模式可以舉一個簡單的例子,你去一家公司洽談業務,你不需要了解這個公司內部是如何運作的,你甚至可以對這個公司一無所知,去的時候只需要找到公司入口處的前臺美女,告訴她們你要做什麼,她們會找到合適的人跟你接洽,前臺的美女就是公司這個系統的門面。再複雜的系統都可以為使用者提供一個簡單的門面,Java Web開發中作為前端控制器的Servlet或Filter不就是一個門面嗎,瀏覽器對伺服器的運作方式一無所知,但是通過前端控制器就能夠根據你的請求得到相應的服務。調停者模式也可以舉一個簡單的例子來說明,例如一臺計算機,CPU、記憶體、硬碟、顯示卡、音效卡各種裝置需要相互配合才能很好的工作,但是如果這些東西都直接連線到一起,計算機的佈線將異常複雜,在這種情況下,主機板作為一個調停者的身份出現,它將各個裝置連線在一起而不需要每個裝置之間直接交換資料,這樣就減小了系統的耦合度和複雜度,如下圖所示。迪米特法則用通俗的話來將就是不要和陌生人打交道,如果真的需要,找一個自己的朋友,讓他替你和陌生人打交道。)
    在這裡插入圖片描述
    在這裡插入圖片描述

86、簡述一下你瞭解的設計模式。
答:所謂設計模式,就是一套被反覆使用的程式碼設計經驗的總結(情境中一個問題經過證實的一個解決方案)。使用設計模式是為了可重用程式碼、讓程式碼更容易被他人理解、保證程式碼可靠性。設計模式使人們可以更加簡單方便的複用成功的設計和體系結構。將已證實的技術表述成設計模式也會使新系統開發者更加容易理解其設計思路。
在GoF的《Design Patterns: Elements of Reusable Object-Oriented Software》中給出了三類(建立型[對類的例項化過程的抽象化]、結構型[描述如何將類或物件結合在一起形成更大的結構]、行為型[對在不同的物件之間劃分責任和演算法的抽象化])共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(責任鏈模式)。
面試被問到關於設計模式的知識時,可以揀最常用的作答,例如:

  • 工廠模式: 工廠類可以根據條件生成不同的子類例項,這些子類有一個公共的抽象父類並且實現了相同的方法,但是這些方法針對不同的資料進行了不同的操作(多型方法)。當得到子類的例項後,開發人員可以呼叫基類中的方法而不必考慮到底返回的是哪一個子類的例項。
  • 代理模式: 給一個物件提供一個代理物件,並由代理物件控制原物件的引用。實際開發中,按照使用目的的不同,代理可以分為:遠端代理、虛擬代理、保護代理、Cache代理、防火牆代理、同步化代理、智慧引用代理。
  • 介面卡模式: 把一個類的介面變換成客戶端所期待的另一種介面,從而使原本因介面不匹配而無法在一起使用的類能夠一起工作。
  • 模板方法模式: 提供一個抽象類,將部分邏輯以具體方法或構造器的形式實現,然後宣告一些抽象方法來迫使子類實現剩餘的邏輯。不同的子類可以以不同的方式實現這些抽象方法(多型實現),從而實現不同的業務邏輯。
    除此之外,還可以講講上面提到的門面模式、橋樑模式、單例模式、裝潢模式(Collections工具類和I/O系統中都使用裝潢模式)等,反正基本原則就是揀自己最熟悉的、用得最多的作答,以免言多必失。

87、用Java寫一個單例類。
答:

  • 餓漢式單例
public class Singleton {
    private Singleton(){}
    private static Singleton instance = new Singleton();
    public static Singleton getInstance(){
        return instance;
    }
}

  • 懶漢式單例
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容器可以為普通的類建立單例,它是怎麼做到的呢?

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

89、UML中有哪些常用的圖?
答:UML定義了多種圖形化的符號來描述軟體系統部分或全部的靜態結構和動態結構,包括:
用例圖(use case diagram)、
類圖(class diagram)、
時序圖(sequence diagram)、
協作圖(collaboration diagram)、
狀態圖(statechart diagram)、
活動圖(activity diagram)、
構件圖(component diagram)、
部署圖(deployment diagram)等。在這些圖形化符號中,有三種圖最為重要,分別是:

  • 用例圖 (用來捕獲需求,描述系統的功能,通過該圖可以迅速的瞭解系統的功能模組及其關係);
  • 類圖 (描述類以及類與類之間的關係,通過該圖可以快速瞭解系統);
  • 時序圖 (描述執行特定任務時物件之間的互動關係以及執行順序,通過該圖可以瞭解物件能接收的訊息也就是說物件能夠向外界提供的服務)。

    用例圖:

在這裡插入圖片描述


類圖:

在這裡插入圖片描述


時序圖:

在這裡插入圖片描述

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

import java.util.Comparator;

/**
 * 排序器介面(策略模式: 將演算法封裝到具有共同介面的獨立的類中使得它們可以相互替換)
 * @author駱昊
 *
 */
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;

/**
 * 氣泡排序
 * 
 * @author駱昊
 *
 */
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;
                }
            }
        }
    }
}

91、用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(>>>是邏輯右移,是不帶符號位的右移)

92、Spring中Bean的作用域有哪些?
答:在Spring的早期版本中,僅有兩個作用域:singleton和prototype,前者表示Bean以單例的方式存在;後者表示每次從容器中呼叫Bean時,都會返回一個新的例項,prototype通常翻譯為原型。

補充: 設計模式中的建立型模式中也有一個原型模式,原型模式也是一個常用的模式,例如做一個室內設計軟體,所有的素材都在工具箱中,而每次從工具箱中取出的都是素材物件的一個原型,可以通過物件克隆來實現原型模式。

93、你使用過的應用伺服器優化技術有哪些?
答:
① 分散式快取:快取的本質就是記憶體中的雜湊表,如果設計一個優質的雜湊函式,那麼理論上雜湊表讀寫的漸近時間複雜度為O(1)。快取主要用來存放那些讀寫比很高、變化很少的資料,這樣應用程式讀取資料時先到快取中讀取,如果沒有或者資料已經失效再去訪問資料庫或檔案系統,並根據擬定的規則將資料寫入快取。對網站資料的訪問也符合二八定律(Pareto分佈,冪律分佈),即80%的訪問都集中在20%的資料上,如果能夠將這20%的資料快取起來,那麼系統的效能將得到顯著的改善。當然,使用快取需要解決以下幾個問題:

  • 頻繁修改的資料;
  • 資料不一致與髒讀;
  • 快取雪崩(可以採用分散式快取伺服器叢集加以解決,memcached是廣泛採用的解決方案);
  • 快取預熱;
  • 快取穿透(惡意持續請求不存在的資料)。
    ② 非同步操作:可以使用訊息佇列將呼叫非同步化,通過非同步處理將短時間高併發產生的事件訊息儲存在訊息佇列中,從而起到削峰作用。電商網站在進行促銷活動時,可以將使用者的訂單請求存入訊息佇列,這樣可以抵禦大量的併發訂單請求對系統和資料庫的衝擊。目前,絕大多數的電商網站即便不進行促銷活動,訂單系統都採用了訊息佇列來處理。
    ③ 使用叢集。
    ④ 程式碼優化:
  • 多執行緒:基於Java的Web開發基本上都通過多執行緒的方式響應使用者的併發請求,使用多執行緒技術在程式設計上要解決執行緒安全問題,主要可以考慮以下幾個方面:A. 將物件設計為無狀態物件(這和麵向物件的程式設計觀點是矛盾的,在物件導向的世界中被視為不良設計),這樣就不會存在併發訪問時物件狀態不一致的問題。B. 在方法內部建立物件,這樣物件由進入方法的執行緒建立,不會出現多個執行緒訪問同一物件的問題。使用ThreadLocal將物件與執行緒繫結也是很好的做法,這一點在前面已經探討過了。C. 對資源進行併發訪問時應當使用合理的鎖機制。
  • 非阻塞I/O: 使用單執行緒和非阻塞I/O是目前公認的比多執行緒的方式更能充分發揮伺服器效能的應用模式,基於Node.js構建的伺服器就採用了這樣的方式。Java在JDK 1.4中就引入了NIO(Non-blocking I/O),在Servlet 3規範中又引入了非同步Servlet的概念,這些都為在伺服器端採用非阻塞I/O提供了必要的基礎。
  • 資源複用:資源複用主要有兩種方式,一是單例,二是物件池,我們使用的資料庫連線池、執行緒池都是物件池化技術,這是典型的用空間換取時間的策略,另一方面也實現對資源的複用,從而避免了不必要的建立和釋放資源所帶來的開銷。

94、什麼是XSS攻擊?什麼是SQL隱碼攻擊?什麼是CSRF攻擊?
答:

  • XSS(Cross Site Script,跨站指令碼攻擊)是向網頁中注入惡意指令碼在使用者瀏覽網頁時在使用者瀏覽器中執行惡意指令碼的攻擊方式。跨站指令碼攻擊分有兩種形式:反射型攻擊(誘使使用者點選一個嵌入惡意指令碼的連結以達到攻擊的目標,目前有很多攻擊者利用論壇、微博釋出含有惡意指令碼的URL就屬於這種方式)和持久型攻擊(將惡意指令碼提交到被攻擊網站的資料庫中,使用者瀏覽網頁時,惡意指令碼從資料庫中被載入到頁面執行,QQ郵箱的早期版本就曾經被利用作為持久型跨站指令碼攻擊的平臺)。XSS雖然不是什麼新鮮玩意,但是攻擊的手法卻不斷翻新,防範XSS主要有兩方面:消毒(對危險字元進行轉義)和HttpOnly(防範XSS攻擊者竊取Cookie資料)。
  • SQL隱碼攻擊是注入攻擊最常見的形式(此外還有OS注入攻擊(Struts 2的高危漏洞就是通過OGNL實施OS注入攻擊導致的)),當伺服器使用請求引數構造SQL語句時,惡意的SQL被嵌入到SQL中交給資料庫執行。SQL隱碼攻擊需要攻擊者對資料庫結構有所瞭解才能進行,攻擊者想要獲得表結構有多種方式:(1)如果使用開源系統搭建網站,資料庫結構也是公開的(目前有很多現成的系統可以直接搭建論壇,電商網站,雖然方便快捷但是風險是必須要認真評估的);(2)錯誤回顯(如果將伺服器的錯誤資訊直接顯示在頁面上,攻擊者可以通過非法引數引發頁面錯誤從而通過錯誤資訊瞭解資料庫結構,Web應用應當設定友好的錯誤頁,一方面符合最小驚訝原則,一方面遮蔽掉可能給系統帶來危險的錯誤回顯資訊);(3)盲注。防範SQL隱碼攻擊也可以採用消毒的方式,通過正規表示式對請求引數進行驗證,此外,引數繫結也是很好的手段,這樣惡意的SQL會被當做SQL的引數而不是命令被執行,JDBC中的PreparedStatement就是支援引數繫結的語句物件,從效能和安全性上都明顯優於Statement。
  • CSRF攻擊(Cross Site Request Forgery,跨站請求偽造)是攻擊者通過跨站請求,以合法的使用者身份進行非法操作(如轉賬或發帖等)。CSRF的原理是利用瀏覽器的Cookie或伺服器的Session,盜取使用者身份,其原理如下圖所示。防範CSRF的主要手段是識別請求者的身份,主要有以下幾種方式:(1)在表單中新增令牌(token);(2)驗證碼;(3)檢查請求頭中的Referer(前面提到防圖片盜連結也是用的這種方式)。令牌和驗證都具有一次消費性的特徵,因此在原理上一致的,但是驗證碼是一種糟糕的使用者體驗,不是必要的情況下不要輕易使用驗證碼,目前很多網站的做法是如果在短時間內多次提交一個表單未獲得成功後才要求提供驗證碼,這樣會獲得較好的使用者體驗。

相關文章