【面經】Java面試突擊

yyyyfly發表於2022-03-11

基礎語法

基本資料結構

Java 的基本資料型別有 8 種,包括 6 種數字型別、1 種字元型別和 1 種布林型別。

基本資料型別總覽

數字型別包括 4 種整數型別和 2 種浮點數型別,4 種整數型別是 byte、short、int 和 long,2 種浮點數型別是 float 和 double。

字元型別是 char,用於表示單個字元。Java 使用統一碼對字元進行編碼。

布林型別是 boolean,包括 true 和 false 兩種取值。

【面經】Java面試突擊

數字型別直接量

直接量是在程式中直接出現的常量值。

將整數型別的直接量賦值給整數型別的變數時,只要直接量沒有超出變數的取值範圍,即可直接賦值,如果直接量超出了變數的取值範圍,則會導致編譯錯誤。

整數型別的直接量預設是 int 型別,如果直接量超出了 int 型別的取值範圍,則必須在其後面加上字母 L 或 l,將直接量顯性宣告為 long 型別,否則會導致編譯錯誤。

浮點型別的直接量預設是 double 型別,如果要將直接量表示成 float 型別,則必須在其後面加上字母 F 或 f。將 double 型別的直接量賦值給 float 型別的變數是不允許的,會導致編譯錯誤。

基本資料型別之間的轉換

有時需要把不同型別的值混合運算,因此需要對資料型別進行轉換。

數字型別轉換

不同的數字型別對應不同的範圍,按照範圍從小到大的順序依次是:byte、short、int、long、float、double。

將小範圍型別的變數轉換為大範圍型別稱為拓寬型別,不需要顯性宣告型別轉換。將大範圍型別的變數轉換為小範圍型別稱為縮窄型別,必須顯性宣告型別轉換,否則會導致編譯錯誤。

字元型別與數字型別之間的轉換

字元型別與數字型別之間可以進行轉換。

將數字型別轉換成字元型別時,只使用整數的低 16 位(浮點數型別將整數部分轉換成字元型別)。

將字元型別轉換成數字型別時,字元的統一碼轉換成指定的數值型別。如果字元的統一碼超出了轉換成的數值型別的取值範圍,則必須顯性宣告型別轉換。

布林型別不能與其他基本資料型別進行轉換

布林型別不能轉換成其他基本資料型別,其他基本資料型別也不能轉換成布林型別。

方法

Java 中的方法,在其他語言中也可能被稱為過程或函式,是為執行一個操作而組合在一起的語句組。如果一個操作會被多次執行,則可以將該操作定義成一個方法,執行該操作的時候呼叫方法即可。

方法的語法結構

方法包括方法頭和方法體,方法頭又可以分成修飾符、返回值型別、方法名和引數列表,因此方法包括 5 個部分。

  • 修飾符:修飾符是可選的,告訴編譯器如何呼叫該方法。
  • 返回值型別:方法可以返回一個值,此時返回值型別是方法要返回的值的資料型別。方法也可以沒有返回值,此時返回值型別是 void。
  • 方法名:方法的實際名稱。
  • 引數列表:定義在方法頭中的變數稱為形式引數或引數,簡稱形參。當呼叫方法時,需要給引數傳遞一個值,稱為實際引數,簡稱實參。引數列表指明方法中的引數型別、次序和數量。引數是可選的,方法可以不包含引數。
  • 方法體:方法體包含具體的語句集合。

方法名和參數列共同構成方法簽名。

引數的值傳遞

呼叫方法時,需要提供實參,實參必須與形參的次序相同,稱為引數順序匹配。實參必須與方法簽名中的形參在次序上和數量上匹配,在型別上相容,相容的意思是不需要顯性宣告型別轉換,即型別相同或者型別轉換為拓寬型別。

在呼叫帶引數的方法時,實參的值賦給形參,稱為值傳遞。Java 中只有值傳遞,無論形參在方法中如何改變,實參不受影響。

  • 當引數型別是基本資料型別時,傳遞的是實參的值,因此不能對實參進行修改。
  • 當引數型別是物件時,傳遞的是物件的引用,此時可以對實參引用的物件進行修改,但是不能讓實參引用新的物件。

方法的過載

方法的過載是指在同一個類中的多個方法有相同的名稱,但是方法簽名不同,編譯器能夠根據方法簽名決定呼叫哪個方法。由於方法簽名由方法名和參數列共同構成,因此方法的過載等同於多個方法有相同的名稱和不同的引數列表。

方法的過載可以增加程式的可讀性,執行相似操作的方法應該有相同的名稱。

關於方法的過載,需要注意以下兩點。

  • 方法簽名只由方法名和引數列表共同構成,因此被過載的方法必須具有不同的引數列表,而不能通過不同的修飾符和返回值型別進行方法的過載。
  • 如果一個方法呼叫有多個可能的匹配,則編譯器會呼叫最合適的匹配方法,如果編譯器無法判斷哪個方法最匹配,則稱為歧義呼叫,會導致編譯錯誤。

下面用兩段示例程式碼說明方法的過載。

檢視程式碼
public class Main {
    public static void main(String[] args) {
        getSum(1, 2);
        getSum(1.5, 2.5);
        getSum(5, 5.5);
    }

    public static void getSum(int num1, int num2) {
        System.out.println(num1 + "+" + num2 + "=" + (num1 + num2));
    }

    public static void getSum(double num1, double num2) {
        System.out.println(num1 + "+" + num2 + "=" + (num1 + num2));
    }
}
public class Main {
    public static void main(String[] args) {
        getSum(1, 2);// 歧義呼叫,編譯錯誤
    }

    public static void getSum(int num1, double num2) {
        System.out.println(num1 + "+" + num2 + "=" + (num1 + num2));
    }

    public static void getSum(double num1, int num2) {
        System.out.println(num1 + "+" + num2 + "=" + (num1 + num2));
    }
}

在示例 1 中,getSum(1, 2) 呼叫的是引數為兩個 int 型的方法,getSum(1.5, 2.5) 和 getSum(5, 5.5) 呼叫的是引數為兩個 double 型的方法,因此執行上述程式碼得到的輸出結果是:

1+2=3
1.5+2.5=4.0
5.0+5.5=10.5

在示例 2 中,getSum(1, 2) 可以同時匹配兩個方法,任何一個方法都不比另一個方法更匹配,因此為歧義呼叫,導致編譯錯誤。

遞迴

程式呼叫自身的程式設計技巧稱為遞迴。遞迴方法是直接或間接呼叫自身的方法。

遞迴的要點

定義遞迴方法時,需要定義遞迴的初始狀態、初始狀態的處理和遞迴呼叫。

初始狀態也稱為終止條件,即最簡單的情況,此時應該直接給出如何處理初始狀態。

對於非初始狀態,則需要進行遞迴呼叫,對子問題進行求解,直到初始狀態,然後將結果返回給呼叫者,直到傳回原始的呼叫者。

遞迴必須定義初始狀態,且保證所有的遞迴呼叫都能到達初始狀態,否則會發生無限遞迴,導致棧溢位。

遞迴的優點

遞迴的優點是程式碼簡潔且易於理解。如果問題滿足遞迴的特點,即可以分解成子問題且子問題與原始問題相似,則可以使用遞迴給出自然、直接、簡單的解法。

遞迴的缺點

時間和空間的消耗比較大。每一次函式呼叫都需要在記憶體棧中分配空間,對棧的操作還需要時間,因此時間複雜度和空間複雜度都會比較高。

如果子問題之間存在重疊,則在不加記憶化的情況下,遞迴會產生重複計算,導致時間複雜度過高。

由於棧的空間有限,如果遞迴呼叫的次數太多,則可能導致呼叫棧溢位。

尾遞迴

當遞迴呼叫是方法中最後執行的語句且它的返回值不屬於表示式的一部分時,這個遞迴呼叫就是尾遞迴。

尾遞迴的特點是在返回時直接傳回原始的呼叫者,而不用經過中間的呼叫者,這個特點很重要,因為大多數現代的編譯器會利用該特點自動生成優化的程式碼。

使用尾遞迴代替普通的遞迴,可以在時間和空間方面都帶來顯著的提升。

示例程式碼

以下程式碼是計算斐波那契數的普通遞迴和尾遞迴的實現。

使用普通遞迴,會產生大量重複計算,導致時間複雜度過高。

使用尾遞迴,則不會有重複計算。

public class Fibonacci {
    public static long fibonacci(long index) {
        if (index <= 1) {
            return index;
        } else {
            return fibonacci(index - 1) + fibonacci(index - 2);
        }
    }

    public static long fibonacciTailRecursion(long index) {
        return fibonacciTailRecursion(index, 0, 1);
    }

    public static long fibonacciTailRecursion(long index, int curr, int next) {
        if (index == 0) {
            return curr;
        } else {
            return fibonacciTailRecursion(index - 1, next, curr + next);
        }
    }
}

物件導向

物件導向的概念

物件導向和麵向過程的區別

程式導向:將問題分解成步驟,然後按照步驟實現函式,執行時依次呼叫函式。資料和對資料的操作是分離的。

物件導向:將問題分解成物件,描述事物在解決問題的步驟中的行為。物件與屬性和行為是關聯的。

程式導向的優點是效能比物件導向高,不需要物件導向的例項化;缺點是不容易維護、複用和擴充套件。

物件導向的優點是具有封裝、繼承、多型的特性,因而容易維護、複用和擴充套件,可以設計出低耦合的系統;缺點是由於需要例項化物件,因此效能比程式導向低。

物件和類

物件是現實世界中可以明確標識的實體,物件有自己獨有的狀態和行為。物件的狀態由資料域的集合構成,物件的行為由方法的集合構成。

