這不只是一篇面試題的匯總量瓜,也有自己在學(xué)習(xí) Java 過程總結(jié)的比較重要的或容易模糊的知識點醋火,故整理如下
1. 為什么說內(nèi)部類會隱式持有外部類的引用
編譯器會在編譯階段做4件事:
- 給內(nèi)部類添加一個類型為外部類的字段娃磺;
- 給內(nèi)部類的構(gòu)造函數(shù)增加一個類型為外部類的參數(shù);
- 在內(nèi)部類的所有構(gòu)造函數(shù)中增加初始化外部類字段的代碼;
- 使用內(nèi)部類的任何構(gòu)造函數(shù)實例化內(nèi)部類的地方,編譯器都會為構(gòu)造函數(shù)傳入外部類的引用
這樣就實現(xiàn)了內(nèi)部類隱式持有外部類凤跑,在代碼層面看不到,但是通過 javap 命令反編譯叛复,從字節(jié)碼層面就能清晰看到仔引,例如如下代碼:
public class Outer {
class Inner{
}
}
反編譯 Outer$Inner.class 的結(jié)果如圖(省略常量池部分):
2. 為什么在方法中定義的內(nèi)部類可以引用方法的局部變量?并且該局部變量必須為 final 類型致扯?
其原理和上個問題類似肤寝,也是編譯器在編譯階段對內(nèi)部類進行了一些改造:
- 為內(nèi)部類增加一個類型和所使用的局部變量相同的字段
- 為內(nèi)部類的所有構(gòu)造函數(shù)增加一個局部變量類型的參數(shù)
- 在內(nèi)部類的所有構(gòu)造函數(shù)中給添加的字段賦值,這個值即是所引用的外部方法的局部變量的值
簡單的說抖僵,就是在方法中定義內(nèi)部類時,如果引用了方法中的局部變量缘揪,那么編譯器就會把該局部變量拷貝一份保存在內(nèi)部類中耍群。
而被引用的局部變量必須為 final 類型的原因也很清楚了,因為內(nèi)部類只是拷貝了一份局部變量的值找筝,如果之后局部變量發(fā)生改變蹈垢,內(nèi)部類是無法獲知的,這樣就可能出現(xiàn)不符合預(yù)期的結(jié)果袖裕。所以強制局部變量為 final 類型主要是為了在編譯階段就發(fā)現(xiàn)這種可能的錯誤曹抬。比如下面的代碼,假設(shè)編譯器沒有強制局部變量為 final :
public class Outer {
Runnable runnable;
public Outer(){
int i = 1;
runnable = new Runnable(){
@Override
public void run() {
System.out.println(i);
}
};
i = 2; // 錯誤代碼急鳄,編譯器會報錯谤民,僅為說明問題
runnable.run();
}
}
我們可能會預(yù)期打印出的值為 2堰酿,但實際上 runnable 對象中保存的僅是 i 的一份拷貝,在定義之后對 i 的改變無法反映到 runnable 中张足。所以強制 i 為 final 類型触创,就確保了內(nèi)部類和局部變量之間的一致性。
最后为牍,在 Java8 中有一點變化哼绑,編譯器變得更加智能,對于邏輯上和 final 類型等價的局部變量 可以不用強制聲明為 final碉咆。簡單說就是如果這個局部變量初始化之后抖韩,再沒有改變其值的操作,那么不用聲明為 final 也不會報錯
3. Java 中的數(shù)組是對象么疫铜?有哪些特點茂浮?
Java 中的數(shù)組類型也是一種對象,從其具有 length 字段和 toString()块攒,clone() 方法就能看出励稳。數(shù)組對象的父類是 Object,所以以下代碼都正確:
int[] array = new int[10];
//可以向上轉(zhuǎn)型成 Object
Object obj = array ;
//可以向下轉(zhuǎn)型成 int[]
int[] b = (int[])obj;
//可以用instanceof關(guān)鍵字進行
if(obj instanceof int[]){
...
}
數(shù)組還有一些令人迷惑的特性囱井,比如下面這段代碼:
String[] s = new String[5];
Object[] obja = s;
這段代碼是正確的驹尼!而前面我們已經(jīng)知道 String[] 是 Object 的子類,不可能也同時是 Object[] 的子類庞呕,不然就違反了單繼承原則新翎。只能把這個當(dāng)作數(shù)組對象的一種特殊性質(zhì)來理解了(背后原理還有待研究)。概括一下就是:
** 如果B繼承(extends)了A住练,那么A[]類型的引用就可以指向B[]類型的對象地啰。
**
另外這種用法不包括基本類型,這也很好理解讲逛,因為基本類型并不繼承于 Object:
int[] a = new int[4];
//Object[] obja = a; //錯誤亏吝,不能通過編譯
再看下面這段代碼:
List list = new LinkedList<String>();
list.add("a");
// String[] strs = (String[]) list.toArray(); // 錯誤,運行時異常盏混,無法強轉(zhuǎn)
Object[] objs = list.toArray(new String[1]);
String[] strs = (String[]) objs; // 正確蔚鸥,可以強轉(zhuǎn)
System.out.println(strs[0] );
List.toArray() 方法返回 Object[],無法強轉(zhuǎn)成 String[]许赃,盡管其數(shù)組成員實際上都是 String 類型止喷。而 List.toArray(T[] a) 方法返回 T[],在本例中也就是返回 String[]混聊,可以用一個 Object[] 類型的變量指向 String[]弹谁,然后還能強轉(zhuǎn)。
所以進一步總結(jié)就是:
一個類型為 Object[] 的數(shù)組對象,盡管其數(shù)組元素類型為 A预愤, 但是也不能強轉(zhuǎn)成 A[]沟于。但是一個類型為 A[] 的數(shù)組對象,可以用 Object 或者 A的父類類型的數(shù)組 類型的變量來指向鳖粟,并且可以再強轉(zhuǎn)成 A[]社裆。
4. Java 中對象的初始化順序遵循怎樣的規(guī)則?
- 先基類向图,后父類
- 先成員變量泳秀,后構(gòu)造函數(shù)
- 先靜態(tài)成員,后非靜態(tài)成員
- 靜態(tài)變量只在初次使用時初始化一次榄攀,之后不再執(zhí)行
- 觸發(fā)靜態(tài)變量(或靜態(tài)塊)初始化的動作有:
- 使用 new 關(guān)鍵字實例化對象嗜傅;
- 讀取或設(shè)置一個類的靜態(tài)字段;
- 調(diào)用一個類的靜態(tài)方法
- 對類進行反射調(diào)用
- 初始化子類時檩赢,如果父類還未初始化吕嘀,會觸發(fā)父類的初始化
- 虛擬機啟動時用戶需要指定一個要執(zhí)行的主類(包含 main() 函數(shù)的那個類),虛擬機會先初始化這個類
5. Java 虛擬機是怎樣實現(xiàn)方法的重載(Overload)和重寫(Override)的贞瞒?
概括的說偶房,方法的重載是在編譯期確定,根據(jù)變量的靜態(tài)類型決定要調(diào)用的方法军浆,方法的重寫是在運行時確定棕洋,根據(jù)變量的實際類型決定要調(diào)用的方法。
- 重載舉例(引用自《深入理解 Java 虛擬機》):
public class StaticDispatch {
static abstract class Human {}
static class Man extends Human {}
static class Woman extends Human {}
public void sayHello(Human guy) {
System.out.println("hello,guy!");
}
public void sayHello(Man guy) {
System.out.println("hello,gentleman!");
}
public void sayHello(Woman guy) {
System.out.println("hello,lady!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
}
變量 man 和 woman 的靜態(tài)類型都是 Human乒融,所以 sr.sayHello(man) 和 sr.sayHello(woman) 這兩條語句掰盘,編譯器在編譯時就確定了調(diào)用的版本為sayHello(Human guy) ,這點通過反編譯后字節(jié)碼也可以看出赞季,在第26和第31行的字節(jié)碼可以看到愧捕,調(diào)用的方法已經(jīng)確定為 sayHello(Human guy) 。這也被叫做方法的 靜態(tài)分派
另外對于基本類型的重載需要單獨說明一下申钩,以 char 為例次绘,其匹配重載方法的優(yōu)先級是
char->int->long->float->double->Character->Serializable/Comparable(這兩個優(yōu)先級一樣不能同時出現(xiàn))-> Object。注意 byte-char-short 三者之間不能轉(zhuǎn)型撒遣,因為 char 是無符號數(shù)断盛,short 是有符號數(shù),所以數(shù)據(jù)范圍不同8
- 重寫舉例(引用自《深入理解 Java 虛擬機》):
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}
顯然這里 man 和 woman 會調(diào)用到各自重寫的方法愉舔,背后的原理還是從字節(jié)碼角度說明比較清楚:
第16、17行的 aload_1 和 invokevirtual 指令伙菜。aload_1 把剛剛創(chuàng)建的 Man 對象壓到操作數(shù)棧頂轩缤。Java 虛擬機規(guī)定執(zhí)行 invokevirtual 指令時,會找到操作數(shù)棧頂?shù)牡谝粋€元素所指向的對象的實際類型(本例中就是 Man),在其類型定義中查找對應(yīng)的方法火的,如果找到那么就返回該方法的直接引用壶愤,否則在其父類中查找。
6. HashMap 的實現(xiàn)原理(基于Android SDK 里的實現(xiàn)馏鹤,與 OpenSDK 略有不同)
一句話概括征椒,橫向是一個 HashMapEntry 數(shù)組,縱向是一個 HashMapEntry 鏈表湃累。另外有一個單獨的 HashMapEntry 保存 Key == null 的元素
- Map 接口有個內(nèi)部接口 Entry勃救,它定義了 Map 的基本元素,鍵值對治力。HashMap 中實現(xiàn) Entry 接口的內(nèi)部類時 HashMapEntry
- HashMap 維護一個 HashMapEntry 的數(shù)組 table蒙秒,初始化大小總是為2的n次冪
- put():
- 根據(jù) Key 的 hashCode() 做二次hash計算出 Key 的hash值
- hash值取模數(shù)組長度,得到應(yīng)該插入數(shù)組的位置 index
- 如果 index 位置不空宵统,遍歷 table[index] 為頭的鏈表晕讲,查找是否有 Key 的 hash值相等且 equal() 為 true 的元素,如果有則返回舊值马澈,保存新值
- 如果 table[index] == null瓢省,或者鏈表中未找到 Key 值相等的 Entry,那么size++(size > threshold 需要擴容痊班,新建一個大小*2的數(shù)組勤婚,然后把之前的元素全部取出重新找到各自的位置),然后插入新 Entry 到數(shù)組 index 位置辩块,新 Entry.next 指向之前的 table[index] (其實就是鏈表在頭部的插入操作)
- get() 和 remove() 很簡單蛔六,前兩步跟 put() 一樣,之后就是遍歷鏈表根據(jù) Key 查找废亭。
- 遍歷實現(xiàn)都基于 HashIterator.nextEntry() 方法国章,會從數(shù)組的第一個元素開始,按照先縱向后橫向的順序遍歷
Java8 里的優(yōu)化:HashMap 的實現(xiàn)在 Java8 里做了進一步的優(yōu)化豆村,當(dāng)一個 index 下面的鏈表長度超過8時液兽,該鏈表就轉(zhuǎn)變成一顆紅黑樹,這樣的查找效率就更高掌动,一圖勝千言:
7. 使用 AtomicInteger 和 使用 synchronized 實現(xiàn)對變量的原子操作有什么不同四啰?
- synchronized 是阻塞式的,會導(dǎo)致線程上下文的切換粗恢,對于簡單的賦值操作來說柑晒,代價太高。AtomicInteger 通過 CPU 對 CAS(compare and swap) 操作的原子性 以及 volatile 關(guān)鍵字實現(xiàn)了非阻塞式的原子操作眷射,是非阻塞的匙赞,沒有線程切換的開銷
- synchronized 是悲觀的佛掖,它假設(shè)一定會有競爭,所以會先獲取鎖再執(zhí)行操作涌庭;AtomicInteger 是樂觀的芥被,它先嘗試更新操作,如果當(dāng)前值與期望值不等坐榆,則代表出現(xiàn)競爭拴魄,返回false,然后不斷嘗試直到成功
以 AtomicInteger.getAndIncrement() 為例席镀,它實現(xiàn)了 i++ 的原子操作:
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
可以看到有一個死循環(huán)(這也是自旋鎖說法的由來)匹中,只要 compareAndSet() 不成功,就不斷嘗試愉昆,直到成功再返回职员。