Java 面試八股文之基礎(chǔ)篇(一)

前言

image

從今天開始,我將開啟一個(gè)系列的文章——【 Java 面試八股文】。

這個(gè)系列會(huì)陸續(xù)更新 Java 面試中的高頻問題,旨在從問題出發(fā),理解 Java 基礎(chǔ),數(shù)據(jù)結(jié)構(gòu)與算法,數(shù)據(jù)庫,常用框架等。

首先要做幾點(diǎn)說明:

  1. 【 Java 面試八股文】中的面試題來源于社區(qū)論壇,書籍等資源;感謝使我讀到這些寶貴的面經(jīng)的作者們。
  2. 對(duì)于【 Java 面試八股文】中的每個(gè)問題,我都會(huì)盡可能地寫出我自己認(rèn)為的“完美解答”。但是畢竟我的身份不是一個(gè)“真理持有者”,只是一個(gè)秉承著開源分享精神的 “knowledge transmitter” & 菜雞,所以,如果這些答案出現(xiàn)了錯(cuò)誤,可以留言寫出你認(rèn)為更好的解答,并指正我。非常感謝您的分享。
  3. 知識(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è)生命周期為:

image

各個(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 語言只有值傳遞

Java 語言之所以只有值傳遞,是因?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。

image

程序二中,Java 會(huì)將引用的地址復(fù)制一份放在棧區(qū),復(fù)制的拷貝和原始引用都指向堆區(qū)的同一個(gè)對(duì)象。方法通過拷貝地址找到堆區(qū)的實(shí)例,對(duì)堆區(qū)的實(shí)例進(jìn)行修改,而此時(shí),原始引用仍然指向著堆區(qū)的實(shí)例,所以程序二的輸出結(jié)果為:[1,1,1,1,1]

image
總結(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è)過程:

  1. 類加載
  2. 為對(duì)象分配內(nèi)存空間
  3. 完善對(duì)象內(nèi)存布局信息
  4. 調(diào)用對(duì)象的實(shí)例化方法<init>
  5. 在棧中新建對(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è)生命周期為:

image

各個(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)系:

image

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ì)象大小相等的距離。

image

而如果 JVM 堆內(nèi)存并不是規(guī)整的,即:已用內(nèi)存空間與空閑內(nèi)存相互交錯(cuò),JVM 會(huì)維護(hù)一個(gè)空閑列表,記錄哪些內(nèi)存塊是可用的,在為該對(duì)象分配空間時(shí),JVM 會(huì)從空閑列表中找到一塊足夠大的空間劃分給對(duì)象使用。

image
完善對(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>

  1. 父類靜態(tài)變量初始化
  2. 父類靜態(tài)代碼塊
  3. 子類靜態(tài)變量初始化
  4. 子類靜態(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>

  1. 父類變量初始化
  2. 父類普通代碼塊
  3. 父類構(gòu)造函數(shù)
  4. 子類變量初始化
  5. 子類普通代碼塊
  6. 子類構(gòu)造函數(shù)

關(guān)于<init> 方法:

  1. 有多少個(gè)構(gòu)造器就會(huì)有多少個(gè) <init> 方法
  2. <init> 具體執(zhí)行的內(nèi)容包括非靜態(tài)變量的賦值操作,非靜態(tài)代碼塊的執(zhí)行,與構(gòu)造器的代碼
  3. 非靜態(tài)代碼賦值操作與非靜態(tài)代碼塊的執(zhí)行是從上至下順序執(zhí)行,構(gòu)造器在最后執(zhí)行

關(guān)于 <clinit><init> 方法的差異:

  1. <clinit>方法在類加載的初始化步驟執(zhí)行,<init> 在進(jìn)行實(shí)例初始化時(shí)執(zhí)行
  2. <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ì)象可以分為:

  1. 類加載
  2. 為對(duì)象分配內(nèi)存空間
  3. 完善對(duì)象內(nèi)存布局信息
  4. 調(diào)用對(duì)象的實(shí)例化方法<init>
  5. 在棧中新建對(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ì)象的目的。

image

句柄的英文是 “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)單短語”。