類是定義同一型別物件的結構,是對具有相同特徵的物件的抽象。類是一個模板,用來定義物件的資料域和方法。可以從一個類建立多個物件,建立物件稱為例項化。

構造方法

構造方法是一種特殊的方法,呼叫構造方法可以建立新物件。構造方法可以執行任何操作,實際應用中,構造方法一般用於初始化操作,例如初始化物件的資料域。

定義和呼叫構造方法

構造方法的名稱必須和構造方法所在類的名稱相同。構造方法可以被過載,即允許在同一個類中定義多個引數列表不同的構造方法。

使用 new 操作符呼叫構造方法,通過呼叫構造方法建立物件。

預設構造方法

類可以不顯性宣告構造方法。此時類中隱性宣告瞭一個方法體為空的沒有引數的構造方法,稱為預設構造方法。只有當類中沒有顯性宣告任何構造方法時,才會有預設構造方法。

構造方法與普通方法的區別

構造方法與普通方法有三點區別。

  • 構造方法的名稱必須與所在的類的名稱相同。
  • 構造方法沒有返回型別,包括沒有 void。
  • 構造方法通過 new 操作符呼叫,通過呼叫構造方法建立物件。

示例程式碼

以下程式碼定義了一個類 Square,該類描述正方形。

每個正方形都包含邊長的資料域,確定邊長以後即可確定正方形的大小。

類中有兩個構造方法,無引數構造方法建立邊長為 1 的正方形物件,有引數構造方法建立指定邊長的正方形物件。

類中有兩個方法 getPerimeter 和 getArea,分別計算正方形的周長和麵積。

檢視程式碼
class Square {
    int side = 1;

    public Square() {
    }

    public Square(int newSide) {
        side = newSide;
    }

    public int getPerimeter() {
        return side * 4;
    }

    public int getArea() {
        return side * side;
    }
}

靜態和例項

Java 的類成員(成員變數、方法等)可以是靜態的或例項的。使用關鍵字 static 修飾的類成員是靜態的類成員,不使用關鍵字 static 修飾的類成員則是例項的類成員。

靜態和例項的區別

外部呼叫

從外部呼叫靜態的類成員時,可以通過類名呼叫,也可以通過物件名呼叫。從外部呼叫例項的類成員,則只能通過物件名呼叫。

例如對於字串型別 String,方法 format 是靜態的,可以通過 String.format 呼叫,而方法 length 是例項的,只能通過 str.length 呼叫,其中 str 是 String 型別的例項。

建議通過類名呼叫靜態的類成員,因為通過類名呼叫靜態的類成員是不需要建立物件的,而且可以提高程式碼的可讀性。

內部訪問

靜態方法只能訪問靜態的類成員,不能訪問例項的類成員。例項方法既可以訪問例項的類成員,也可以訪問靜態的類成員。

為什麼靜態方法不能訪問例項的類成員呢?因為例項的類成員是依賴於具體物件(例項)的,而靜態方法不依賴於任何例項,因此不存在靜態方法直接或間接地訪問例項或例項的類成員的情況。

判斷使用靜態或例項

如何判斷一個類成員應該被定義成靜態的還是例項的呢?取決於類成員是否依賴於具體例項。如果一個類成員依賴於具體例項,則該類成員應該被定義成例項的類成員,否則就應該被定義成靜態的類成員。

例如對於字串類 String,考慮方法 format 和方法 length。

  • 方法 format 的作用是建立格式化的字串,該方法不依賴於任何 String 的例項,因此是靜態方法(類成員)。
  • 方法 length 的作用是獲得字串的長度,由於字串的長度依賴於具體字串,因此該方法依賴於 String 的例項,是例項方法(類成員)。

對於數學類 Math,所有的類成員都不依賴於具體的例項,因此都被定義成靜態的類成員。

初始化塊

程式碼初始化塊屬於類成員,在載入類時或建立物件時會隱式呼叫程式碼初始塊。使用初始化塊的好處是可以減少多個構造器內的重複程式碼。

初始化塊的分類

初始化塊可以分成靜態初始化塊和非靜態初始化塊,前者在載入類時被隱式呼叫,後者在建立物件時被隱式呼叫。

單個類的初始化塊的執行順序

如果有初始化塊,則初始化塊會在其他程式碼之前被執行。具體而言,靜態初始化塊會在靜態方法之前被執行,非靜態初始化塊會在構造器和例項方法之前被執行。

由於靜態初始化塊在載入類時被呼叫,因此靜態初始化塊會最先執行,且只會執行一次。

由於非靜態初始化塊在建立物件時被呼叫,因此每次建立物件時都會執行非靜態初始化塊以及執行構造器。非靜態初始化塊的執行在靜態初始化塊的執行之後、構造器的執行之前。

存在繼承關係的初始化塊的執行順序
如果存在繼承關係,則在對子類進行類的載入和建立物件時,也會對父類進行類的載入和建立物件。執行順序仍然是靜態初始化塊、非靜態初始化塊、構造器,由於存在繼承關係,因此情況較為複雜。

對於兩個類的情況,即一個父類和一個子類,執行順序如下。

  1. 執行父類的靜態初始化塊。
  2. 執行子類的靜態初始化塊。
  3. 執行父類的非靜態初始化塊。
  4. 執行父類的構造器。
  5. 執行子類的非靜態初始化塊。
  6. 執行子類的構造器。

更一般的情況,對於多個類之間的繼承關係(可能超過兩個類,例如 B 繼承了 A,C 繼承了 B),執行順序如下。

  1. 按照從父類到子類的順序,依次執行每個類的靜態初始化塊。
  2. 按照從父類到子類的順序,對於每個類,依次執行非靜態初始化塊和構造器,然後執行子類的非靜態初始化塊和構造器,直到所有類執行完畢。

示例程式碼

以下程式碼可以說明初始化塊和構造器的執行順序。程式碼中定義了四個類,分別是 Main、Class1、Class2 和 Class3,其中 Class2 是 Class1 的子類,Class3 是 Class2 的子類,每個類都有靜態初始化塊、非靜態初始化塊和構造器。靜態方法 main 定義在 Main 中,建立了 Class3 的例項。

public class Main {
    static {
        System.out.println("Static initialization of Main");
    }

    {
        System.out.println("Instance initialization of Main");
    }

    public Test() {
        System.out.println("Constructor of Main");
    }

    public static void main(String[] args) {
        new Class3();
    }
}

class Class1 {
    static {
        System.out.println("Static initialization of Class1");
    }

    {
        System.out.println("Instance initialization of Class1");
    }

    Class1() {
        System.out.println("Constructor of Class1");
    }
}

class Class2 extends Class1 {
    static {
        System.out.println("Static initialization of Class2");
    }

    {
        System.out.println("Instance initialization of Class2");
    }

    Class2() {
        System.out.println("Constructor of Class2");
    }
}

class Class3 extends Class2 {
    static {
        System.out.println("Static initialization of Class3");
    }

    {
        System.out.println("Instance initialization of Class3");
    }

    Class3() {
        System.out.println("Constructor of Class3");
    }
}

執行程式碼得到的輸出結果是:

Static initialization of Main
Static initialization of Class1
Static initialization of Class2
Static initialization of Class3
Instance initialization of Class1
Constructor of Class1
Instance initialization of Class2
Constructor of Class2
Instance initialization of Class3
Constructor of Class3

由於沒有建立 Main 的例項,因此 Main 的非靜態初始化塊不會被執行,但是由於程式的入口即靜態方法 main 定義在 Main 中,因此 Main 的靜態初始化塊首先被執行。

在方法 main 中建立了 Class3 的例項,按照父類到子類的順序,依次執行每個類的靜態初始化塊,因此 Class1、Class2 和 Class3 的靜態初始化塊被依次執行。

在所有類的靜態初始化塊被執行之後,按照父類到子類的順序,依次執行每個類的非靜態初始化塊和構造器,因此按照 Class1、Class2 和 Class3 的順序,每個類的非靜態初始化塊和構造器被執行。

關鍵詞this

關鍵字 this 代表當前物件的引用。當前物件指的是呼叫類中的屬性或方法的物件。

關鍵字 this 用於引用隱藏變數

在方法和構造方法中,可能將屬性名用作引數名,在這種情況下,需要引用隱藏的屬性名才能給屬性設定新值。例如,當屬性名和引數名都是 var 時,需要通過 this.var = var 對屬性進行賦值。

當方法內部有區域性變數和屬性名相同時,同樣需要通過關鍵字 this 引用物件的屬性。

如果方法內部不存在和屬性名相同的區域性變數,則在使用屬性時,屬性前面的 this 可以省略。

關鍵字 this 用於呼叫其他構造方法

在構造方法中,可以通過關鍵字 this 呼叫其他構造方法,具體用法是 this(引數列表)。

Java 要求,在構造方法中如果使用關鍵字 this 呼叫其他構造方法,則 this(引數列表) 語句必須出現在其他語句之前。

關鍵字 this 不能在靜態程式碼塊中使用

由於關鍵字 this 代表的是物件的引用,因此依賴於具體物件,而靜態方法和靜態初始化塊不依賴於類的具體物件,因此靜態方法和靜態初始化塊中不能使用關鍵字 this。

示例程式碼

第 2 節的示例程式碼定義了一個類 Square。使用關鍵字 this,可以將類 Square 按照如下方式實現。

在有引數構造方法中,關鍵字 this 的作用是引用隱藏變數。在無引數構造方法中,關鍵字 this 的作用是呼叫有引數構造方法。

在方法中,通過關鍵字 this 引用物件的屬性。由於兩個方法 getPerimeter 和 getArea 中都沒有區域性變數和物件的屬性名相同,因此這兩個方法中的 this.side 都可以簡寫為 side。

