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),剩下的都是引用類型(referencetype),Java 5 以後引入的枚舉類型也算是一種比較特殊的引用類型。

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

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

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

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

6、Java 有沒有 goto?

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

7、int 和 Integer 有什麼區別?

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

Java 爲每個原始類型提供了包裝類型:

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

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

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

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

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

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

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

IntegerCache 是 Integer 的內部類,其代碼如下所示:

/**
* Cache to support the object identity semantics of autoboxing for
values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>}
option.
* During VM initialization, java.lang.Integer.IntegerCache.high
property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
private static class IntegerCache {
	static final int low = -128;
	static final int high;
	static final Integer cache[];
	static {
		// high value may be configured by property
		int h = 127;
		String integerCacheHighPropValue =
		sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
		if (integerCacheHighPropValue != null) {
			try {
				int i = parseint(integerCacheHighPropValue);
				i = Math.max(i, 127);
				// Maximum array size is Integer.MAX_VALUE
				h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
			}
			catch( NumberFormatException nfe) {
				// If the property cannot be parsed into an int,
				ignore it.
			}
		}
		high = h;
		cache = new Integer[(high - low) + 1];
		int j = low;
		for (int k = 0; k < cache.length; k++)
		cache[k] = new Integer(j++);
		// range [-128, 127] must be interned (JLS7 5.1.7)
		assert IntegerCache.high >= 127;
	}
	private IntegerCache() {
	}
}

簡單的說,如果整型字面量的值在-128 到 127 之間,那麼不會 new 新的 Integer對象,而是直接引用常量池中的 Integer 對象,所以上面的面試題中 f1f4 的結果是 false。

提醒:越是貌似簡單的面試題其中的玄機就越多,需要面試者有相當深厚的功力。

8、&和&&的區別?

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

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

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

String str = new String("hello");

上面的語句中變量 str 放在棧上,用 new 創建出來的字符串對象放在堆上,而”hello”這個字面量是放在方法區的。

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

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

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

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

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

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

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

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

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

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

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

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

13、數組有沒有 length()方法?String 有沒有 length()方法?

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

 

14、在 Java 中,如何跳出當前的多重嵌套循環?

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

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

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

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

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

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

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

補充:關於 equals 和 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;
			第 225 頁 共 485 頁
			Console.WriteLine ("a = {0}, b = {1}", a, b);
		}
	}
}

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

19、String 和 StringBuilder、StringBuffer 的區別?

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

面試題 1 - 什麼情況下用+運算符進行字符串連接比調用

StringBuffer/StringBuilder 對象的 append 方法連接字符串性能更好?

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

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

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

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

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

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

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

21、描述一下 JVM 加載 class 文件的原理機制?

JVM 中類的裝載是由類加載器(ClassLoader)和它的子類來實現的,Java 中的類加載器是一個重要的 Java 運行時系統組件,它負責在運行時查找和裝入類文件中的類。

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

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

(1) Bootstrap:一般用本地代碼實現,負責加載 JVM 基礎核心類庫(rt.jar);

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

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

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

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

補充:使用 Unicode 意味着字符在 JVM 內部和外部有不同的表現形式,在 JVM內部都是 Unicode,當這個字符被從 JVM 內部轉移到外部時(例如存入文件系統中),需要進行編碼轉換。所以 Java 中有字節流和字符流,以及在字符流和字節流之間進行轉換的轉換流,如 InputStreamReader 和 OutputStreamReader,這兩個類是字節流和字符流之間的適配器類,承擔了編碼轉換的任務;對於 C 程序員來說,要完成這樣的編碼轉換恐怕要依賴於 union(聯合體/共用體)共享內存的特徵來實現了。

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

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

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

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

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

面試題 - 下面的代碼哪些地方會產生編譯錯誤?

class Outer {
	class Inner {
	}
	public static void foo() {
		new Inner();
	}
	public void bar() {
		new Inner();
	}
	public static void main(String[] args) {
		new Inner();
	}
}

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

new Outer().new Inner();

25、Java 中會存在內存泄漏嗎,請簡單描述。

理論上 Java 因爲有垃圾回收機制(GC)不會存在內存泄露問題(這也是 Java 被廣泛使用於服務器端編程的一個重要原因);然而在實際開發中,可能會存在無用但可達的對象,這些對象不能被 GC 回收,因此也會導致內存泄露的發生。例如Hibernate 的 Session(一級緩存)中的對象屬於持久態,垃圾回收器是不會回收這些對象的,然而這些對象中可能存在無用的垃圾對象,如果不及時關閉(close)或清空(flush)一級緩存就可能導致內存泄露。下面例子中的代碼也會導致內存泄露。

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

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

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

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

27、闡述靜態變量和實例變量的區別。

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

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

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

不可以,靜態方法只能訪問靜態成員,因爲非靜態方法的調用要先創建對象,在調用靜態方法時可能對象並沒有被初始化。

29、如何實現對象克隆?

有兩種方式:

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

2). 實現 Serializable 接口,通過對象的序列化和反序列化實現克隆,可以實現真正的深度克隆,代碼如下。

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

下面是測試代碼:

import java.io.Serializable;
/**
* 人類
* @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對象的生命週期將堆內存劃分爲不同的區域,在垃圾收集過程中,可能會將對象移動到不同區域:

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

(2)倖存者樂園(Survivor):從伊甸園倖存下來的對象會被挪到這裏。

(3)終身頤養園(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)接口?抽象類是否可繼承具體類(concreteclass)?

接口可以繼承接口,而且支持多重繼承。抽象類可以實現(implements)接口,抽象類可繼承具體類也可以繼承抽象類。

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

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

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

可以繼承其他類或實現其他接口,在 Swing 編程和 Android 開發中常用此方式來實現事件監聽和回調。

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

一個內部類對象可以訪問創建它的外部類對象的成員,包括私有成員。

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

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

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

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

37、指出下面程序的運行結果

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

執行結果:1a2b2b。創建對象時構造器的調用順序是:先初始化靜態成員,然後調用父類構造器,再初始化非靜態成員,最後調用自身構造器。

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

38、數據類型之間的轉換:

(1) 如何將字符串轉換爲基本數據類型?

(2) 如何將基本數據類型轉換爲字符串?

答:

(1)調用基本數據類型對應的包裝類中的方法 parseXXX(String)或valueOf(String)即可返回相應基本類型;

(2)一種方法是將基本數據類型與空字符串(”“)連接(+)即可獲得其所對應的字符串;另一種方法是調用 String 類中的 valueOf()方法返回相應字符串

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

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

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

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

代碼如下所示:

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

41、日期和時間:

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

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

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

(4)如何格式化日期?

答:

問題 1:創建 java.util.Calendar 實例,調用其 get()方法傳入不同的參數即可獲得參數所對應的值。Java 8 中可以使用 java.time.LocalDateTimel 來獲取,代碼如下所示。

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

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

Calendar.getInstance().getTimeInMillis();
System.currentTimeMillis();
Clock.systemDefaultZone().millis();
// 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 一直以來都是被詬病的東西,爲了解決這一問題,Java8 中引入了新的時間日期 API,其中包括 LocalDate、LocalTime、LocalDateTime、Clock、Instant 等類,這些的類的設計都使用了不變模式,因此是線程安全的設計。

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

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

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

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

43、比較一下 Java 和 JavaSciprt。

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

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

(1)基於對象和麪向對象:Java 是一種真正的面向對象的語言,即使是開發簡單的程序,必須設計對象;JavaScript 是種腳本語言,它可以用來製作與網絡無關的,與用戶交互作用的複雜軟件。它是一種基於對象(Object-Based)和事件驅動(Event-Driven)的編程語言,因而它本身提供了非常豐富的內部對象供設計人員使用。

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

(3)強類型變量和類型弱變量:Java 採用強類型變量檢查,即所有變量在編譯之前必須作聲明;JavaScript 中變量是弱類型的,甚至在使用變量前可以不作聲明,JavaScript 的解釋器在運行時檢查推斷其數據類型。

(4)代碼格式不一樣。

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

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

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

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

斷言可以有兩種形式:

assert Expression1;

assert Expression1 : Expression2 ;

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

Expression2 可以是得出一個值的任意表達式;這個值用於生成顯示更多調試信息的字符串消息。

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

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

45、Error 和 Exception 有什麼區別?

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

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

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

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

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

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

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

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

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

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

(2)對可以恢復的情況使用受檢異常,對編程錯誤使用運行時異常

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

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

(5)每個方法拋出的異常都要有文檔

(6)保持異常的原子性

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

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

(1)ArithmeticException(算術異常)

(2) ClassCastException (類轉換異常)

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

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

(5) NullPointerException (空指針異常)

(6) SecurityException (安全異常)

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

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

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章