本文收集了一些經典的Java面試題
1、面向對象的特征有哪些方面?
答:面向對象的特征主要有以下幾個方面:
- 抽象:抽象是将一類對象的共同特征總結出來構造類的過程,包括資料抽象和行為抽象兩方面。抽象隻關注對象有哪些屬性和行為,并不關注這些行為的細節是什麼。
- 繼承:繼承是從已有類得到繼承資訊建立新類的過程。提供繼承資訊的類被稱為父類(超類、基類);得到繼承資訊的類被稱為子類(派生類)。繼承讓變化中的軟體系統有了一定的延續性,同時繼承也是封裝程式中可變因素的重要手段(如果不能了解請閱讀閻宏博士的《Java與模式》或《設計模式精解》中關于橋梁模式的部分)。
- 封裝:通常認為封裝是把資料和操作資料的方法綁定起來,對資料的通路隻能通過已定義的接口。面向對象的本質就是将現實世界描繪成一系列完全自治、封閉的對象。我們在類中編寫的方法就是對實作細節的一種封裝;我們編寫一個類就是對資料和資料操作的封裝。可以說,封裝就是隐藏一切可隐藏的東西,隻向外界提供最簡單的程式設計接口(可以想想普通洗衣機和全自動洗衣機的差别,明顯全自動洗衣機封裝更好是以操作起來更簡單;我們現在使用的智能手機也是封裝得足夠好的,因為幾個按鍵就搞定了所有的事情)。
- 多态性:多态性是指允許不同子類型的對象對同一消息作出不同的響應。簡單的說就是用同樣的對象引用調用同樣的方法但是做了不同的事情。多态性分為編譯時的多态性和運作時的多态性。如果将對象的方法視為對象向外界提供的服務,那麼運作時的多态性可以解釋為:當A系統通路B系統提供的服務時,B系統有多種提供服務的方式,但一切對A系統來說都是透明的(就像電動刮胡刀是A系統,它的供電系統是B系統,B系統可以使用電池供電或者用交流電,甚至還有可能是太陽能,A系統隻會通過B類對象調用供電的方法,但并不知道供電系統的底層實作是什麼,究竟通過何種方式獲得了動力)。方法重載(overload)實作的是編譯時的多态性(也稱為前綁定),而方法重寫(override)實作的是運作時的多态性(也稱為後綁定)。運作時的多态是面向對象最精髓的東西,要實作多态需要做兩件事:1). 方法重寫(子類繼承父類并重寫父類中已有的或抽象的方法);2). 對象造型(用父類型引用引用子類型對象,這樣同樣的引用調用同樣的方法就會根據子類對象的不同而表現出不同的行為)。
2、通路修飾符public,private,protected,以及不寫(預設)時的差別?
類的成員不寫通路修飾時預設為default。預設對于同一個包中的其他類相當于公開(public),對于不是同一個包中的其他類相當于私有(private)。受保護(protected)對子類相當于公開,對不是同一包中的沒有父子關系的類相當于私有。Java中,外部類的修飾符隻能是public或預設,類的成員(包括内部類)的修飾符可以是以上四種。
3、String 是最基本的資料類型嗎?
答:不是。Java中的基本資料類型隻有8個:byte、short、int、long、float、double、char、boolean;除了基本類型(primitive type),剩下的都是引用類型(reference type),Java 5以後引入的枚舉類型也算是一種比較特殊的引用類型。
4、float f=3.4;是否正确?
答:不正确。3.4是雙精度數,将雙精度型(double)指派給浮點型(float)屬于下轉型(down-casting,也稱為窄化)會造成精度損失,是以需要強制類型轉換float f =(float)3.4; 或者寫成float f =3.4F;
5、short s1 = 1; s1 = s1 + 1;有錯嗎?short s1 = 1; s1 += 1;有錯嗎?
答:對于short s1 = 1; s1 = s1 + 1;由于1是int類型,是以s1+1運算結果也是int 型,需要強制轉換類型才能指派給short型。而short s1 = 1; s1 += 1;可以正确編譯,因為s1+= 1;相當于s1 = (short)(s1 + 1);其中有隐含的強制類型轉換。
6、Java有沒有goto?
答:goto 是Java中的保留字,在目前版本的Java中沒有使用。(根據James Gosling(Java之父)編寫的《The Java Programming Language》一書的附錄中給出了一個Java關鍵字清單,其中有goto和const,但是這兩個是目前無法使用的關鍵字,是以有些地方将其稱之為保留字,其實保留字這個詞應該有更廣泛的意義,因為熟悉C語言的程式員都知道,在系統類庫中使用過的有特殊意義的單詞或單詞的組合都被視為保留字)
7、int和Integer有什麼差別?
答:Java是一個近乎純潔的面向對象程式設計語言,但是為了程式設計的友善還是引入了基本資料類型,但是為了能夠将這些基本資料類型當成對象操作,Java為每一個基本資料類型都引入了對應的包裝類型(wrapper class),int的包裝類就是Integer,從Java 5開始引入了自動裝箱/拆箱機制,使得二者可以互相轉換。
Java 為每個原始類型提供了包裝類型:
- 原始類型: boolean,char,byte,short,int,long,float,double
- 包裝類型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
class AutoUnboxingTest {
public static void main(String[] args) {
Integer a = new Integer(3);
Integer b = 3; // 将3自動裝箱成Integer類型
int c = 3;
System.out.println(a == b); // false 兩個引用沒有引用同一對象
System.out.println(a == c); // true a自動拆箱成int類型再和c比較
}
}
最近還遇到一個面試題,也是和自動裝箱和拆箱有點關系的,代碼如下所示:
public class Test03 {
public static void main(String[] args) {
Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;
System.out.println(f1 == f2);
System.out.println(f3 == f4);
}
}
如果不明就裡很容易認為兩個輸出要麼都是true要麼都是false。首先需要注意的是f1、f2、f3、f4四個變量都是Integer對象引用,是以下面的==運算比較的不是值而是引用。裝箱的本質是什麼呢?當我們給一個Integer對象賦一個int值的時候,會調用Integer類的靜态方法valueOf,如果看看valueOf的源代碼就知道發生了什麼。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
IntegerCache是Integer的内部類,其代碼如下所示:
/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
簡單的說,如果整型字面量的值在-128到127之間,那麼不會new新的Integer對象,而是直接引用常量池中的Integer對象,是以上面的面試題中
f1==f2
的結果是true,而
f3==f4
的結果是false。
提醒:越是貌似簡單的面試題其中的玄機就越多,需要面試者有相當深厚的功力。
8、&和&&的差別?
答:&運算符有兩種用法:(1)按位與;(2)邏輯與。&&運算符是短路與運算。邏輯與跟短路與的差别是非常巨大的,雖然二者都要求運算符左右兩端的布爾值都是true整個表達式的值才是true。&&之是以稱為短路運算是因為,如果&&左邊的表達式的值是false,右邊的表達式會被直接短路掉,不會進行運算。很多時候我們可能都需要用&&而不是&,例如在驗證使用者登入時判定使用者名不是null而且不是空字元串,應當寫為:username != null &&!username.equals(""),二者的順序不能交換,更不能用&運算符,因為第一個條件如果不成立,根本不能進行字元串的equals比較,否則會産生NullPointerException異常。注意:邏輯或運算符(|)和短路或運算符(||)的差别也是如此。
補充:如果你熟悉JavaScript,那你可能更能感受到短路運算的強大,想成為JavaScript的高手就先從玩轉短路運算開始吧。
9、解釋記憶體中的棧(stack)、堆(heap)和方法區(method area)的用法。
答:通常我們定義一個基本資料類型的變量,一個對象的引用,還有就是函數調用的現場儲存都使用JVM中的棧空間;而通過new關鍵字和構造器建立的對象則放在堆空間,堆是垃圾收集器管理的主要區域,由于現在的垃圾收集器都采用分代收集算法,是以堆空間還可以細分為新生代和老生代,再具體一點可以分為Eden、Survivor(又可分為From Survivor和To Survivor)、Tenured;方法區和堆都是各個線程共享的記憶體區域,用于存儲已經被JVM加載的類資訊、常量、靜态變量、JIT編譯器編譯後的代碼等資料;程式中的字面量(literal)如直接書寫的100、"hello"和常量都是放在常量池中,常量池是方法區的一部分,。棧空間操作起來最快但是棧很小,通常大量的對象都是放在堆空間,棧和堆的大小都可以通過JVM的啟動參數來進行調整,棧空間用光了會引發StackOverflowError,而堆和常量池空間不足則會引發OutOfMemoryError。
上面的語句中變量str放在棧上,用new建立出來的字元串對象放在堆上,而"hello"這個字面量是放在方法區的。
補充1:較新版本的Java(從Java 6的某個更新開始)中,由于JIT編譯器的發展和"逃逸分析"技術的逐漸成熟,棧上配置設定、标量替換等優化技術使得對象一定配置設定在堆上這件事情已經變得不那麼絕對了。
補充2:運作時常量池相當于Class檔案常量池具有動态性,Java語言并不要求常量一定隻有編譯期間才能産生,運作期間也可以将新的常量放入池中,String類的intern()方法就是這樣的。
看看下面代碼的執行結果是什麼并且比較一下Java 7以前和以後的運作結果是否一緻。
String s1 = new StringBuilder("go")
.append("od").toString();
System.out.println(s1.intern() == s1);
String s2 = new StringBuilder("ja")
.append("va").toString();
System.out.println(s2.intern() == s2);
10、Math.round(11.5) 等于多少?Math.round(-11.5)等于多少?
答:Math.round(11.5)的傳回值是12,Math.round(-11.5)的傳回值是-11。四舍五入的原理是在參數上加0.5然後進行下取整。
11、switch 是否能作用在byte 上,是否能作用在long 上,是否能作用在String上?
答:在Java 5以前,switch(expr)中,expr隻能是byte、short、char、int。從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";
String s4 = "ming";
String s5 = "Program" + "ming";
String s6 = s3 + s4;
System.out.println(s1 == s2);
System.out.println(s1 == s5);
System.out.println(s1 == s6);
System.out.println(s1 == s6.intern());
System.out.println(s2 == s2.intern());
}
}
補充:解答上面的面試題需要清除兩點:1. String對象的intern方法會得到字元串對象在常量池中對應的版本的引用(如果常量池中有一個字元串與String對象的equals結果是true),如果常量池中沒有對應的字元串,則該字元串将被添加到常量池中,然後傳回常量池中字元串的引用;2. 字元串的+操作其本質是建立了StringBuilder對象進行append操作,然後将拼接後的StringBuilder對象用toString方法處理成String對象,這一點可以用javap -c StringEqualTest.class指令獲得class檔案對應的JVM位元組碼指令就可以看出來。
20、重載(Overload)和重寫(Override)的差別。重載的方法能否根據傳回類型進行區分?
答:方法的重載和重寫都是實作多态的方式,差別在于前者實作的是編譯時的多态性,而後者實作的是運作時的多态性。重載發生在一個類中,同名的方法如果有不同的參數清單(參數類型不同、參數個數不同或者二者都不同)則視為重載;重寫發生在子類與父類之間,重寫要求子類被重寫方法與父類被重寫方法有相同的傳回類型,比父類被重寫方法更好通路,不能比父類被重寫方法聲明更多的異常(裡氏代換原則)。重載對傳回類型沒有特殊的要求。
面試題:華為的面試題中曾經問過這樣一個問題 - “為什麼不能根據傳回類型來區分重載”,快說出你的答案吧!
因為調用時不能指定類型資訊,編譯器不知道你要調用哪個函數。
例如:
float max(int a, int b);
int max(int a, int b);
當調用
max(1, 2);
時無法确定調用的是哪個,單從這一點上來說,僅傳回值類型不同的重載是不應該允許的。
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)的内部類,它可以不依賴于外部類執行個體被執行個體化。而通常的内部類需要在外部類執行個體化後才能執行個體化,其文法看起來挺詭異的,如下所示。
/**
* 撲克類(一副撲克)
*
*/
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];
}
/**
* 卡片類(一張撲克)
* [内部類]
*
*/
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,也就是說沒有所謂的外部類對象,是以無法建立内部類對象,如果要在靜态方法中建立内部類對象,可以這樣做:
25、Java 中會存在記憶體洩漏嗎,請簡單描述。
答:理論上Java因為有垃圾回收機制(GC)不會存在記憶體洩露問題(這也是Java被廣泛使用于伺服器端程式設計的一個重要原因);然而在實際開發中,可能會存在無用但可達的對象,這些對象不能被GC回收,是以也會導緻記憶體洩露的發生。例如Hibernate的Session(一級緩存)中的對象屬于持久态,垃圾回收器是不會回收這些對象的,然而這些對象中可能存在無用的垃圾對象,如果不及時關閉(close)或清空(flush)一級緩存就可能導緻記憶體洩露。下面例子中的代碼也會導緻記憶體洩露。
import java.util.Arrays;
import java.util.EmptyStackException;
public class MyStack<T> {
private T[] elements;
private int size = 0;
private static final int INIT_CAPACITY = 16;
public MyStack() {
elements = (T[]) new Object[INIT_CAPACITY];
}
public void push(T elem) {
ensureCapacity();
elements[size++] = elem;
}
public T pop() {
if(size == 0)
throw new EmptyStackException();
return elements[--size];
}
private void ensureCapacity() {
if(elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
上面的代碼實作了一個棧(先進後出(FILO))結構,乍看之下似乎沒有什麼明顯的問題,它甚至可以通過你編寫的各種單元測試。然而其中的pop方法卻存在記憶體洩露的問題,當我們用pop方法彈出棧中的對象時,該對象不會被當作垃圾回收,即使使用棧的程式不再引用這些對象,因為棧内部維護着對這些對象的過期引用(obsolete reference)。在支援垃圾回收的語言中,記憶體洩露是很隐蔽的,這種記憶體洩露其實就是無意識的對象保持。如果一個對象引用被無意識的保留起來了,那麼垃圾回收器不會處理這個對象,也不會處理該對象引用的其他對象,即使這樣的對象隻有少數幾個,也可能會導緻很多的對象被排除在垃圾回收之外,進而對性能造成重大影響,極端情況下會引發Disk Paging(實體記憶體與硬碟的虛拟記憶體交換資料),甚至造成OutOfMemoryError。
26、抽象的(abstract)方法是否可同時是靜态的(static),是否可同時是本地方法(native),是否可同時被synchronized修飾?
答:都不能。抽象方法需要子類重寫,而靜态的方法是無法被重寫的,是以二者是沖突的。本地方法是由本地代碼(如C代碼)實作的方法,而抽象方法是沒有實作的,也是沖突的。synchronized和方法的實作細節有關,抽象方法不涉及實作細節,是以也是互相沖突的。
27、闡述靜态變量和執行個體變量的差別。
答:靜态變量是被static修飾符修飾的變量,也稱為類變量,它屬于類,不屬于類的任何一個對象,一個類不管建立多少個對象,靜态變量在記憶體中有且僅有一個拷貝;執行個體變量必須依存于某一執行個體,需要先建立對象然後通過對象才能通路到它。靜态變量可以實作讓多個對象共享記憶體。
補充:在Java開發中,上下文類和工具類中通常會有大量的靜态成員。
28、是否可以從一個靜态(static)方法内部發出對非靜态(non-static)方法的調用?
答:不可以,靜态方法隻能通路靜态成員,因為非靜态方法的調用要先建立對象,在調用靜态方法時可能對象并沒有被初始化。
29、如何實作對象克隆?
答:有兩種方式:
1). 實作Cloneable接口并重寫Object類中的clone()方法;
2). 實作Serializable接口,通過對象的序列化和反序列化實作克隆,可以實作真正的深度克隆,代碼如下:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class MyUtil {
private MyUtil() {
throw new AssertionError();
}
@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T obj) throws Exception {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bout);
oos.writeObject(obj);
ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bin);
return (T) ois.readObject();
// 說明:調用ByteArrayInputStream或ByteArrayOutputStream對象的close方法沒有任何意義
// 這兩個基于記憶體的流隻要垃圾回收器清理對象就能夠釋放資源,這一點不同于對外部資源(如檔案流)的釋放
}
}
下面是測試代碼:
import java.io.Serializable;
/**
* 人類
* @author nnngu
*
*/
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 nnngu
*
*/
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("郭靖", 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)接口,抽象類可繼承具體類也可以繼承抽象類。
舉一個多繼承的例子,我們定義一個動物(類)既是狗(父類1)也是貓(父類2),兩個父類都有“叫”這個方法。那麼當我們調用“叫”這個方法時,它就不知道是狗叫還是貓叫了,這就是多重繼承的沖突。
而接口沒有具體的方法實作,是以多繼承接口也不會出現這種沖突。
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 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<Student> {
private String name; // 姓名
private int age; // 年齡
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
@Override
public int compareTo(Student o) {
return this.age - o.age; // 比較年齡(年齡的升序)
}
}
import java.util.Set;
import java.util.TreeSet;
class Test01 {
public static void main(String[] args) {
Set<Student> set = new TreeSet<>(); // Java 7的鑽石文法(構造器後面的尖括号中不需要寫類型)
set.add(new Student("Hao LUO", 33));
set.add(new Student("XJ WANG", 32));
set.add(new Student("Bruce LEE", 60));
set.add(new Student("Bob YANG", 22));
for(Student stu : set) {
System.out.println(stu);
}
// 輸出結果:
// Student [name=Bob YANG, age=22]
// Student [name=XJ WANG, age=32]
// Student [name=Hao LUO, age=33]
// Student [name=Bruce LEE, age=60]
}
}
例子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 nnngu
*
*/
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 nnngu
*
*/
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 nnngu
*
*/
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 nnngu
*
*/
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 nnngu
*
*/
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<Integer> {
private int upperBounds;
public MyTask(int upperBounds) {
this.upperBounds = upperBounds;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 1; i <= upperBounds; i++) {
sum += i;
}
return sum;
}
}
class Test {
public static void main(String[] args) throws Exception {
List<Future<Integer>> list = new ArrayList<>();
ExecutorService service = Executors.newFixedThreadPool(10);
for(int i = 0; i < 10; i++) {
list.add(service.submit(new MyTask((int) (Math.random() * 100))));
}
int sum = 0;
for(Future<Integer> future : list) {
// while(!future.isDone()) ;
sum += future.get();
}
System.out.println(sum);
}
}
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:建立一個大小無限的線程池。此線程池支援定時以及周期性執行任務的需求。
第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/nnngu/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/nnngu/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/nnngu/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(文檔對象模型,Document Object Model)、SAX(Simple API for XML)和StAX(Java 6中引入的新的解析XML的方式,Streaming API for XML),其中DOM處理大型檔案時其性能下降的非常厲害,這個問題是由DOM樹結構占用的記憶體較多造成的,而且DOM解析方式必須在解析檔案之前把整個文檔裝入記憶體,适合對XML的随機通路(典型的用空間換取時間的政策);SAX是事件驅動型的XML解析方式,它順序讀取XML檔案,不需要一次全部裝載整個檔案。當遇到像檔案開頭,文檔結束,或者标簽開頭與标簽結束時,它會觸發一個事件,使用者通過事件回調代碼來處理XML檔案,适合對XML的順序通路;顧名思義,StAX把重點放在流上,實際上StAX與其他解析方式的本質差別就在于應用程式能夠把XML作為一個事件流來處理。将XML作為一組事件來處理的想法并不新穎(SAX就是這樣做的),但不同之處在于StAX允許應用程式代碼把這些事件逐個拉出來,而不用提供在解析器友善時從解析器中接收事件的處理程式。
74、你在項目中哪些地方用到了XML?
答:XML的主要作用有兩個方面:資料交換和資訊配置。在做資料交換時,XML将資料用标簽組裝成起來,然後壓縮打包加密後通過網絡傳送給接收者,接收解密與解壓縮後再從XML檔案中還原相關資訊進行處理,XML曾經是異構系統間交換資料的事實标準,但此項功能幾乎已經被JSON(JavaScript Object Notation)取而代之。當然,目前很多軟體仍然使用XML來存儲配置資訊,我們在很多項目中通常也會将作為配置資訊的硬代碼寫在XML檔案中,Java的很多架構也是這麼做的,而且這些架構都選擇了dom4j作為處理XML的工具,因為Sun公司的官方API實在不怎麼好用。
補充:現在有很多時髦的軟體(如Sublime)已經開始将配置檔案書寫成JSON格式,我們已經強烈的感受到XML的另一項功能也将逐漸被業界抛棄。
75、闡述JDBC操作資料庫的步驟。
答:下面的代碼以連接配接本機的Oracle資料庫為例,示範JDBC操作資料庫的步驟。
加載驅動。
建立連接配接。
建立語句。
PreparedStatement ps = con.prepareStatement("select * from emp where sal between ? and ?");
ps.setInt(1, 1000);
ps.setInt(2, 3000);
執行語句。
處理結果。
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讀取到的資料就是髒資料。
不可重複讀(Unrepeatable Read):事務A重新讀取前面讀取過的資料,發現該資料已經被另一個已送出的事務B修改過了。
幻讀(Phantom Read):事務A重新執行一個查詢,傳回一系列符合查詢條件的行,發現其中插入了被事務B送出的行。
第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(儲存點)的概念,允許通過代碼設定儲存點并讓事務復原到指定的儲存點。
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 nnngu
*
*/
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 nnngu
*
*/
public interface Sorter {
/**
* 排序
* @param list 待排序的數組
*/
public <T extends Comparable<T>> void sort(T[] list);
/**
* 排序
* @param list 待排序的數組
* @param comp 比較兩個對象的比較器
*/
public <T> void sort(T[] list, Comparator<T> comp);
}
import java.util.Comparator;
/**
* 冒泡排序
*
* @author nnngu
*
*/
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<T>> int binarySearch(T[] x, T key) {
return binarySearch(x, 0, x.length- 1, key);
}
// 使用循環實作的二分查找
public static <T> int binarySearch(T[] x, T key, Comparator<T> comp) {
int low = 0;
int high = x.length - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
int cmp = comp.compare(x[mid], key);
if (cmp < 0) {
low= mid + 1;
}
else if (cmp > 0) {
high= mid - 1;
}
else {
return mid;
}
}
return -1;
}
// 使用遞歸實作的二分查找
private static<T extends Comparable<T>> int binarySearch(T[] x, int low, int high, T key) {
if(low <= high) {
int mid = low + ((high - low) >> 1);
if(key.compareTo(x[mid])== 0) {
return mid;
}
else if(key.compareTo(x[mid])< 0) {
return binarySearch(x,low, mid - 1, key);
}
else {
return binarySearch(x,mid + 1, high, key);
}
}
return -1;
}
}
說明:上面的代碼中給出了折半查找的兩個版本,一個用遞歸實作,一個用循環實作。需要注意的是計算中間位置時不應該使用(high+ low) / 2的方式,因為加法運算可能導緻整數越界,這裡應該使用以下三種方式之一:low + (high - low) / 2或low + (high – low) >> 1或(low + high) >>> 1(>>>是邏輯右移,是不帶符号位的右移)