class Square {
    int side;

    public Square() {
        this(1);
    }

    public Square(int side) {
        this.side = side;
    }

    public int getPerimeter() {
        return this.side * 4;
    }

    public int getArea() {
        return this.side * this.side;
    }
}

可見性修飾符和資料域封裝

Java 的可見性修飾符用於控制對類成員的訪問。可見性修飾符包括 public、private、protected 和預設修飾符,此處介紹 public、private 和預設修飾符,protected 將在繼承和多型部分介紹。

不同的可見性修飾符的含義

可見性修飾符 public 表示類成員可以在任何類中訪問。

可見性修飾符 private 表示類成員只能從自身所在的類中訪問。

如果不加任何可見性修飾符,則稱為預設修飾符,表示類成員可以在同一個包裡的任何類中訪問,此時也稱為包私有或包內訪問。

以上三種可見性修飾符定義的類成員的可見性如下面的表格所示。

【面經】Java面試突擊

資料域封裝

可見性修飾符可以用於控制對類成員的訪問,也可以用於對資料域進行封裝。

資料域封裝的含義是,對資料域使用 private 修飾符,將資料域宣告為私有域。如果不使用資料域封裝,則資料域的值可以從類的外部直接修改,導致資料被篡改以及類難以維護。使用資料域封裝的目的是為了避免直接修改資料域的值。

在定義私有資料域的類之外,不能通過直接引用的方式訪問該私有資料域,但是仍然可能需要讀取和修改資料域的值。為了能夠讀取私有資料域的值,可以編寫 get 方法(稱為讀取器或訪問器)返回資料域的值。為了能夠修改私有資料域的值,可以編寫 set 方法(稱為設定器或修改器)將資料域的值設定為新值。

示例程式碼

在第 5 節的示例程式碼中,資料域沒有加可見性修飾符,其可見性為預設。為了避免從類的外部直接訪問和修改資料域的值,需要對資料域加 private 修飾符。為了能從類的外部得到資料域的值,需要編寫 get 方法返回資料域的值。

使用資料域封裝的程式碼如下。

class Square {
    private int side;

    public Square() {
        this(1);
    }

    public Square(int side) {
        this.side = side;
    }

    public int getSide() {
        return this.side;
    }

    public int getPerimeter() {
        return this.side * 4;
    }

    public int getArea() {
        return this.side * this.side;
    }
}

字串

字串是常用的資料型別。在 Java 中,常見的字串型別包括 String、StringBuffer 和 StringBuilder。

String

從 String 的原始碼可以看到,String 使用陣列儲存字串的內容,陣列使用關鍵詞 final 修飾,因此陣列內容不可變,使用 String 定義的字串的值也是不可變的。

由於 String 型別的值不可變,因此每次對 String 的修改操作都會建立新的 String 物件,導致效率低下且佔用大量記憶體空間。

StringBuffer 和 StringBuilder

StringBuffer 和 StringBuilder 都是 AbstractStringBuilder 的子類,同樣使用陣列儲存字串的內容,由於陣列沒有使用關鍵詞 final 修飾,因此陣列內容可變,StringBuffer 和 StringBuilder 都是可變型別,可以對字串的內容進行修改,且不會因為修改而建立新的物件。

在需要經常對字串的內容進行修改的情況下,應使用 StringBuffer 或 StringBuilder,在時間和空間方面都顯著優於 String。

StringBuffer 和 StringBuilder 有哪些區別呢?

從原始碼可以看到,StringBuffer 對定義的方法或者呼叫的方法使用了關鍵詞 synchronized 修飾,而 StringBuilder 的方法沒有使用關鍵詞 synchronized 修飾。由於 StringBuffer 對方法加了同步鎖,因此其效率略低於 StringBuilder,但是在多執行緒的環境下,StringBuilder 不能保證執行緒安全,因此 StringBuffer 是更優的選擇。

總結

  1. String 是不可變型別,每次對 String 的修改操作都會建立新的 String 物件,導致效率低下且佔用大量記憶體空間,因此 String 適用於字串常量的情形,不適合需要對字串進行大量修改的情形。
  2. StringBuffer 是可變型別,可以修改字串的內容且不會建立新的物件,且 StringBuffer 是執行緒安全的,適用於多執行緒環境。
  3. StringBuilder 是可變型別,與 StringBuffer 相似,在單執行緒環境下 StringBuilder 的效率略高於 StringBuffer,但是在多執行緒環境下 StringBuilder 不保證執行緒安全,因此 StringBuilder 不適合多執行緒環境。

繼承

在物件導向程式設計中,可以從已有的類(父類)派生出新類(子類),稱為繼承。

父類和子類

如果已有的類 C1 派生出一個新類 C2,則稱 C1 為 C2 的父類,C2 為 C1 的子類。子類從父類中繼承可訪問的類成員,也可以新增新的類成員。子類通常包含比父類更多的類成員。

繼承用來為 is-a 關係建模,子類和父類之間必須存在 is-a 關係。

如果一個類在定義時沒有指定繼承,它的父類預設是 Object。

關鍵字 super

關鍵字 super 指向當前類的的父類。關鍵字 super 可以用於兩種途徑,一是呼叫父類的構造方法,二是呼叫父類的方法。

呼叫父類的構造方法,使用 super() 或 super(引數),該語句必須是子類構造方法的第一個語句,且這是呼叫父類構造方法的唯一方式。

呼叫父類的方法,使用 super.方法名(引數)。

構造方法鏈

如果構造方法沒有顯式地呼叫同一個類中其他的構造方法或父類的構造方法,將隱性地呼叫父類的無引數構造方法,即編譯器會把 super() 作為構造方法的第一個語句。

構造一個類的例項時,將會沿著繼承鏈呼叫所有父類的構造方法,父類的構造方法在子類的構造方法之前呼叫,稱為構造方法鏈。

下面用一個例子說明構造方法鏈。考慮如下程式碼。

public class Class3 extends Class2 {
    public static void main(String[] args) {
        new Class3();
    }

    public Class3() {
        System.out.println("D");
    }
}

class Class2 extends Class1 {
    public Class2() {
        this("B");
        System.out.println("C");
    }

    public Class2(String s) {
        System.out.println(s);
    }
}

class Class1 {
    public Class1() {
        System.out.println("A");
    }
}

Object類的部分方法

toString

方法定義:

public String toString()

該方法返回一個代表該物件的字串。該方法的預設實現返回的字串在絕大多數情況下是沒有資訊量的,因此通常都需要在子類中重寫該方法。

equals

方法定義:

public boolean equals(Object obj)

該方法檢驗兩個物件是否相等。該方法的預設實現使用 == 運算子檢驗兩個物件是否相等,通常都需要在子類中重寫該方法。

hashCode

方法定義:

public native int hashCode()

該方法返回物件的雜湊碼。關鍵字 native 表示實現方法的程式語言不是 Java。

雜湊碼是一個整數,用於在雜湊集合中儲存並能快速查詢物件。

根據雜湊約定,如果兩個物件相同,它們的雜湊碼一定相同,因此如果在子類中重寫了 equals 方法,必須在該子類中重寫 hashCode 方法,以保證兩個相等的物件對應的雜湊碼是相同的。

兩個相等的物件一定具有相同的雜湊碼,兩個不同的物件也可能具有相同的雜湊碼。實現 hashCode 方法時,應避免過多地出現兩個不同的物件也可能具有相同的雜湊碼的情況。

finalize

方法定義:

protected void finalize() throws Throwable

該方法用於垃圾回收。如果一個物件不再能被訪問,就變成了垃圾,finalize 方法會被該物件的垃圾回收程式呼叫。該方法的預設實現不做任何事,如果必要,子類應該重寫該方法。

該方法可能丟擲 Throwable 異常。

clone

方法定義:

protected native Object clone() throws CloneNotSupportedException

該方法用於複製一個物件,建立一個有單獨記憶體空間的新物件。

不是所有的物件都可以複製,只有當一個類實現了 java.lang.Cloneable 介面時,這個類的物件才能被複制。

該方法可能丟擲 CloneNotSupportedException 異常。

getClass

方法定義:

public final native Class<?> getClass()

該方法返回物件的元物件。元物件是一個包含類資訊的物件,包括類名、構造方法和方法等。

抽象類和介面

抽象類指抽象而沒有具體例項的類。介面是一種與類相似的結構,在很多方面與抽象類相近。

抽象類

抽象類使用關鍵字 abstract 修飾。抽象類和常規類一樣具有資料域、方法和構造方法,但是不能用 new 操作符建立例項。

抽象類可以包含抽象方法。抽象方法使用關鍵字 abstract 修飾,只有方法簽名而沒有實現,其實現由子類提供。抽象方法都是非靜態的。包含抽象方法的類必須宣告為抽象類。

非抽象類不能包含抽象方法。如果一個抽象父類的子類不能實現所有的抽象方法,則該子類也必須宣告為抽象類。

包含抽象方法的類必須宣告為抽象類,但是抽象類可以不包含抽象方法。

介面

介面使用關鍵字 interface 定義。介面只包含可見性為 public 的常量和抽象方法,不包含變數和具體方法。

和抽象類一樣,介面不能用 new 操作符建立例項。

新版本的 JDK 關於介面的規則有以下變化。

從 Java 8 開始,介面方法可以由預設實現。

從 Java 9 開始,介面內允許定義私有方法。

一個類只能繼承一個父類,但對介面允許多重繼承。一個介面可以繼承多個介面,這樣的介面稱為子介面。

抽象類和介面的區別

抽象類的變數沒有限制,介面只包含常量,即介面的所有變數必須是 public static final。

