Java 面試&基礎(chǔ)100問(持續(xù)更新)

這不只是一篇面試題的匯總量瓜,也有自己在學(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é)果如圖(省略常量池部分):


反編譯Outer$Inner.class
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)分派

反編譯字節(jié)碼

另外對于基本類型的重載需要單獨說明一下申钩,以 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é)碼角度說明比較清楚:


反編譯字節(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():
    1. 根據(jù) Key 的 hashCode() 做二次hash計算出 Key 的hash值
    2. hash值取模數(shù)組長度,得到應(yīng)該插入數(shù)組的位置 index
    3. 如果 index 位置不空宵统,遍歷 table[index] 為頭的鏈表晕讲,查找是否有 Key 的 hash值相等且 equal() 為 true 的元素,如果有則返回舊值马澈,保存新值
    4. 如果 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)變成一顆紅黑樹,這樣的查找效率就更高掌动,一圖勝千言:

http://coding-geek.com/how-does-a-hashmap-work-in-java/

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() 不成功,就不斷嘗試愉昆,直到成功再返回职员。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市跛溉,隨后出現(xiàn)的幾起案子焊切,更是在濱河造成了極大的恐慌,老刑警劉巖芳室,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件专肪,死亡現(xiàn)場離奇詭異,居然都是意外死亡堪侯,警方通過查閱死者的電腦和手機嚎尤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來伍宦,“玉大人芽死,你說我怎么就攤上這事〈瓮荩” “怎么了关贵?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長卖毁。 經(jīng)常有香客問我揖曾,道長,這世上最難降的妖魔是什么亥啦? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任炭剪,我火速辦了婚禮,結(jié)果婚禮上翔脱,老公的妹妹穿的比我還像新娘奴拦。我一直安慰自己,他們只是感情好届吁,可當(dāng)我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布粱坤。 她就那樣靜靜地躺著隶糕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪站玄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天濒旦,我揣著相機與錄音株旷,去河邊找鬼。 笑死尔邓,一個胖子當(dāng)著我的面吹牛晾剖,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播梯嗽,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼齿尽,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了灯节?” 一聲冷哼從身側(cè)響起循头,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎炎疆,沒想到半個月后卡骂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡形入,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年全跨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片亿遂。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡浓若,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蛇数,到底是詐尸還是另有隱情挪钓,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布苞慢,位于F島的核電站诵原,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏挽放。R本人自食惡果不足惜绍赛,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望辑畦。 院中可真熱鬧吗蚌,春花似錦、人聲如沸纯出。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至箩言,卻和暖如春硬贯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背陨收。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工饭豹, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人务漩。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓拄衰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親饵骨。 傳聞我的和親對象是個殘疾皇子翘悉,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容

  • Java虛擬機的執(zhí)行引擎:輸入的是字節(jié)碼文件,處理過程是字節(jié)碼解析的等效過程居触,輸出的是執(zhí)行結(jié)果妖混。本章主要是從概念模...
    maxwellyue閱讀 542評論 0 0
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法饼煞,內(nèi)部類的語法源葫,繼承相關(guān)的語法,異常的語法砖瞧,線程的語...
    子非魚_t_閱讀 31,631評論 18 399
  • 本文主要參考自 深入理解Java虛擬機 概述 Java能夠做到“一處編譯息堂,處處運行”,這與.class文件的...
    東溪95閱讀 1,840評論 0 4
  • Win7下如何打開DOS控制臺块促? a:開始--所有程序--附件--命令提示符 b:開始--搜索程序和文件--cmd...
    逍遙嘆6閱讀 1,595評論 4 12
  • 文| 涂山狐貍 圖| 網(wǎng)絡(luò) “只管走過去竭翠,不必逗留著采了花朵來保存振坚,因為一路上花朵自會繼續(xù)開放的≌牛” 1 有一個親...
    涂山狐貍閱讀 320評論 1 2