前言
從今天開始疹启,我將開啟一個(gè)系列的文章——【 Java 面試八股文】婆咸。
這個(gè)系列會(huì)陸續(xù)更新 Java 面試中的高頻問題唉铜,旨在從問題出發(fā),理解 Java 基礎(chǔ)菠红,數(shù)據(jù)結(jié)構(gòu)與算法第岖,數(shù)據(jù)庫(kù),常用框架等试溯。
首先要做幾點(diǎn)說明:
- 【 Java 面試八股文】中的面試題來源于社區(qū)論壇蔑滓,書籍等資源;感謝使我讀到這些寶貴的面經(jīng)的作者們遇绞。
- 對(duì)于【 Java 面試八股文】中的每個(gè)問題键袱,我都會(huì)盡可能地寫出我自己認(rèn)為的“完美解答”。但是畢竟我的身份不是一個(gè)“真理持有者”摹闽,只是一個(gè)秉承著開源分享精神的 “knowledge transmitter” & 菜雞蹄咖,所以,如果這些答案出現(xiàn)了錯(cuò)誤付鹿,可以留言寫出你認(rèn)為更好的解答澜汤,并指正我。非常感謝您的分享舵匾。
- 知識(shí)在于“融釋貫通”俊抵,而非“死記硬背”;現(xiàn)在市面上固然有很多類似于“Java 面試必考 300 題” 這類的文章坐梯,但是普遍上都是糟粕徽诲,僅講述其果,而不追其源;希望我的【 Java 面試八股文】可以讓你知其然谎替,且知其所以然~
那么偷溺,我們正式開始吧!
Java 基礎(chǔ)篇(一)
1、分析程序的運(yùn)行結(jié)果院喜,并解釋為什么亡蓉?
程序一:
public class MyTestClass {
private static MyTestClass myTestClass = new MyTestClass();
private static int a = 0;
private static int b;
private MyTestClass() {
a++;
b++;
}
public static MyTestClass getInstance() {
return myTestClass;
}
public int getA() {
return a;
}
public int getB() {
return b;
}
}
public class Test {
public static void main(String[] args) {
MyTestClass myTestClass = MyTestClass.getInstance();
System.out.println("myTestClass.a : " + myTestClass.getA());
System.out.println("myTestClass.b : " + myTestClass.getB());
}
}
程序二:
public class MyTestClass2 {
private static int a = 0;
private static int b;
private MyTestClass2(){
a++;
b++;
}
private static final MyTestClass2 myTestClass2 = new MyTestClass2();
public static MyTestClass2 getInstance(){
return myTestClass2;
}
public int getA() {
return a;
}
public int getB() {
return b;
}
}
public class Test {
public static void main(String[] args) {
MyTestClass2 myTestClass2 = MyTestClass2.getInstance();
System.out.println("myTestClass2.a : " + myTestClass2.getA());
System.out.println("myTestClass2.b : " + myTestClass2.getB());
}
}
答
第一個(gè)程序執(zhí)行的結(jié)果為:
myTestClass.a : 0
myTestClass.b : 1
第二個(gè)程序執(zhí)行的結(jié)果為:
myTestClass2.a : 1
myTestClass2.b : 1
本題考查的知識(shí)點(diǎn)為【類加載的順序】。一個(gè)類從被加載至 JVM 到卸載出內(nèi)存的整個(gè)生命周期為:
各個(gè)階段的主要功能如下:
加載:查找并加載類文件的二進(jìn)制數(shù)據(jù)
-
鏈接:將已經(jīng)讀入內(nèi)存的類的二進(jìn)制數(shù)據(jù)合并到 JVM 的運(yùn)行時(shí)環(huán)境中去喷舀,包含如下幾個(gè)步驟:
- 驗(yàn)證:確保被加載類的正確性
- 準(zhǔn)備:為類的靜態(tài)變量分配內(nèi)存砍濒,賦默認(rèn)值;例如:
public static int a = 1;
在準(zhǔn)備階段對(duì)靜態(tài)變量 a 賦默認(rèn)值 0 - 解析:把常量池中的符號(hào)引用轉(zhuǎn)換成直接引用
初始化:為類的靜態(tài)變量賦初始值硫麻;例如:
public static int a = 1;
這個(gè)時(shí)候才對(duì)靜態(tài)變量 a 賦初始值 1
我們可以從 Java 類加載的這個(gè)過程中看到爸邢,類的靜態(tài)變量在類加載時(shí)就已經(jīng)被加載到內(nèi)存中并完成賦值了!
對(duì)于第一個(gè)程序來說:
首先拿愧,在鏈接的準(zhǔn)備階段杠河,JVM 會(huì)為類的靜態(tài)變量分配內(nèi)存,并賦默認(rèn)值浇辜,這里面我們也可以使用更加專業(yè)的計(jì)算機(jī)詞匯——“缺省值”來形容券敌,即:
myTestClass = null;
a = 0;
b = 0;
接著柳洋,在類的初始化階段待诅,JVM 會(huì)為這些靜態(tài)變量真正地賦初始值。
private static MyTestClass myTestClass = new MyTestClass();
對(duì)靜態(tài)變量 myTestClass 賦初始值時(shí)會(huì)回調(diào)構(gòu)造器熊镣,構(gòu)造器中執(zhí)行 a++
與 b++
卑雁,使得靜態(tài)變量 a 與 b 的結(jié)果均為 1 。
對(duì) myTestClass 這個(gè)靜態(tài)變量賦值完畢后绪囱,接下來代碼會(huì)繼續(xù)執(zhí)行测蹲,對(duì) a 和 b 這兩個(gè)靜態(tài)變量賦初始值,繼而又將 a 變?yōu)榱?0鬼吵,而 b 則沒有初始值扣甲,所以其結(jié)果仍然為 1。
綜上所示而柑,程序一的輸出結(jié)果為:
myTestClass.a : 0
myTestClass.b : 1
程序二的分析過程和程序一是一樣的文捶,這里我就不再贅述了。
總結(jié)
本題考查的知識(shí)點(diǎn)是童鞋們對(duì)類加載的理解媒咳。一定要銘記的是:靜態(tài)變量的加載與初始化發(fā)生在【類加載階段】粹排。
2、普通內(nèi)部類與靜態(tài)內(nèi)部類有什么區(qū)別涩澡?
答
普通內(nèi)部類:
- 可以訪問外部類的所有屬性和方法
- 普通內(nèi)部類中不能包含靜態(tài)的屬性和方法
靜態(tài)內(nèi)部類:
- 靜態(tài)內(nèi)部類只能訪問外部類的靜態(tài)屬性及方法顽耳,無法訪問外部類的普通成員(變量和方法)
- 靜態(tài)內(nèi)部類可以包含靜態(tài)的屬性和方法
本題回答到這里并非完美,面試官可能會(huì)繼續(xù)提問:你知道為什么普通內(nèi)部類可以訪問到外部類的成員變量么?或者是:我應(yīng)該優(yōu)先選用普通內(nèi)部類還是靜態(tài)內(nèi)部類射富,為什么膝迎?
我們先來看一個(gè)示例:
Home
package com.github.test;
public class Home {
class A {
}
}
Home2
package com.github.test;
public class Home2 {
static class A {
}
}
執(zhí)行編譯后,我們來到 target 目錄下胰耗,并執(zhí)行反編譯命令:
javap -private 'Home$A'
Home$A.class
class com.github.test.Home$A {
final com.github.test.Home this$0;
com.github.test.Home$A(com.github.test.Home);
}
執(zhí)行命令:
javap -private 'Home2$A'
Home2$A.class
class com.github.test.Home2$A {
com.github.test.Home2$A();
}
我們可以看到 Home 類當(dāng)中含有普通內(nèi)部類 A限次,而 Home2 這個(gè)類中含有靜態(tài)內(nèi)部類 A 。并且我們對(duì)這兩個(gè)內(nèi)部類執(zhí)行了反解析柴灯。
執(zhí)行javap
命令后卖漫,我們看到普通內(nèi)部類 A 比靜態(tài)內(nèi)部類 A 多了一個(gè)特殊的字段:com.github.test.Home this$0
。
普通內(nèi)部類多出的這個(gè)字段是 JDK “偷偷”為我們添加的赠群,它指向了外部類 Home羊始。
所以,我們也就搞清楚了查描,之所以普通內(nèi)部類可以直接訪問外部類的所有成員突委,是因?yàn)?JDK 為普通內(nèi)部類偷偷添加了這么一個(gè)隱式的變量 this$0,指向外部類冬三。
那么匀油,我們應(yīng)該優(yōu)先選擇普通內(nèi)部類還是靜態(tài)內(nèi)部類呢?
《Effective java》 Item 24 的內(nèi)容是:Favor static member classes over nonstatic勾笆,即:優(yōu)先考慮使用靜態(tài)內(nèi)部類钧唐。
因?yàn)榉庆o態(tài)內(nèi)部類會(huì)持有外部類的一個(gè)隱式引用(this$0), 存儲(chǔ)這個(gè)引用需要占用時(shí)間和空間。更嚴(yán)重的是有可能會(huì)導(dǎo)致宿主類在滿足垃圾回收的條件時(shí)卻仍然駐留在內(nèi)存中匠襟,由此引發(fā)內(nèi)存泄漏的問題。
所以该园,在需要使用內(nèi)部類的情況下酸舍,我們應(yīng)該盡可能選擇使用靜態(tài)內(nèi)部類。
總結(jié)
怎么樣里初?一道看似非常簡(jiǎn)答的問題也可能暗藏殺機(jī)啃勉。如果不知道的小伙伴們,不妨敲一下代碼双妨,自己按照流程執(zhí)行一遍淮阐,這樣才會(huì)加深你的印象哦~
3、分析程序的運(yùn)行結(jié)果刁品,并解釋為什么泣特?
程序一:
public class Polymorphic {
public static void main(String[] args) {
Animal cat = new Cat();
System.out.println(cat.name);
}
}
class Animal {
String name = "animal";
}
class Cat extends Animal{
String name = "cat";
}
程序二:
public class Polymorphic {
public static void main(String[] args) {
Animal cat = new Cat();
cat.speak();
}
}
class Animal {
public void speak(){
System.out.println("我是一個(gè)動(dòng)物");
}
}
class Cat extends Animal{
@Override
public void speak() {
System.out.println("我是一只貓");
}
}
答
程序一的輸出結(jié)果為:
animal
程序二的輸出結(jié)果為:
我是一只貓
本題考查的知識(shí)點(diǎn)為多態(tài)哈恰。需要知道椭员,多態(tài)分為編譯時(shí)的多態(tài)性與運(yùn)行時(shí)的多態(tài)性。
- 多態(tài)的應(yīng)用中欧漱,對(duì)于成員變量訪問的特點(diǎn)為:
- 編譯看左邊,運(yùn)行看左邊
- 多態(tài)的應(yīng)用中膏孟,對(duì)于成員方法調(diào)用的特點(diǎn)為:
- 編譯看左邊眯分,運(yùn)行看右邊
對(duì)于程序一,在程序編譯時(shí)期柒桑,首先 JVM 會(huì)看向 Animal cat = new Cat();
這句話等號(hào)左邊的父類 Animal 是否有該變量(name)的定義弊决,如果有則編譯成功,如果沒有則編譯失斂尽飘诗;在程序運(yùn)行時(shí)期,對(duì)于成員變量先改,JVM 仍然會(huì)看向左邊的所屬類型疚察,獲取的是父類的成員變量。
對(duì)于程序二仇奶,在程序編譯時(shí)期貌嫡,首先 JVM 會(huì)看向 Animal cat = new Cat();
這句話等號(hào)左邊的類是否有該方法的定義,如果有則編譯成功该溯,如果沒有則編譯失數撼;在程序運(yùn)行時(shí)狈茉,則是要看等號(hào)右邊的對(duì)象是如何實(shí)現(xiàn)該方法的夫椭,所以最終呈現(xiàn)的結(jié)果為右邊對(duì)象對(duì)這個(gè)方法重寫后的結(jié)果。
總結(jié)
這是一道非常經(jīng)典(老掉牙)的面試筆試題了氯庆,考察 Java 多態(tài)的基礎(chǔ)蹭秋,答錯(cuò)的小伙伴可要好好回顧復(fù)習(xí)下了~
4、請(qǐng)談一下值傳遞與引用傳遞堤撵?Java 中只有值傳遞么仁讨?
答
值傳遞(Pass by value)與引用傳遞(Pass by reference)屬于函數(shù)調(diào)用時(shí),參數(shù)的求值策略(Evaluation Strategy)实昨。求值策略的關(guān)注點(diǎn)在于洞豁,求值的時(shí)間以及傳值方式:
求值策略 | 求值時(shí)間 | 傳值方式 |
---|---|---|
Pass by value | 函數(shù)調(diào)用前 | 原值的副本 |
Pass by reference | 函數(shù)調(diào)用前 | 原值(原始對(duì)象) |
所以,區(qū)別值傳遞與引用傳遞的實(shí)質(zhì)并不是傳遞的類型是值還是引用荒给,而是傳值方式丈挟,傳遞的是原值還是原值的副本。
如果傳遞的是原值(原對(duì)象)志电,就是引用傳遞曙咽;如果傳遞的是一個(gè)副本(拷貝),就是值傳遞溪北。再次強(qiáng)調(diào)一遍桐绒,值傳遞和引用傳遞的區(qū)別在于傳值方式夺脾,和你傳遞的類型是值還是引用沒有一毛錢關(guān)系!
Java 語(yǔ)言只有值傳遞茉继。
Java 語(yǔ)言之所以只有值傳遞咧叭,是因?yàn)椋簜鬟f的類型無論是值類型還是引用類型,Java 都會(huì)在調(diào)用棧上創(chuàng)建一個(gè)副本烁竭,不同的是菲茬,對(duì)于值類型而言,這個(gè)副本就是整個(gè)原始值的復(fù)制派撕;而對(duì)于引用類型而言婉弹,由于引用類型的實(shí)例存儲(chǔ)在堆中,在棧上只有它的一個(gè)引用终吼,指向堆的實(shí)例镀赌,其副本也只是這個(gè)引用的復(fù)制,而不是整個(gè)原始對(duì)象的復(fù)制际跪。
我們通過兩個(gè)程序來理解下:
程序一:
public class Test {
public static void setNum1(int num){
num = 1;
}
public static void main(String[] args) {
int a = 2;
setNum1(a);
System.out.println(a);
}
}
程序二:
public class Test2 {
public static void setArr1(int[] arr){
Arrays.fill(arr,1);
}
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
setArr1(arr);
System.out.println(Arrays.toString(arr));
}
}
程序一輸出的結(jié)果為:2商佛;
程序二輸出的結(jié)果為:[1,1,1,1,1]
。
程序一中姆打,Java 會(huì)將原值復(fù)制一份放在棧區(qū)良姆,并將這個(gè)拷貝傳遞到方法參數(shù)中,方法里面僅僅是對(duì)這個(gè)拷貝進(jìn)行了修改幔戏,并沒有影響到原值玛追,所以程序一的輸出結(jié)果為 2。
程序二中闲延,Java 會(huì)將引用的地址復(fù)制一份放在棧區(qū)痊剖,復(fù)制的拷貝和原始引用都指向堆區(qū)的同一個(gè)對(duì)象。方法通過拷貝地址找到堆區(qū)的實(shí)例垒玲,對(duì)堆區(qū)的實(shí)例進(jìn)行修改邢笙,而此時(shí),原始引用仍然指向著堆區(qū)的實(shí)例侍匙,所以程序二的輸出結(jié)果為:[1,1,1,1,1]
總結(jié)
這實(shí)際上也是一個(gè)老掉牙的問題了。
不過叮雳,請(qǐng)不要忽視它想暗,它也許沒有你想的那么簡(jiǎn)單。絕大部分初學(xué)者很難搞懂究竟什么是值傳遞帘不,什么是引用傳遞说莫。
很多博客中,作者不僅沒有解釋清楚“值傳遞”與“引用傳遞”寞焙,還混淆了很多錯(cuò)誤的引導(dǎo)储狭。
這些錯(cuò)誤的理解包括:
- 【觀點(diǎn)1】Java 中既有值傳遞也有引用傳遞
- 【觀點(diǎn)2】Java 中只有值傳遞互婿,因?yàn)橐玫谋举|(zhì)就是指向堆區(qū)的一個(gè)地址,也是一個(gè)值辽狈。
如果你的觀點(diǎn)符合上述兩種觀點(diǎn)的其中一種慈参,那么你多半沒有理解值傳遞和引用傳遞到底是啥子?xùn)|西~
5、請(qǐng)描述當(dāng)我們 new 一個(gè)對(duì)象時(shí)刮萌,發(fā)生了什么驮配?
答
new 一個(gè)對(duì)象時(shí),可以將發(fā)生的活動(dòng)分為以下的幾個(gè)過程:
- 類加載
- 為對(duì)象分配內(nèi)存空間
- 完善對(duì)象內(nèi)存布局信息
- 調(diào)用對(duì)象的實(shí)例化方法
<init>
- 在棧中新建對(duì)象的引用着茸,并指向堆中的實(shí)例
類加載
當(dāng) JVM 遇到一條 new 指令時(shí)壮锻,首先會(huì)去檢查該指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用(Symbolic Reference),并檢查這個(gè)符號(hào)引用代表的類是否已經(jīng)被加載涮阔,解析猜绣,初始化過。如果該類是第一次被使用敬特,那么就會(huì)執(zhí)行類的加載過程掰邢。
注:符號(hào)引用是指,一個(gè)類中引入了其他的類擅羞,可是 JVM 并不知道引入其他類在什么位置尸变,所以就用唯一的符號(hào)來代替,等到類加載器去解析時(shí)减俏,就會(huì)使用符號(hào)引用找到引用類的具體地址召烂,這個(gè)地址就是直接引用
類的加載過程在上文中已經(jīng)有提過,我們?cè)俨粎捚錈┑貜?fù)習(xí)一下:
一個(gè)類從被加載至 JVM 到卸載出內(nèi)存的整個(gè)生命周期為:
各個(gè)階段的主要功能如下:
加載:查找并加載類文件的二進(jìn)制數(shù)據(jù)
-
鏈接:將已經(jīng)讀入內(nèi)存的類的二進(jìn)制數(shù)據(jù)合并到 JVM 的運(yùn)行時(shí)環(huán)境中去娃承,包含如下幾個(gè)步驟:
- 驗(yàn)證:確保被加載類的正確性
- 準(zhǔn)備:為類的靜態(tài)變量分配內(nèi)存奏夫,賦默認(rèn)值;例如:
public static int a = 1;
在準(zhǔn)備階段對(duì)靜態(tài)變量 a 賦默認(rèn)值 0 - 解析:把常量池中的符號(hào)引用轉(zhuǎn)換成直接引用
初始化:為類的靜態(tài)變量賦初始值历筝;例如:
public static int a = 1;
這個(gè)時(shí)候才對(duì)靜態(tài)變量 a 賦初始值 1
談到了類加載酗昼,就不得不提類加載器(ClassLoader)。
以 HotSpot VM 舉例梳猪,從 JDK 9 開始麻削,其自帶的類加載器如下:
- BootstrapClassLoader
- PlatformClassLoader
- AppClassLoader
而 JDK 8 虛擬機(jī)自帶的加載器為:
- BootstrapClassLoader
- ExtensionClassLoader
- AppClassLoader
除了虛擬機(jī)自帶的類加載器以外,用戶也可以自定義類加載器(UserClassLoader)春弥。
這些類加載器的加載順序具有一定的層級(jí)關(guān)系:
JVM 中的 ClassLoader 會(huì)按照這樣的層級(jí)關(guān)系呛哟,采用一種叫做雙親委派模型的方式去加載一個(gè)類:
那么什么是雙親委派模型呢?
雙親委托模型就是:如果一個(gè)類加載器(ClassLoader)收到了類加載的請(qǐng)求匿沛,它首先不會(huì)自己去嘗試加載這個(gè)類扫责,而是把這個(gè)請(qǐng)求委托給父類加載器去完成,每一個(gè)層次的類加載器都是如此逃呼,因此所有的加載請(qǐng)求最終都應(yīng)該傳送到頂層的啟動(dòng)類加載器(BootstrapClassLoader)中鳖孤,只有當(dāng)父類加載器反饋?zhàn)约簾o法完成這個(gè)加載請(qǐng)求(它的搜索范圍中沒有找到所需要加載的類)時(shí)者娱,子加載器才會(huì)嘗試自己去加載。
使用雙親委托機(jī)制的好處是:能夠有效確保一個(gè)類的全局唯一性苏揣,當(dāng)程序中出現(xiàn)多個(gè)限定名相同的類時(shí)黄鳍,類加載器在執(zhí)行加載時(shí),始終只會(huì)加載其中的某一個(gè)類腿准。
為對(duì)象分配內(nèi)存空間
在類加載完成后际起,JVM 就可以完全確定 new 出來的對(duì)象的內(nèi)存大小了,接下來吐葱,JVM 會(huì)執(zhí)行為該對(duì)象分配內(nèi)存的工作街望。
為對(duì)象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從 JVM 堆中劃分出來,目前常用的有兩種方式(根據(jù)使用的垃圾收集器的不同而使用不同的分配機(jī)制):
- Bump the Pointer(指針碰撞)
- Free List(空閑列表)
所謂的指針碰撞是指:假設(shè) JVM 堆內(nèi)存是絕對(duì)規(guī)整的弟跑,所有用過的內(nèi)存都放在一邊灾前,空閑的內(nèi)存放在另一半,中間有一個(gè)指針指向分界點(diǎn)孟辑,那新的對(duì)象分配的內(nèi)存就是把那個(gè)指針向空閑空間挪動(dòng)一段與對(duì)象大小相等的距離哎甲。
而如果 JVM 堆內(nèi)存并不是規(guī)整的,即:已用內(nèi)存空間與空閑內(nèi)存相互交錯(cuò)饲嗽,JVM 會(huì)維護(hù)一個(gè)空閑列表炭玫,記錄哪些內(nèi)存塊是可用的,在為該對(duì)象分配空間時(shí)貌虾,JVM 會(huì)從空閑列表中找到一塊足夠大的空間劃分給對(duì)象使用吞加。
完善對(duì)象內(nèi)存布局信息
在我們?yōu)閷?duì)象分配好內(nèi)存空間后,JVM 會(huì)設(shè)置對(duì)象的內(nèi)存布局的一些信息尽狠。
對(duì)象在內(nèi)存中存儲(chǔ)的布局(以 HotSpot 虛擬機(jī)為例)分為:對(duì)象頭衔憨,實(shí)例數(shù)據(jù)以及對(duì)齊填充。
-
對(duì)象頭
對(duì)象頭包含兩個(gè)部分:
- Mark Word:存儲(chǔ)對(duì)象自身的運(yùn)行數(shù)據(jù)袄膏,如:Hash Code,GC 分代年齡践图,鎖狀態(tài)標(biāo)志等等
- 類型指針:對(duì)象指向它的類的元數(shù)據(jù)的指針
-
實(shí)例數(shù)據(jù)
- 實(shí)例數(shù)據(jù)是真正存放對(duì)象實(shí)例的地方
-
對(duì)齊填充
- 這部分不一定存在,也沒有什么特別含義沉馆,僅僅是占位符码党。因?yàn)?HotSpot 要求對(duì)象起始地址都是 8 字節(jié)的整數(shù)倍,如果不是就對(duì)齊
JVM 會(huì)為所有實(shí)例數(shù)據(jù)賦缺省值斥黑,例如整型的缺省值為 0闽瓢,引用類型的缺省值為 null 等等。
并且心赶,JVM 會(huì)為對(duì)象頭進(jìn)行必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類的實(shí)例缺猛,如何才能找到類的元數(shù)據(jù)信息缨叫,對(duì)象的 Hash Code,對(duì)象的 GC 分帶年齡等等椭符,這些信息都存放在對(duì)象的對(duì)象頭中。
調(diào)用對(duì)象的實(shí)例化方法 <init>
在 JVM 完善好對(duì)象內(nèi)存布局的信息后耻姥,會(huì)調(diào)用對(duì)象的 <init>
方法销钝,根據(jù)傳入的屬性值為對(duì)象的變量賦值。
我們?cè)谏厦娼榻B了類加載的過程(加載 -> 鏈接 -> 初始化)琐簇,在初始化這一步驟蒸健,JVM 為類的靜態(tài)變量進(jìn)行賦值,并且執(zhí)行了靜態(tài)代碼塊婉商。實(shí)際上這一步驟是由 JVM 生成的 <clinit>
方法完成的似忧。
<clinit>
的執(zhí)行的順序?yàn)椋?/p>
- 父類靜態(tài)變量初始化
- 父類靜態(tài)代碼塊
- 子類靜態(tài)變量初始化
- 子類靜態(tài)代碼塊
而我們?cè)趧?chuàng)建實(shí)例 new 一個(gè)對(duì)象時(shí),會(huì)調(diào)用該對(duì)象類構(gòu)造器進(jìn)行初始化丈秩,這里面就會(huì)執(zhí)行 <init>
方法盯捌。
<init>
的執(zhí)行順序?yàn)椋?/p>
- 父類變量初始化
- 父類普通代碼塊
- 父類構(gòu)造函數(shù)
- 子類變量初始化
- 子類普通代碼塊
- 子類構(gòu)造函數(shù)
關(guān)于<init>
方法:
- 有多少個(gè)構(gòu)造器就會(huì)有多少個(gè)
<init>
方法 -
<init>
具體執(zhí)行的內(nèi)容包括非靜態(tài)變量的賦值操作,非靜態(tài)代碼塊的執(zhí)行蘑秽,與構(gòu)造器的代碼 - 非靜態(tài)代碼賦值操作與非靜態(tài)代碼塊的執(zhí)行是從上至下順序執(zhí)行饺著,構(gòu)造器在最后執(zhí)行
關(guān)于 <clinit>
與 <init>
方法的差異:
-
<clinit>
方法在類加載的初始化步驟執(zhí)行,<init>
在進(jìn)行實(shí)例初始化時(shí)執(zhí)行 -
<clinit>
執(zhí)行靜態(tài)變量的賦值與執(zhí)行靜態(tài)代碼塊肠牲,而<init>
執(zhí)行非靜態(tài)變量的賦值與執(zhí)行非靜態(tài)代碼塊以及構(gòu)造器
在棧中新建對(duì)象的引用幼衰,并指向堆中的實(shí)例
這一點(diǎn)沒什么好解釋的,我們是通過操作棧的引用來操作一個(gè)對(duì)象的缀雳。
總結(jié)
如果可以這么詳細(xì)地將 new 一個(gè)對(duì)象的過程表達(dá)出來渡嚣,這個(gè)回答我想應(yīng)該是滿分了。其實(shí)也不難俏险,我們只需要記住严拒,new 一個(gè)對(duì)象可以分為:
- 類加載
- 為對(duì)象分配內(nèi)存空間
- 完善對(duì)象內(nèi)存布局信息
- 調(diào)用對(duì)象的實(shí)例化方法
<init>
- 在棧中新建對(duì)象的引用,并指向堆中的實(shí)例
以上這五個(gè)步驟竖独,并對(duì)每個(gè)步驟進(jìn)行細(xì)分與歸納即可~
6裤唠、Java 對(duì)象的訪問方式有哪些?
答
在 JVM 規(guī)范中只規(guī)定了 reference 類型是一個(gè)指向?qū)ο蟮囊糜。珱]有規(guī)定這個(gè)引用具體如何去定位种蘸,訪問堆中對(duì)象,因此對(duì)象的訪問取決于 JVM 的具體實(shí)現(xiàn)竞膳,目前主流的訪問對(duì)象的方式有兩種:句柄間接訪問 與 直接指針訪問航瞭。
句柄間接訪問
所謂的句柄間接訪問是指,JVM 堆中會(huì)劃分一塊內(nèi)存來作為句柄池坦辟,reference 中存儲(chǔ)句柄的地址刊侯,句柄中則存儲(chǔ)對(duì)象的實(shí)例數(shù)據(jù)以及類的元數(shù)據(jù)的地址,所以我們通過訪問句柄進(jìn)而達(dá)到訪問對(duì)象的目的锉走。
句柄的英文是 “Handle”滨彻。這個(gè)詞的翻譯最早追述于 David Gries所著的《Compiler Construction for Digital Computer》(1971)有句話 "A handle of any sentential form is a leftmost simple phrase." 藕届。該書中譯本,《數(shù)字計(jì)算機(jī)的編譯程序構(gòu)造》(仲萃豪譯, 1976 版)翻譯成 “任一句型的句柄就是此句型的最左簡(jiǎn)單短語(yǔ)”亭饵。
直接指針訪問
直接指針訪問對(duì)象的方式為:JVM 堆中會(huì)存放訪問訪問類的元數(shù)據(jù)的地址休偶,reference存儲(chǔ)的是對(duì)象實(shí)例的地址:
我們看到,通過句柄訪問對(duì)象使用的是一種間接引用(2次引用)的方式來進(jìn)行訪問堆內(nèi)存的對(duì)象辜羊,它導(dǎo)致的缺點(diǎn)是運(yùn)行的速度稍微慢一些踏兜;通過直接指針的方式則速度快一些,因?yàn)樗倭艘淮沃羔樁ㄎ坏拈_銷八秃,所以碱妆,當(dāng)前最主流的 JVM: HotSpot 采用的就是直接指針這種方式來訪問堆區(qū)的對(duì)象。
總結(jié)
本題考查的是 JVM 比較基礎(chǔ)的問題喜德,看示意圖就非常容易理解哦~
7山橄、分析程序的運(yùn)行結(jié)果,并解釋為什么舍悯?
程序一:
public class Main {
public static void main(String[] args) {
int a = 1000;
int b = 1000;
System.out.println(a == b);
}
}
程序二:
public class Main {
public static void main(String[] args) {
Integer a = 1000;
Integer b = 1000;
System.out.println(a == b);
}
}
程序三:
public class Main {
public static void main(String[] args) {
Integer a = 1;
Integer b = 1;
System.out.println(a == b);
}
}
程序四:
public class Main {
public static void main(String[] args) {
Integer a = new Integer(1);
Integer b = new Integer(1);
System.out.println(a == b);
}
}
答
- 程序一輸出結(jié)果為:true
- 程序二輸出結(jié)果為:false
- 程序三輸出結(jié)果為:true
- 程序四輸出結(jié)果為:false
首先航棱,程序一輸出結(jié)果為 true 肯定沒什么好解釋的,本題考察的重點(diǎn)在于對(duì)后面程序輸出結(jié)果的分析萌衬。
Integer 是 int 的裝箱類型饮醇,它修飾的是一個(gè)對(duì)象。當(dāng)我們使用 Integer a = xxx;
的方式聲明一個(gè)變量時(shí)秕豫,Java 實(shí)際上會(huì)調(diào)用 Integer.valueOf()
方法朴艰。
我們來看下 Integer.valueOf()
的源代碼:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
JDK 文檔中的說明:
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
也就是說,由于 -128 ~ 127 這個(gè)區(qū)間的值使用頻率非常高混移,Java 為了減少申請(qǐng)內(nèi)存的開銷祠墅,將這些對(duì)象存儲(chǔ)在 IntegerCache 中。
所以歌径,如果使用 Integer 聲明的值在 -128 ~ 127 這個(gè)區(qū)間內(nèi)的話毁嗦,就會(huì)直接從常量池中取出并返回,于是我們看到回铛,程序二輸出的結(jié)果為 false狗准,因?yàn)?Integer = 1000;Integer b = 1000;
a 和 b 的值并不是從常量池中取出的,它們指向的是堆中兩塊不同的內(nèi)存區(qū)域茵肃。而程序三:Integer a = 1;Integer b = 1;
中的 a 和 b 指向的都是常量池中同一塊內(nèi)存腔长,所以結(jié)果返回 true。
對(duì)于程序四的輸出結(jié)果验残,我們需要知道捞附,當(dāng) new 一個(gè)對(duì)象時(shí),則一定會(huì)在堆中開辟一塊新的內(nèi)存保存對(duì)象,所以 a 和 b 指向的是不同的內(nèi)存區(qū)域鸟召,結(jié)果自然返回 false~
總結(jié)
還是一道老掉牙的題目想鹰,不過一些走排場(chǎng)的筆試題中還是有出現(xiàn)過的。
8药版、說一下 Error 和 Exception 的區(qū)別?
答
先上圖:
首先喻犁,Error類和Exception類都繼承自Throwable類槽片。
先談一下 Error 吧~
Error表示系統(tǒng)級(jí)的錯(cuò)誤,一般是指與虛擬機(jī)相關(guān)的問題肢础,由虛擬機(jī)生成并拋出还栓,常見的虛擬機(jī)錯(cuò)誤有:OutOfMemoryError
,StackOverflowError
等等传轰。
OutOfMemoryError
剩盒,StackOverflowError
這兩種錯(cuò)誤是要求大家務(wù)必掌握的。
StackOverflowError
慨蛙,即棧溢出錯(cuò)誤辽聊,一般無限制地遞歸調(diào)用會(huì)導(dǎo)致 StackOverflowError
的發(fā)生,所以期贫,再一次提醒大家跟匆,在寫遞歸函數(shù)的時(shí)候一定要寫 base case,否則就會(huì)導(dǎo)致棧溢出錯(cuò)誤的發(fā)生通砍。
如程序:
public class StackOverflowErrorTest {
public static void foo(){
System.out.println("StackOverflowError");
foo();
}
public static void main(String[] args) {
foo();
}
}
該程序會(huì)導(dǎo)致拋出 StackOverflowError
玛臂。
OutOfMemoryError
,即堆內(nèi)存溢出錯(cuò)誤,導(dǎo)致 OutOfMemoryError
可能有如下幾點(diǎn)原因:
- JVM啟動(dòng)參數(shù)內(nèi)存值設(shè)定過小
- 代碼中存在死循環(huán)導(dǎo)致產(chǎn)生過多對(duì)象實(shí)體
- 內(nèi)存中加載的數(shù)據(jù)量過于龐大封孙,一次從數(shù)據(jù)庫(kù)取出過多的數(shù)據(jù)也會(huì)導(dǎo)致堆溢出
- 集合類中有對(duì)對(duì)象的引用迹冤,使用完后未清空,使得JVM無法回收
如程序:
public class OutOfMemoryErrorTest {
public static void main(String[] args) {
while (true){
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) { }
}).start();
}
}
}
該程序?yàn)橐粋€(gè)不斷創(chuàng)建新線程的死循環(huán)虎忌,運(yùn)行會(huì)拋出 OutOfMemoryError
泡徙。
接下來,我們?cè)賮碚勔幌率裁?Exception呐籽?(一本正經(jīng))
Exception表示異常锋勺,通俗地講,它表示如果程序運(yùn)行正常狡蝶,則不會(huì)發(fā)生的情況庶橱。
Exception可以劃分為
- 運(yùn)行時(shí)異常(RuntimeException)
- 非運(yùn)行時(shí)異常
或者也可以劃分為:
- 受檢查異常(CheckedException)
- 不受檢查異常(UncheckedException)
實(shí)際上,運(yùn)行時(shí)異常就是不受檢查異常贪惹。
什么是運(yùn)行時(shí)異常(RuntimeException)苏章,或者說什么是不受檢查異常(UncheckedException)呢?
通俗地講,不受檢查異常是指程序員沒有細(xì)心檢查代碼枫绅,造成例如:空指針泉孩,數(shù)組越界等情況導(dǎo)致的異常。這些異常通常在編碼過程中是能夠避免的并淋。并且寓搬,我可以在代碼中直接拋出一個(gè)運(yùn)行時(shí)異常,程序編譯不會(huì)出錯(cuò)县耽,譬如這段代碼:
public class Test {
public static void main(String[] args) {
throw new IllegalArgumentException("wrong");
}
}
什么是受檢查異常呢句喷?
受檢查異常是指在編譯時(shí)被強(qiáng)制檢查的異常。受檢查異常要么使用try-catch
語(yǔ)句進(jìn)行捕獲兔毙,要么使用throws
向上拋出唾琼,否則是無法通過編譯的。常見的受檢查異常有:FileNotFoundException
澎剥,SQLException
等等锡溯。
總結(jié)
細(xì)心的小伙伴一定會(huì)發(fā)現(xiàn),我的解答中實(shí)際上涵蓋了很多的考點(diǎn)哑姚,面試官可以采用多種問法來考察你對(duì) Java 異常體系的了解程度祭饭。譬如:哪些情況會(huì)發(fā)生 OutOfMemoryError
?什么是運(yùn)行時(shí)異常蜻懦,什么是受檢查異常甜癞?請(qǐng)列舉一些常見的運(yùn)行時(shí)異常和受檢查異常?等等......
在這道題目的回答中宛乃,我已經(jīng)將上述問題的答案都寫進(jìn)去了悠咱,慢慢尋找吧~
9、當(dāng)代碼執(zhí)行到 try 塊時(shí)征炼,finally 塊一定會(huì)被執(zhí)行么析既?
答
不一定。
有兩種情況會(huì)導(dǎo)致即便代碼執(zhí)行到 try 塊谆奥,finally 塊也有可能不執(zhí)行:
- 系統(tǒng)終止
- 守護(hù)線程被終止
示例程序一:
package com.github.test;
public class Test {
public static void main(String[] args) {
foo();
}
public static void foo() {
try {
System.out.println("In try block...");
System.exit(0);
} finally {
System.out.println("In finally block...");
}
}
}
該程序運(yùn)行的結(jié)果為:
In try block...
原因在于眼坏,try 塊中,我們使用了 System.exit(0)
方法酸些,該方法會(huì)終止當(dāng)前正在運(yùn)行的虛擬機(jī)宰译,也就是終止了系統(tǒng),既然系統(tǒng)被終止魄懂,自然而然也就執(zhí)行不到 finally 塊的代碼了沿侈。
示例程序二:
package com.github.test;
public class Test {
public static void main(String[] args) {
Thread thread = new Thread(new Task());
thread.setDaemon(true);
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Task implements Runnable {
@Override
public void run() {
try {
System.out.println("In try block...");
Thread.sleep(5000); // 阻塞線程 5 s
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("In finally block");
}
}
}
該程序的輸出結(jié)果為:
In try block...
Java 的線程可以分為兩大類:
- Daemon Thread(守護(hù)線程)
- User Thread(用戶線程)
所謂的守護(hù)線程就是指程序運(yùn)行的時(shí)候,再后臺(tái)提供一種通用服務(wù)的線程市栗,比如垃圾回收線程就是一個(gè)守護(hù)線程缀拭。守護(hù)線程并不屬于程序中不可或缺的部分咳短,因此,當(dāng)所有的用戶線程結(jié)束蛛淋,程序也就終止咙好,程序終止的同時(shí)也會(huì)殺死進(jìn)程中所有的守護(hù)線程。
上面的實(shí)例程序中褐荷,main 執(zhí)行完畢勾效,程序就終止了,所以守護(hù)線程也就被殺死叛甫,finally 塊的代碼也就無法執(zhí)行到了葵第。
總結(jié)
現(xiàn)在 finally 使用的很少了,關(guān)閉資源都會(huì)選擇 try with resources合溺。不過這道題仍然是一個(gè)比較經(jīng)典的題目~
10、談一談你對(duì) Java 異常處理的心得缀台?
答
本題是一道開放性問答題棠赛,答案并不唯一。面試官旨在考察面試者對(duì) Java 異常的理解膛腐,本回答為我個(gè)人對(duì)異常處理的心得體會(huì)睛约,并非標(biāo)準(zhǔn)答案,如果大家有更好的回答哲身,可以評(píng)論提醒我進(jìn)行查漏補(bǔ)缺辩涝。
原則一:使用 try-with-resources 來關(guān)閉資源
《Effective Java》中給出的一條最佳實(shí)踐是:Prefer try-with-resources to try-finally 。
我們知道勘天,Java 類庫(kù)中包含許多必須通過調(diào)用 close 方法手動(dòng)關(guān)閉資源的類怔揩,比如:InputStream
,OutputStream
脯丝,java.sql.Connection
等等商膊。在 JDK 1.7 以前,try-finally
語(yǔ)句是保證資源正確關(guān)閉的最佳實(shí)踐宠进。
不過晕拆,try-finally
帶來的最大問題有兩點(diǎn):
- 有一些資源需要保證按順序關(guān)閉
- 當(dāng)我們的代碼中引入了很多需要關(guān)閉的資源時(shí),代碼就會(huì)變得冗長(zhǎng)難以維護(hù)
從 JDK 1.7 開始材蹬,便引入了 try-with-resources
实幕,這些問題一下子都得到了解決。使用 try-with-resouces
這個(gè)構(gòu)造的前提是堤器,資源必須實(shí)現(xiàn)了 AutoCloseable
接口昆庇。Java 類庫(kù)和第三方類庫(kù)中的許多類和接口現(xiàn)在都實(shí)現(xiàn)或繼承了 AutoCloseable 接口。
所以吼旧,我們應(yīng)該使用 try-with-resources
代替 try-finally
來關(guān)閉資源凰锡。
原則二:如果你需要使用到 finally,那么請(qǐng)避免在 finally 塊中使用 return 語(yǔ)句
我們來看兩個(gè)示例程序
程序一:
package com.github.test;
public class Test {
public static int test() {
int i = 1;
try {
Integer.valueOf("abc");
} catch (NumberFormatException e) {
i++;
return i;
} finally {
i++;
return i;
}
}
public static void main(String[] args) {
System.out.println(test());
}
}
程序二:
package com.github.test;
public class Test {
public static int test() {
int i = 1;
try {
Integer.valueOf("abc");
i++;
} catch (NumberFormatException e) {
i++;
return i;
} finally {
i++;
}
return i;
}
public static void main(String[] args) {
System.out.println(test());
}
}
程序一的輸出結(jié)果為:
3
程序二的輸出結(jié)果為:
2
導(dǎo)致兩個(gè)程序輸出不同結(jié)果的原因在于:程序一,我們將 return 語(yǔ)句寫在了 finally 塊中掂为;而程序二則是將 return 語(yǔ)句寫在了代碼的最后部分裕膀。
在 finally 塊中寫 return 語(yǔ)句是一種非常不好的實(shí)踐,因?yàn)槌绦驎?huì)將 try-catch 塊里面的語(yǔ)句勇哗,或者是拋出的異常全部丟棄掉昼扛。如上面的代碼,Integer.valueOf("abc");
會(huì)拋出一個(gè) NumberFormatException 欲诺,該異常被 catch 捕獲處理抄谐,我們的本意是,在 catch 塊中將異常處理并返回扰法,但是由于示例一 finally 塊中有 return 語(yǔ)句蛹含,導(dǎo)致 catch 塊的返回值被丟棄。
我們需要銘記一點(diǎn)塞颁,如果 finally 代碼塊中有 return 語(yǔ)句浦箱,那么程序會(huì)優(yōu)先返回 finally 塊中 return 的結(jié)果。
為了避免這樣的事情發(fā)生祠锣,我們應(yīng)該避免在 finally 塊中使用 return 語(yǔ)句酷窥。
原則三:Throw early,Catch late
關(guān)于異常處理伴网,有一個(gè)非常著名的原則叫做:Throw early缓屠,Catch late疏哗。
微信公眾號(hào)防鏈接丟失:
https://howtodoinjava.com/best-practices/java-exception-handling-best-practices
Remember “Throw early catch late” principle. This is probably the most famous principle about Exception handling. It basically says that you should throw an exception as soon as you can, and catch it late as much as possible. You should wait until you have all the information to handle it properly.
This principle implicitly says that you will be more likely to throw it in the low-level methods, where you will be checking if single values are null or not appropriate. And you will be making the exception climb the stack trace for quite several levels until you reach a sufficient level of abstraction to be able to handle the problem.
上文的含義是川抡,遇到異常较曼,你應(yīng)該盡早地拋出,并且盡可能晚地捕獲它动分。如果當(dāng)前方法會(huì)拋出一個(gè)異常馋评,我們應(yīng)該判斷,該異常是否應(yīng)該交給這個(gè)方法處理刺啦,如果不是留特,那么最好的選擇是將這個(gè)異常向上拋出,交給更高的調(diào)用級(jí)去處理它玛瘸。
這樣做的好處是蜕青,我們可以打印出更多的異常棧軌跡(Stacktrace),從最頂層的邏輯開始逐步向下糊渊,清楚地看到方法調(diào)用關(guān)系右核,以便我們理清報(bào)錯(cuò)原因。
原則四:捕獲具體的異常渺绒,而不是它的父類
如果某個(gè)被調(diào)用的模塊拋出了多個(gè)異常贺喝,那么只捕獲這些異常的父類是非常不好的實(shí)踐菱鸥。
例如,某一個(gè)模塊拋出了 FileNotFoundException
和 IOException
躏鱼,那么調(diào)用這個(gè)模塊的代碼最好使用 catch 語(yǔ)句的級(jí)聯(lián)分別捕獲這兩個(gè)異常氮采,而不是只寫一個(gè) Exception 的 catch 塊。
try {
// ...
}catch(FileNotFoundException e) {
// handle
}catch(IOException e) {
// handle
}
總結(jié)
這個(gè)問題是一個(gè)非常好的問題染苛,我們其實(shí)可以發(fā)揮更多的空間鹊漠,譬如談一下如何避免 OOM ——當(dāng)我們的內(nèi)存中加載的數(shù)據(jù)量過于龐大,一次從數(shù)據(jù)庫(kù)取出過多的數(shù)據(jù)或者是讀取一個(gè)非常大的文件時(shí)很容易導(dǎo)致 OOM茶行,所以我們可以使用 Buffer 來緩沖避免一次讀取太多的數(shù)據(jù)躯概,從而達(dá)到避免 OOM 的發(fā)生......
總結(jié)
今天我主要分享了 Java 基礎(chǔ)部分的一些常考題和知識(shí)點(diǎn)畔师,雖然只有十道題娶靡,但是也涵蓋了非常多的知識(shí)點(diǎn),希望看到這篇文章的你能受益良多看锉。
后續(xù)的內(nèi)容我會(huì)盡快更新固蛾,不過為了保證內(nèi)容的質(zhì)量,也可能沒那么快
好啦度陆,至此為止,這篇文章就到這里了~歡迎大家關(guān)注我的公眾號(hào)献幔,在這里希望你可以收獲更多的知識(shí)懂傀,我們下一期再見!