抽象類包含構造方法,子類通過構造方法鏈呼叫構造方法,介面不包含構造方法。

抽象類的方法沒有限制,介面的方法必須是 public abstract 的例項方法(注:新版本的 JDK 關於介面的規則有變化,見上文)。

一個類只能繼承一個父類,但是可以實現多個介面。一個介面可以繼承多個介面。

自定義比較方法

有兩個介面可以實現物件之間的排序和比較大小。

Comparable 介面是排序介面。如果一個類實現了 Comparable 介面,則該類的物件可以排序。Comparable 介面包含一個抽象方法 compareTo,實現 Comparable 介面的類需要實現該方法,定義排序的依據。

Comparator 介面是比較器介面。如果一個類本身不支援排序(即沒有實現 Comparable 介面),但是又需要對該類的物件排序,則可以通過實現 Comparator 介面的方式建立比較器。Comparator 介面包含兩個抽象方法 compare 和 equals,其中 compare 方法是必須在實現類中實現的,而 equals 方法在任何類中預設已經實現。

如果需要對一個陣列或列表中的多個物件進行排序,則可以將物件的類定義成實現 Comparable 介面,也可以在排序時定義 Comparator 比較器。

基本資料型別和包裝類

Java 有 8 種基本資料型別,基本資料型別不屬於物件。但是在很多時候需要把物件作為引數,因此需要把基本資料型別包裝成物件,對應的類稱為包裝類。

基本資料型別和包裝類的對應關係

每種基本資料型別都有對應的包裝類,如以下表格所示。

基本資料型別    包裝類

byte    Byte
short    Short
int    Integer
long    Long
float    Float
double    Double
char    Character
boolean    Boolean

包裝類的名稱和對應的基本資料型別相同,包裝類的名稱的首字母大寫,Integer 和 Character 例外。

包裝類的構造方法

可以通過包裝類的構造方法建立包裝物件。呼叫構造方法時,構造方法的引數值可以是基本資料型別的值,也可以是表示值的字串。

包裝類的構造方法都是有引數的,沒有無引數構造方法。

包裝類的例項都是不可變的,一旦建立了包裝物件,其內部的值就不能再改變。

自動裝箱和自動拆箱

從 JDK 1.5 開始,基本資料型別和包裝類之間可以進行自動轉換。

將基本資料型別的值轉換為包裝物件,稱為裝箱。將包裝物件轉換為基本資料型別的值,稱為拆箱。

物件導向思想

類的關係

關聯

關聯是一種描述兩個類之間行為的一般二元關係。

關聯中的每個類可以指定一個數字或一個數字區間,表示該關聯涉及類中的多少個物件。

在 Java 程式碼中,關聯可以用資料域和方法進行實現,一個類中的方法包含另一個類的引數。

下面的程式碼是關聯的例子。儲戶在銀行開賬戶是儲戶類 Depositor 和銀行類 Bank 之間的關聯。每個儲戶可以在多個銀行開設賬戶,每個銀行也可以有多個儲戶在該銀行開設賬戶。

class Depositor {
    private List<Bank> bankList;

    public Depositor() {
        bankList = new ArrayList<Bank>();
    }

    public void addBank(Bank account) {
        bankList.add(account);
    }
}

class Bank {
    private List<Depositor> depositorList;

    public Bank() {
        depositorList = new ArrayList<Depositor>();
    }

    public void addUser(Depositor depositor) {
        depositorList.add(depositor);
    }
}

聚集和組合

聚集是一種特殊的關聯形式,表示兩個物件之間的所屬關係。聚集模擬具有(has-a)關係。

所有者物件稱為聚集物件,對應的類稱為聚集類;從屬物件稱為被聚集物件,對應的類稱為被聚集類。

一個物件可以被幾個聚集物件所擁有。如果一個物件被一個聚集物件所專有,該物件和聚集物件之間的關係就稱為組合。

下面的程式碼是聚集和組合的例子。司機類 Driver 是聚集類,汽車類 Car 和身份證類 IDCard 是被聚集類。每個司機駕駛一輛汽車,並擁有一張身份證。由於一輛汽車可以被多個司機駕駛,因此 Driver 和 Car 之間的關係是聚集關係。由於一張身份證只能被一個人擁有,因此 Driver 和 IDCard 之間的關係是組合關係。

檢視程式碼

class Driver {
    private Car car;
    private IDCard idCard;
}
class Car {
}
class IDCard {
}

依賴

依賴指兩個類之間一個類使用另一個類的關係,前者稱為客戶(client),後者稱為供應方(supplier)。

在 Java 程式碼中,實現依賴的方式是,客戶類中的方法包含供應方類的引數。

下面的程式碼是依賴的例子。Java 自帶的抽象類 Calendar 包含方法 setTime,該方法包含一個 Date 型別的引數,此處 Calendar 是客戶類,Date 是供應方類。

public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar> {
    public final void setTime(Date date) {
        setTimeInMillis(date.getTime());
    }
}

繼承

繼承模擬是(is-a)關係。

強是(strong is-a)關係描述兩個類之間的直接繼承關係,弱是(weak is-a)關係描述一個類具有某些屬性。

強是關係可以用類的繼承表示,弱是關係可以用介面表示。

類的設計原則

內聚性

同一個類/模組的所有操作應該有高度關聯性,支援共同的目標,只負責一項任務,即單一責任原則,稱為高內聚。

不同類/模組之間的關聯程度應該儘量低,對一個類/模組的修改應該儘量減少對其他類/模組的影響,稱為低耦合。

封裝性

類中的資料域應該使用 private 修飾符隱藏其可見性,避免從外部直接訪問資料域。

如果需要從外部讀取資料域的值,則提供讀取器方法。如果需要從外部修改資料域的值,提供設定器方法。

如果一個方法只在類的內部使用,則應該對該方法使用 private 修飾符,避免從外部呼叫該方法。

例項和靜態

依賴於類的具體例項的資料域和方法應宣告為例項資料域和例項方法,反之應宣告為靜態資料域和靜態方法。

如果一個資料域被所有例項共享,該資料域應宣告為靜態資料域。如果一個方法不依賴於具體例項,該方法應宣告為靜態方法。

繼承和聚集

繼承模擬是(is-a)關係,聚集模擬具有(has-a)關係。應考慮兩個類之間的關係為是關係還是具有關係,決定使用繼承或聚集。

序列化和反序列化

把物件轉換成位元組序列的過程稱為物件的序列化,把位元組序列恢復成物件的過程稱為物件的反序列化。

可序列化介面 Serializable

只有當一個類實現了 Serializable 介面時,這個類的例項才是可序列化的。

Serializable 介面是一個標識介面,用於標識一個物件是否可被序列化,該介面不包含任何資料域和方法。

如果試圖對一個沒有實現 Serializable 介面的類的例項進行序列化,會丟擲 NotSerializableException 異常。

將一個物件序列化時,會將該物件的資料域進行序列化,不會對靜態資料域進行序列化。

關鍵字 transient

如果一個物件的類實現了 Serializable 介面,但是包含一個不可序列化的資料域,則該物件不可序列化。為了使該物件可序列化,需要給不可序列化的資料域加上關鍵字 transient。

如果一個資料域可序列化,但是不想將這個資料域序列化,也可以給該資料域加上關鍵字 transient。

在序列化的過程中,加了關鍵字 transient 的資料域將被忽略。

反射機制

反射機制

Java 反射機制的核心是在程式執行時動態載入類以及獲取類的資訊,從而使用類和物件的資料域和方法。

Class 類

Class 類的作用是在程式執行時儲存每個物件所屬的類的資訊,在程式執行時分析類。一個 Class 型別的物件表示一個特定類的屬性。

有三種方法可以得到 Class 型別的例項。

第一種方法是對一個物件呼叫 getClass 方法,獲得該物件所屬的類的 Class 物件。

第二種方法是呼叫靜態方法 Class.forName,將類名作為引數,獲得類名對應的 Class 物件。

第三種方法是對任意的 Java 型別 T(包括基本資料型別、引用型別、陣列、關鍵字 void),呼叫 T.class 獲得型別 T 對應的 Class 物件,此時獲得的 Class 物件表示一個型別,但是這個型別不一定是一種類。

三種方法中,通過靜態方法 Class.forName 獲得 Class 物件是最常用的。

Class 類的常用方法

Class 類中最常用的方法是 getName,該方法返回類的名字。

Class 類中的 getFields、getMethods 和 getConstructors 方法分別返回類中所有的公有(即使用可見修飾符 public 修飾)的資料域、方法和構造方法。

Class 類中的 getDeclaredFields、getDeclaredMethods 和 getDeclaredConstructors 方法分別返回類中所有的資料域、方法和構造方法(包括所有可見修飾符)。

Class 類中的 getField、getMethod 和 getConstructor 方法分別返回類中單個的公有(即使用可見修飾符 public 修飾)的資料域、方法和構造方法。

Class 類中的 getDeclaredField、getDeclaredMethod 和 getDeclaredConstructor 方法分別返回類中單個的資料域、方法和構造方法(包括所有可見修飾符)。

異常處理

程式設計錯誤可以分成三類:語法錯誤、邏輯錯誤和執行錯誤。

語法錯誤(也稱編譯錯誤)是在編譯過程中出現的錯誤,由編譯器檢查發現語法錯誤。

邏輯錯誤指程式的執行結果與預期不符,可以通過除錯定位並發現錯誤的原因。

執行錯誤是引起程式非正常中斷的錯誤,需要通過異常處理的方式處理執行錯誤。

異常處理概念

執行錯誤會引起異常,如果不對異常進行捕獲和處理,程式就會非正常終止,並可能引起別的問題。