直接指針訪問

直接指針訪問對(duì)象的方式為:JVM 堆中會(huì)存放訪問訪問類的元數(shù)據(jù)的地址,reference存儲(chǔ)的是對(duì)象實(shí)例的地址:

image

我們看到,通過句柄訪問對(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ū)別?


先上圖:

image

首先,Error類和Exception類都繼承自Throwable類。

先談一下 Error 吧~

Error表示系統(tǒng)級(jí)的錯(cuò)誤,一般是指與虛擬機(jī)相關(guān)的問題,由虛擬機(jī)生成并拋出,常見的虛擬機(jī)錯(cuò)誤有:OutOfMemoryErrorStackOverflowError 等等。

OutOfMemoryErrorStackOverflowError 這兩種錯(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)原因:

  1. JVM啟動(dòng)參數(shù)內(nèi)存值設(shè)定過小
  2. 代碼中存在死循環(huán)導(dǎo)致產(chǎn)生過多對(duì)象實(shí)體
  3. 內(nèi)存中加載的數(shù)據(jù)量過于龐大,一次從數(shù)據(jù)庫取出過多的數(shù)據(jù)也會(huì)導(dǎo)致堆溢出
  4. 集合類中有對(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語句進(jìn)行捕獲,要么使用throws向上拋出,否則是無法通過編譯的。常見的受檢查異常有:FileNotFoundExceptionSQLException等等。

總結(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í)行:

  1. 系統(tǒng)終止
  2. 守護(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 類庫中包含許多必須通過調(diào)用 close 方法手動(dòng)關(guān)閉資源的類,比如:InputStreamOutputStreamjava.sql.Connection 等等。在 JDK 1.7 以前,try-finally 語句是保證資源正確關(guān)閉的最佳實(shí)踐。

不過,try-finally 帶來的最大問題有兩點(diǎn):

  1. 有一些資源需要保證按順序關(guān)閉
  2. 當(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 類庫和第三方類庫中的許多類和接口現(xiàn)在都實(shí)現(xiàn)或繼承了 AutoCloseable 接口。

所以,我們應(yīng)該使用 try-with-resources 代替 try-finally 來關(guān)閉資源。

原則二:如果你需要使用到 finally,那么請(qǐng)避免在 finally 塊中使用 return 語句

我們來看兩個(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 語句寫在了 finally 塊中;而程序二則是將 return 語句寫在了代碼的最后部分。

在 finally 塊中寫 return 語句是一種非常不好的實(shí)踐,因?yàn)槌绦驎?huì)將 try-catch 塊里面的語句,或者是拋出的異常全部丟棄掉。如上面的代碼,Integer.valueOf("abc"); 會(huì)拋出一個(gè) NumberFormatException ,該異常被 catch 捕獲處理,我們的本意是,在 catch 塊中將異常處理并返回,但是由于示例一 finally 塊中有 return 語句,導(dǎo)致 catch 塊的返回值被丟棄。

我們需要銘記一點(diǎn),如果 finally 代碼塊中有 return 語句,那么程序會(huì)優(yōu)先返回 finally 塊中 return 的結(jié)果

為了避免這樣的事情發(fā)生,我們應(yīng)該避免在 finally 塊中使用 return 語句。

原則三: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è)模塊拋出了 FileNotFoundExceptionIOException ,那么調(diào)用這個(gè)模塊的代碼最好使用 catch 語句的級(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ù)庫取出過多的數(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ì)量,也可能沒那么快

image

好啦,至此為止,這篇文章就到這里了~歡迎大家關(guān)注我的公眾號(hào),在這里希望你可以收獲更多的知識(shí),我們下一期再見!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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