2020-08-11
Java面試筆試題大彙總(最全+詳細答案)
Java-碼農進階之路 2018-05-28 09:08:36 165699 收藏 555
分類專欄: Java面試題 文章標籤: Java面試題
宣告:有人說, 有些面試題很變態,個人認為其實是因為我們基礎不紮實或者沒有深入。本篇文章來自一位很資深的前輩對於最近java面試題目所做的總結歸納,有170道題目 ,知識面很廣 ,而且這位前輩對於每個題都自己測試給出了答案 ,如果你對某個題有疑問或者不明白,可以電腦端登入把題目複製下來然後發表評論,大家一起探討,也可以電腦端登入後關注我給我發私信,我們一起進步!
以下內容來自這位前輩
2013年年底的時候,我看到了網上流傳的一個叫做《Java面試題大全》的東西,認真的閱讀了以後發現裡面的很多題目是重複且沒有價值的題目,還有不少的參考答案也是錯誤的,於是我花了半個月時間對這個所謂的《Java面試大全》進行了全面的修訂並重新發布在我的CSDN部落格。在修訂的過程中,參照了當時JDK最新版本(Java 7)給出了題目的答案和相關程式碼,去掉了EJB 2.x、JSF等無用內容或過時內容,補充了資料結構和演算法、大型網站技術架構、設計模式、UML、Spring MVC等內容並對很多知識點進行了深入的剖析,例如hashCode方法的設計、垃圾收集、併發程式設計、資料庫事務等。當時我甚至希望把面試中經常出現的作業系統、資料庫、軟體測試等內容也補充進去,但是由於各種原因,最終只整理出了150道面試題。讓我欣慰的是,這150道題還是幫助到了很多人,而且在我CSDN部落格上的總訪問量超過了5萬次,最終還被很多網站和個人以原創的方式轉載了。最近一年內,用百度搜尋"Java面試"我寫的這些東西基本上都排在搜尋結果的前5名,這讓我覺得"亞歷山大",因為我寫的這些東西一旦不準確就可能誤導很多人。2014年的時候我又整理了30道題,希望把之前遺漏的面試題和知識點補充上去,但是仍然感覺掛一漏萬,而且Java 8問世後很多新的東西又需要去總結和整理。為此,我不止一次的修改了之前的180題,修改到自己已經感覺有些疲憊或者厭煩了。2014年至今,自己帶的學生又有很多走上了Java程式設計師、Java工程師的工作崗位,他們的面試經驗也還沒來得及跟大家分享,冥冥之中似乎有一股力量在刺激我要重新寫一篇《Java面試題全集》,於是這篇文章就誕生了。請不要責備我把那些出現過的內容又寫了一次,因為每次寫東西就算是重複的內容,我也需要對程式語言和相關技術進行重新思考,不僅字斟句酌更是力求至臻完美,所以請相信我分享的一定是更新的、更好的、更有益的東西,這些內容也訴說著一個職業程式設計師和培訓師的思想、精神和情感。
1、物件導向的特徵有哪些方面?
答:物件導向的特徵主要有以下幾個方面:
- 抽象:抽象是將一類物件的共同特徵總結出來構造類的過程,包括資料抽象和行為抽象兩方面。抽象只關注物件有哪些屬性和行為,並不關注這些行為的細節是什麼。
- 繼承:繼承是從已有類得到繼承資訊建立新類的過程。提供繼承資訊的類被稱為父類(超類、基類);得到繼承資訊的類被稱為子類(派生類)。繼承讓變化中的軟體系統有了一定的延續性,同時繼承也是封裝程式中可變因素的重要手段(如果不能理解請閱讀閻巨集博士的《Java與模式》或《設計模式精解》中關於橋樑模式的部分)。
- 封裝:通常認為封裝是把資料和運算元據的方法繫結起來,對資料的訪問只能通過已定義的介面。物件導向的本質就是將現實世界描繪成一系列完全自治、封閉的物件。我們在類中編寫的方法就是對實現細節的一種封裝;我們編寫一個類就是對資料和資料操作的封裝。可以說,封裝就是隱藏一切可隱藏的東西,只向外界提供最簡單的程式設計介面(可以想想普通洗衣機和全自動洗衣機的差別,明顯全自動洗衣機封裝更好因此操作起來更簡單;我們現在使用的智慧手機也是封裝得足夠好的,因為幾個按鍵就搞定了所有的事情)。
- 多型性:多型性是指允許不同子型別的物件對同一訊息作出不同的響應。簡單的說就是用同樣的物件引用呼叫同樣的方法但是做了不同的事情。多型性分為編譯時的多型性和執行時的多型性。如果將物件的方法視為物件向外界提供的服務,那麼執行時的多型性可以解釋為:當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)對子類相當於公開,對不是同一包中的沒有父子關係的類相當於私有。Java中,外部類的修飾符只能是public或預設,類的成員(包括內部類)的修飾符可以是以上四種。
3、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,從Java 5開始引入了自動裝箱/拆箱機制,使得二者可以相互轉換。
Java 為每個原始型別提供了包裝型別:
- 原始型別: boolean,char,byte,short,int,long,float,double
- 包裝型別:Boolean,Character,Byte,Short,Integer,Long,Float,Double
[java] view plain copy
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)和靜態區(static area)的用法。
答:通常我們定義一個基本資料型別的變數,一個物件的引用,還有就是函式呼叫的現場儲存都使用記憶體中的棧空間;而通過new關鍵字和構造器建立的物件放在堆空間;程式中的字面量(literal)如直接書寫的100、"hello"和常量都是放在靜態區中。棧空間操作起來最快但是棧很小,通常大量的物件都是放在堆空間,理論上整個記憶體沒有被其他程式使用的空間甚至硬碟上的虛擬記憶體都可以被當成堆空間來使用。
String str = new String(“hello”);
上面的語句中變數str放在棧上,用new建立出來的字串物件放在堆上,而"hello"這個字面量放在靜態區。
補充:較新版本的Java(從Java 6的某個更新開始)中使用了一項叫"逃逸分析"的技術,可以將一些區域性物件放在棧上以提升物件的操作效能。
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上?
答:在Java 5以前,switch(expr)中,expr只能是byte、short、char、int。從Java 5開始,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和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)和依賴關係(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" + "ming";
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s1 == s1.intern());
}
}
補充:String物件的intern方法會得到字串物件在常量池中對應的版本的引用(如果常量池中有一個字串與String物件的equals結果是true),如果常量池中沒有對應的字串,則該字串將被新增到常量池中,然後返回常量池中字串的引用。
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)開始,類載入過程採取了父親委託機制(PDM)。PDM更好的保證了Java平臺的安全性,在該機制中,JVM自帶的Bootstrap是根載入器,其他的載入器都有且僅有一個父類載入器。類的載入首先請求父類載入器載入,父類載入器無能為力時才由其子類載入器自行載入。JVM不會向Java程式提供對Bootstrap的引用。下面是關於幾個類載入器的說明:
Bootstrap:一般用原生程式碼實現,負責載入JVM基礎核心類庫(rt.jar);
Extension:從java.ext.dirs系統屬性所指定的目錄中載入類庫,它的父載入器是Bootstrap;
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 {
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;
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方法沒有任何意義
// 這兩個基於記憶體的流只要垃圾回收器清理物件就能夠釋放資源,這一點不同於對外部資源(如檔案流)的釋放
}
}
下面是測試程式碼:
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:+PrintGCDetails — 列印GC的細節
-XX:+PrintGCDateStamps — 列印GC操作的時間戳
-XX:NewSize / XX:MaxNewSize — 設定新生代大小/新生代最大大小
-XX:NewRatio — 可以設定老生代和新生代的比例
-XX:PrintTenuringDistribution — 設定每次新生代GC後輸出倖存者樂園中物件年齡的分佈
-XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:設定老年代閥值的初始值和最大值
-XX:TargetSurvivorRatio:設定倖存區的目標使用率
31、String s = new String(“xyz”);建立了幾個字串物件?
答:兩個物件,一個是靜態區的"xyz",一個是用new建立在堆上的物件。
32、介面是否可繼承(extends)介面?抽象類是否可實現(implements)介面?抽象類是否可繼承具體類(concrete class)?
答:介面可以繼承介面,而且支援多重繼承。抽象類可以實現(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、資料型別之間的轉換:
- 如何將字串轉換為基本資料型別?
- 如何將基本資料型別轉換為字串?
答: - 呼叫基本資料型別對應的包裝類中的方法parseXXX(String)或valueOf(String)即可返回相應基本型別;
- 一種方法是將基本資料型別與空字串("")連線(+)即可獲得其所對應的字串;另一種方法是呼叫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、日期和時間:
- 如何取得年月日、小時分鐘秒?
- 如何取得從1970年1月1日0時0分0秒到現在的毫秒數?
- 如何取得某月的最後一天?
- 如何格式化日期?
答:
問題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(); // Java 8
問題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一直以來都是被詬病的東西,為了解決這一問題,Java 8中引入了新的時間日期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 是原Sun Microsystems公司推出的物件導向的程式設計語言,特別適合於網際網路應用程式開發;而JavaScript是Netscape公司的產品,為了擴充套件Netscape瀏覽器的功能而開發的一種可以嵌入Web頁面中執行的基於物件和事件驅動的解釋性語言。JavaScript的前身是LiveScript;而Java的前身是Oak語言。
下面對兩種語言間的異同作如下比較:
-
基於物件和麵向物件:Java是一種真正的物件導向的語言,即使是開發簡單的程式,必須設計物件;JavaScript是種指令碼語言,它可以用來製作與網路無關的,與使用者互動作用的複雜軟體。它是一種基於物件(Object-Based)和事件驅動(Event-Driven)的程式語言,因而它本身提供了非常豐富的內部物件供設計人員使用。
-
解釋和編譯:Java的原始碼在執行之前,必須經過編譯。JavaScript是一種解釋性程式語言,其原始碼不需經過編譯,由瀏覽器解釋執行。(目前的瀏覽器幾乎都使用了JIT(即時編譯)技術來提升JavaScript的執行效率)
-
強型別變數和型別弱變數:Java採用強型別變數檢查,即所有變數在編譯之前必須作宣告;JavaScript中變數是弱型別的,甚至在使用變數前可以不作宣告,JavaScript的直譯器在執行時檢查推斷其資料型別。
-
程式碼格式不一樣。
補充:上面列出的四點是網上流傳的所謂的標準答案。其實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表示需要捕捉或者需要程式進行處理的異常,是一種設計或實現問題;也就是說,它表示如果程式執行正常,從不會發生的情況。
面試題:2005年摩托羅拉的面試中曾經問過這麼一個問題“If a process reports a stack overflow run-time error, what’s the most possible cause?”,給了四個選項a. lack of memory; b. write on an invalid memory space; c. recursive function calling; d. array index out of boundary. Java程式在執行時也可能會遭遇StackOverflowError,這是一個無法恢復的錯誤,只能重新修改程式碼了,這個面試題的答案是c。如果寫了不能迅速收斂的遞迴,則很有可能引發棧溢位的錯誤,如下所示:
class StackOverflowErrorTest {
public static void main(String[] args) {
main(null);
}
}
提示:用遞迴編寫程式時一定要牢記兩點:1. 遞迴公式;2. 收斂條件(什麼時候就不再繼續遞迴)。
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中對異常的使用給出了以下指導原則:
- 不要將異常處理用於正常的控制流(設計良好的API不應該強迫它的呼叫者為了正常的控制流而使用異常)
- 對可以恢復的情況使用受檢異常,對程式設計錯誤使用執行時異常
- 避免不必要的使用受檢異常(可以通過一些狀態檢測手段來避免異常的發生)
- 優先使用標準的異常
- 每個方法丟擲的異常都要有文件
- 保持異常的原子性
- 不要在catch中忽略掉捕獲到的異常
49、列出一些你常見的執行時異常?
答:
- ArithmeticException(算術異常)
- ClassCastException (類轉換異常)
- IllegalArgumentException (非法引數異常)
- IndexOutOfBoundsException (下標越界異常)
- NullPointerException (空指標異常)
- SecurityException (安全異常)
50、闡述final、finally、finalize的區別。
答:
- final:修飾符(關鍵字)有三種用法:如果一個類被宣告為final,意味著它不能再派生出新的子類,即不能被繼承,因此它和abstract是反義詞。將變數宣告為final,可以保證它們在使用中不被改變,被宣告為final的變數必須在宣告時給定初值,而在以後的引用中只能讀取不可修改。被宣告為final的方法也同樣只能使用,不能在子類中被重寫。
- finally:通常放在try…catch…的後面構造總是執行程式碼塊,這就意味著程式無論正常執行還是發生異常,這裡的程式碼只要JVM不關閉都能執行,可以將釋放外部資源的程式碼寫在finally塊中。
- 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中對函數語言程式設計的支援)。
例子1:
public class Student implements Comparable {
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]
}
}
例子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]
}
}
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()方法有什麼區別?
答:
① sleep()方法給其他執行緒執行機會時不考慮執行緒的優先順序,因此會給低優先順序的執行緒以執行的機會;yield()方法只會給相同優先順序或更高優先順序的執行緒以執行的機會;
② 執行緒執行sleep()方法後轉入阻塞(blocked)狀態,而執行yield()方法後轉入就緒(ready)狀態;
③ sleep()方法宣告丟擲InterruptedException,而yield()方法沒有宣告任何異常;
④ sleep()方法比yield()方法(跟作業系統CPU排程相關)具有更好的可移植性。
59、當一個執行緒進入一個物件的synchronized方法A之後,其它執行緒是否可進入此物件的synchronized方法B?
答:不能。其它執行緒只能訪問該物件的非同步方法,同步方法則不能進入。因為非靜態方法上的synchronized修飾符要求執行方法時要獲得物件的鎖,如果已經進入A方法說明物件鎖已經被取走,那麼試圖進入B方法的執行緒就只能在等鎖池(注意不是等待池哦)中等待物件的鎖。
60、請說出與執行緒同步以及執行緒排程相關的方法。
答:
-
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來實現同步。
61、編寫多執行緒程式有幾種實現方式?
答: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 {
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);
}
}
62、synchronized關鍵字的用法?
答:synchronized關鍵字可以將物件或者方法標記為同步,以實現對物件和方法的互斥訪問,可以用synchronized(物件) { … }定義同步程式碼塊,或者在宣告方法時將synchronized作為方法的修飾符。在第60題的例子中已經展示了synchronized關鍵字的用法。
63、舉例說明同步和非同步。
答:如果系統中存在臨界資源(資源數量少於競爭資源的執行緒數量的資源),例如正在寫的資料以後可能被另一個執行緒讀到,或者正在讀的資料可能已經被另一個執行緒寫過了,那麼這些資料就必須進行同步存取(資料庫操作中的排他鎖就是最好的例子)。當應用程式在物件上呼叫了一個需要花費很長時間來執行的方法,並且不希望讓程式等待方法的返回時,就應該使用非同步程式設計,在很多情況下采用非同步途徑往往更有效率。事實上,所謂的同步就是指阻塞式操作,而非同步就是非阻塞式操作。
64、啟動一個執行緒是呼叫run()還是start()方法?
答:啟動一個執行緒是呼叫start()方法,使執行緒所代表的虛擬處理機處於可執行狀態,這意味著它可以由JVM 排程並執行,這並不意味著執行緒就會立即執行。run()方法是執行緒啟動後要進行回撥(callback)的方法。
65、什麼是執行緒池(thread pool)?
答:在物件導向程式設計中,建立和銷燬物件是很費時間的,因為建立一個物件要獲取記憶體資源或者其它更多資源。在Java中更是如此,虛擬機器將試圖跟蹤每一個物件,以便能夠在物件銷燬後進行垃圾回收。所以提高服務程式效率的一個手段就是儘可能減少建立和銷燬物件的次數,特別是一些很耗資源的物件建立和銷燬,這就是”池化資源”技術產生的原因。執行緒池顧名思義就是事先建立若干個可執行的執行緒放入一個池(容器)中,需要的時候從池中獲取執行緒不用自行建立,使用完畢不需要銷燬執行緒而是放回池中,從而減少建立和銷燬執行緒物件的開銷。
Java 5+中的Executor介面定義一個執行執行緒的工具。它的子型別即執行緒池介面是ExecutorService。要配置一個執行緒池是比較複雜的,尤其是對於執行緒池的原理不是很清楚的情況下,因此在工具類Executors面提供了一些靜態工廠方法,生成一些常用的執行緒池,如下所示:
- newSingleThreadExecutor:建立一個單執行緒的執行緒池。這個執行緒池只有一個執行緒在工作,也就是相當於單執行緒序列執行所有任務。如果這個唯一的執行緒因為異常結束,那麼會有一個新的執行緒來替代它。此執行緒池保證所有任務的執行順序按照任務的提交順序執行。
- newFixedThreadPool:建立固定大小的執行緒池。每次提交一個任務就建立一個執行緒,直到執行緒達到執行緒池的最大大小。執行緒池的大小一旦達到最大值就會保持不變,如果某個執行緒因為執行異常而結束,那麼執行緒池會補充一個新執行緒。
- newCachedThreadPool:建立一個可快取的執行緒池。如果執行緒池的大小超過了處理任務所需要的執行緒,那麼就會回收部分空閒(60秒不執行任務)的執行緒,當任務數增加時,此執行緒池又可以智慧的新增新執行緒來處理任務。此執行緒池不會對執行緒池大小做限制,執行緒池大小完全依賴於作業系統(或者說JVM)能夠建立的最大執行緒大小。
- newScheduledThreadPool:建立一個大小無限的執行緒池。此執行緒池支援定時以及週期性執行任務的需求。
- 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#的是它只有一個維度一個方向。
面試題 - 程式設計實現檔案拷貝。(這個題目在筆試的時候經常出現,下面的程式碼給出了兩種實現方案)
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中釋放外部資源 ,從而讓程式碼更加優雅。
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、用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);
}
}
73、XML文件定義有幾種形式?它們之間有何本質區別?解析XML文件有哪幾種方式?
答:XML文件定義分為DTD和Schema兩種形式,二者都是對XML語法的約束,其本質區別在於Schema本身也是一個XML檔案,可以被XML解析器解析,而且可以為XML承載的資料定義型別,約束能力較之DTD更強大。對XML的解析主要有DOM(文件物件模型,DocumentObjectModel)、SAX(Simple API for XML)和StAX(Java 6中引入的新的解析XML的方式,StreamingAPI forXML),其中DOM處理大型檔案時其效能下降的非常厲害,這個問題是由DOM樹結構佔用的記憶體較多造成的,而且DOM解析方式必須在解析檔案之前把整個文件裝入記憶體,適合對XML的隨機訪問(典型的用空間換取時間的策略);SAX是事件驅動型的XML解析方式,它順序讀取XML檔案,不需要一次全部裝載整個檔案。當遇到像檔案開頭,文件結束,或者標籤開頭與標籤結束時,它會觸發一個事件,使用者通過事件回撥程式碼來處理XML檔案,適合對XML的順序訪問;顧名思義,StAX把重點放在流上,實際上StAX與其他解析方式的本質區別就在於應用程式能夠把XML作為一個事件流來處理。將XML作為一組事件來處理的想法並不新穎(SAX就是這樣做的),但不同之處在於StAX允許應用程式程式碼把這些事件逐個拉出來,而不用提供在解析器方便時從解析器中接收事件的處理程式。
74、你在專案中哪些地方用到了XML?
答:XML的主要作用有兩個方面:資料交換和資訊配置。在做資料交換時,XML將資料用標籤組裝成起來,然後壓縮打包加密後通過網路傳送給接收者,接收解密與解壓縮後再從XML檔案中還原相關資訊進行處理,XML曾經是異構系統間交換資料的事實標準,但此項功能幾乎已經被JSON(JavaScriptObjectNotation)取而代之。當然,目前很多軟體仍然使用XML來儲存配置資訊,我們在很多專案中通常也會將作為配置資訊的硬程式碼寫在XML檔案中,Java的很多框架也是這麼做的,而且這些框架都選擇了dom4j作為處理XML的工具,因為Sun公司的官方API實在不怎麼好用。
補充:現在有很多時髦的軟體(如Sublime)已經開始將配置檔案書寫成JSON格式,我們已經強烈的感受到XML的另一項功能也將逐漸被業界拋棄。
75、闡述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();
}
}
}
提示:關閉外部資源的順序應該和開啟的順序相反,也就是說先關閉ResultSet、再關閉Statement、在關閉Connection。上面的程式碼只關閉了Connection(連線),雖然通常情況下在關閉連線時,連線上建立的語句和開啟的遊標也會關閉,但不能保證總是如此,因此應該按照剛才說的順序分別關閉。此外,第一步載入驅動在JDBC 4.0中是可以省略的(自動從類路徑中載入驅動),但是我們建議保留。
76、Statement和PreparedStatement有什麼區別?哪個效能更好?
答:與Statement相比,①PreparedStatement介面代表預編譯的語句,它主要的優勢在於可以減少SQL的編譯錯誤並增加SQL的安全性(減少SQL注射攻擊的可能性);②PreparedStatement中的SQL語句是可以帶引數的,避免了用字串連線拼接SQL語句的麻煩和不安全;③當批量處理SQL或頻繁執行相同的查詢時,PreparedStatement有明顯的效能上的優勢,由於資料庫可以將編譯優化後的SQL語句快取起來,下次執行相同結構的語句時就會很快(不用再次編譯和生成執行計劃)。
補充:為了提供對儲存過程的呼叫,JDBC API中還提供了CallableStatement介面。儲存過程(Stored Procedure)是資料庫中一組為了完成特定功能的SQL語句的集合,經編譯後儲存在資料庫中,使用者通過指定儲存過程的名字並給出引數(如果該儲存過程帶有引數)來執行它。雖然呼叫儲存過程會在網路開銷、安全性、效能上獲得很多好處,但是存在如果底層資料庫發生遷移時就會有很多麻煩,因為每種資料庫的儲存過程在書寫上存在不少的差別。
77、使用JDBC運算元據庫時,如何提升讀取資料的效能?如何提升更新資料的效能?
答:要提升讀取資料的效能,可以指定通過結果集(ResultSet)物件的setFetchSize()方法指定每次抓取的記錄數(典型的空間換時間策略);要提升更新資料的效能可以使用PreparedStatement語句構建批處理,將若干SQL語句置於一個批處理中執行。
78、在進行資料庫程式設計時,連線池有什麼作用?
答:由於建立連線和釋放連線都有很大的開銷(尤其是資料庫伺服器不在本地時,每次建立連線都需要進行TCP的三次握手,釋放連線需要進行TCP四次握手,造成的開銷是不可忽視的),為了提升系統訪問資料庫的效能,可以事先建立若干連線置於連線池中,需要時直接從連線池獲取,使用結束時歸還連線池而不必關閉連線,從而避免頻繁建立和釋放連線所造成的開銷,這是典型的用空間換取時間的策略(浪費了空間儲存連線,但節省了建立和釋放連線的時間)。池化技術在Java開發中是很常見的,在使用執行緒時建立執行緒池的道理與此相同。基於Java的開源資料庫連線池主要有:C3P0、Proxool、DBCP、BoneCP、Druid等。
補充:在計算機系統中時間和空間是不可調和的矛盾,理解這一點對設計滿足效能要求的演算法是至關重要的。大型網站效能優化的一個關鍵就是使用快取,而快取跟上面講的連線池道理非常類似,也是使用空間換時間的策略。可以將熱點資料置於快取中,當使用者查詢這些資料時可以直接從快取中得到,這無論如何也快過去資料庫中查詢。當然,快取的置換策略等也會對系統效能產生重要影響,對於這個問題的討論已經超出了這裡要闡述的範圍。
79、什麼是DAO模式?
答:DAO(Data Access Object)顧名思義是一個為資料庫或其他持久化機制提供了抽象介面的物件,在不暴露底層持久化方案實現細節的前提下提供了各種資料訪問操作。在實際的開發中,應該將所有對資料來源的訪問操作進行抽象化後封裝在一個公共API中。用程式設計語言來說,就是建立一個介面,介面中定義了此應用程式中將會用到的所有事務方法。在這個應用程式中,當需要和資料來源進行互動的時候則使用這個介面,並且編寫一個單獨的類來實現這個介面,在邏輯上該類對應一個特定的資料儲存。DAO模式實際上包含了兩個模式,一是Data Accessor(資料訪問器),二是Data Object(資料物件),前者要解決如何訪問資料的問題,而後者要解決的是如何用物件封裝資料。
80、事務的ACID是指什麼? 答: - 原子性(Atomic):事務中各項操作,要麼全做要麼全不做,任何一項操作的失敗都會導致整個事務的失敗; - 一致性(Consistent):事務結束後系統狀態是一致的; - 隔離性(Isolated):併發執行的事務彼此無法看到對方的中間狀態; - 永續性(Durable):事務完成後所做的改動都會被持久化,即使發生災難性的失敗。通過日誌和同步備份可以在故障發生後重建資料。
補充:關於事務,在面試中被問到的概率是很高的,可以問的問題也是很多的。首先需要知道的是,只有存在併發資料訪問時才需要事務。當多個事務訪問同一資料時,可能會存在5類問題,包括3類資料讀取問題(髒讀、不可重複讀和幻讀)和2類資料更新問題(第1類丟失更新和第2類丟失更新)。
髒讀(Dirty Read):A事務讀取B事務尚未提交的資料並在此基礎上操作,而B事務執行回滾,那麼A讀取到的資料就是髒資料。
時間 轉賬事務A 取款事務B
T1
開始事務
T2 開始事務
T3
查詢賬戶餘額為1000元
T4
取出500元餘額修改為500元
T5 查詢賬戶餘額為500元(髒讀)
T6
撤銷事務餘額恢復為1000元
T7 匯入100元把餘額修改為600元
T8 提交事務
不可重複讀(Unrepeatable Read):事務A重新讀取前面讀取過的資料,發現該資料已經被另一個已提交的事務B修改過了。
時間 轉賬事務A 取款事務B
T1
開始事務
T2 開始事務
T3
查詢賬戶餘額為1000元
T4 查詢賬戶餘額為1000元
T5
取出100元修改餘額為900元
T6
提交事務
T7 查詢賬戶餘額為900元(不可重複讀)
幻讀(Phantom Read):事務A重新執行一個查詢,返回一系列符合查詢條件的行,發現其中插入了被事務B提交的行。
時間 統計金額事務A 轉賬事務B
T1
開始事務
T2 開始事務
T3 統計總存款為10000元
T4
新增一個存款賬戶存入100元
T5
提交事務
T6 再次統計總存款為10100元(幻讀)
第1類丟失更新:事務A撤銷時,把已經提交的事務B的更新資料覆蓋了。
時間 取款事務A 轉賬事務B
T1 開始事務
T2
開始事務
T3 查詢賬戶餘額為1000元
T4
查詢賬戶餘額為1000元
T5
匯入100元修改餘額為1100元
T6
提交事務
T7 取出100元將餘額修改為900元
T8 撤銷事務
T9 餘額恢復為1000元(丟失更新)
第2類丟失更新:事務A覆蓋事務B已經提交的資料,造成事務B所做的操作丟失。
時間 轉賬事務A 取款事務B
T1
開始事務
T2 開始事務
T3
查詢賬戶餘額為1000元
T4 查詢賬戶餘額為1000元
T5
取出100元將餘額修改為900元
T6
提交事務
T7 匯入100元將餘額修改為1100元
T8 提交事務
T9 查詢賬戶餘額為1100元(丟失更新)
資料併發訪問所產生的問題,在有些場景下可能是允許的,但是有些場景下可能就是致命的,資料庫通常會通過鎖機制來解決資料併發訪問問題,按鎖定物件不同可以分為表級鎖和行級鎖;按併發事務鎖定關係可以分為共享鎖和獨佔鎖,具體的內容大家可以自行查閱資料進行了解。直接使用鎖是非常麻煩的,為此資料庫為使用者提供了自動鎖機制,只要使用者指定會話的事務隔離級別,資料庫就會通過分析SQL語句然後為事務訪問的資源加上合適的鎖,此外,資料庫還會維護這些鎖通過各種手段提高系統的效能,這些對使用者來說都是透明的(就是說你不用理解,事實上我確實也不知道)。ANSI/ISO SQL 92標準定義了4個等級的事務隔離級別,如下表所示:
隔離級別 髒讀 不可重複讀 幻讀 第一類丟失更新 第二類丟失更新
READ UNCOMMITED 允許 允許 允許 不允許 允許
READ COMMITTED 不允許 允許 允許 不允許 允許
REPEATABLE READ 不允許 不允許 允許 不允許 不允許
SERIALIZABLE 不允許 不允許 不允許 不允許 不允許
需要說明的是,事務隔離級別和資料訪問的併發性是對立的,事務隔離級別越高併發性就越差。所以要根據具體的應用來確定合適的事務隔離級別,這個地方沒有萬能的原則。
81、JDBC中如何進行事務處理?答:Connection提供了事務處理的方法,通過呼叫setAutoCommit(false)可以設定手動提交事務;當事務完成後用commit()顯式提交事務;如果在事務處理過程中發生異常則通過rollback()進行事務回滾。除此之外,從JDBC 3.0中還引入了Savepoint(儲存點)的概念,允許通過程式碼設定儲存點並讓事務回滾到指定的儲存點。這裡寫圖片描述
81、JDBC中如何進行事務處理?
答:Connection提供了事務處理的方法,通過呼叫setAutoCommit(false)可以設定手動提交事務;當事務完成後用commit()顯式提交事務;如果在事務處理過程中發生異常則通過rollback()進行事務回滾。除此之外,從JDBC 3.0中還引入了Savepoint(儲存點)的概念,允許通過程式碼設定儲存點並讓事務回滾到指定的儲存點。
這裡寫圖片描述
82、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();
}
}
}
}
83、簡述正規表示式及其用途。
答:在編寫處理字串的程式時,經常會有查詢符合某些複雜規則的字串的需要。正規表示式就是用於描述這些規則的工具。換句話說,正規表示式就是記錄文字規則的程式碼。
說明:計算機誕生初期處理的資訊幾乎都是數值,但是時過境遷,今天我們使用計算機處理的資訊更多的時候不是數值而是字串,正規表示式就是在進行字串匹配和處理的時候最為強大的工具,絕大多數語言都提供了對正規表示式的支援。
84、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());
}
}
}
說明:上面的正規表示式中使用了懶惰匹配和前瞻,如果不清楚這些內容,推薦讀一下網上很有名的《正規表示式30分鐘入門教程》。
85、獲得一個類的類物件有哪些方式?
答:
- 方法1:型別.class,例如:String.class
- 方法2:物件.getClass(),例如:“hello”.getClass()
- 方法3:Class.forName(),例如:Class.forName(“java.lang.String”)
86、如何通過反射建立物件?
答:
- 方法1:通過類物件呼叫newInstance()方法,例如:String.class.newInstance()
- 方法2:通過類物件的getConstructor()或getDeclaredConstructor()方法獲得構造器(Constructor)物件並呼叫其newInstance()方法建立物件,例如:String.class.getConstructor(String.class).newInstance(“Hello”);
87、如何通過反射獲取和設定物件私有欄位的值?
答:可以通過類物件的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);
}
}
}
88、如何通過反射呼叫物件的方法?
答:請看下面的程式碼:
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
}
}
89、簡述一下物件導向的"六原則一法則"。答: - 單一職責原則:一個類只做它該做的事情。(單一職責原則想表達的就是"高內聚",寫程式碼最終極的原則只有六個字"高內聚、低耦合",就如同葵花寶典或辟邪劍譜的中心思想就八個字"欲練此功必先自宮",所謂的高內聚就是一個程式碼模組只完成一項功能,在物件導向中,如果只讓一個類完成它該做的事,而不涉及與它無關的領域就是踐行了高內聚的原則,這個類就只有單一職責。我們都知道一句話叫"因為專注,所以專業",一個物件如果承擔太多的職責,那麼註定它什麼都做不好。這個世界上任何好的東西都有兩個特徵,一個是功能單一,好的相機絕對不是電視購物裡面賣的那種一個機器有一百多種功能的,它基本上只能照相;另一個是模組化,好的自行車是組裝車,從減震叉、剎車到變速器,所有的部件都是可以拆卸和重新組裝的,好的乒乓球拍也不是成品拍,一定是底板和膠皮可以拆分和自行組裝的,一個好的軟體系統,它裡面的每個功能模組也應該是可以輕易的拿到其他系統中使用的,這樣才能實現軟體複用的目標。)- 開閉原則:軟體實體應當對擴充套件開放,對修改關閉。(在理想的狀態下,當我們需要為一個軟體系統增加新功能時,只需要從原來的系統派生出一些新類就可以,不需要修改原來的任何一行程式碼。要做到開閉有兩個要點:①抽象是關鍵,一個系統中如果沒有抽象類或介面系統就沒有擴充套件點;②封裝可變性,將系統中的各種可變因素封裝到一個繼承結構中,如果多個可變因素混雜在一起,系統將變得複雜而換亂,如果不清楚如何封裝可變性,可以參考《設計模式精解》一書中對橋樑模式的講解的章節。)- 依賴倒轉原則:面向介面程式設計。(該原則說得直白和具體一些就是宣告方法的引數型別、方法的返回型別、變數的引用型別時,儘可能使用抽象型別而不用具體型別,因為抽象型別可以被它的任何一個子型別所替代,請參考下面的里氏替換原則。)里氏替換原則:任何時候都可以用子型別替換掉父型別。(關於里氏替換原則的描述,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、記憶體、硬碟、顯示卡、音效卡各種裝置需要相互配合才能很好的工作,但是如果這些東西都直接連線到一起,計算機的佈線將異常複雜,在這種情況下,主機板作為一個調停者的身份出現,它將各個裝置連線在一起而不需要每個裝置之間直接交換資料,這樣就減小了系統的耦合度和複雜度,如下圖所示。迪米特法則用通俗的話來將就是不要和陌生人打交道,如果真的需要,找一個自己的朋友,讓他替你和陌生人打交道。)
這裡寫圖片描述這裡寫圖片描述
90、簡述一下你瞭解的設計模式。
答:所謂設計模式,就是一套被反覆使用的程式碼設計經驗的總結(情境中一個問題經過證實的一個解決方案)。使用設計模式是為了可重用程式碼、讓程式碼更容易被他人理解、保證程式碼可靠性。設計模式使人們可以更加簡單方便的複用成功的設計和體系結構。將已證實的技術表述成設計模式也會使新系統開發者更加容易理解其設計思路。
在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系統中都使用裝潢模式)等,反正基本原則就是揀自己最熟悉的、用得最多的作答,以免言多必失。
91、用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容器可以為普通的類建立單例,它是怎麼做到的呢?
92、什麼是UML?
答:UML是統一建模語言(Unified Modeling Language)的縮寫,它發表於1997年,綜合了當時已經存在的物件導向的建模語言、方法和過程,是一個支援模型化和軟體系統開發的圖形化語言,為軟體開發的所有階段提供模型化和視覺化支援。使用UML可以幫助溝通與交流,輔助應用設計和文件的生成,還能夠闡釋系統的結構和行為。
93、UML中有哪些常用的圖? 答:UML定義了多種圖形化的符號來描述軟體系統部分或全部的靜態結構和動態結構,包括:用例圖(use case diagram)、類圖(class diagram)、時序圖(sequence diagram)、協作圖(collaboration diagram)、狀態圖(statechart diagram)、活動圖(activity diagram)、構件圖(component diagram)、部署圖(deployment diagram)等。在這些圖形化符號中,有三種圖最為重要,分別是:用例圖(用來捕獲需求,描述系統的功能,通過該圖可以迅速的瞭解系統的功能模組及其關係)、類圖(描述類以及類與類之間的關係,通過該圖可以快速瞭解系統)、時序圖(描述執行特定任務時物件之間的互動關係以及執行順序,通過該圖可以瞭解物件能接收的訊息也就是說物件能夠向外界提供的服務)。用例圖: 這裡寫圖片描述 類圖: 這裡寫圖片描述 時序圖: 這裡寫圖片描述
94、用Java寫一個氣泡排序。
答:氣泡排序幾乎是個程式設計師都寫得出來,但是面試的時候如何寫一個逼格高的氣泡排序卻不是每個人都能做到,下面提供一個參考程式碼:
import java.util.Comparator;
/**
- 排序器介面(策略模式: 將演算法封裝到具有共同介面的獨立的類中使得它們可以相互替換)
- @author駱昊
*/
public interface Sorter {
/**
* 排序
* @param list 待排序的陣列
*/
public <T extends Comparable> void sort(T[] list);
/**
* 排序
* @param list 待排序的陣列
* @param comp 比較兩個物件的比較器
*/
public void sort(T[] list, Comparator 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;
}
}
}
}
}
95、用Java寫一個折半查詢。
答:折半查詢,也稱二分查詢、二分搜尋,是一種在有序陣列中查詢某一特定元素的搜尋演算法。搜素過程從陣列的中間元素開始,如果中間元素正好是要查詢的元素,則搜素過程結束;如果某一特定元素大於或者小於中間元素,則在陣列大於或小於中間元素的那一半中查詢,而且跟開始一樣從中間元素開始比較。如果在某一步驟陣列已經為空,則表示找不到指定的元素。這種搜尋演算法每一次比較都使搜尋範圍縮小一半,其時間複雜度是O(logN)。
import java.util.Comparator;
public class MyUtil {
public static <T extends Comparable> int binarySearch(T[] x, T key) {
return binarySearch(x, 0, x.length- 1, key);
}
// 使用迴圈實現的二分查詢
public static int binarySearch(T[] x, T key, Comparator 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> 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(>>>是邏輯右移,是不帶符號位的右移)
這部分主要是與Java Web和Web Service相關的面試題。
96、闡述Servlet和CGI的區別?
答:Servlet與CGI的區別在於Servlet處於伺服器程式中,它通過多執行緒方式執行其service()方法,一個例項可以服務於多個請求,並且其例項一般不會銷燬,而CGI對每個請求都產生新的程式,服務完成後就銷燬,所以效率上低於Servlet。
補充:Sun Microsystems公司在1996年釋出Servlet技術就是為了和CGI進行競爭,Servlet是一個特殊的Java程式,一個基於Java的Web應用通常包含一個或多個Servlet類。Servlet不能夠自行建立並執行,它是在Servlet容器中執行的,容器將使用者的請求傳遞給Servlet程式,並將Servlet的響應回傳給使用者。通常一個Servlet會關聯一個或多個JSP頁面。以前CGI經常因為效能開銷上的問題被詬病,然而Fast CGI早就已經解決了CGI效率上的問題,所以面試的時候大可不必信口開河的詬病CGI,事實上有很多你熟悉的網站都使用了CGI技術。
97、Servlet介面中有哪些方法?
答:Servlet介面定義了5個方法,其中前三個方法與Servlet生命週期相關:
- void init(ServletConfig config) throws ServletException
- void service(ServletRequest req, ServletResponse resp) throws ServletException, java.io.IOException
- void destory()
- java.lang.String getServletInfo()
- ServletConfig getServletConfig()
Web容器載入Servlet並將其例項化後,Servlet生命週期開始,容器執行其init()方法進行Servlet的初始化;請求到達時呼叫Servlet的service()方法,service()方法會根據需要呼叫與請求對應的doGet或doPost等方法;當伺服器關閉或專案被解除安裝時伺服器會將Servlet例項銷燬,此時會呼叫Servlet的destroy()方法。
98、轉發(forward)和重定向(redirect)的區別?
答:forward是容器中控制權的轉向,是伺服器請求資源,伺服器直接訪問目標地址的URL,把那個URL 的響應內容讀取過來,然後把這些內容再發給瀏覽器,瀏覽器根本不知道伺服器傳送的內容是從哪兒來的,所以它的位址列中還是原來的地址。redirect就是伺服器端根據邏輯,傳送一個狀態碼,告訴瀏覽器重新去請求那個地址,因此從瀏覽器的位址列中可以看到跳轉後的連結地址,很明顯redirect無法訪問到伺服器保護起來資源,但是可以從一個網站redirect到其他網站。forward更加高效,所以在滿足需要時儘量使用forward(通過呼叫RequestDispatcher物件的forward()方法,該物件可以通過ServletRequest物件的getRequestDispatcher()方法獲得),並且這樣也有助於隱藏實際的連結;在有些情況下,比如需要訪問一個其它伺服器上的資源,則必須使用重定向(通過HttpServletResponse物件呼叫其sendRedirect()方法實現)。
99、JSP有哪些內建物件?作用分別是什麼?
答:JSP有9個內建物件:
-
request:封裝客戶端的請求,其中包含來自GET或POST請求的引數;
-
response:封裝伺服器對客戶端的響應;
-
pageContext:通過該物件可以獲取其他物件;
-
session:封裝使用者會話的物件;
-
application:封裝伺服器執行環境的物件;
-
out:輸出伺服器響應的輸出流物件;
-
config:Web應用的配置物件;
-
page:JSP頁面本身(相當於Java程式中的this);
-
exception:封裝頁面丟擲異常的物件。
補充:如果用Servlet來生成網頁中的動態內容無疑是非常繁瑣的工作,另一方面,所有的文字和HTML標籤都是硬編碼,即使做出微小的修改,都需要進行重新編譯。JSP解決了Servlet的這些問題,它是Servlet很好的補充,可以專門用作為使用者呈現檢視(View),而Servlet作為控制器(Controller)專門負責處理使用者請求並轉發或重定向到某個頁面。基於Java的Web開發很多都同時使用了Servlet和JSP。JSP頁面其實是一個Servlet,能夠執行Servlet的伺服器(Servlet容器)通常也是JSP容器,可以提供JSP頁面的執行環境,Tomcat就是一個Servlet/JSP容器。第一次請求一個JSP頁面時,Servlet/JSP容器首先將JSP頁面轉換成一個JSP頁面的實現類,這是一個實現了JspPage介面或其子介面HttpJspPage的Java類。JspPage介面是Servlet的子介面,因此每個JSP頁面都是一個Servlet。轉換成功後,容器會編譯Servlet類,之後容器載入和例項化Java位元組碼,並執行它通常對Servlet所做的生命週期操作。對同一個JSP頁面的後續請求,容器會檢視這個JSP頁面是否被修改過,如果修改過就會重新轉換並重新編譯並執行。如果沒有則執行記憶體中已經存在的Servlet例項。我們可以看一段JSP程式碼對應的Java程式就知道一切了,而且9個內建物件的神祕面紗也會被揭開。
JSP頁面:
<%@ page pageEncoding=“UTF-8”%>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + “?/” + request.getServerName() + “:” + request.getServerPort() + path + “/”;
%>
Hello, World!
Current time is: <%= new java.util.Date().toString() %>
對應的Java程式碼:
package org.apache.jsp;
import javax.servlet.;
import javax.servlet.http.;
import javax.servlet.jsp.*;
public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase
implements org.apache.jasper.runtime.JspSourceDependent {
private static final javax.servlet.jsp.JspFactory _jspxFactory = javax.servlet.jsp.JspFactory
.getDefaultFactory();
private static java.util.Map<java.lang.String, java.lang.Long> _jspx_dependants;
private javax.el.ExpressionFactory _el_expressionfactory;
private org.apache.tomcat.InstanceManager _jsp_instancemanager;
public java.util.Map<java.lang.String, java.lang.Long> getDependants() {
return _jspx_dependants;
}
public void _jspInit() {
_el_expressionfactory = _jspxFactory.getJspApplicationContext(
getServletConfig().getServletContext()).getExpressionFactory();
_jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory
.getInstanceManager(getServletConfig());
}
public void _jspDestroy() {
}
public void _jspService(
final javax.servlet.http.HttpServletRequest request,
final javax.servlet.http.HttpServletResponse response)
throws java.io.IOException, javax.servlet.ServletException {
// 內建物件就是在這裡定義的
final javax.servlet.jsp.PageContext pageContext;
javax.servlet.http.HttpSession session = null;
final javax.servlet.ServletContext application;
final javax.servlet.ServletConfig config;
javax.servlet.jsp.JspWriter out = null;
final java.lang.Object page = this;
javax.servlet.jsp.JspWriter _jspx_out = null;
javax.servlet.jsp.PageContext _jspx_page_context = null;
try {
response.setContentType("text/html;charset=UTF-8");
pageContext = _jspxFactory.getPageContext(this, request, response,
null, true, 8192, true);
_jspx_page_context = pageContext;
application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();
_jspx_out = out;
out.write('\r');
out.write('\n');
String path = request.getContextPath();
String basePath = request.getScheme() + "://"
+ request.getServerName() + ":" + request.getServerPort()
+ path + "/";
// 以下程式碼通過輸出流將HTML標籤輸出到瀏覽器中
out.write("\r\n");
out.write("\r\n");
out.write("\r\n");
out.write("\r\n");
out.write(" \r\n");
out.write(" <base href="");
out.print(basePath);
out.write("">\r\n");
out.write("
out.write(" <style type=“text/css”>\r\n");
out.write(" \t* { font-family: “Arial”; }\r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write("
Hello, World!
\r\n");out.write("
\r\n");
out.write("
Current time is: “);
out.print(new java.util.Date().toString());
out.write(”
\r\n");
out.write(" \r\n");
out.write("\r\n");
} catch (java.lang.Throwable t) {
if (!(t instanceof javax.servlet.jsp.SkipPageException)) {
out = _jspx_out;
if (out != null && out.getBufferSize() != 0)
try {
out.clearBuffer();
} catch (java.io.IOException e) {
}
if (_jspx_page_context != null)
_jspx_page_context.handlePageException(t);
else
throw new ServletException(t);
}
} finally {
_jspxFactory.releasePageContext(_jspx_page_context);
}
}
}
100、get和post請求的區別?
答:
①get請求用來從伺服器上獲得資源,而post是用來向伺服器提交資料;
②get將表單中資料按照name=value的形式,新增到action 所指向的URL 後面,並且兩者使用"?“連線,而各個變數之間使用”&“連線;post是將表單中的資料放在HTTP協議的請求頭或訊息體中,傳遞到action所指向URL;
③get傳輸的資料要受到URL長度限制(1024位元組);而post可以傳輸大量的資料,上傳檔案通常要使用post方式;
④使用get時引數會顯示在位址列上,如果這些資料不是敏感資料,那麼可以使用get;對於敏感資料還是應用使用post;
⑤get使用MIME型別application/x-www-form-urlencoded的URL編碼(也叫百分號編碼)文字的格式傳遞引數,保證被傳送的引數由遵循規範的文字組成,例如一個空格的編碼是”%20"。
101、常用的Web伺服器有哪些?
答:Unix和Linux平臺下使用最廣泛的免費HTTP伺服器是Apache伺服器,而Windows平臺的伺服器通常使用IIS作為Web伺服器。選擇Web伺服器應考慮的因素有:效能、安全性、日誌和統計、虛擬主機、代理伺服器、緩衝服務和整合應用程式等。下面是對常見伺服器的簡介:
- IIS:Microsoft的Web伺服器產品,全稱是Internet Information Services。IIS是允許在公共Intranet或Internet上釋出資訊的Web伺服器。IIS是目前最流行的Web伺服器產品之一,很多著名的網站都是建立在IIS的平臺上。IIS提供了一個圖形介面的管理工具,稱為Internet服務管理器,可用於監視配置和控制Internet服務。IIS是一種Web服務元件,其中包括Web伺服器、FTP伺服器、NNTP伺服器和SMTP伺服器,分別用於網頁瀏覽、檔案傳輸、新聞服務和郵件傳送等方面,它使得在網路(包括網際網路和區域網)上釋出資訊成了一件很容易的事。它提供ISAPI(Intranet Server API)作為擴充套件Web伺服器功能的程式設計介面;同時,它還提供一個Internet資料庫聯結器,可以實現對資料庫的查詢和更新。
- Kangle:Kangle Web伺服器是一款跨平臺、功能強大、安全穩定、易操作的高效能Web伺服器和反向代理伺服器軟體。此外,Kangle也是一款專為做虛擬主機研發的Web伺服器。實現虛擬主機獨立程式、獨立身份執行。使用者之間安全隔離,一個使用者出問題不影響其他使用者。支援PHP、ASP、ASP.NET、Java、Ruby等多種動態開發語言。
- WebSphere:WebSphere Application Server是功能完善、開放的Web應用程式伺服器,是IBM電子商務計劃的核心部分,它是基於Java的應用環境,用於建立、部署和管理Internet和Intranet Web應用程式,適應各種Web應用程式伺服器的需要。
- WebLogic:WebLogic Server是一款多功能、基於標準的Web應用伺服器,為企業構建企業應用提供了堅實的基礎。針對各種應用開發、關鍵性任務的部署,各種系統和資料庫的整合、跨Internet協作等Weblogic都提供了相應的支援。由於它具有全面的功能、對開放標準的遵從性、多層架構、支援基於元件的開發等優勢,很多公司的企業級應用都選擇它來作為開發和部署的環境。WebLogic Server在使應用伺服器成為企業應用架構的基礎方面一直處於領先地位,為構建整合化的企業級應用提供了穩固的基礎。
- Apache:目前Apache仍然是世界上用得最多的Web伺服器,其市場佔有率很長時間都保持在60%以上(目前的市場份額約40%左右)。世界上很多著名的網站都是Apache的產物,它的成功之處主要在於它的原始碼開放、有一支強大的開發團隊、支援跨平臺的應用(可以執行在幾乎所有的Unix、Windows、Linux系統平臺上)以及它的可移植性等方面。
- Tomcat:Tomcat是一個開放原始碼、執行Servlet和JSP的容器。Tomcat實現了Servlet和JSP規範。此外,Tomcat還實現了Apache-Jakarta規範而且比絕大多數商業應用軟體伺服器要好,因此目前也有不少的Web伺服器都選擇了Tomcat。
- Nginx:讀作"engine x",是一個高效能的HTTP和反向代理伺服器,也是一個IMAP/POP3/SMTP代理伺服器。 Nginx是由Igor Sysoev為俄羅斯訪問量第二的Rambler站點開發的,第一個公開版本0.1.0釋出於2004年10月4日。其將原始碼以類BSD許可證的形式釋出,因它的穩定性、豐富的功能集、示例配置檔案和低系統資源的消耗而聞名。在2014年下半年,Nginx的市場份額達到了14%。
102、JSP和Servlet是什麼關係?
答:其實這個問題在上面已經闡述過了,Servlet是一個特殊的Java程式,它執行於伺服器的JVM中,能夠依靠伺服器的支援向瀏覽器提供顯示內容。JSP本質上是Servlet的一種簡易形式,JSP會被伺服器處理成一個類似於Servlet的Java程式,可以簡化頁面內容的生成。Servlet和JSP最主要的不同點在於,Servlet的應用邏輯是在Java檔案中,並且完全從表示層中的HTML分離開來。而JSP的情況是Java和HTML可以組合成一個副檔名為.jsp的檔案。有人說,Servlet就是在Java中寫HTML,而JSP就是在HTML中寫Java程式碼,當然這個說法是很片面且不夠準確的。JSP側重於檢視,Servlet更側重於控制邏輯,在MVC架構模式中,JSP適合充當檢視(view)而Servlet適合充當控制器(controller)。
103、講解JSP中的四種作用域。
答:JSP中的四種作用域包括page、request、session和application,具體來說:
- page代表與一個頁面相關的物件和屬性。
- request代表與Web客戶機發出的一個請求相關的物件和屬性。一個請求可能跨越多個頁面,涉及多個Web元件;需要在頁面顯示的臨時資料可以置於此作用域。
- session代表與某個使用者與伺服器建立的一次會話相關的物件和屬性。跟某個使用者相關的資料應該放在使用者自己的session中。
- application代表與整個Web應用程式相關的物件和屬性,它實質上是跨越整個Web應用程式,包括多個頁面、請求和會話的一個全域性作用域。
104、如何實現JSP或Servlet的單執行緒模式?
答:
對於JSP頁面,可以通過page指令進行設定。
<%@page isThreadSafe=”false”%>
對於Servlet,可以讓自定義的Servlet實現SingleThreadModel標識介面。
說明:如果將JSP或Servlet設定成單執行緒工作模式,會導致每個請求建立一個Servlet例項,這種實踐將導致嚴重的效能問題(伺服器的記憶體壓力很大,還會導致頻繁的垃圾回收),所以通常情況下並不會這麼做。
105、實現會話跟蹤的技術有哪些?
答:由於HTTP協議本身是無狀態的,伺服器為了區分不同的使用者,就需要對使用者會話進行跟蹤,簡單的說就是為使用者進行登記,為使用者分配唯一的ID,下一次使用者在請求中包含此ID,伺服器據此判斷到底是哪一個使用者。
①URL 重寫:在URL中新增使用者會話的資訊作為請求的引數,或者將唯一的會話ID新增到URL結尾以標識一個會話。
②設定表單隱藏域:將和會話跟蹤相關的欄位新增到隱式表單域中,這些資訊不會在瀏覽器中顯示但是提交表單時會提交給伺服器。
這兩種方式很難處理跨越多個頁面的資訊傳遞,因為如果每次都要修改URL或在頁面中新增隱式表單域來儲存使用者會話相關資訊,事情將變得非常麻煩。
③cookie:cookie有兩種,一種是基於視窗的,瀏覽器視窗關閉後,cookie就沒有了;另一種是將資訊儲存在一個臨時檔案中,並設定存在的時間。當使用者通過瀏覽器和伺服器建立一次會話後,會話ID就會隨響應資訊返回儲存在基於視窗的cookie中,那就意味著只要瀏覽器沒有關閉,會話沒有超時,下一次請求時這個會話ID又會提交給伺服器讓伺服器識別使用者身份。會話中可以為使用者儲存資訊。會話物件是在伺服器記憶體中的,而基於視窗的cookie是在客戶端記憶體中的。如果瀏覽器禁用了cookie,那麼就需要通過下面兩種方式進行會話跟蹤。當然,在使用cookie時要注意幾點:首先不要在cookie中存放敏感資訊;其次cookie儲存的資料量有限(4k),不能將過多的內容儲存cookie中;再者瀏覽器通常只允許一個站點最多存放20個cookie。當然,和使用者會話相關的其他資訊(除了會話ID)也可以存在cookie方便進行會話跟蹤。
④HttpSession:在所有會話跟蹤技術中,HttpSession物件是最強大也是功能最多的。當一個使用者第一次訪問某個網站時會自動建立HttpSession,每個使用者可以訪問他自己的HttpSession。可以通過HttpServletRequest物件的getSession方法獲得HttpSession,通過HttpSession的setAttribute方法可以將一個值放在HttpSession中,通過呼叫HttpSession物件的getAttribute方法,同時傳入屬性名就可以獲取儲存在HttpSession中的物件。與上面三種方式不同的是,HttpSession放在伺服器的記憶體中,因此不要將過大的物件放在裡面,即使目前的Servlet容器可以在記憶體將滿時將HttpSession中的物件移到其他儲存裝置中,但是這樣勢必影響效能。新增到HttpSession中的值可以是任意Java物件,這個物件最好實現了Serializable介面,這樣Servlet容器在必要的時候可以將其序列化到檔案中,否則在序列化時就會出現異常。
**補充:**HTML5中可以使用Web Storage技術通過JavaScript來儲存資料,例如可以使用localStorage和sessionStorage來儲存使用者會話的資訊,也能夠實現會話跟蹤。
106、過濾器有哪些作用和用法?
答: Java Web開發中的過濾器(filter)是從Servlet 2.3規範開始增加的功能,並在Servlet 2.4規範中得到增強。對Web應用來說,過濾器是一個駐留在伺服器端的Web元件,它可以擷取客戶端和伺服器之間的請求與響應資訊,並對這些資訊進行過濾。當Web容器接受到一個對資源的請求時,它將判斷是否有過濾器與這個資源相關聯。如果有,那麼容器將把請求交給過濾器進行處理。在過濾器中,你可以改變請求的內容,或者重新設定請求的報頭資訊,然後再將請求傳送給目標資源。當目標資源對請求作出響應時候,容器同樣會將響應先轉發給過濾器,在過濾器中你可以對響應的內容進行轉換,然後再將響應傳送到客戶端。
常見的過濾器用途主要包括:對使用者請求進行統一認證、對使用者的訪問請求進行記錄和稽核、對使用者傳送的資料進行過濾或替換、轉換圖象格式、對響應內容進行壓縮以減少傳輸量、對請求或響應進行加解密處理、觸發資源訪問事件、對XML的輸出應用XSLT等。
和過濾器相關的介面主要有:Filter、FilterConfig和FilterChain。
編碼過濾器的例子:
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
@WebFilter(urlPatterns = { “*” },
initParams = {@WebInitParam(name=“encoding”, value=“utf-8”)})
public class CodingFilter implements Filter {
private String defaultEncoding = “utf-8”;
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain chain) throws IOException, ServletException {
req.setCharacterEncoding(defaultEncoding);
resp.setCharacterEncoding(defaultEncoding);
chain.doFilter(req, resp);
}
@Override
public void init(FilterConfig config) throws ServletException {
String encoding = config.getInitParameter("encoding");
if (encoding != null) {
defaultEncoding = encoding;
}
}
}
下載計數過濾器的例子:
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
@WebFilter(urlPatterns = {"/*"})
public class DownloadCounterFilter implements Filter {
private ExecutorService executorService = Executors.newSingleThreadExecutor();
private Properties downloadLog;
private File logFile;
@Override
public void destroy() {
executorService.shutdown();
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
final String uri = request.getRequestURI();
executorService.execute(new Runnable() {
@Override
public void run() {
String value = downloadLog.getProperty(uri);
if(value == null) {
downloadLog.setProperty(uri, "1");
}
else {
int count = Integer.parseInt(value);
downloadLog.setProperty(uri, String.valueOf(++count));
}
try {
downloadLog.store(new FileWriter(logFile), "");
}
catch (IOException e) {
e.printStackTrace();
}
}
});
chain.doFilter(req, resp);
}
@Override
public void init(FilterConfig config) throws ServletException {
String appPath = config.getServletContext().getRealPath("/");
logFile = new File(appPath, "downloadLog.txt");
if(!logFile.exists()) {
try {
logFile.createNewFile();
}
catch(IOException e) {
e.printStackTrace();
}
}
downloadLog = new Properties();
try {
downloadLog.load(new FileReader(logFile));
} catch (IOException e) {
e.printStackTrace();
}
}
}
說明:這裡使用了Servlet 3規範中的註解來部署過濾器,當然也可以在web.xml中使用<filter>和<filter-mapping>標籤部署過濾器,如108題中所示。
107、監聽器有哪些作用和用法?
答:Java Web開發中的監聽器(listener)就是application、session、request三個物件建立、銷燬或者往其中新增修改刪除屬性時自動執行程式碼的功能元件,如下所示:
①ServletContextListener:對Servlet上下文的建立和銷燬進行監聽。
②ServletContextAttributeListener:監聽Servlet上下文屬性的新增、刪除和替換。
③HttpSessionListener:對Session的建立和銷燬進行監聽。
補充:session的銷燬有兩種情況:1). session超時(可以在web.xml中通過<session-config>/<session-timeout>標籤配置超時時間);2). 通過呼叫session物件的invalidate()方法使session失效。
④HttpSessionAttributeListener:對Session物件中屬性的新增、刪除和替換進行監聽。
⑤ServletRequestListener:對請求物件的初始化和銷燬進行監聽。
⑥ServletRequestAttributeListener:對請求物件屬性的新增、刪除和替換進行監聽。
下面是一個統計網站最多線上人數監聽器的例子。
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
/**
上下文監聽器,在伺服器啟動時初始化onLineCount和maxOnLineCount兩個變數
並將其置於伺服器上下文(ServletContext)中,其初始值都是0
*/
@WebListener
public class InitListener implements ServletContextListener {
@Override
public void contextDestroyed(ServletContextEvent evt) {
}
@Override
public void contextInitialized(ServletContextEvent evt) {
evt.getServletContext().setAttribute("onLineCount", 0);
evt.getServletContext().setAttribute("maxOnLineCount", 0);
}
}
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.servlet.ServletContext;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
/**
會話監聽器,在使用者會話建立和銷燬的時候根據情況
修改onLineCount和maxOnLineCount的值
*/
@WebListener
public class MaxCountListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent event) {
ServletContext ctx = event.getSession().getServletContext();
int count = Integer.parseInt(ctx.getAttribute("onLineCount").toString());
count++;
ctx.setAttribute("onLineCount", count);
int maxOnLineCount = Integer.parseInt(ctx.getAttribute("maxOnLineCount").toString());
if (count > maxOnLineCount) {
ctx.setAttribute("maxOnLineCount", count);
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
ctx.setAttribute("date", df.format(new Date()));
}
}
@Override
public void sessionDestroyed(HttpSessionEvent event) {
ServletContext app = event.getSession().getServletContext();
int count = Integer.parseInt(app.getAttribute("onLineCount").toString());
count--;
app.setAttribute("onLineCount", count);
}
}
說明:這裡使用了Servlet 3規範中的@WebListener註解配置監聽器,當然你可以在web.xml檔案中用<listener>標籤配置監聽器,如108題中所示。
108、web.xml檔案中可以配置哪些內容?
答:web.xml用於配置Web應用的相關資訊,如:監聽器(listener)、過濾器(filter)、 Servlet、相關引數、會話超時時間、安全驗證方式、錯誤頁面等,下面是一些開發中常見的配置:
①配置Spring上下文載入監聽器載入Spring配置檔案並建立IoC容器:
contextConfigLocation classpath:applicationContext.xml org.springframework.web.context.ContextLoaderListener②配置Spring的OpenSessionInView過濾器來解決延遲載入和Hibernate會話關閉的矛盾:
openSessionInView org.springframework.orm.hibernate3.support.OpenSessionInViewFilter openSessionInView /*③配置會話超時時間為10分鐘:
10④配置404和Exception的錯誤頁面:
404 /error.jsp java.lang.Exception /error.jsp⑤配置安全認證方式:
ProtectedArea /admin/* GET POST admin BASIC admin說明:對Servlet(小服務)、Listener(監聽器)和Filter(過濾器)等Web元件的配置,Servlet 3規範提供了基於註解的配置方式,可以分別使用@WebServlet、@WebListener、@WebFilter註解進行配置。
補充:如果Web提供了有價值的商業資訊或者是敏感資料,那麼站點的安全性就是必須考慮的問題。安全認證是實現安全性的重要手段,認證就是要解決“Are you who you say you are?”的問題。認證的方式非常多,簡單說來可以分為三類:
A. What you know? — 口令
B. What you have? — 數字證照(U盾、密保卡)
C. Who you are? — 指紋識別、虹膜識別
在Tomcat中可以通過建立安全套接字層(Secure Socket Layer, SSL)以及通過基本驗證或表單驗證來實現對安全性的支援。
109、你的專案中使用過哪些JSTL標籤?
答:專案中主要使用了JSTL的核心標籤庫,包括<c:if>、<c:choose>、<c: when>、<c: otherwise>、<c:forEach>等,主要用於構造迴圈和分支結構以控制顯示邏輯。
說明:雖然JSTL標籤庫提供了core、sql、fmt、xml等標籤庫,但是實際開發中建議只使用核心標籤庫(core),而且最好只使用分支和迴圈標籤並輔以表示式語言(EL),這樣才能真正做到資料顯示和業務邏輯的分離,這才是最佳實踐。
110、使用標籤庫有什麼好處?如何自定義JSP標籤?
答:使用標籤庫的好處包括以下幾個方面:
- 分離JSP頁面的內容和邏輯,簡化了Web開發;
- 開發者可以建立自定義標籤來封裝業務邏輯和顯示邏輯;
- 標籤具有很好的可移植性、可維護性和可重用性;
- 避免了對Scriptlet(小指令碼)的使用(很多公司的專案開發都不允許在JSP中書寫小指令碼)
自定義JSP標籤包括以下幾個步驟:
- 編寫一個Java類實現實現Tag/BodyTag/IterationTag介面(開發中通常不直接實現這些介面而是繼承TagSupport/BodyTagSupport/SimpleTagSupport類,這是對預設適配模式的應用),重寫doStartTag()、doEndTag()等方法,定義標籤要完成的功能
- 編寫副檔名為tld的標籤描述檔案對自定義標籤進行部署,tld檔案通常放在WEB-INF資料夾下或其子目錄中
- 在JSP頁面中使用taglib指令引用該標籤庫
下面是一個自定義標籤庫的例子。
步驟1 - 標籤類原始碼TimeTag.java:
package com.jackfrued.tags;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.tagext.TagSupport;
public class TimeTag extends TagSupport {
private static final long serialVersionUID = 1L;
private String format = "yyyy-MM-dd hh:mm:ss";
private String foreColor = "black";
private String backColor = "white";
public int doStartTag() throws JspException {
SimpleDateFormat sdf = new SimpleDateFormat(format);
JspWriter writer = pageContext.getOut();
StringBuilder sb = new StringBuilder();
sb.append(String.format("<span style='color:%s;background-color:%s'>%s</span>",
foreColor, backColor, sdf.format(new Date())));
try {
writer.print(sb.toString());
} catch(IOException e) {
e.printStackTrace();
}
return SKIP_BODY;
}
public void setFormat(String format) {
this.format = format;
}
public void setForeColor(String foreColor) {
this.foreColor = foreColor;
}
public void setBackColor(String backColor) {
this.backColor = backColor;
}
}
步驟2 - 編寫標籤庫描述檔案my.tld:
<?xml version="1.0" encoding="UTF-8" ?><description>定義標籤庫</description>
<tlib-version>1.0</tlib-version>
<short-name>MyTag</short-name>
<tag>
<name>time</name>
<tag-class>com.jackfrued.tags.TimeTag</tag-class>
<body-content>empty</body-content>
<attribute>
<name>format</name>
<required>false</required>
</attribute>
<attribute>
<name>foreColor</name>
</attribute>
<attribute>
<name>backColor</name>
</attribute>
</tag>
步驟3 - 在JSP頁面中使用自定義標籤:
<%@ page pageEncoding=“UTF-8”%>
<%@ taglib prefix=“my” uri="/WEB-INF/tld/my.tld" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + “?/” + request.getServerName() + “:” + request.getServerPort() + path + “/”;
%>
提示:如果要將自定義的標籤庫釋出成JAR檔案,需要將標籤庫描述檔案(tld檔案)放在JAR檔案的META-INF目錄下,可以JDK中的jar工具完成JAR檔案的生成。
111、說一下表示式語言(EL)的隱式物件及其作用。
答:EL的隱式物件包括:pageContext、initParam(訪問上下文引數)、param(訪問請求引數)、paramValues、header(訪問請求頭)、headerValues、cookie(訪問cookie)、applicationScope(訪問application作用域)、sessionScope(訪問session作用域)、requestScope(訪問request作用域)、pageScope(訪問page作用域)。
用法如下所示:
${pageContext.request.method}
${pageContext[“request”][“method”]}
${pageContext.request[“method”]}
${pageContext[“request”].method}
${initParam.defaultEncoding}
${header[“accept-language”]}
${headerValues[“accept-language”][0]}
${cookie.jsessionid.value}
${sessionScope.loginUser.username}
補充:表示式語言的.和[]運算作用是一致的,唯一的差別在於如果訪問的屬性名不符合Java識別符號命名規則,例如上面的accept-language就不是一個有效的Java識別符號,那麼這時候就只能用[]運算子而不能使用.運算子獲取它的值
112、表示式語言(EL)支援哪些運算子?
答:除了.和[]運算子,EL還提供了:
- 算術運算子:+、-、*、/或div、%或mod
- 關係運算子:==或eq、!=或ne、>或gt、>=或ge、<或lt、<=或le
- 邏輯運算子:&&或and、||或or、!或not
- 條件運算子:${statement? A : B}(跟Java的條件運算子類似)
- empty運算子:檢查一個值是否為null或者空(陣列長度為0或集合中沒有元素也返回true)
113、Java Web開發的Model 1和Model 2分別指的是什麼?
答:Model 1是以頁面為中心的Java Web開發,使用JSP+JavaBean技術將頁面顯示邏輯和業務邏輯處理分開,JSP實現頁面顯示,JavaBean物件用來儲存資料和實現業務邏輯。Model 2是基於MVC(模型-檢視-控制器,Model-View-Controller)架構模式的開發模型,實現了模型和檢視的徹底分離,利於團隊開發和程式碼複用,如下圖所示。
這裡寫圖片描述
114、Servlet 3中的非同步處理指的是什麼?
答:在Servlet 3中引入了一項新的技術可以讓Servlet非同步處理請求。有人可能會質疑,既然都有多執行緒了,還需要非同步處理請求嗎?答案是肯定的,因為如果一個任務處理時間相當長,那麼Servlet或Filter會一直佔用著請求處理執行緒直到任務結束,隨著併發使用者的增加,容器將會遭遇執行緒超出的風險,這這種情況下很多的請求將會被堆積起來而後續的請求可能會遭遇拒絕服務,直到有資源可以處理請求為止。非同步特性可以幫助應用節省容器中的執行緒,特別適合執行時間長而且使用者需要得到結果的任務,如果使用者不需要得到結果則直接將一個Runnable物件交給Executor並立即返回即可。
補充:多執行緒在Java誕生初期無疑是一個亮點,而Servlet單例項多執行緒的工作方式也曾為其贏得美名,然而技術的發展往往會顛覆我們很多的認知,就如同當年愛因斯坦的相對論顛覆了牛頓的經典力學一般。事實上,非同步處理絕不是Serlvet 3首創,如果你瞭解Node.js的話,對Servlet 3的這個重要改進就不以為奇了。
下面是一個支援非同步處理請求的Servlet的例子。
import java.io.IOException;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(urlPatterns = {"/async"}, asyncSupported = true)
public class AsyncServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 開啟Tomcat非同步Servlet支援
req.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
final AsyncContext ctx = req.startAsync(); // 啟動非同步處理的上下文
// ctx.setTimeout(30000);
ctx.start(new Runnable() {
@Override
public void run() {
// 在此處新增非同步處理的程式碼
ctx.complete();
}
});
}
}
115、如何在基於Java的Web專案中實現檔案上傳和下載?
答:在Sevlet 3 以前,Servlet API中沒有支援上傳功能的API,因此要實現上傳功能需要引入第三方工具從POST請求中獲得上傳的附件或者通過自行處理輸入流來獲得上傳的檔案,我們推薦使用Apache的commons-fileupload。
從Servlet 3開始,檔案上傳變得無比簡單,相信看看下面的例子一切都清楚了。
上傳頁面index.jsp:
<%@ page pageEncoding=“utf-8”%>
Select your photo and upload
支援上傳的Servlet:
package com.jackfrued.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
@WebServlet("/UploadServlet")
@MultipartConfig
public class UploadServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
// 可以用request.getPart()方法獲得名為photo的上傳附件
// 也可以用request.getParts()獲得所有上傳附件(多檔案上傳)
// 然後通過迴圈分別處理每一個上傳的檔案
Part part = request.getPart("photo");
if (part != null && part.getSubmittedFileName().length() > 0) {
// 用ServletContext物件的getRealPath()方法獲得上傳資料夾的絕對路徑
String savePath = request.getServletContext().getRealPath("/upload");
// Servlet 3.1規範中可以用Part物件的getSubmittedFileName()方法獲得上傳的檔名
// 更好的做法是為上傳的檔案進行重新命名(避免同名檔案的相互覆蓋)
part.write(savePath + "/" + part.getSubmittedFileName());
request.setAttribute("hint", "Upload Successfully!");
} else {
request.setAttribute("hint", "Upload failed!");
}
// 跳轉回到上傳頁面
request.getRequestDispatcher("index.jsp").forward(request, response);
}
}
116、伺服器收到使用者提交的表單資料,到底是呼叫Servlet的doGet()還是doPost()方法?
答:HTML的元素有一個method屬性,用來指定提交表單的方式,其值可以是get或post。我們自定義的Servlet一般情況下會重寫doGet()或doPost()兩個方法之一或全部,如果是GET請求就呼叫doGet()方法,如果是POST請求就呼叫doPost()方法,那為什麼為什麼這樣呢?我們自定義的Servlet通常繼承自HttpServlet,HttpServlet繼承自GenericServlet並重寫了其中的service()方法,這個方法是Servlet介面中定義的。HttpServlet重寫的service()方法會先獲取使用者請求的方法,然後根據請求方法呼叫doGet()、doPost()、doPut()、doDelete()等方法,如果在自定義Servlet中重寫了這些方法,那麼顯然會呼叫重寫過的(自定義的)方法,這顯然是對模板方法模式的應用(如果不理解,請參考閻巨集博士的《Java與模式》一書的第37章)。當然,自定義Servlet中也可以直接重寫service()方法,那麼不管是哪種方式的請求,都可以通過自己的程式碼進行處理,這對於不區分請求方法的場景比較合適。
117、JSP中的靜態包含和動態包含有什麼區別?
答:靜態包含是通過JSP的include指令包含頁面,動態包含是通過JSP標準動作jsp:forward包含頁面。靜態包含是編譯時包含,如果包含的頁面不存在則會產生編譯錯誤,而且兩個頁面的"contentType"屬性應保持一致,因為兩個頁面會合二為一,只產生一個class檔案,因此被包含頁面發生的變動再包含它的頁面更新前不會得到更新。動態包含是執行時包含,可以向被包含的頁面傳遞引數,包含頁面和被包含頁面是獨立的,會編譯出兩個class檔案,如果被包含的頁面不存在,不會產生編譯錯誤,也不影響頁面其他部分的執行。程式碼如下所示:
<%-- 靜態包含 --%>
<%@ include file="…" %>
<%-- 動態包含 --%>
<jsp:include page="…">
<jsp:param name="…" value="…" />
</jsp:include>
118、Servlet中如何獲取使用者提交的查詢引數或表單資料?
答:可以通過請求物件(HttpServletRequest)的getParameter()方法通過引數名獲得引數值。如果有包含多個值的引數(例如核取方塊),可以通過請求物件的getParameterValues()方法獲得。當然也可以通過請求物件的getParameterMap()獲得一個引數名和引數值的對映(Map)。
119、Servlet中如何獲取使用者配置的初始化引數以及伺服器上下文引數?
答:可以通過重寫Servlet介面的init(ServletConfig)方法並通過ServletConfig物件的getInitParameter()方法來獲取Servlet的初始化引數。可以通過ServletConfig物件的getServletContext()方法獲取ServletContext物件,並通過該物件的getInitParameter()方法來獲取伺服器上下文引數。當然,ServletContext物件也在處理使用者請求的方法(如doGet()方法)中通過請求物件的getServletContext()方法來獲得。
120、如何設定請求的編碼以及響應內容的型別?
答:通過請求物件(ServletRequest)的setCharacterEncoding(String)方法可以設定請求的編碼,其實要徹底解決亂碼問題就應該讓頁面、伺服器、請求和響應、Java程式都使用統一的編碼,最好的選擇當然是UTF-8;通過響應物件(ServletResponse)的setContentType(String)方法可以設定響應內容的型別,當然也可以通過HttpServletResponsed物件的setHeader(String, String)方法來設定。
說明:現在如果還有公司在面試的時候問JSP的宣告標記、表示式標記、小指令碼標記這些內容的話,這樣的公司也不用去了,其實JSP內建物件、JSP指令這些東西基本上都可以忘卻了,關於Java Web開發的相關知識,可以看一下我的《Servlet&JSP思維導圖》,上面有完整的知識點的羅列。想了解如何實現自定義MVC框架的,可以看一下我的《Java Web自定義MVC框架詳解》。
121、解釋一下網路應用的模式及其特點。
答:典型的網路應用模式大致有三類:B/S、C/S、P2P。其中B代表瀏覽器(Browser)、C代表客戶端(Client)、S代表伺服器(Server),P2P是對等模式,不區分客戶端和伺服器。B/S應用模式中可以視為特殊的C/S應用模式,只是將C/S應用模式中的特殊的客戶端換成了瀏覽器,因為幾乎所有的系統上都有瀏覽器,那麼只要開啟瀏覽器就可以使用應用,沒有安裝、配置、升級客戶端所帶來的各種開銷。P2P應用模式中,成千上萬臺彼此連線的計算機都處於對等的地位,整個網路一般來說不依賴專用的集中伺服器。網路中的每一臺計算機既能充當網路服務的請求者,又對其它計算機的請求作出響應,提供資源和服務。通常這些資源和服務包括:資訊的共享和交換、計算資源(如CPU的共享)、儲存共享(如快取和磁碟空間的使用)等,這種應用模式最大的阻力安全性、版本等問題,目前有很多應用都混合使用了多種應用模型,最常見的網路視訊應用,它幾乎把三種模式都用上了。
補充:此題要跟"電子商務模式"區分開,因為有很多人被問到這個問題的時候馬上想到的是B2B(如阿里巴巴)、B2C(如噹噹、亞馬遜、京東)、C2C(如淘寶、拍拍)、C2B(如威客)、O2O(如美團、餓了麼)。對於這類問題,可以去百度上面科普一下。
122、什麼是Web Service(Web服務)?
答:從表面上看,Web Service就是一個應用程式,它向外界暴露出一個能夠通過Web進行呼叫的API。這就是說,你能夠用程式設計的方法透明的呼叫這個應用程式,不需要了解它的任何細節,跟你使用的程式語言也沒有關係。例如可以建立一個提供天氣預報的Web Service,那麼無論你用哪種程式語言開發的應用都可以通過呼叫它的API並傳入城市資訊來獲得該城市的天氣預報。之所以稱之為Web Service,是因為它基於HTTP協議傳輸資料,這使得執行在不同機器上的不同應用無須藉助附加的、專門的第三方軟體或硬體,就可相互交換資料或整合。
補充:這裡必須要提及的一個概念是SOA(Service-Oriented Architecture,面向服務的架構),SOA是一種思想,它將應用程式的不同功能單元通過中立的契約聯絡起來,獨立於硬體平臺、作業系統和程式語言,使得各種形式的功能單元能夠更好的整合。顯然,Web Service是SOA的一種較好的解決方案,它更多的是一種標準,而不是一種具體的技術。
123、概念解釋:SOAP、WSDL、UDDI。
答:
-
SOAP:簡單物件訪問協議(Simple Object Access Protocol),是Web Service中交換資料的一種協議規範。
-
WSDL:Web服務描述語言(Web Service Description Language),它描述了Web服務的公共介面。這是一個基於XML的關於如何與Web服務通訊和使用的服務描述;也就是描述與目錄中列出的Web服務進行互動時需要繫結的協議和資訊格式。通常採用抽象語言描述該服務支援的操作和資訊,使用的時候再將實際的網路協議和資訊格式繫結給該服務。
-
UDDI:統一描述、發現和整合(Universal Description, Discovery and Integration),它是一個基於XML的跨平臺的描述規範,可以使世界範圍內的企業在網際網路上釋出自己所提供的服務。簡單的說,UDDI是訪問各種WSDL的一個門面(可以參考設計模式中的門面模式)。
提示:關於Web Service的相關概念和知識可以在W3CSchool上找到相關的資料。
124、Java規範中和Web Service相關的規範有哪些?
答:Java規範中和Web Service相關的有三個:
- JAX-WS(JSR 224):這個規範是早期的基於SOAP的Web Service規範JAX-RPC的替代版本,它並不提供向下相容性,因為RPC樣式的WSDL以及相關的API已經在Java EE5中被移除了。WS-MetaData是JAX-WS的依賴規範,提供了基於註解配置Web Service和SOAP訊息的相關API。
- JAXM(JSR 67):定義了傳送和接收訊息所需的API,相當於Web Service的伺服器端。
- JAX-RS(JSR 311 & JSR 339 & JSR 370):是Java針對REST(Representation State Transfer)架構風格制定的一套Web Service規範。REST是一種軟體架構模式,是一種風格,它不像SOAP那樣本身承載著一種訊息協議, (兩種風格的Web Service均採用了HTTP做傳輸協議,因為HTTP協議能穿越防火牆,Java的遠端方法呼叫(RMI)等是重量級協議,通常不能穿越防火牆),因此可以將REST視為基於HTTP協議的軟體架構。REST中最重要的兩個概念是資源定位和資源操作,而HTTP協議恰好完整的提供了這兩個點。HTTP協議中的URI可以完成資源定位,而GET、POST、OPTION、DELETE方法可以完成資源操作。因此REST完全依賴HTTP協議就可以完成Web Service,而不像SOAP協議那樣只利用了HTTP的傳輸特性,定位和操作都是由SOAP協議自身完成的,也正是由於SOAP訊息的存在使得基於SOAP的Web Service顯得笨重而逐漸被淘汰。
125、介紹一下你瞭解的Java領域的Web Service框架。
答:Java領域的Web Service框架很多,包括Axis2(Axis的升級版本)、Jersey(RESTful的Web Service框架)、CXF(XFire的延續版本)、Hessian、Turmeric、JBoss SOA等,其中絕大多數都是開源框架。
提示:面試被問到這類問題的時候一定選擇自己用過的最熟悉的作答,如果之前沒有了解過就應該在面試前花一些時間瞭解其中的兩個,並比較其優缺點,這樣才能在面試時給出一個漂亮的答案。
這部分主要是開源Java EE框架方面的內容,包括Hibernate、MyBatis、Spring、Spring MVC等,由於Struts 2已經是明日黃花,在這裡就不討論Struts 2的面試題,如果需要了解相關內容,可以參考Java程式設計師面試題集(86-115)。
126、什麼是ORM?
答:物件關係對映(Object-Relational Mapping,簡稱ORM)是一種為了解決程式的物件導向模型與資料庫的關係模型互不匹配問題的技術;簡單的說,ORM是通過使用描述物件和資料庫之間對映的後設資料(在Java中可以用XML或者是註解),將程式中的物件自動持久化到關聯式資料庫中或者將關聯式資料庫表中的行轉換成Java物件,其本質上就是將資料從一種形式轉換到另外一種形式。
127、持久層設計要考慮的問題有哪些?你用過的持久層框架有哪些?
答:所謂"持久"就是將資料儲存到可掉電式儲存裝置中以便今後使用,簡單的說,就是將記憶體中的資料儲存到關係型資料庫、檔案系統、訊息佇列等提供持久化支援的裝置中。持久層就是系統中專注於實現資料持久化的相對獨立的層面。
持久層設計的目標包括:
- 資料儲存邏輯的分離,提供抽象化的資料訪問介面。
- 資料訪問底層實現的分離,可以在不修改程式碼的情況下切換底層實現。
- 資源管理和排程的分離,在資料訪問層實現統一的資源排程(如快取機制)。
- 資料抽象,提供更物件導向的資料操作。
持久層框架有:
- Hibernate
- MyBatis
- TopLink
- Guzz
- jOOQ
- Spring Data
- ActiveJDBC
128、Hibernate中SessionFactory是執行緒安全的嗎?Session是執行緒安全的嗎(兩個執行緒能夠共享同一個Session嗎)?
答:SessionFactory對應Hibernate的一個資料儲存的概念,它是執行緒安全的,可以被多個執行緒併發訪問。SessionFactory一般只會在啟動的時候構建。對於應用程式,最好將SessionFactory通過單例模式進行封裝以便於訪問。Session是一個輕量級非執行緒安全的物件(執行緒間不能共享session),它表示與資料庫進行互動的一個工作單元。Session是由SessionFactory建立的,在任務完成之後它會被關閉。Session是持久層服務對外提供的主要介面。Session會延遲獲取資料庫連線(也就是在需要的時候才會獲取)。為了避免建立太多的session,可以使用ThreadLocal將session和當前執行緒繫結在一起,這樣可以讓同一個執行緒獲得的總是同一個session。Hibernate 3中SessionFactory的getCurrentSession()方法就可以做到。
129、Hibernate中Session的load和get方法的區別是什麼?
答:主要有以下三項區別:
① 如果沒有找到符合條件的記錄,get方法返回null,load方法丟擲異常。
② get方法直接返回實體類物件,load方法返回實體類物件的代理。
③ 在Hibernate 3之前,get方法只在一級快取中進行資料查詢,如果沒有找到對應的資料則越過二級快取,直接發出SQL語句完成資料讀取;load方法則可以從二級快取中獲取資料;從Hibernate 3開始,get方法不再是對二級快取只寫不讀,它也是可以訪問二級快取的。
說明:對於load()方法Hibernate認為該資料在資料庫中一定存在可以放心的使用代理來實現延遲載入,如果沒有資料就丟擲異常,而通過get()方法獲取的資料可以不存在。
130、Session的save()、update()、merge()、lock()、saveOrUpdate()和persist()方法分別是做什麼的?有什麼區別?
答:Hibernate的物件有三種狀態:瞬時態(transient)、持久態(persistent)和遊離態(detached),如第135題中的圖所示。瞬時態的例項可以通過呼叫save()、persist()或者saveOrUpdate()方法變成持久態;遊離態的例項可以通過呼叫 update()、saveOrUpdate()、lock()或者replicate()變成持久態。save()和persist()將會引發SQL的INSERT語句,而update()或merge()會引發UPDATE語句。save()和update()的區別在於一個是將瞬時態物件變成持久態,一個是將遊離態物件變為持久態。merge()方法可以完成save()和update()方法的功能,它的意圖是將新的狀態合併到已有的持久化物件上或建立新的持久化物件。對於persist()方法,按照官方文件的說明:① persist()方法把一個瞬時態的例項持久化,但是並不保證識別符號被立刻填入到持久化例項中,識別符號的填入可能被推遲到flush的時間;② persist()方法保證當它在一個事務外部被呼叫的時候並不觸發一個INSERT語句,當需要封裝一個長會話流程的時候,persist()方法是很有必要的;③ save()方法不保證第②條,它要返回識別符號,所以它會立即執行INSERT語句,不管是在事務內部還是外部。至於lock()方法和update()方法的區別,update()方法是把一個已經更改過的脫管狀態的物件變成持久狀態;lock()方法是把一個沒有更改過的脫管狀態的物件變成持久狀態。
131、闡述Session載入實體物件的過程。
答:Session載入實體物件的步驟是:
① Session在呼叫資料庫查詢功能之前,首先會在一級快取中通過實體型別和主鍵進行查詢,如果一級快取查詢命中且資料狀態合法,則直接返回;
② 如果一級快取沒有命中,接下來Session會在當前NonExists記錄(相當於一個查詢黑名單,如果出現重複的無效查詢可以迅速做出判斷,從而提升效能)中進行查詢,如果NonExists中存在同樣的查詢條件,則返回null;
③ 如果一級快取查詢失敗則查詢二級快取,如果二級快取命中則直接返回;
④ 如果之前的查詢都未命中,則發出SQL語句,如果查詢未發現對應記錄則將此次查詢新增到Session的NonExists中加以記錄,並返回null;
⑤ 根據對映配置和SQL語句得到ResultSet,並建立對應的實體物件;
⑥ 將物件納入Session(一級快取)的管理;
⑦ 如果有對應的攔截器,則執行攔截器的onLoad方法;
⑧ 如果開啟並設定了要使用二級快取,則將資料物件納入二級快取;
⑨ 返回資料物件。
132、Query介面的list方法和iterate方法有什麼區別?
答:
① list()方法無法利用一級快取和二級快取(對快取只寫不讀),它只能在開啟查詢快取的前提下使用查詢快取;iterate()方法可以充分利用快取,如果目標資料只讀或者讀取頻繁,使用iterate()方法可以減少效能開銷。
② list()方法不會引起N+1查詢問題,而iterate()方法可能引起N+1查詢問題
說明:關於N+1查詢問題,可以參考CSDN上的一篇文章《什麼是N+1查詢》
133、Hibernate如何實現分頁查詢?
答:通過Hibernate實現分頁查詢,開發人員只需要提供HQL語句(呼叫Session的createQuery()方法)或查詢條件(呼叫Session的createCriteria()方法)、設定查詢起始行數(呼叫Query或Criteria介面的setFirstResult()方法)和最大查詢行數(呼叫Query或Criteria介面的setMaxResults()方法),並呼叫Query或Criteria介面的list()方法,Hibernate會自動生成分頁查詢的SQL語句。
134、鎖機制有什麼用?簡述Hibernate的悲觀鎖和樂觀鎖機制。
答:有些業務邏輯在執行過程中要求對資料進行排他性的訪問,於是需要通過一些機制保證在此過程中資料被鎖住不會被外界修改,這就是所謂的鎖機制。
Hibernate支援悲觀鎖和樂觀鎖兩種鎖機制。悲觀鎖,顧名思義悲觀的認為在資料處理過程中極有可能存在修改資料的併發事務(包括本系統的其他事務或來自外部系統的事務),於是將處理的資料設定為鎖定狀態。悲觀鎖必須依賴資料庫本身的鎖機制才能真正保證資料訪問的排他性,關於資料庫的鎖機制和事務隔離級別在《Java面試題大全(上)》中已經討論過了。樂觀鎖,顧名思義,對併發事務持樂觀態度(認為對資料的併發操作不會經常性的發生),通過更加寬鬆的鎖機制來解決由於悲觀鎖排他性的資料訪問對系統效能造成的嚴重影響。最常見的樂觀鎖是通過資料版本標識來實現的,讀取資料時獲得資料的版本號,更新資料時將此版本號加1,然後和資料庫表對應記錄的當前版本號進行比較,如果提交的資料版本號大於資料庫中此記錄的當前版本號則更新資料,否則認為是過期資料無法更新。Hibernate中通過Session的get()和load()方法從資料庫中載入物件時可以通過引數指定使用悲觀鎖;而樂觀鎖可以通過給實體類加整型的版本欄位再通過XML或@Version註解進行配置。
提示:使用樂觀鎖會增加了一個版本欄位,很明顯這需要額外的空間來儲存這個版本欄位,浪費了空間,但是樂觀鎖會讓系統具有更好的併發性,這是對時間的節省。因此樂觀鎖也是典型的空間換時間的策略。
135、闡述實體物件的三種狀態以及轉換關係。
答:最新的Hibernate文件中為Hibernate物件定義了四種狀態(原來是三種狀態,面試的時候基本上問的也是三種狀態),分別是:瞬時態(new, or transient)、持久態(managed, or persistent)、遊狀態(detached)和移除態(removed,以前Hibernate文件中定義的三種狀態中沒有移除態),如下圖所示,就以前的Hibernate文件中移除態被視為是瞬時態。
這裡寫圖片描述
瞬時態:當new一個實體物件後,這個物件處於瞬時態,即這個物件只是一個儲存臨時資料的記憶體區域,如果沒有變數引用這個物件,則會被JVM的垃圾回收機制回收。這個物件所儲存的資料與資料庫沒有任何關係,除非通過Session的save()、saveOrUpdate()、persist()、merge()方法把瞬時態物件與資料庫關聯,並把資料插入或者更新到資料庫,這個物件才轉換為持久態物件。
持久態:持久態物件的例項在資料庫中有對應的記錄,並擁有一個持久化標識(ID)。對持久態物件進行delete操作後,資料庫中對應的記錄將被刪除,那麼持久態物件與資料庫記錄不再存在對應關係,持久態物件變成移除態(可以視為瞬時態)。持久態物件被修改變更後,不會馬上同步到資料庫,直到資料庫事務提交。
遊離態:當Session進行了close()、clear()、evict()或flush()後,實體物件從持久態變成遊離態,物件雖然擁有持久和與資料庫對應記錄一致的標識值,但是因為物件已經從會話中清除掉,物件不在持久化管理之內,所以處於遊離態(也叫脫管態)。遊離態的物件與臨時狀態物件是十分相似的,只是它還含有持久化標識。
提示:關於這個問題,在Hibernate的官方文件中有更為詳細的解讀。
136、如何理解Hibernate的延遲載入機制?在實際應用中,延遲載入與Session關閉的矛盾是如何處理的?
答:延遲載入就是並不是在讀取的時候就把資料載入進來,而是等到使用時再載入。Hibernate使用了虛擬代理機制實現延遲載入,我們使用Session的load()方法載入資料或者一對多關聯對映在使用延遲載入的情況下從一的一方載入多的一方,得到的都是虛擬代理,簡單的說返回給使用者的並不是實體本身,而是實體物件的代理。代理物件在使用者呼叫getter方法時才會去資料庫載入資料。但載入資料就需要資料庫連線。而當我們把會話關閉時,資料庫連線就同時關閉了。
延遲載入與session關閉的矛盾一般可以這樣處理:
① 關閉延遲載入特性。這種方式操作起來比較簡單,因為Hibernate的延遲載入特性是可以通過對映檔案或者註解進行配置的,但這種解決方案存在明顯的缺陷。首先,出現"no session or session was closed"通常說明系統中已經存在主外來鍵關聯,如果去掉延遲載入的話,每次查詢的開銷都會變得很大。
② 在session關閉之前先獲取需要查詢的資料,可以使用工具方法Hibernate.isInitialized()判斷物件是否被載入,如果沒有被載入則可以使用Hibernate.initialize()方法載入物件。
③ 使用攔截器或過濾器延長Session的生命週期直到檢視獲得資料。Spring整合Hibernate提供的OpenSessionInViewFilter和OpenSessionInViewInterceptor就是這種做法。
137、舉一個多對多關聯的例子,並說明如何實現多對多關聯對映。
答:例如:商品和訂單、學生和課程都是典型的多對多關係。可以在實體類上通過@ManyToMany註解配置多對多關聯或者通過對映檔案中的和標籤配置多對多關聯,但是實際專案開發中,很多時候都是將多對多關聯對映轉換成兩個多對一關聯對映來實現的。
138、談一下你對繼承對映的理解。
答:繼承關係的對映策略有三種:
① 每個繼承結構一張表(table per class hierarchy),不管多少個子類都用一張表。
② 每個子類一張表(table per subclass),公共資訊放一張表,特有資訊放單獨的表。
③ 每個具體類一張表(table per concrete class),有多少個子類就有多少張表。
第一種方式屬於單表策略,其優點在於查詢子類物件的時候無需表連線,查詢速度快,適合多型查詢;缺點是可能導致表很大。後兩種方式屬於多表策略,其優點在於資料儲存緊湊,其缺點是需要進行連線查詢,不適合多型查詢。
139、簡述Hibernate常見優化策略。
答:這個問題應當挑自己使用過的優化策略回答,常用的有:
① 制定合理的快取策略(二級快取、查詢快取)。
② 採用合理的Session管理機制。
③ 儘量使用延遲載入特性。
④ 設定合理的批處理引數。
⑤ 如果可以,選用UUID作為主鍵生成器。
⑥ 如果可以,選用基於版本號的樂觀鎖替代悲觀鎖。
⑦ 在開發過程中, 開啟hibernate.show_sql選項檢視生成的SQL,從而瞭解底層的狀況;開發完成後關閉此選項。
⑧ 考慮資料庫本身的優化,合理的索引、恰當的資料分割槽策略等都會對持久層的效能帶來可觀的提升,但這些需要專業的DBA(資料庫管理員)提供支援。
140、談一談Hibernate的一級快取、二級快取和查詢快取。
答:Hibernate的Session提供了一級快取的功能,預設總是有效的,當應用程式儲存持久化實體、修改持久化實體時,Session並不會立即把這種改變提交到資料庫,而是快取在當前的Session中,除非顯示呼叫了Session的flush()方法或通過close()方法關閉Session。通過一級快取,可以減少程式與資料庫的互動,從而提高資料庫訪問效能。
SessionFactory級別的二級快取是全域性性的,所有的Session可以共享這個二級快取。不過二級快取預設是關閉的,需要顯示開啟並指定需要使用哪種二級快取實現類(可以使用第三方提供的實現)。一旦開啟了二級快取並設定了需要使用二級快取的實體類,SessionFactory就會快取訪問過的該實體類的每個物件,除非快取的資料超出了指定的快取空間。
一級快取和二級快取都是對整個實體進行快取,不會快取普通屬性,如果希望對普通屬性進行快取,可以使用查詢快取。查詢快取是將HQL或SQL語句以及它們的查詢結果作為鍵值對進行快取,對於同樣的查詢可以直接從快取中獲取資料。查詢快取預設也是關閉的,需要顯示開啟。
141、Hibernate中DetachedCriteria類是做什麼的?
答:DetachedCriteria和Criteria的用法基本上是一致的,但Criteria是由Session的createCriteria()方法建立的,也就意味著離開建立它的Session,Criteria就無法使用了。DetachedCriteria不需要Session就可以建立(使用DetachedCriteria.forClass()方法建立),所以通常也稱其為離線的Criteria,在需要進行查詢操作的時候再和Session繫結(呼叫其getExecutableCriteria(Session)方法),這也就意味著一個DetachedCriteria可以在需要的時候和不同的Session進行繫結。
142、@OneToMany註解的mappedBy屬性有什麼作用?
答:@OneToMany用來配置一對多關聯對映,但通常情況下,一對多關聯對映都由多的一方來維護關聯關係,例如學生和班級,應該在學生類中新增班級屬性來維持學生和班級的關聯關係(在資料庫中是由學生表中的外來鍵班級編號來維護學生表和班級表的多對一關係),如果要使用雙向關聯,在班級類中新增一個容器屬性來存放學生,並使用@OneToMany註解進行對映,此時mappedBy屬性就非常重要。如果使用XML進行配置,可以用標籤的inverse="true"設定來達到同樣的效果。
143、MyBatis中使用#和KaTeX parse error: Expected 'EOF', got '#' at position 15: 書寫佔位符有什麼區別? 答:#̲將傳入的資料都當成一個字串,…將傳入的資料直接顯示生成在SQL中。注意:使用KaTeX parse error: Expected 'EOF', got '#' at position 19: …可能會導致SQL注射攻擊,能用#̲的地方就不要使用,寫order by子句的時候應該用$而不是#。
144、解釋一下MyBatis中名稱空間(namespace)的作用。
答:在大型專案中,可能存在大量的SQL語句,這時候為每個SQL語句起一個唯一的標識(ID)就變得並不容易了。為了解決這個問題,在MyBatis中,可以為每個對映檔案起一個唯一的名稱空間,這樣定義在這個對映檔案中的每個SQL語句就成了定義在這個名稱空間中的一個ID。只要我們能夠保證每個名稱空間中這個ID是唯一的,即使在不同對映檔案中的語句ID相同,也不會再產生衝突了。
145、MyBatis中的動態SQL是什麼意思?
答:對於一些複雜的查詢,我們可能會指定多個查詢條件,但是這些條件可能存在也可能不存在,例如在58同城上面找房子,我們可能會指定面積、樓層和所在位置來查詢房源,也可能會指定面積、價格、戶型和所在位置來查詢房源,此時就需要根據使用者指定的條件動態生成SQL語句。如果不使用持久層框架我們可能需要自己拼裝SQL語句,還好MyBatis提供了動態SQL的功能來解決這個問題。MyBatis中用於實現動態SQL的元素主要有:
- if
- choose / when / otherwise
- trim
- where
- set
- foreach
下面是對映檔案的片段。
select * from t_blog where 1 = 1 and title = #{title} and content = #{content} and owner = #{owner}當然也可以像下面這些書寫。
select * from t_blog where 1 = 1 and title = #{title} and content = #{content} and owner = "owner1"再看看下面這個例子。
select * from t_blog where id in #{item}146、什麼是IoC和DI?DI是如何實現的?
答:IoC叫控制反轉,是Inversion of Control的縮寫,DI(Dependency Injection)叫依賴注入,是對IoC更簡單的詮釋。控制反轉是把傳統上由程式程式碼直接操控的物件的呼叫權交給容器,通過容器來實現物件元件的裝配和管理。所謂的"控制反轉"就是對元件物件控制權的轉移,從程式程式碼本身轉移到了外部容器,由容器來建立物件並管理物件之間的依賴關係。IoC體現了好萊塢原則 - “Don’t call me, we will call you”。依賴注入的基本原則是應用元件不應該負責查詢資源或者其他依賴的協作物件。配置物件的工作應該由容器負責,查詢資源的邏輯應該從應用元件的程式碼中抽取出來,交給容器來完成。DI是對IoC更準確的描述,即元件之間的依賴關係由容器在執行期決定,形象的來說,即由容器動態的將某種依賴關係注入到元件之中。
舉個例子:一個類A需要用到介面B中的方法,那麼就需要為類A和介面B建立關聯或依賴關係,最原始的方法是在類A中建立一個介面B的實現類C的例項,但這種方法需要開發人員自行維護二者的依賴關係,也就是說當依賴關係發生變動的時候需要修改程式碼並重新構建整個系統。如果通過一個容器來管理這些物件以及物件的依賴關係,則只需要在類A中定義好用於關聯介面B的方法(構造器或setter方法),將類A和介面B的實現類C放入容器中,通過對容器的配置來實現二者的關聯。
依賴注入可以通過setter方法注入(設值注入)、構造器注入和介面注入三種方式來實現,Spring支援setter注入和構造器注入,通常使用構造器注入來注入必須的依賴關係,對於可選的依賴關係,則setter注入是更好的選擇,setter注入需要類提供無參構造器或者無參的靜態工廠方法來建立物件。
147、Spring中Bean的作用域有哪些?
答:在Spring的早期版本中,僅有兩個作用域:singleton和prototype,前者表示Bean以單例的方式存在;後者表示每次從容器中呼叫Bean時,都會返回一個新的例項,prototype通常翻譯為原型。
補充:設計模式中的建立型模式中也有一個原型模式,原型模式也是一個常用的模式,例如做一個室內設計軟體,所有的素材都在工具箱中,而每次從工具箱中取出的都是素材物件的一個原型,可以通過物件克隆來實現原型模式。
Spring 2.x中針對WebApplicationContext新增了3個作用域,分別是:request(每次HTTP請求都會建立一個新的Bean)、session(同一個HttpSession共享同一個Bean,不同的HttpSession使用不同的Bean)和globalSession(同一個全域性Session共享一個Bean)。
說明:單例模式和原型模式都是重要的設計模式。一般情況下,無狀態或狀態不可變的類適合使用單例模式。在傳統開發中,由於DAO持有Connection這個非執行緒安全物件因而沒有使用單例模式;但在Spring環境下,所有DAO類對可以採用單例模式,因為Spring利用AOP和Java API中的ThreadLocal對非執行緒安全的物件進行了特殊處理。
ThreadLocal為解決多執行緒程式的併發問題提供了一種新的思路。ThreadLocal,顧名思義是執行緒的一個本地化物件,當工作於多執行緒中的物件使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒分配一個獨立的變數副本,所以每一個執行緒都可以獨立的改變自己的副本,而不影響其他執行緒所對應的副本。從執行緒的角度看,這個變數就像是執行緒的本地變數。
ThreadLocal類非常簡單好用,只有四個方法,能用上的也就是下面三個方法:
- void set(T value):設定當前執行緒的執行緒區域性變數的值。
- T get():獲得當前執行緒所對應的執行緒區域性變數的值。
- void remove():刪除當前執行緒中執行緒區域性變數的值。
ThreadLocal是如何做到為每一個執行緒維護一份獨立的變數副本的呢?在ThreadLocal類中有一個Map,鍵為執行緒物件,值是其執行緒對應的變數的副本,自己要模擬實現一個ThreadLocal類其實並不困難,程式碼如下所示:
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class MyThreadLocal {
private Map<Thread, T> map = Collections.synchronizedMap(new HashMap<Thread, T>());
public void set(T newValue) {
map.put(Thread.currentThread(), newValue);
}
public T get() {
return map.get(Thread.currentThread());
}
public void remove() {
map.remove(Thread.currentThread());
}
}
148、解釋一下什麼叫AOP(面向切面程式設計)?
答:AOP(Aspect-Oriented Programming)指一種程式設計範型,該範型以一種稱為切面(aspect)的語言構造為基礎,切面是一種新的模組化機制,用來描述分散在物件、類或方法中的橫切關注點(crosscutting concern)。
149、你是如何理解"橫切關注"這個概念的?
答:"橫切關注"是會影響到整個應用程式的關注功能,它跟正常的業務邏輯是正交的,沒有必然的聯絡,但是幾乎所有的業務邏輯都會涉及到這些關注功能。通常,事務、日誌、安全性等關注就是應用中的橫切關注功能。
150、你如何理解AOP中的連線點(Joinpoint)、切點(Pointcut)、增強(Advice)、引介(Introduction)、織入(Weaving)、切面(Aspect)這些概念?
答:
a. 連線點(Joinpoint):程式執行的某個特定位置(如:某個方法呼叫前、呼叫後,方法丟擲異常後)。一個類或一段程式程式碼擁有一些具有邊界性質的特定點,這些程式碼中的特定點就是連線點。Spring僅支援方法的連線點。
b. 切點(Pointcut):如果連線點相當於資料中的記錄,那麼切點相當於查詢條件,一個切點可以匹配多個連線點。Spring AOP的規則解析引擎負責解析切點所設定的查詢條件,找到對應的連線點。
c. 增強(Advice):增強是織入到目標類連線點上的一段程式程式碼。Spring提供的增強介面都是帶方位名的,如:BeforeAdvice、AfterReturningAdvice、ThrowsAdvice等。很多資料上將增強譯為“通知”,這明顯是個詞不達意的翻譯,讓很多程式設計師困惑了許久。
說明: Advice在國內的很多書面資料中都被翻譯成"通知",但是很顯然這個翻譯無法表達其本質,有少量的讀物上將這個詞翻譯為"增強",這個翻譯是對Advice較為準確的詮釋,我們通過AOP將橫切關注功能加到原有的業務邏輯上,這就是對原有業務邏輯的一種增強,這種增強可以是前置增強、後置增強、返回後增強、拋異常時增強和包圍型增強。
d. 引介(Introduction):引介是一種特殊的增強,它為類新增一些屬性和方法。這樣,即使一個業務類原本沒有實現某個介面,通過引介功能,可以動態的未該業務類新增介面的實現邏輯,讓業務類成為這個介面的實現類。
e. 織入(Weaving):織入是將增強新增到目標類具體連線點上的過程,AOP有三種織入方式:①編譯期織入:需要特殊的Java編譯期(例如AspectJ的ajc);②裝載期織入:要求使用特殊的類載入器,在裝載類的時候對類進行增強;③執行時織入:在執行時為目標類生成代理實現增強。Spring採用了動態代理的方式實現了執行時織入,而AspectJ採用了編譯期織入和裝載期織入的方式。
f. 切面(Aspect):切面是由切點和增強(引介)組成的,它包括了對橫切關注功能的定義,也包括了對連線點的定義。
補充:代理模式是GoF提出的23種設計模式中最為經典的模式之一,代理模式是物件的結構模式,它給某一個物件提供一個代理物件,並由代理物件控制對原物件的引用。簡單的說,代理物件可以完成比原物件更多的職責,當需要為原物件新增橫切關注功能時,就可以使用原物件的代理物件。我們在開啟Office系列的Word文件時,如果文件中有插圖,當文件剛載入時,文件中的插圖都只是一個虛框佔位符,等使用者真正翻到某頁要檢視該圖片時,才會真正載入這張圖,這其實就是對代理模式的使用,代替真正圖片的虛框就是一個虛擬代理;Hibernate的load方法也是返回一個虛擬代理物件,等使用者真正需要訪問物件的屬性時,才向資料庫發出SQL語句獲得真實物件。
下面用一個找槍手代考的例子演示代理模式的使用:
/**
- 參考人員介面
- @author 駱昊
*/
public interface Candidate {
/**
* 答題
*/
public void answerTheQuestions();
}
/**
- 懶學生
- @author 駱昊
*/
public class LazyStudent implements Candidate {
private String name; // 姓名
public LazyStudent(String name) {
this.name = name;
}
@Override
public void answerTheQuestions() {
// 懶學生只能寫出自己的名字不會答題
System.out.println("姓名: " + name);
}
}
/**
- 槍手
- @author 駱昊
*/
public class Gunman implements Candidate {
private Candidate target; // 被代理物件
public Gunman(Candidate target) {
this.target = target;
}
@Override
public void answerTheQuestions() {
// 槍手要寫上代考的學生的姓名
target.answerTheQuestions();
// 槍手要幫助懶學生答題並交卷
System.out.println("奮筆疾書正確答案");
System.out.println("交卷");
}
}
public class ProxyTest1 {
public static void main(String[] args) {
Candidate c = new Gunman(new LazyStudent("王小二"));
c.answerTheQuestions();
}
}
說明:從JDK 1.3開始,Java提供了動態代理技術,允許開發者在執行時建立介面的代理例項,主要包括Proxy類和InvocationHandler介面。下面的例子使用動態代理為ArrayList編寫一個代理,在新增和刪除元素時,在控制檯列印新增或刪除的元素以及ArrayList的大小:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;
public class ListProxy implements InvocationHandler {
private List target;
public ListProxy(List<T> target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Object retVal = null;
System.out.println("[" + method.getName() + ": " + args[0] + "]");
retVal = method.invoke(target, args);
System.out.println("[size=" + target.size() + "]");
return retVal;
}
}
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
public class ProxyTest2 {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
Class<?> clazz = list.getClass();
ListProxy<String> myProxy = new ListProxy<String>(list);
List<String> newList = (List<String>)
Proxy.newProxyInstance(clazz.getClassLoader(),
clazz.getInterfaces(), myProxy);
newList.add("apple");
newList.add("banana");
newList.add("orange");
newList.remove("banana");
}
}
說明:使用Java的動態代理有一個侷限性就是代理的類必須要實現介面,雖然面向介面程式設計是每個優秀的Java程式都知道的規則,但現實往往不盡如人意,對於沒有實現介面的類如何為其生成代理呢?繼承!繼承是最經典的擴充套件已有程式碼能力的手段,雖然繼承常常被初學者濫用,但繼承也常常被進階的程式設計師忽視。CGLib採用非常底層的位元組碼生成技術,通過為一個類建立子類來生成代理,它彌補了Java動態代理的不足,因此Spring中動態代理和CGLib都是建立代理的重要手段,對於實現了介面的類就用動態代理為其生成代理類,而沒有實現介面的類就用CGLib通過繼承的方式為其建立代理。
151、Spring中自動裝配的方式有哪些?
答:
-
no:不進行自動裝配,手動設定Bean的依賴關係。
-
byName:根據Bean的名字進行自動裝配。
-
byType:根據Bean的型別進行自動裝配。
-
constructor:類似於byType,不過是應用於構造器的引數,如果正好有一個Bean與構造器的引數型別相同則可以自動裝配,否則會導致錯誤。
-
autodetect:如果有預設的構造器,則通過constructor的方式進行自動裝配,否則使用byType的方式進行自動裝配。
說明:自動裝配沒有自定義裝配方式那麼精確,而且不能自動裝配簡單屬性(基本型別、字串等),在使用時應注意。
152、Spring中如何使用註解來配置Bean?有哪些相關的註解?
答:首先需要在Spring配置檔案中增加如下配置:
<context:component-scan base-package=“org.example”/>
然後可以用@Component、@Controller、@Service、@Repository註解來標註需要由Spring IoC容器進行物件託管的類。這幾個註解沒有本質區別,只不過@Controller通常用於控制器,@Service通常用於業務邏輯類,@Repository通常用於倉儲類(例如我們的DAO實現類),普通的類用@Component來標註。
153、Spring支援的事務管理型別有哪些?你在專案中使用哪種方式?
答:Spring支援程式設計式事務管理和宣告式事務管理。許多Spring框架的使用者選擇宣告式事務管理,因為這種方式和應用程式的關聯較少,因此更加符合輕量級容器的概念。宣告式事務管理要優於程式設計式事務管理,儘管在靈活性方面它弱於程式設計式事務管理,因為程式設計式事務允許你通過程式碼控制業務。
事務分為全域性事務和區域性事務。全域性事務由應用伺服器管理,需要底層伺服器JTA支援(如WebLogic、WildFly等)。區域性事務和底層採用的持久化方案有關,例如使用JDBC進行持久化時,需要使用Connetion物件來操作事務;而採用Hibernate進行持久化時,需要使用Session物件來操作事務。
Spring提供瞭如下所示的事務管理器。
事務管理器實現類 目標物件
DataSourceTransactionManager 注入DataSource
HibernateTransactionManager 注入SessionFactory
JdoTransactionManager 管理JDO事務
JtaTransactionManager 使用JTA管理事務
PersistenceBrokerTransactionManager 管理Apache的OJB事務
這些事務的父介面都是PlatformTransactionManager。Spring的事務管理機制是一種典型的策略模式,PlatformTransactionManager代表事務管理介面,該介面定義了三個方法,該介面並不知道底層如何管理事務,但是它的實現類必須提供getTransaction()方法(開啟事務)、commit()方法(提交事務)、rollback()方法(回滾事務)的多型實現,這樣就可以用不同的實現類代表不同的事務管理策略。使用JTA全域性事務策略時,需要底層應用伺服器支援,而不同的應用伺服器所提供的JTA全域性事務可能存在細節上的差異,因此實際配置全域性事務管理器是可能需要使用JtaTransactionManager的子類,如:WebLogicJtaTransactionManager(Oracle的WebLogic伺服器提供)、UowJtaTransactionManager(IBM的WebSphere伺服器提供)等。
程式設計式事務管理如下所示。
<?xml version="1.0" encoding="UTF-8"?> <context:component-scan base-package="com.jackfrued"/>
<bean id="propertyConfig"
class="org.springframework.beans.factory.config.
PropertyPlaceholderConfigurer">
jdbc.properties
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName">
<value>${db.driver}</value>
</property>
<property name="url">
<value>${db.url}</value>
</property>
<property name="username">
<value>${db.username}</value>
</property>
<property name="password">
<value>${db.password}</value>
</property>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource">
<ref bean="dataSource" />
</property>
</bean>
<!-- JDBC事務管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.
DataSourceTransactionManager" scope="singleton">
<property name="dataSource">
<ref bean="dataSource" />
</property>
</bean>
<!-- 宣告事務模板 -->
<bean id="transactionTemplate"
class="org.springframework.transaction.support.
TransactionTemplate">
package com.jackfrued.dao.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import com.jackfrued.dao.EmpDao;
import com.jackfrued.entity.Emp;
@Repository
public class EmpDaoImpl implements EmpDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public boolean save(Emp emp) {
String sql = "insert into emp values (?,?,?)";
return jdbcTemplate.update(sql, emp.getId(), emp.getName(), emp.getBirthday()) == 1;
}
}
package com.jackfrued.biz.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import com.jackfrued.biz.EmpService;
import com.jackfrued.dao.EmpDao;
import com.jackfrued.entity.Emp;
@Service
public class EmpServiceImpl implements EmpService {
@Autowired
private TransactionTemplate txTemplate;
@Autowired
private EmpDao empDao;
@Override
public void addEmp(final Emp emp) {
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus txStatus) {
empDao.save(emp);
}
});
}
}
宣告式事務如下圖所示,以Spring整合Hibernate 3為例,包括完整的DAO和業務邏輯程式碼。
<?xml version="1.0" encoding="UTF-8"?><!-- 配置由Spring IoC容器託管的物件對應的被註解的類所在的包 -->
<context:component-scan base-package="com.jackfrued" />
<!-- 配置通過自動生成代理實現AOP功能 -->
<aop:aspectj-autoproxy />
<!-- 配置資料庫連線池 (DBCP) -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<!-- 配置驅動程式類 -->
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<!-- 配置連線資料庫的URL -->
<property name="url" value="jdbc:mysql://localhost:3306/myweb" />
<!-- 配置訪問資料庫的使用者名稱 -->
<property name="username" value="root" />
<!-- 配置訪問資料庫的口令 -->
<property name="password" value="123456" />
<!-- 配置最大連線數 -->
<property name="maxActive" value="150" />
<!-- 配置最小空閒連線數 -->
<property name="minIdle" value="5" />
<!-- 配置最大空閒連線數 -->
<property name="maxIdle" value="20" />
<!-- 配置初始連線數 -->
<property name="initialSize" value="10" />
<!-- 配置連線被洩露時是否生成日誌 -->
<property name="logAbandoned" value="true" />
<!-- 配置是否刪除超時連線 -->
<property name="removeAbandoned" value="true" />
<!-- 配置刪除超時連線的超時門限值(以秒為單位) -->
<property name="removeAbandonedTimeout" value="120" />
<!-- 配置超時等待時間(以毫秒為單位) -->
<property name="maxWait" value="5000" />
<!-- 配置空閒連線回收器執行緒執行的時間間隔(以毫秒為單位) -->
<property name="timeBetweenEvictionRunsMillis" value="300000" />
<!-- 配置連線空閒多長時間後(以毫秒為單位)被斷開連線 -->
<property name="minEvictableIdleTimeMillis" value="60000" />
</bean>
<!-- 配置Spring提供的支援註解ORM對映的Hibernate會話工廠 -->
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
<!-- 通過setter注入資料來源屬性 -->
<property name="dataSource" ref="dataSource" />
<!-- 配置實體類所在的包 -->
<property name="packagesToScan" value="com.jackfrued.entity" />
<!-- 配置Hibernate的相關屬性 -->
<property name="hibernateProperties">
<!-- 在專案除錯完成後要刪除show_sql和format_sql屬性否則對效能有顯著影響 -->
<value>
hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
</value>
</property>
</bean>
<!-- 配置Spring提供的Hibernate事務管理器 -->
<bean id="transactionManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<!-- 通過setter注入Hibernate會話工廠 -->
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<!-- 配置基於註解配置宣告式事務 -->
<tx:annotation-driven />
package com.jackfrued.dao;
import java.io.Serializable;
import java.util.List;
import com.jackfrued.comm.QueryBean;
import com.jackfrued.comm.QueryResult;
/**
-
資料訪問物件介面(以物件為單位封裝CRUD操作)
-
@author 駱昊
-
@param 實體型別
-
@param 實體標識欄位的型別
*/
public interface BaseDao <E, K extends Serializable> {/**
- 新增
- @param entity 業務實體物件
- @return 增加成功返回實體物件的標識
*/
public K save(E entity);
/**
- 刪除
- @param entity 業務實體物件
*/
public void delete(E entity);
/**
- 根據ID刪除
- @param id 業務實體物件的標識
- @return 刪除成功返回true否則返回false
*/
public boolean deleteById(K id);
/**
- 修改
- @param entity 業務實體物件
- @return 修改成功返回true否則返回false
*/
public void update(E entity);
/**
- 根據ID查詢業務實體物件
- @param id 業務實體物件的標識
- @return 業務實體物件物件或null
*/
public E findById(K id);
/**
- 根據ID查詢業務實體物件
- @param id 業務實體物件的標識
- @param lazy 是否使用延遲載入
- @return 業務實體物件物件
*/
public E findById(K id, boolean lazy);
/**
- 查詢所有業務實體物件
- @return 裝所有業務實體物件的列表容器
*/
public List findAll();
/**
- 分頁查詢業務實體物件
- @param page 頁碼
- @param size 頁面大小
- @return 查詢結果物件
*/
public QueryResult findByPage(int page, int size);
/**
- 分頁查詢業務實體物件
- @param queryBean 查詢條件物件
- @param page 頁碼
- @param size 頁面大小
- @return 查詢結果物件
*/
public QueryResult findByPage(QueryBean queryBean, int page, int size);
}
package com.jackfrued.dao;
import java.io.Serializable;
import java.util.List;
import com.jackfrued.comm.QueryBean;
import com.jackfrued.comm.QueryResult;
/**
-
BaseDao的預設介面卡
-
@author 駱昊
-
@param 實體型別
-
@param 實體標識欄位的型別
*/
public abstract class BaseDaoAdapter<E, K extends Serializable> implements
BaseDao<E, K> {@Override
public K save(E entity) {
return null;
}@Override
public void delete(E entity) {
}@Override
public boolean deleteById(K id) {
E entity = findById(id);
if(entity != null) {
delete(entity);
return true;
}
return false;
}@Override
public void update(E entity) {
}@Override
public E findById(K id) {
return null;
}@Override
public E findById(K id, boolean lazy) {
return null;
}@Override
public List findAll() {
return null;
}@Override
public QueryResult findByPage(int page, int size) {
return null;
}@Override
public QueryResult findByPage(QueryBean queryBean, int page, int size) {
return null;
}
}
package com.jackfrued.dao;
import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.jackfrued.comm.HQLQueryBean;
import com.jackfrued.comm.QueryBean;
import com.jackfrued.comm.QueryResult;
/**
-
基於Hibernate的BaseDao實現類
-
@author 駱昊
-
@param 實體型別
-
@param 主鍵型別
*/
@SuppressWarnings(value = {“unchecked”})
public abstract class BaseDaoHibernateImpl<E, K extends Serializable> extends BaseDaoAdapter<E, K> {
@Autowired
protected SessionFactory sessionFactory;private Class<?> entityClass; // 業務實體的類物件
private String entityName; // 業務實體的名字public BaseDaoHibernateImpl() {
ParameterizedType pt = (ParameterizedType) this.getClass().getGenericSuperclass();
entityClass = (Class<?>) pt.getActualTypeArguments()[0];
entityName = entityClass.getSimpleName();
}@Override
public K save(E entity) {
return (K) sessionFactory.getCurrentSession().save(entity);
}@Override
public void delete(E entity) {
sessionFactory.getCurrentSession().delete(entity);
}@Override
public void update(E entity) {
sessionFactory.getCurrentSession().update(entity);
}@Override
public E findById(K id) {
return findById(id, false);
}@Override
public E findById(K id, boolean lazy) {
Session session = sessionFactory.getCurrentSession();
return (E) (lazy? session.load(entityClass, id) : session.get(entityClass, id));
}@Override
public List findAll() {
return sessionFactory.getCurrentSession().createCriteria(entityClass).list();
}@Override
public QueryResult findByPage(int page, int size) {
return new QueryResult(
findByHQLAndPage("from " + entityName , page, size),
getCountByHQL("select count(*) from " + entityName)
);
}@Override
public QueryResult findByPage(QueryBean queryBean, int page, int size) {
if(queryBean instanceof HQLQueryBean) {
HQLQueryBean hqlQueryBean = (HQLQueryBean) queryBean;
return new QueryResult(
findByHQLAndPage(hqlQueryBean.getQueryString(), page, size, hqlQueryBean.getParameters()),
getCountByHQL(hqlQueryBean.getCountString(), hqlQueryBean.getParameters())
);
}
return null;
}/**
- 根據HQL和可變引數列表進行查詢
- @param hql 基於HQL的查詢語句
- @param params 可變引數列表
- @return 持有查詢結果的列表容器或空列表容器
*/
protected List findByHQL(String hql, Object… params) {
return this.findByHQL(hql, getParamList(params));
}
/**
- 根據HQL和引數列表進行查詢
- @param hql 基於HQL的查詢語句
- @param params 查詢引數列表
- @return 持有查詢結果的列表容器或空列表容器
*/
protected List findByHQL(String hql, List params) {
List list = createQuery(hql, params).list();
return list != null && list.size() > 0 ? list : Collections.EMPTY_LIST;
}
/**
- 根據HQL和引數列表進行分頁查詢
- @param hql 基於HQL的查詢語句
- @page 頁碼
- @size 頁面大小
- @param params 可變引數列表
- @return 持有查詢結果的列表容器或空列表容器
*/
protected List findByHQLAndPage(String hql, int page, int size, Object… params) {
return this.findByHQLAndPage(hql, page, size, getParamList(params));
}
/**
- 根據HQL和引數列表進行分頁查詢
- @param hql 基於HQL的查詢語句
- @page 頁碼
- @size 頁面大小
- @param params 查詢引數列表
- @return 持有查詢結果的列表容器或空列表容器
*/
protected List findByHQLAndPage(String hql, int page, int size, List params) {
List list = createQuery(hql, params)
.setFirstResult((page - 1) * size)
.setMaxResults(size)
.list();
return list != null && list.size() > 0 ? list : Collections.EMPTY_LIST;
}
/**
- 查詢滿足條件的記錄數
- @param hql 基於HQL的查詢語句
- @param params 可變引數列表
- @return 滿足查詢條件的總記錄數
*/
protected long getCountByHQL(String hql, Object… params) {
return this.getCountByHQL(hql, getParamList(params));
}
/**
- 查詢滿足條件的記錄數
- @param hql 基於HQL的查詢語句
- @param params 引數列表容器
- @return 滿足查詢條件的總記錄數
*/
protected long getCountByHQL(String hql, List params) {
return (Long) createQuery(hql, params).uniqueResult();
}
// 建立Hibernate查詢物件(Query)
private Query createQuery(String hql, List params) {
Query query = sessionFactory.getCurrentSession().createQuery(hql);
for(int i = 0; i < params.size(); i++) {
query.setParameter(i, params.get(i));
}
return query;
}// 將可變引數列表組裝成列表容器
private List getParamList(Object… params) {
List paramList = new ArrayList<>();
if(params != null) {
for(int i = 0; i < params.length; i++) {
paramList.add(params[i]);
}
}
return paramList.size() == 0? Collections.EMPTY_LIST : paramList;
}
}
package com.jackfrued.comm;
import java.util.List;
/**
- 查詢條件的介面
- @author 駱昊
*/
public interface QueryBean {
/**
* 新增排序欄位
* @param fieldName 用於排序的欄位
* @param asc 升序還是降序
* @return 查詢條件物件自身(方便級聯程式設計)
*/
public QueryBean addOrder(String fieldName, boolean asc);
/**
* 新增排序欄位
* @param available 是否新增此排序欄位
* @param fieldName 用於排序的欄位
* @param asc 升序還是降序
* @return 查詢條件物件自身(方便級聯程式設計)
*/
public QueryBean addOrder(boolean available, String fieldName, boolean asc);
/**
* 新增查詢條件
* @param condition 條件
* @param params 替換掉條件中引數佔位符的引數
* @return 查詢條件物件自身(方便級聯程式設計)
*/
public QueryBean addCondition(String condition, Object... params);
/**
* 新增查詢條件
* @param available 是否需要新增此條件
* @param condition 條件
* @param params 替換掉條件中引數佔位符的引數
* @return 查詢條件物件自身(方便級聯程式設計)
*/
public QueryBean addCondition(boolean available, String condition, Object... params);
/**
* 獲得查詢語句
* @return 查詢語句
*/
public String getQueryString();
/**
* 獲取查詢記錄數的查詢語句
* @return 查詢記錄數的查詢語句
*/
public String getCountString();
/**
* 獲得查詢引數
* @return 查詢引數的列表容器
*/
public List<Object> getParameters();
}
package com.jackfrued.comm;
import java.util.List;
/**
-
查詢結果
-
@author 駱昊
-
@param 泛型引數
*/
public class QueryResult {
private List result; // 持有查詢結果的列表容器
private long totalRecords; // 查詢到的總記錄數/**
- 構造器
*/
public QueryResult() {
}
/**
- 構造器
- @param result 持有查詢結果的列表容器
- @param totalRecords 查詢到的總記錄數
*/
public QueryResult(List result, long totalRecords) {
this.result = result;
this.totalRecords = totalRecords;
}
public List getResult() {
return result;
}public void setResult(List result) {
this.result = result;
}public long getTotalRecords() {
return totalRecords;
}public void setTotalRecords(long totalRecords) {
this.totalRecords = totalRecords;
}
} - 構造器
package com.jackfrued.dao;
import com.jackfrued.comm.QueryResult;
import com.jackfrued.entity.Dept;
/**
- 部門資料訪問物件介面
- @author 駱昊
*/
public interface DeptDao extends BaseDao<Dept, Integer> {
/**
* 分頁查詢頂級部門
* @param page 頁碼
* @param size 頁碼大小
* @return 查詢結果物件
*/
public QueryResult<Dept> findTopDeptByPage(int page, int size);
}
package com.jackfrued.dao.impl;
import java.util.List;
import org.springframework.stereotype.Repository;
import com.jackfrued.comm.QueryResult;
import com.jackfrued.dao.BaseDaoHibernateImpl;
import com.jackfrued.dao.DeptDao;
import com.jackfrued.entity.Dept;
@Repository
public class DeptDaoImpl extends BaseDaoHibernateImpl<Dept, Integer> implements DeptDao {
private static final String HQL_FIND_TOP_DEPT = " from Dept as d where d.superiorDept is null ";
@Override
public QueryResult<Dept> findTopDeptByPage(int page, int size) {
List<Dept> list = findByHQLAndPage(HQL_FIND_TOP_DEPT, page, size);
long totalRecords = getCountByHQL(" select count(*) " + HQL_FIND_TOP_DEPT);
return new QueryResult<>(list, totalRecords);
}
}
package com.jackfrued.comm;
import java.util.List;
/**
-
分頁器
-
@author 駱昊
-
@param 分頁資料物件的型別
*/
public class PageBean {
private static final int DEFAUL_INIT_PAGE = 1;
private static final int DEFAULT_PAGE_SIZE = 10;
private static final int DEFAULT_PAGE_COUNT = 5;private List data; // 分頁資料
private PageRange pageRange; // 頁碼範圍
private int totalPage; // 總頁數
private int size; // 頁面大小
private int currentPage; // 當前頁碼
private int pageCount; // 頁碼數量/**
- 構造器
- @param currentPage 當前頁碼
- @param size 頁碼大小
- @param pageCount 頁碼數量
*/
public PageBean(int currentPage, int size, int pageCount) {
this.currentPage = currentPage > 0 ? currentPage : 1;
this.size = size > 0 ? size : DEFAULT_PAGE_SIZE;
this.pageCount = pageCount > 0 ? size : DEFAULT_PAGE_COUNT;
}
/**
- 構造器
- @param currentPage 當前頁碼
- @param size 頁碼大小
*/
public PageBean(int currentPage, int size) {
this(currentPage, size, DEFAULT_PAGE_COUNT);
}
/**
- 構造器
- @param currentPage 當前頁碼
*/
public PageBean(int currentPage) {
this(currentPage, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_COUNT);
}
/**
- 構造器
*/
public PageBean() {
this(DEFAUL_INIT_PAGE, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_COUNT);
}
public List getData() {
return data;
}public int getStartPage() {
return pageRange != null ? pageRange.getStartPage() : 1;
}public int getEndPage() {
return pageRange != null ? pageRange.getEndPage() : 1;
}public long getTotalPage() {
return totalPage;
}public int getSize() {
return size;
}public int getCurrentPage() {
return currentPage;
}/**
-
將查詢結果轉換為分頁資料
-
@param queryResult 查詢結果物件
*/
public void transferQueryResult(QueryResult queryResult) {
long totalRecords = queryResult.getTotalRecords();data = queryResult.getResult();
totalPage = (int) ((totalRecords + size - 1) / size);
totalPage = totalPage >= 0 ? totalPage : Integer.MAX_VALUE;
this.pageRange = new PageRange(pageCount, currentPage, totalPage);
}
}
package com.jackfrued.comm;
/**
- 頁碼範圍
- @author 駱昊
*/
public class PageRange {
private int startPage; // 起始頁碼
private int endPage; // 終止頁碼
/**
* 構造器
* @param pageCount 總共顯示幾個頁碼
* @param currentPage 當前頁碼
* @param totalPage 總頁數
*/
public PageRange(int pageCount, int currentPage, int totalPage) {
startPage = currentPage - (pageCount - 1) / 2;
endPage = currentPage + pageCount / 2;
if(startPage < 1) {
startPage = 1;
endPage = totalPage > pageCount ? pageCount : totalPage;
}
if (endPage > totalPage) {
endPage = totalPage;
startPage = (endPage - pageCount > 0) ? endPage - pageCount + 1 : 1;
}
}
/**
* 獲得起始頁頁碼
* @return 起始頁頁碼
*/
public int getStartPage() {
return startPage;
}
/**
* 獲得終止頁頁碼
* @return 終止頁頁碼
*/
public int getEndPage() {
return endPage;
}
}
package com.jackfrued.biz;
import com.jackfrued.comm.PageBean;
import com.jackfrued.entity.Dept;
/**
- 部門業務邏輯介面
- @author 駱昊
*/
public interface DeptService {
/**
* 建立新的部門
* @param department 部門物件
* @return 建立成功返回true否則返回false
*/
public boolean createNewDepartment(Dept department);
/**
* 刪除指定部門
* @param id 要刪除的部門的編號
* @return 刪除成功返回true否則返回false
*/
public boolean deleteDepartment(Integer id);
/**
* 分頁獲取頂級部門
* @param page 頁碼
* @param size 頁碼大小
* @return 部門物件的分頁器物件
*/
public PageBean<Dept> getTopDeptByPage(int page, int size);
}
package com.jackfrued.biz.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.jackfrued.biz.DeptService;
import com.jackfrued.comm.PageBean;
import com.jackfrued.comm.QueryResult;
import com.jackfrued.dao.DeptDao;
import com.jackfrued.entity.Dept;
@Service
@Transactional // 宣告式事務的註解
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptDao deptDao;
@Override
public boolean createNewDepartment(Dept department) {
return deptDao.save(department) != null;
}
@Override
public boolean deleteDepartment(Integer id) {
return deptDao.deleteById(id);
}
@Override
public PageBean<Dept> getTopDeptByPage(int page, int size) {
QueryResult<Dept> queryResult = deptDao.findTopDeptByPage(page, size);
PageBean<Dept> pageBean = new PageBean<>(page, size);
pageBean.transferQueryResult(queryResult);
return pageBean;
}
}
154、如何在Web專案中配置Spring的IoC容器?
答:如果需要在Web專案中使用Spring的IoC容器,可以在Web專案配置檔案web.xml中做出如下配置:
155、如何在Web專案中配置Spring MVC?
答:要使用Spring MVC需要在Web專案配置檔案中配置其前端控制器DispatcherServlet,如下所示:
說明:上面的配置中使用了*.html的字尾對映,這樣做一方面不能夠通過URL推斷採用了何種伺服器端的技術,另一方面可以欺騙搜尋引擎,因為搜尋引擎不會搜尋動態頁面,這種做法稱為偽靜態化。
<web-app>
<servlet>
<servlet-name>example</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>example</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
</web-app>
156、Spring MVC的工作原理是怎樣的?
答:Spring MVC的工作原理如下圖所示:
這裡寫圖片描述
① 客戶端的所有請求都交給前端控制器DispatcherServlet來處理,它會負責呼叫系統的其他模組來真正處理使用者的請求。
② DispatcherServlet收到請求後,將根據請求的資訊(包括URL、HTTP協議方法、請求頭、請求引數、Cookie等)以及HandlerMapping的配置找到處理該請求的Handler(任何一個物件都可以作為請求的Handler)。
③在這個地方Spring會通過HandlerAdapter對該處理器進行封裝。
④ HandlerAdapter是一個介面卡,它用統一的介面對各種Handler中的方法進行呼叫。
⑤ Handler完成對使用者請求的處理後,會返回一個ModelAndView物件給DispatcherServlet,ModelAndView顧名思義,包含了資料模型以及相應的檢視的資訊。
⑥ ModelAndView的檢視是邏輯檢視,DispatcherServlet還要藉助ViewResolver完成從邏輯檢視到真實檢視物件的解析工作。
⑦ 當得到真正的檢視物件後,DispatcherServlet會利用檢視物件對模型資料進行渲染。
⑧ 客戶端得到響應,可能是一個普通的HTML頁面,也可以是XML或JSON字串,還可以是一張圖片或者一個PDF檔案。
157、如何在Spring IoC容器中配置資料來源?
答:
DBCP配置:
<context:property-placeholder location=“jdbc.properties”/>
C3P0配置:
<context:property-placeholder location=“jdbc.properties”/>
提示: DBCP的詳細配置在第153題中已經完整的展示過了。
158、如何配置配置事務增強?
答:
<tx:advice id=“txAdvice” transaction-manager=“txManager”>
tx:attributes
<tx:method name=“get*” read-only=“true”/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
aop:config
<aop:pointcut id=“fooServiceOperation”
expression=“execution(* x.y.service.FooService.*(…))”/>
<aop:advisor advice-ref=“txAdvice” pointcut-ref=“fooServiceOperation”/>
</aop:config>
159、選擇使用Spring框架的原因(Spring框架為企業級開發帶來的好處有哪些)?
答:可以從以下幾個方面作答:
- 非侵入式:支援基於POJO的程式設計模式,不強制性的要求實現Spring框架中的介面或繼承Spring框架中的類。
- IoC容器:IoC容器幫助應用程式管理物件以及物件之間的依賴關係,物件之間的依賴關係如果發生了改變只需要修改配置檔案而不是修改程式碼,因為程式碼的修改可能意味著專案的重新構建和完整的迴歸測試。有了IoC容器,程式設計師再也不需要自己編寫工廠、單例,這一點特別符合Spring的精神"不要重複的發明輪子"。
- AOP(面向切面程式設計):將所有的橫切關注功能封裝到切面(aspect)中,通過配置的方式將橫切關注功能動態新增到目的碼上,進一步實現了業務邏輯和系統服務之間的分離。另一方面,有了AOP程式設計師可以省去很多自己寫代理類的工作。
- MVC:Spring的MVC框架是非常優秀的,從各個方面都可以甩Struts 2幾條街,為Web表示層提供了更好的解決方案。
- 事務管理:Spring以寬廣的胸懷接納多種持久層技術,並且為其提供了宣告式的事務管理,在不需要任何一行程式碼的情況下就能夠完成事務管理。
- 其他:選擇Spring框架的原因還遠不止於此,Spring為Java企業級開發提供了一站式選擇,你可以在需要的時候使用它的部分和全部,更重要的是,你甚至可以在感覺不到Spring存在的情況下,在你的專案中使用Spring提供的各種優秀的功能。
160、Spring IoC容器配置Bean的方式?
答:
- 基於XML檔案進行配置。
- 基於註解進行配置。
- 基於Java程式進行配置(Spring 3+)
package com.jackfrued.bean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Person {
private String name;
private int age;
@Autowired
private Car car;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void setCar(Car car) {
this.car = car;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", car=" + car + "]";
}
}
package com.jackfrued.bean;
import org.springframework.stereotype.Component;
@Component
public class Car {
private String brand;
private int maxSpeed;
public Car(String brand, int maxSpeed) {
this.brand = brand;
this.maxSpeed = maxSpeed;
}
@Override
public String toString() {
return "Car [brand=" + brand + ", maxSpeed=" + maxSpeed + "]";
}
}
package com.jackfrued.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.jackfrued.bean.Car;
import com.jackfrued.bean.Person;
@Configuration
public class AppConfig {
@Bean
public Car car() {
return new Car("Benz", 320);
}
@Bean
public Person person() {
return new Person("駱昊", 34);
}
}
package com.jackfrued.test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.jackfrued.bean.Person;
import com.jackfrued.config.AppConfig;
class Test {
public static void main(String[] args) {
// TWR (Java 7+)
try(ConfigurableApplicationContext factory = new AnnotationConfigApplicationContext(AppConfig.class)) {
Person person = factory.getBean(Person.class);
System.out.println(person);
}
}
}
161、闡述Spring框架中Bean的生命週期?
答:
① Spring IoC容器找到關於Bean的定義並例項化該Bean。
② Spring IoC容器對Bean進行依賴注入。
③ 如果Bean實現了BeanNameAware介面,則將該Bean的id傳給setBeanName方法。
④ 如果Bean實現了BeanFactoryAware介面,則將BeanFactory物件傳給setBeanFactory方法。
⑤ 如果Bean實現了BeanPostProcessor介面,則呼叫其postProcessBeforeInitialization方法。
⑥ 如果Bean實現了InitializingBean介面,則呼叫其afterPropertySet方法。
⑦ 如果有和Bean關聯的BeanPostProcessors物件,則這些物件的postProcessAfterInitialization方法被呼叫。
⑧ 當銷燬Bean例項時,如果Bean實現了DisposableBean介面,則呼叫其destroy方法。
162、依賴注入時如何注入集合屬性?
答:可以在定義Bean屬性時,通過 / /
163、Spring中的自動裝配有哪些限制?
答:
- 如果使用了構造器注入或者setter注入,那麼將覆蓋自動裝配的依賴關係。
- 基本資料型別的值、字串字面量、類字面量無法使用自動裝配來注入。
- 優先考慮使用顯式的裝配來進行更精確的依賴注入而不是使用自動裝配。
164、在Web專案中如何獲得Spring的IoC容器?
答:
WebApplicationContext ctx =
WebApplicationContextUtils.getWebApplicationContext(servletContext);
- 大型網站在架構上應當考慮哪些問題?
答:
- 分層:分層是處理任何複雜系統最常見的手段之一,將系統橫向切分成若干個層面,每個層面只承擔單一的職責,然後通過下層為上層提供的基礎設施和服務以及上層對下層的呼叫來形成一個完整的複雜的系統。計算機網路的開放系統互聯參考模型(OSI/RM)和Internet的TCP/IP模型都是分層結構,大型網站的軟體系統也可以使用分層的理念將其分為持久層(提供資料儲存和訪問服務)、業務層(處理業務邏輯,系統中最核心的部分)和表示層(系統互動、檢視展示)。需要指出的是:(1)分層是邏輯上的劃分,在物理上可以位於同一裝置上也可以在不同的裝置上部署不同的功能模組,這樣可以使用更多的計算資源來應對使用者的併發訪問;(2)層與層之間應當有清晰的邊界,這樣分層才有意義,才更利於軟體的開發和維護。
- 分割:分割是對軟體的縱向切分。我們可以將大型網站的不同功能和服務分割開,形成高內聚低耦合的功能模組(單元)。在設計初期可以做一個粗粒度的分割,將網站分割為若干個功能模組,後期還可以進一步對每個模組進行細粒度的分割,這樣一方面有助於軟體的開發和維護,另一方面有助於分散式的部署,提供網站的併發處理能力和功能的擴充套件。
- 分散式:除了上面提到的內容,網站的靜態資源(JavaScript、CSS、圖片等)也可以採用獨立分散式部署並採用獨立的域名,這樣可以減輕應用伺服器的負載壓力,也使得瀏覽器對資源的載入更快。資料的存取也應該是分散式的,傳統的商業級關係型資料庫產品基本上都支援分散式部署,而新生的NoSQL產品幾乎都是分散式的。當然,網站後臺的業務處理也要使用分散式技術,例如查詢索引的構建、資料分析等,這些業務計算規模龐大,可以使用Hadoop以及MapReduce分散式計算框架來處理。
- 叢集:叢集使得有更多的伺服器提供相同的服務,可以更好的提供對併發的支援。
- 快取:所謂快取就是用空間換取時間的技術,將資料儘可能放在距離計算最近的位置。使用快取是網站優化的第一定律。我們通常說的CDN、反向代理、熱點資料都是對快取技術的使用。
- 非同步:非同步是實現軟體實體之間解耦合的又一重要手段。非同步架構是典型的生產者消費者模式,二者之間沒有直接的呼叫關係,只要保持資料結構不變,彼此功能實現可以隨意變化而不互相影響,這對網站的擴充套件非常有利。使用非同步處理還可以提高系統可用性,加快網站的響應速度(用Ajax載入資料就是一種非同步技術),同時還可以起到削峰作用(應對瞬時高併發)。";能推遲處理的都要推遲處理"是網站優化的第二定律,而非同步是踐行網站優化第二定律的重要手段。
- 冗餘:各種伺服器都要提供相應的冗餘伺服器以便在某臺或某些伺服器當機時還能保證網站可以正常工作,同時也提供了災難恢復的可能性。冗餘是網站高可用性的重要保證。
166、你用過的網站前端優化的技術有哪些?
答:
① 瀏覽器訪問優化:
- 減少HTTP請求數量:合併CSS、合併JavaScript、合併圖片(CSS Sprite)
- 使用瀏覽器快取:通過設定HTTP響應頭中的Cache-Control和Expires屬性,將CSS、JavaScript、圖片等在瀏覽器中快取,當這些靜態資源需要更新時,可以更新HTML檔案中的引用來讓瀏覽器重新請求新的資源
- 啟用壓縮
- CSS前置,JavaScript後置
- 減少Cookie傳輸
② CDN加速:CDN(Content Distribute Network)的本質仍然是快取,將資料快取在離使用者最近的地方,CDN通常部署在網路運營商的機房,不僅可以提升響應速度,還可以減少應用伺服器的壓力。當然,CDN快取的通常都是靜態資源。
③ 反向代理:反向代理相當於應用伺服器的一個門面,可以保護網站的安全性,也可以實現負載均衡的功能,當然最重要的是它快取了使用者訪問的熱點資源,可以直接從反向代理將某些內容返回給使用者瀏覽器。
167、你使用過的應用伺服器優化技術有哪些?
答:
① 分散式快取:快取的本質就是記憶體中的雜湊表,如果設計一個優質的雜湊函式,那麼理論上雜湊表讀寫的漸近時間複雜度為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提供了必要的基礎。
- 資源複用:資源複用主要有兩種方式,一是單例,二是物件池,我們使用的資料庫連線池、執行緒池都是物件池化技術,這是典型的用空間換取時間的策略,另一方面也實現對資源的複用,從而避免了不必要的建立和釋放資源所帶來的開銷。
168、什麼是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(前面提到防圖片盜連結也是用的這種方式)。令牌和驗證都具有一次消費性的特徵,因此在原理上一致的,但是驗證碼是一種糟糕的使用者體驗,不是必要的情況下不要輕易使用驗證碼,目前很多網站的做法是如果在短時間內多次提交一個表單未獲得成功後才要求提供驗證碼,這樣會獲得較好的使用者體驗。
這裡寫圖片描述
補充:防火牆的架設是Web安全的重要保障,ModSecurity是開源的Web防火牆中的佼佼者。企業級防火牆的架設應當有兩級防火牆,Web伺服器和部分應用伺服器可以架設在兩級防火牆之間的DMZ,而資料和資源伺服器應當架設在第二級防火牆之後。
- 什麼是領域模型(domain model)?貧血模型(anaemic domain model)和充血模型(rich domain model)有什麼區別?
答:領域模型是領域內的概念類或現實世界中物件的視覺化表示,又稱為概念模型或分析物件模型,它專注於分析問題領域本身,發掘重要的業務領域概念,並建立業務領域概念之間的關係。貧血模型是指使用的領域物件中只有setter和getter方法(POJO),所有的業務邏輯都不包含在領域物件中而是放在業務邏輯層。有人將我們這裡說的貧血模型進一步劃分成失血模型(領域物件完全沒有業務邏輯)和貧血模型(領域物件有少量的業務邏輯),我們這裡就不對此加以區分了。充血模型將大多數業務邏輯和持久化放在領域物件中,業務邏輯(業務門面)只是完成對業務邏輯的封裝、事務和許可權等的處理。下面兩張圖分別展示了貧血模型和充血模型的分層架構。
貧血模型
這裡寫圖片描述
充血模型
這裡寫圖片描述
貧血模型下組織領域邏輯通常使用事務指令碼模式,讓每個過程對應使用者可能要做的一個動作,每個動作由一個過程來驅動。也就是說在設計業務邏輯介面的時候,每個方法對應著使用者的一個操作,這種模式有以下幾個有點:
- 它是一個大多數開發者都能夠理解的簡單過程模型(適合國內的絕大多數開發者)。
- 它能夠與一個使用行資料入口或表資料入口的簡單資料訪問層很好的協作。
- 事務邊界的顯而易見,一個事務開始於指令碼的開始,終止於指令碼的結束,很容易通過代理(或切面)實現宣告式事務。
然而,事務指令碼模式的缺點也是很多的,隨著領域邏輯複雜性的增加,系統的複雜性將迅速增加,程式結構將變得極度混亂。開源中國社群上有一篇很好的譯文《貧血領域模型是如何導致糟糕的軟體產生》對這個問題做了比較細緻的闡述。
- 談一談測試驅動開發(TDD)的好處以及你的理解。
答:TDD是指在編寫真正的功能實現程式碼之前先寫測試程式碼,然後根據需要重構實現程式碼。在JUnit的作者Kent Beck的大作《測試驅動開發:實戰與模式解析》(Test-Driven Development: by Example)一書中有這麼一段內容:“消除恐懼和不確定性是編寫測試驅動程式碼的重要原因”。因為編寫程式碼時的恐懼會讓你小心試探,讓你迴避溝通,讓你羞於得到反饋,讓你變得焦躁不安,而TDD是消除恐懼、讓Java開發者更加自信更加樂於溝通的重要手段。TDD會帶來的好處可能不會馬上呈現,但是你在某個時候一定會發現,這些好處包括:
- 更清晰的程式碼 — 只寫需要的程式碼
- 更好的設計
- 更出色的靈活性 — 鼓勵程式設計師面向介面程式設計
- 更快速的反饋 — 不會到系統上線時才知道bug的存在
補充:敏捷軟體開發的概念已經有很多年了,而且也部分的改變了軟體開發這個行業,TDD也是敏捷開發所倡導的。
TDD可以在多個層級上應用,包括單元測試(測試一個類中的程式碼)、整合測試(測試類之間的互動)、系統測試(測試執行的系統)和系統整合測試(測試執行的系統包括使用的第三方元件)。TDD的實施步驟是:紅(失敗測試)- 綠(通過測試) - 重構。
在使用TDD開發時,經常會遇到需要被測物件需要依賴其他子系統的情況,但是你希望將測試程式碼跟依賴項隔離,以保證測試程式碼僅僅針對當前被測物件或方法展開,這時候你需要的是測試替身。測試替身可以分為四類:
- 虛設替身:只傳遞但是不會使用到的物件,一般用於填充方法的引數列表
- 存根替身:總是返回相同的預設響應,其中可能包括一些虛設狀態
- 偽裝替身:可以取代真實版本的可用版本(比真實版本還是會差很多)
- 模擬替身:可以表示一系列期望值的物件,並且可以提供預設響應
Java世界中實現模擬替身的第三方工具非常多,包括EasyMock、Mockito、jMock等。