為了避免程式非正常終止,保證程式的穩定性,需要使用異常處理的功能。

Java 提供了 try-catch 塊的結構,用於捕獲並處理異常。該結構包括 try 塊和 catch 塊兩部分,分別以關鍵字 try 和 catch 開始,catch 塊包含特定異常型別的引數。在 try 塊中包含可能丟擲異常的語句,當 try 塊中的一個語句丟擲異常且和 catch 塊的引數的異常型別一致時,try 塊中剩下的語句會被跳過,catch 塊中的語句會被執行。

異常型別

Java 的異常型別是 Exception 類,它是 Throwable 類的子類。

Exception 類描述由程式和外部環境引起的錯誤,可以通過程式捕獲和處理這些異常。Exception 類有多個子類,分別描述特定的異常。

RuntimeException 類是 Exception 類的子類,描述執行時異常。RuntimeException 類有多個子類,分別描述特定的執行時異常。

異常處理的操作

Java 的異常處理基於三種操作:宣告異常、丟擲異常和捕獲異常。

宣告異常

如果一個方法可能丟擲異常,則需要在方法宣告中使用關鍵字 throws 宣告異常。如果一個方法可能丟擲多種型別的異常,則需要在關鍵字 throws 之後依次列舉可能丟擲的異常型別。

丟擲異常

如果程式檢查到錯誤,則可以建立一個異常的例項並丟擲該異常例項。使用關鍵字 throw 丟擲異常。

需要注意宣告異常的關鍵字 throws 和丟擲異常的關鍵字 throw 的區別。

捕獲異常

捕獲異常通過 try-catch 塊實現。每個 catch 塊包含一個特定異常型別的引數,如果需要捕獲多種異常,則需要使用多個 catch 塊,每個 catch 塊分別包含一個特定異常型別的引數。

如果 try 塊的執行過程中沒有出現異常,則跳過 catch 塊。

如果 try 塊中的一個語句丟擲一個異常,則跳過 try 塊中剩下的語句,尋找可以處理該異常的程式碼,處理異常的程式碼稱為異常處理器。具體而言,依次檢查每個 catch 塊,尋找可以處理該異常的 catch 塊。

  • 如果發現一個 catch 塊的引數的異常型別和丟擲的異常例項匹配,則將異常例項賦給該 catch 塊的引數,執行該 catch 塊的語句。
  • 如果在當前方法中沒有發現異常處理器,則異常沒有被捕獲和處理,退出當前的方法,並將異常傳遞給當前方法的呼叫者,繼續尋找異常處理器。

如果一個 catch 塊可以捕獲一個父類的異常物件,則該 catch 塊也能捕獲該父類的所有子類的異常物件。

由於父類包含子類,因此需要注意 catch 塊的順序,子類異常對應的 catch 塊必須出現在父類異常的 catch 塊之前,否則會出現編譯錯誤。

finally 子句

有時,無論異常是否出現或者被捕獲,都需要執行一些語句,可以通過 finally 子句實現。使用關鍵字 finally 宣告 finally 子句。

如果在 try-catch 塊中包含 return 語句,finally 子句將在方法返回之前被執行。

示例程式碼

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String str1 = scanner.next();
        String opStr = scanner.next();
        String str2 = scanner.next();
        scanner.close();
        System.out.println(calculate(str1, opStr, str2));
    }

    public static int calculate(String str1, String opStr, String str2) {
        int result = 0;
        try {
            int num1 = Integer.parseInt(str1);
            int num2 = Integer.parseInt(str2);
            char op = opStr.charAt(0);
            switch (op) {
            case '+':
                result = num1 + num2;
                break;
            case '-':
                result = num1 - num2;
                break;
            case '*':
                result = num1 * num2;
                break;
            case '/':
                result = num1 / num2;
                break;
            default:
                throw new RuntimeException("Invalid expression");
            }
            return result;
        } catch (NumberFormatException ex) {
            System.out.println("NumberFormatException");
        } catch (ArithmeticException ex) {
            System.out.println("ArithmeticException");
        } catch (Exception ex) {
            System.out.println(ex.getMessage());
        } finally {
            System.out.println("The finally block");
        }
        return result;
    }
}

上述程式碼的作用是,在命令列依次輸入第一個數、運算子和第二個數,計算表示式的結果並輸出。方法 calculate 根據三個引數計算結果,包含 try-catch 塊和 finally 子句。

由於 Exception 類包含了 NumberFormatException 類和 ArithmeticException 類,因此 Exception 類的 catch 塊必須放在 NumberFormatException 類和 ArithmeticException 類的 catch 塊之後,否則會出現編譯錯誤。

方法 calculate 中的 finally 子句在任何情況下都會執行。如果 try 塊的執行過程中沒有出現異常,則 finally 子句將在方法返回之前被執行。

Java虛擬機器

執行時資料區域

Java 虛擬機器在執行 Java 程式的過程中會把它管理的記憶體劃分成若干個不同的資料區域。這些區域有不同的用途。

程式計數器

程式計數器是一塊較小的記憶體空間,可以看作當前執行緒所執行的位元組碼的行號指示器。位元組碼直譯器工作時,通過改變程式計數器的值選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等功能都需要依賴程式計數器完成。

為了執行緒切換後能恢復到正確的執行位置,每個執行緒都需要有獨立的程式計數器。由於每個執行緒的程式計數器是獨立儲存的,因此各執行緒之間的程式計數器互不影響,這類記憶體區域被稱為執行緒私有的記憶體區域。

程式計數器是唯一不會出現 OutOfMemoryError 的記憶體區域。

Java 虛擬機器棧

和程式計數器一樣,Java 虛擬機器棧也是執行緒私有的,它的生命週期與執行緒相同。

虛擬機器棧描述的是 Java 方法執行的記憶體模型,每個方法被執行的時候會建立一個棧幀,用於儲存區域性變數表、操作棧、動態連結、方法出口等資訊。一個方法被呼叫直至執行完成的過程對應一個棧幀在虛擬機器中從入棧到出棧的過程。

區域性變數表存放編譯器可知的各種基本資料型別、物件引用型別和返回地址型別。

Java 虛擬機器棧會出現兩種異常。

  • 如果虛擬機器棧不可以動態擴充套件,當執行緒請求的棧深度大於虛擬機器所允許的深度時,將丟擲 StackOverflowError 異常;
  • 如果虛擬機器棧可以動態擴充套件,當無法申請到足夠的記憶體時,將丟擲 OutOfMemoryError 異常。

本地方法棧

本地方法棧和虛擬機器棧的作用相似。區別在於,虛擬機器棧為虛擬機器執行 Java 方法服務,本地方法棧為虛擬機器使用到的本地方法服務。有的虛擬機器(如 HotSpot 虛擬機器)把本地方法棧和虛擬機器棧合二為一。

和虛擬機器棧一樣,本地方法棧也會出現 StackOverflowError 和 OutOfMemoryError 兩種異常。

Java 堆

對於大多數應用而言,Java 堆是 Java 虛擬機器管理的記憶體中最大的一塊。Java 堆是被所有執行緒共享的記憶體區域,其目的是存放物件例項,幾乎所有的物件例項都在堆中分配記憶體。

Java 堆是垃圾回收器管理的主要記憶體,因此也稱為 GC 堆(Garbage Collected Heap)。從垃圾回收的角度,由於現代編譯器基本都採用分代垃圾回收演算法,所以 Java 堆還可以分成新生代和老年代,新生代又可以細分成 Eden 區、From Survivor 區、To Survivor 區等。細分成多個空間的目的是更好地回收記憶體或者更快地分配記憶體。

方法區

和 Java 堆一樣,方法區也是被所有執行緒共享的記憶體區域。方法區用於儲存已經被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。

當方法區無法滿足記憶體分配需求時,將丟擲 OutOfMemoryError 異常。

JDK 1.8 將方法區徹底移除,取而代之的是元空間,元空間使用的是直接記憶體。

執行時常量池

執行時常量池是方法區的一部分。Class 檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有常量池資訊,用於存放編譯器生成的字面量和符號引用,這些資訊將在類載入後存放到方法區的執行時常量池中。

執行時常量池也受到方法區記憶體的限制,當常量池無法再申請到記憶體時將丟擲 OutOfMemoryError 異常。

直接記憶體

直接記憶體不是虛擬機器執行時資料區域的一部分,也不是虛擬機器規範中定義的記憶體區域,但是這部分也被頻繁地使用,而且也可能導致 OutOfMemoryError 異常出現。

本機直接記憶體的分配不受到 Java 堆大小的限制,但是直接記憶體仍然受到本機總記憶體地大小及處理器定址空間的限制。如果各個記憶體區域的總和大於實體記憶體限制,就會導致動態擴充套件時出現 OutOfMemoryError 異常。

垃圾回收

垃圾回收,顧名思義就是釋放垃圾佔用的空間,從而提升程式效能,防止記憶體洩露。當一個物件不再被需要時,該物件就需要被回收並釋放空間。

Java 記憶體執行時資料區域包括程式計數器、虛擬機器棧、本地方法棧、堆等區域。其中,程式計數器、虛擬機器棧和本地方法棧都是執行緒私有的,當執行緒結束時,這些區域的生命週期也結束了,因此不需要過多考慮回收的問題。而堆是虛擬機器管理的記憶體中最大的一塊,堆中的記憶體的分配和回收是動態的,垃圾回收主要關注的是堆空間。

呼叫垃圾回收器的方法

呼叫垃圾回收器的方法是 gc,該方法在 System 類和 Runtime 類中都存在。

在 Runtime 類中,方法 gc 是例項方法,方法 System.gc 是呼叫該方法的一種傳統而便捷的方法。

在 System 類中,方法 gc 是靜態方法,該方法會呼叫 Runtime 類中的 gc 方法。

其實,java.lang.System.gc 等價於 java.lang.Runtime.getRuntime.gc 的簡寫,都是呼叫垃圾回收器。

方法 gc 的作用是提示 Java 虛擬機器進行垃圾回收,該方法由系統自動呼叫,不需要人為呼叫。該方法被呼叫之後,由 Java 虛擬機器決定是立即回收還是延遲迴收。

finalize 方法

與垃圾回收有關的另一個方法是 finalize 方法。該方法在 Object 類中被定義,在釋放物件佔用的記憶體之前會呼叫該方法。該方法的預設實現不做任何事,如果必要,子類應該重寫該方法,一般建議在該方法中釋放物件持有的資源。

判斷物件是否可回收

垃圾回收器在對堆進行回收之前,首先需要確定哪些物件是可回收的。常用的演算法有兩種,引用計數演算法和根搜尋演算法。

引用計數演算法

引用計數演算法給每個物件新增引用計數器,用於記錄物件被引用的計數,引用計數為 0 的物件即為可回收的物件。

雖然引用計數演算法的實現簡單,判定效率也很高,但是引用計數演算法無法解決物件之間迴圈引用的情況。如果多個物件之間存在迴圈引用,則這些物件的引用計數永遠不為 0,無法被回收。因此 Java 語言沒有使用引用計數演算法。

根搜尋演算法

主流的商用程式語言都是使用根搜尋演算法判斷物件是否可回收。根搜尋演算法的思路是,從若干被稱為 GC Roots 的物件開始進行搜尋,不能到達的物件即為可回收的物件。

在 Java 中,GC Roots 一般包含下面幾種物件:

  • 虛擬機器棧中引用的物件;
  • 本地方法棧中的本地方法引用的物件;
  • 方法區中的類靜態屬性引用的物件;
  • 方法區中的常量引用的物件。

引用的分類

引用計數演算法和根搜尋演算法都需要通過判斷引用的方式判斷物件是否可回收。

在 JDK 1.2 之後,Java 將引用分成四種,按照引用強度從高到低的順序依次是:強引用、軟引用、弱引用、虛引用。

  • 強引用是指在程式程式碼中普遍存在的引用。垃圾回收器永遠不會回收被強引用關聯的物件。
  • 軟引用描述還有用但並非必需的物件。只有在系統將要發生記憶體溢位異常時,被軟引用關聯的物件才會被回收。在 JDK 1.2 之後,提供了 SoftReference 類實現軟引用。
  • 弱引用描述非必需的物件,其強度低於軟引用。被弱引用關聯的物件只能存活到下一次垃圾回收發生之前,當垃圾回收器工作時,被弱引用關聯的物件一定會被回收。在 JDK 1.2 之後,提供了 WeakReference 類實現弱引用。
  • 虛引用是最弱的引用關係。一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用取得一個物件例項。為一個物件設定虛引用關聯的唯一目的就是能在這個物件被回收時收到一個系統通知。在 JDK 1.2 之後,提供了 PhantomReference 類實現虛引用。

垃圾回收演算法

標記—清除演算法

標記—清除演算法是最基礎的垃圾回收演算法,後續的垃圾收集演算法都是基於標記—清除演算法進行改進而得到的。標記—清除演算法分為“標記”和“清除”兩個階段,首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。

標記—清除演算法有兩個主要缺點。一是效率問題,標記和清除的效率都不高,二是空間問題,標記清除之後會產生大量不連續的記憶體碎片,導致程式在之後的執行過程中無法為較大物件找到足夠的連續記憶體。

複製演算法

複製演算法是將可用記憶體分成大小相等的兩塊,每次只使用其中的一塊,當用完一塊記憶體時,將還存活著的物件複製到另外一塊記憶體,然後把已使用過的記憶體空間一次清理掉。

複製演算法解決了效率問題。由於每次都是對整個半區進行記憶體回收,因此在記憶體分配時不需要考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可。複製演算法的優點是實現簡單,執行高效,缺點是將記憶體縮小為了原來的一半,以及在物件存活率較高時複製操作的次數較多,導致效率降低。

標記—整理演算法

標記—整理演算法是根據老年代的特點提出的。標記過程與標記—清除演算法一樣,但後續步驟不是直接回收被標記的物件,而是讓所有存活的物件都向一端移動,然後清除邊界以外的記憶體。

分代收集演算法

分代收集演算法根據物件的存活週期不同將記憶體劃分為多個區域,對每個區域選用不同的垃圾回收演算法。

一般把 Java 堆分為新生代和老年代。在新生代中,大多數物件的生命週期都很短,因此選用複製演算法。在老年代中,物件存活率高,因此選用標記—清除演算法或標記—整理演算法。

分配記憶體與回收策略

Java 堆可以分成新生代和老年代,新生代又可以細分成 Eden 區、From Survivor 區、To Survivor 區等。對於不同的物件,有相應的記憶體分配規則。

Minor GC 和 Full GC

Minor GC 指發生在新生代的垃圾回收操作。因為大多數物件的生命週期都很短,因此 Minor GC 會頻繁執行,一般回收速度也比較快。

Full GC 也稱 Major GC,指發生在老年代的垃圾回收操作。出現了 Full GC,經常會伴隨至少一次的 Minor GC。老年代物件的存活時間長,因此 Full GC 很少執行,而且執行速度會比 Minor GC 慢很多。

物件優先在 Eden 區分配

大多數情況下,物件在新生代 Eden 區分配,當 Eden 區空間不夠時,發起 Minor GC。

大物件直接進入老年代

大物件是指需要連續記憶體空間的物件,最典型的大物件是那種很長的字串以及陣列。大物件對於虛擬機器的記憶體分配而言是壞訊息,經常出現大物件會導致記憶體還有不少空間時就提前觸發垃圾回收以獲取足夠的連續空間分配給大物件。

將大物件直接在老年代中分配的目的是避免在 Eden 區和 Survivor 區之間出現大量記憶體複製。

長期存活的物件進入老年代

虛擬機器採用分代收集的思想管理記憶體,因此需要識別每個物件應該放在新生代還是老年代。虛擬機器給每個物件定義了年齡計數器,物件在 Eden 區出生之後,如果經過第一次 Minor GC 之後仍然存活,將進入 Survivor 區,同時物件年齡變為 1,物件在 Survivor 區每經過一次 Minor GC 且存活,年齡就增加 1,增加到一定閾值時則進入老年代(閾值預設為 15)。

動態物件年齡判定

為了能更好地適應不同程式的記憶體狀況,虛擬機器並不總是要求物件的年齡必須達到閾值才能進入老年代。如果在 Survivor 區中相同年齡的所有物件的空間總和大於 Survivor 區空間的一半,則年齡大於或等於該年齡的物件直接進入老年代。

空間分配擔保

在發生 Minor GC 之前,虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有物件的空間總和,如果這個條件成立,那麼 Minor GC 可以確保是安全的。

只要老年代的連續空間大於新生代物件總大小或者歷次晉升的平均大小,就會進行 Minor GC,否則將進行 Full GC。

多執行緒

程式和執行緒

程式是含有指令和資料的檔案,是靜態的程式碼,被儲存在磁碟或其他的資料儲存裝置中。程式是程式的一次執行過程,執行緒是程式劃分成的更小的執行單位。

程式和執行緒的聯絡和區別

程式是程式的一次執行過程,是系統執行程式的基本單位,因此程式是動態的。系統執行一個程式即為一個程式的建立、執行以及消亡的過程。

執行緒是比程式更小的執行單位。一個程式在其執行的過程中可以產生多個執行緒,多個執行緒共享程式的堆和方法區記憶體資源,每個執行緒都有自己的程式計數器、虛擬機器棧和本地方法棧。由於執行緒共享程式的記憶體,因此係統產生一個執行緒或者在多個執行緒之間切換工作時的負擔比程式小得多,執行緒也稱為輕量級程式。

程式和執行緒最大的區別是,各程式是獨立的,而各執行緒則不一定獨立,因為同一程式中的多個執行緒極有可能會相互影響。執行緒執行開銷小,但不利於資源的管理和保護,程式則相反。

執行緒的狀態

執行緒在執行的生命週期中的任何時刻只能是 6 種不同狀態的其中一種。

  • 初始狀態(NEW):執行緒已經構建,尚未啟動。
  • 執行狀態(RUNNABLE):包括就緒(READY)和執行中(RUNNING)兩種狀態,統稱為執行狀態。
  • 阻塞狀態(BLOCKED):執行緒被鎖阻塞。
  • 等待狀態(WAITING):執行緒需要等待其他執行緒做出特定動作(通知或中斷)。
  • 超時等待狀態(TIME_WAITING):不同於等待狀態,超時等待狀態可以在指定的時間自行返回。
  • 終止狀態(TERMINATED):當前執行緒已經執行完畢。

多執行緒的優點和可能存在的問題

執行緒也稱為輕量級程式,是程式執行的最小單位,執行緒間的切換和排程的成本遠遠小於程式,多個執行緒同時執行可以減少執行緒上下文切換的開銷。多執行緒是開發高併發系統的基礎,利用好多執行緒機制可以顯著提高系統的併發能力和效能。

多執行緒併發程式設計並不總是能提高程式的執行效率和執行速度,而且可能存在一些問題,包括記憶體洩漏、上下文切換、死鎖以及受限於硬體和軟體的資源限制問題等。

關鍵字synchronized和volatile

關鍵字 synchronized 和 volatile 是多執行緒中經常用到的兩個關鍵字。

關鍵字 synchronized

關鍵字 synchronized 解決的是多個執行緒之間訪問資源的同步性,該關鍵字可以保證被它修飾的方法或者程式碼塊在任意時刻只能有一個執行緒執行。

關鍵字 synchronized 最主要的三種使用方式是:修飾例項方法、修飾靜態方法、修飾程式碼塊。

  • 修飾例項方法:給當前物件例項加鎖,進入同步程式碼之前需要獲得當前物件例項的鎖。
  • 修飾靜態方法:給當前類加鎖,進入同步程式碼之前需要獲得當前類的鎖。
  • 修飾程式碼塊:指定加鎖物件,給指定物件加鎖,進入同步程式碼塊之前需要獲得指定物件的鎖。

關鍵字 volatile

關鍵字 volatile 解決的是變數在多個執行緒之間的可見性,該關鍵字修飾的變數會直接在主記憶體中進行讀寫操作,保證了變數的可見性。

除了保證變數的可見性以外,關鍵字 volatile 還有一個作用是確保程式碼的執行順序不變。為了提高執行程式時的效能,編譯器和處理器會對指令進行重排序優化,因此程式碼的執行順序和編寫程式碼的順序可能不一致。新增關鍵字 volatile 可以禁止指令進行重排序優化。

只有當一個變數滿足以下兩個條件時,才能使用關鍵字 volatile。

  • 對變數的寫入操作不依賴變數的當前值,或者能確保只有單個執行緒更新變數的值。
  • 該變數沒有包含在具有其他變數的不變式中。

關鍵字 synchronized 和 volatile 的區別

  • 關鍵字 volatile 是執行緒同步的輕量級實現,不需要加鎖,因此效能優於關鍵字 synchronized。
  • 關鍵字 synchronized 可以修飾方法和程式碼塊,關鍵字 volatile 只能修飾變數。
  • 關鍵字 synchronized 可能發生阻塞,關鍵字 volatile 不會發生阻塞。
  • 關鍵字 synchronized 可以保證資料的可見性和原子性,關鍵字 volatile 只能保證資料的可見性,不能保證資料的原子性。
  • 關鍵字 synchronized 解決的是多個執行緒之間訪問資源的同步性,關鍵字 volatile 解決的是變數在多個執行緒之間的可見性。

多執行緒相關的方法

Thread 類的方法

run 和 start

方法 run 在 Runnable 介面中被定義,方法 start 在 Thread 類中被定義。

建立一個 Thread 類的例項,即為建立了一個處於初始狀態的執行緒。對一個處於初始狀態的執行緒呼叫方法 start,該執行緒被啟動,進入執行狀態。呼叫方法 start 之後,方法 run 會自動執行。

通過呼叫方法 start,執行方法 run,才是多執行緒工作。如果直接執行方法 run,則方法 run 會被當成一個主執行緒下的普通方法執行,而不會在某個執行緒中執行,因此不是多執行緒工作。

sleep

方法 sleep 在 Thread 類中被定義。該方法的作用是使當前執行緒暫停執行一段時間,讓其他執行緒有機會繼續執行,但是該方法不會釋放鎖。

方法 sleep 需要捕獲 InterruptedException 異常。

join

方法 join 在 Thread 類中被定義。該方法的作用是阻塞呼叫該方法的執行緒,直到當前執行緒執行完畢之後,呼叫該方法的執行緒再繼續執行。

方法 join 需要捕獲 InterruptedException 異常。

yield

方法 yield 在 Thread 類中被定義。該方法的作用是暫停當前正在執行的執行緒物件,並執行其他執行緒。實際呼叫方法 yield 時無法保證一定能讓其他執行緒執行,因為執行緒排程時可能再次選中原來的執行緒物件。

Object 類的方法

wait

方法 wait 在 Object 類中被定義。該方法必須在 synchronized 語句塊內使用,作用是釋放鎖,讓其他執行緒可以執行,當前執行緒進入等待池中。

notify 和 notifyAll

方法 notify 和 notifyAll 在 Object 類中被定義。

方法 notify 的作用是從等待池中移走任意一個等待當前物件的執行緒並放到鎖池中,只有鎖池中的執行緒可以獲取鎖。

方法 notifyAll 的作用是從等待池中移走全部等待當前物件的執行緒並放到鎖池中,鎖池中的這些執行緒將爭奪鎖。

中斷執行緒

中斷執行緒的方法是 interrupt,在 Thread 類中被定義。該方法不會中斷一個正在執行的執行緒,只是改變中斷標記。

當執行緒處於等待狀態、超時等待狀態或阻塞狀態時,如果對執行緒呼叫方法 interrupt 將執行緒的中斷標記設為 true,則中斷標記會被清除,同時會丟擲 InterruptedException 異常。可以通過 try-catch 塊捕獲該異常,即可終止執行緒。

當執行緒處於執行狀態時,可以對執行緒呼叫方法 interrupt 將執行緒的中斷標記設為 true,從而達到終止執行緒的目的,也可以新增一個 volatile 修飾的額外標記,當需要終止執行緒時,更改該標記的值即可。

不推薦使用方法 stop 和 destroy 終止執行緒。

  • 方法 stop 會立即停止方法 run 中剩餘的全部工作,並丟擲 ThreadDeath 錯誤,導致清理性工作無法完成,另外方法 stop 會立即釋放該執行緒的所有鎖,導致物件狀態不一致。
  • 方法 destroy 只是丟擲 NoSuchMethodError,沒有做任何事情,因此無法終止執行緒。

執行緒池

執行緒池是一種執行緒的使用模式。建立若干個可執行的執行緒放入一個池(容器)中,有任務需要處理時,會提交到執行緒池中的任務佇列,處理完之後執行緒並不會被銷燬,而是仍然線上程池中等待下一個任務。

執行緒池的好處

在開發過程中,合理地使用執行緒池可以帶來 3 個好處。

  • 降低資源消耗。重複利用執行緒池中已經建立的執行緒,可以避免頻繁地建立和銷燬執行緒,從而減少資源消耗。
  • 提高響應速度。由於執行緒池中有已經建立的執行緒,因此當任務到達時,可以直接執行,不需要等待執行緒建立。
  • 提高執行緒的可管理性。執行緒是稀缺資源,如果無限制地建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一分配、調優和監控。

執行緒池的建立

可以通過 ThreadPoolExecutor 類建立執行緒池。ThreadPoolExecutor 類有 4 個構造方法,其中最一般化的構造方法包含 7 個引數。

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

7 個引數的含義如下。

corePoolSize:核心執行緒數,定義了最少可以同時執行的執行緒數量,當有新的任務時就會建立一個執行緒執行任務,當執行緒池中的執行緒數量達到 corePoolSize 之後,到達的任務進入阻塞佇列。

maximumPoolSize:最大執行緒數,定義了執行緒池中最多能建立的執行緒數量。

keepAliveTime:等待時間,當執行緒池中的執行緒數量大於 corePoolSize 時,如果一個執行緒的空閒時間達到 keepAliveTime 時則會終止,直到執行緒池中的執行緒數不超過 corePoolSize。

unit:引數 keepAliveTime 的單位。

workQueue:阻塞佇列,用來儲存等待執行的任務。

threadFactory:建立執行緒的工廠。

handler:當拒絕處理任務時的策略。

向執行緒池提交任務

可以通過方法 execute 向執行緒池提交任務。該方法被呼叫時,執行緒池會做如下操作。

  1. 如果正在執行的執行緒數量小於 corePoolSize,則建立核心執行緒執行這個任務。
  2. 如果正在執行的執行緒數量大於或等於 corePoolSize,則將這個任務放入阻塞佇列。
  3. 如果阻塞佇列滿了,而且正在執行的執行緒數量小於 maximumPoolSize,則建立非核心執行緒執行這個任務。
  4. 如果阻塞佇列滿了,而且正在執行的執行緒數量大於或等於 maximumPoolSize,則執行緒池丟擲 RejectExecutionException 異常。

上述操作中提到了兩個概念,「核心執行緒」和「非核心執行緒」。核心執行緒和非核心執行緒的最大數目在建立執行緒池時被指定。核心執行緒和非核心執行緒的區別如下。

  • 向執行緒池提交任務時,首先建立核心執行緒執行任務,直到核心執行緒數到達上限,然後將任務放入阻塞佇列。
  • 只有在核心執行緒數到達上限,且阻塞佇列滿的情況下,才會建立非核心執行緒執行任務。

關閉執行緒池

可以通過呼叫執行緒池的方法 shutdown 或 shutdownNow 關閉執行緒池。這兩個方法的原理是遍歷執行緒池中的工作執行緒,對每個工作執行緒呼叫 interrupt 方法中斷執行緒,無法響應中斷的任務可能永遠無法終止。

方法 shutDown 和 shutDownNow 有以下區別。

  • 方法 shutDown 將執行緒池的狀態設定成 SHUTDOWN,正在執行的任務繼續執行,沒有執行的任務將中斷。
  • 方法 shutDownNow 將執行緒池的狀態設定成 STOP,正在執行的任務被停止,沒有執行的任務被返回。

容器

Iterable介面和Iterator介面

Iterable 介面從 JDK 1.5 開始出現,是 Java 容器的最頂級的介面之一,該介面的作用是使容器具備迭代元素的功能。

Iterator 介面從 JDK 1.2 開始出現,其含義是迭代器,可以用於迭代容器中的元素。

Iterable 介面的方法

iterator

方法 iterator 是 Iterable 介面的核心方法,返回 Iterator 類的迭代器例項。

forEach

方法 forEach 從 JDK 1.8 開始出現。該方法有預設實現,其作用是對容器中的每個元素進行處理。

spliterator

方法 spliterator 從 JDK 1.8 開始出現。該方法有預設實現,其作用是並行遍歷元素。

Iterator 介面的方法

hasNext

方法 hasNext 的作用是檢測容器中是否還有需要迭代的元素。

next

方法 next 的作用是返回迭代器指向的元素,並且更新迭代器的狀態,將迭代器指向的元素後移一位。

remove

方法 remove 的作用是將迭代器指向的元素刪除。該方法有預設實現,預設實現為丟擲 UnsupportedOperationException 異常,如果迭代器迭代的容器不支援 remove 操作,則對迭代器呼叫方法 remove 時會丟擲 UnsupportedOperationException 異常。

forEachRemaining

方法 forEachRemaining 從 JDK 1.8 開始出現。該方法有預設實現,其作用是對容器中的剩餘元素進行處理,直到剩餘元素處理完畢或者丟擲異常。

Collection介面

Collection 介面統一定義了單列容器,該介面繼承了 Iterable 介面。

Collection 介面的常用方法

新增元素

新增元素的方法有 add 和 addAll,其中 add 一次新增一個元素,addAll 一次將另一個容器中的元素全部新增到當前容器中。

方法 addAll 和集合的並集運算相似。

刪除元素

刪除元素的方法有 remove、removeAll 和 clear,其中 remove 一次刪除一個元素,removeAll 一次將另一個容器中的元素全部從當前容器中刪除,clear 刪除當前容器中的全部元素。

方法 removeAll 和集合的差集運算相似。

保留元素

保留元素的方法有 retainAll,該方法保留既在當前容器中又在另一個容器中的元素,和集合的交集運算相似。

判斷包含元素

判斷包含元素的方法有 contains 和 containsAll,其中 contains 判斷當前容器是否包含一個指定元素,containsAll 判斷當前容器是否包含另一個容器中的全部元素。

其他方法

方法 isEmpty 判斷當前容器是否為空(即不包含元素),方法 size 返回容器中的元素數目,方法 toArray 將容器轉化成 Object 陣列並返回。

Collection 介面和其他容器類的關係

List 和 Set 是 Collection 介面的子介面。

  • List 是線性表,儲存一組順序排列的元素。
  • Set 是集合,儲存一組互不相同的元素。

另一個描述容器的介面是 Map,該介面與 Collection 並列存在。不同於 Collection 介面存放單值元素,Map 介面存放的是鍵值對。

線性表

List 介面定義線性表,該介面繼承了 Collection 介面。List 介面的實現類有 ArrayList、LinkedList 和 Vector。

List 介面的常用方法

由於 List 介面繼承了 Collection 介面,因此 List 介面支援 Collection 介面的一切方法。List 介面還支援通過下標訪問元素,根據下標進行新增和刪除元素的操作,以及可以生成雙向遍歷線性表的新迭代器。

以下列舉的方法是 List 介面中新定義的方法(和 Collection 相比新定義的方法)。

新增元素

新增元素的方法有 add 和 addAll,其中 add 一次新增一個元素,addAll 一次將另一個容器中的元素全部新增到當前線性表中。這兩個方法都可以指定下標,在指定下標處新增元素。

刪除元素

刪除元素的方法有 remove,可以指定下標,刪除指定下標處的元素。

修改元素

修改元素的方法有 set,將指定下標處的元素修改成新的元素。

獲得元素以及獲得元素下標

獲得元素的方法有 get,返回指定下標處的元素。

獲得元素下標的方法有 indexOf 和 lastIndexOf,分別返回第一個匹配元素的下標和最後一個匹配元素的下標。

獲得子線性表

獲得子線性表的方法有 subList,返回指定下標範圍的子線性表。

生成線性表迭代器

生成線性表迭代器的方法有 listIterator,該方法如果沒有引數則返回線性表中元素的迭代器,如果指定下標則返回從指定下標開始的元素的迭代器。線性表迭代器的介面是 ListIterator,該介面繼承了 Iterator 介面,支援雙向遍歷。

陣列線性表類 ArrayList 和連結串列類 LinkedList

ArrayList 類和 LinkedList 類是 List 介面的兩個實現類,底層實現分別是陣列和雙向連結串列。

隨機訪問元素方面:ArrayList 類的底層實現是陣列,因此可以快速返回指定下標處的元素;LinkedList 類的底層實現是雙向連結串列,因此需要遍歷元素才能得到指定下標處的元素。

插入和刪除元素方面:ArrayList 進行插入和刪除元素時,除了在尾部插入和刪除元素不需要移動其他元素,否則都需要移動其他元素;LinkedList 進行插入和刪除元素時,只需要對插入位置前後的元素的前驅和後繼資訊進行修改即可,因此在頭部和尾部插入和刪除元素可以快速完成,在指定位置插入和刪除元素時則需要首先遍歷元素到指定位置。

ArrayList 類和 LinkedList 類都是不同步的,不保證執行緒安全。

向量類 Vector

Vector 類和 ArrayList 類的方法基本是相同的,主要區別是 Vector 類的所有方法都是同步的,因此可以保證執行緒安全。

雖然 Vector 是執行緒安全的,但是同步操作會花費更多的時間。在不需要保證執行緒安全的情況下,使用 ArrayList 比使用 Vector 效率更高。

對映和集合

Map 介面定義對映,儲存一組鍵值對的對映關係。

Set 介面定義集合,儲存一組互不相同的元素,該介面繼承了 Collection 介面。

Map 介面的概念和常用方法

Map 介面儲存一組鍵值對的對映關係,對映中的每個鍵對應一個值。對映中不能有重複的鍵,否則會出現一個鍵對應多個值的情況,這違背了對映的定義。

放置鍵值對

放置鍵值對的方法有 put 和 putAll,其中 put 一次放置一個鍵值對,putAll 一次將另一個對映中的鍵值對全部新增道當前對映中。在對映中放置鍵值對時,如果對映中沒有對應的鍵,則在對映中新建一個鍵值對,否則用新的鍵值對覆蓋原來的相同鍵的鍵值對。

刪除鍵值對

刪除鍵值對的方法有 remove 和 clear,其中 remove 刪除指定鍵的鍵值對,clear 刪除當前對映中的全部鍵值對。

判斷包含鍵或值

判斷包含鍵或值的方法有 containsKey 和 containsValue,其中 containsKey 判斷對映中是否包含指定的鍵,containsValue` 判斷對映中是否包含指定的值。

根據鍵獲得值

根據鍵獲得值得方法有 get,該方法返回對映中指定鍵的值。

獲得鍵或鍵值對的集合

獲得鍵或鍵值對的集合的方法有 entrySet 和 keySet,其中 entrySet 返回對映的所有鍵值對的集合,keySet 返回對映的所有鍵的集合。

獲得值的容器

獲得值的容器的方法有 values,該方法返回對映的所有值的容器。

其他方法

方法 isEmpty 判斷當前對映是否為空(即不包含鍵值對),方法 size 返回對映中的鍵值對數目。

Map 介面的實現類 HashMap、Hashtable 和 TreeMap

HashMap 和 Hashtable

HashMap 類是雜湊對映,通過雜湊函式計算鍵對應的儲存位置,因此可以快速地完成放置鍵值對、刪除鍵值對、根據鍵獲得值的操作。

JDK 1.8 之前的 HashMap 的底層通過陣列和連結串列實現,如果出現衝突則通過拉鍊法解決衝突。JDK 1.8 在解決衝突時的實現有較大變化,當連結串列長度大於閾值(預設為 8)時,將連結串列轉化為紅黑樹,以減少搜尋時間。

Hashtable 類是雜湊表,其功能和 HashMap 相似。以下是 HashMap 和 Hashtable 的部分割槽別。

  • HashMap 不是執行緒安全的,Hashtable 的大多數方法用關鍵字 synchronized 修飾,因此 Hashtable 是執行緒安全的。
  • 在不需要保證執行緒安全的情況下,HashMap 的效率高於 Hashtable。
  • HashMap 允許鍵或值為 null,只能有一個鍵為 null,可以有一個或多個鍵對應的值為 null,Hashtable 不允許鍵或值為 null。

從 JDK 1.8 開始,HashMap 在連結串列長度大於閾值(預設為 8)時,將連結串列轉化為紅黑樹以減少搜尋時間,Hashtable 沒有這樣的機制。

TreeMap

TreeMap 是有序對映,鍵可以使用 Comparable 介面或 Comparator 介面排序。

TreeMap 的底層實現是紅黑樹,通過紅黑樹維護對映的有序性。由於要維護對映的有序性,因此 TreeMap 的各項操作的平均效率低於 HashMap,但是 TreeMap 可以按照順序獲得鍵值對。

Set 介面的定義和常用方法

Set 介面儲存一組互不相同的元素,一個集合中不存在兩個相等的元素。

Set 介面繼承了 Collection 介面,沒有引入新的方法或常量,只是規定其例項不能包含相等的元素。

Set 介面的實現類 HashSet 和 TreeSet

HashSet

HashSet 類是雜湊集合,其底層實現基於 HashMap。當物件加入雜湊集合時,需要判斷元素是否重複,首先通過方法 hashCode 計算物件的雜湊碼檢查是否有物件具有相同的雜湊碼,如果沒有相同的雜湊碼則沒有重複元素,否則再通過方法 equals 檢查是否有相等的物件。

根據雜湊約定,如果兩個物件相同,它們的雜湊碼一定相同,因此如果在子類中重寫了 equals 方法,必須在該子類中重寫 hashCode 方法,以保證兩個相等的物件對應的雜湊碼是相同的。

TreeSet

TreeSet 類是有序集合,其底層實現基於 TreeMap。和 TreeMap 相似,TreeSet 可以使用 Comparable 介面或 Comparator 介面對元素排序。

相關文章