天天看點

Java核心技術(基礎知識)筆記

第一章 Java程式設計概述

面向對象設計

用木匠打個比方,一個“面向對象的”木匠始終關注的是所制作的椅子,第二位才是所使用的工具;一個“非面向對象”的木匠首先考慮的是所用的工具。

即時編譯

解釋虛拟機指令肯定會比全速運作機器指令慢很多。然而,虛拟機有一個選項,可以将執行最頻繁的位元組碼序列翻譯成機器碼,這一過程被稱為即時編譯。

位元組碼可以(在運作時刻)動态的翻譯成對應運作這個應用的特定CPU機器碼。

即時編譯器還可以消除函數調用(即“内聯”)。

第三章 Java的基本程式設計結構

一個簡單的Java應用程式

根據Java語言規範,Java虛拟機将從指定類中的main方法開始執行。

不過,當main方法不是public時,有些版本的Java解釋器也可以執行Java應用。這是因為Java虛拟機規範并沒有要求main方法一定是public。這個問題已經得到了修複,在Java SE 1.4及以後的版本中強制main方法時public的。

整形

從Java 7開始,加上字首0B或者0b就可以寫二進制數。例如:0b1001就是9。同樣,從Java 7開始,還可以為數字字面量加下劃線,如用1_000_000表示一百萬。

浮點類型

所有”非數值“的值都認為是不同的。

可以使用 Double.isNaN() 來判斷(長得帥的肯定會戳進去看一下源碼,很有意思哦)。

浮點數不适用于無法接受舍入誤差的金融計算中。例如,

這是因為二進制無法精确的表示 1/10。

可移植性是Java的設計目标之一。無論在哪個虛拟機上運作,同一運算應該得到同樣的結果。對于浮點數的算術運算,實作這樣的可移植性是相當困難的。double類型使用64位儲存一個數值,而有些處理器使用80位浮點寄存器。這些寄存器增加了中間過程的計算精度。例如:

使用嚴格浮點計算,截斷中間數,可以了解一下 strictfp 關鍵字。

char類型

轉義字元\u可以出現在加引号的字元常量或字元串之外(其他的轉義字元不行)。例如:

\u005B\u005D其實就是[],是以上面的代碼就是程式的入口main函數。(可以編譯成class檔案并運作,但是不被IDE識别)

Unicode轉義字元會在解析代碼之前得到處理,舉個吓死人的例子:

當你在代碼裡面加上這行注釋的時候,點選運作按鈕就會發現,編譯過不了!!!我的了解是先會将檔案裡面的轉義字元全部處理一下,當處理到這行注釋的時候,發現了

\u

會當成轉義字元來處理,但是

\u

後面的字元不合法,是以就報錯了。

Unicode的基本平面,輔助平面,碼點的概念,可以看維基百科。

一個字元可能有多個碼點,一個char隻能表示一個碼點。

運算符

整數被0除将會産生一個異常,而浮點數被0除将會得到無窮大或者NaN結果。

數值類型之間的轉換

兩個操作數有一個是 double ,則兩個數按照 double 處理,

否則,有一個是 float,則兩個數按照 float 處理,

否則,有一個是 long,則兩個數按照 long 處理,

否則,兩個數按照 int 處理。

byte a = 1;
byte b = 2;
byte c = a + b; // error
           
byte a = 1;
a += 1; // ok,因為 += 會自動進行強制轉換
           

位運算符

處理整形類型時,可以直接對組成整形數值的各個位完成操作,浮點數不行。

int a = 1;
a << 1; // ok
double b = 3;
b << 1; // error
           

位移運算符的右操作數要完成模32的運算(如果左操作是long型,則需要模64)。

字元串

編譯器可以讓字元串共享,隻有字元串常量是共享的,+或者substring等操作産生的結果并不是共享的。

格式化輸出:

使用參數索引來對一個參數進行多次格式化:

可以看到第二個索引對

new Date()

參數格式化了多次。使用

<

标志也可以達到同樣的效果。

數組

Java中,允許數組長度為0,數組長度為0和null不同。

數組排序

利用數組寫一個抽彩遊戲(這個算法還是很有想法的):

int[] numbers = new int[n];

for(int i=0; i<n; i++) {
    numbers[i] = i + 1;
}

int[] result = new int[k];
for (int i=0; i<k; i++) {
    int r = (int)(Math.random() * n);
    result[i] = numbers[r];
    // 最關鍵的代碼,将上面随機出來的數用最後一個數覆寫
    numbers[r] = numbers[n - 1];
    // 将 n 減一,相當于去掉最後一個數
    n--;
}
           

第四章 類與對象

面向對象程式設計概述

OOP将資料放在第一位,然後再考慮操作資料的算法。

對象

對象狀态的改變必須通過調用方法實作,如果不經過方法調用就可以改變對象狀态,隻能說明封裝性遭到了破壞。

類之間的關系

最常見的關系有:

  • 依賴(“use-a”)
  • 聚合(“has-a”)
  • 繼承(“is-a”)

Java類庫中的LocalDate類

将時間與月曆分開是一種很好的面向對象設計。通常,最好使用不同的類表示不同的概念。

使用者自定義類

在一個源檔案中,隻能有一個公有類,但可以有任意數目的非共有類。

第一眼看到這句話我是懵逼的,後來仔細看了代碼,發現應該說的是非内部類。

// Main.java
public class Main{
    public class Inner{}
}

class Main2 {
    
}
           

上面的源檔案是Ok的。但是把Main2改成public的就不行。

封裝的優點

可以改變内部實作,除了該類的方法之外,不會影響其他代碼。

更改器方法可以執行錯誤檢查,然而直接對域進行指派将不會進行這些處理。

靜态常量

我們常使用的System類

public class System {
    ...
    public static final PrintStream out = ...;
}
           

我們知道 final 修飾的變量是不允許将别的值賦給它的,但是System類有這樣的一個方法:

它可以将System.out設定為不同的流,原因是setOut是一個本地方法,它可以繞過Java語言的存取控制機制。

方法參數

Java程式設計語言總是采用按值調用。有些程式員認為Java程式設計語言對對象采用的是引用調用,實際上,這種了解是不對的,下面給出例子:

public static void swap(Car a, Car b) {
    Car temp = a;
    a = b;
    b = temp;
}
           

如果Java對對象采用的是按照引用傳遞,那麼這個方法應該夠實作交換資料的效果,但是,并沒有。參數被初始化為對象引用的拷貝。

初始化塊

調用構造器的具體處理步驟:

  1. 所有資料域被初始化為預設值(0,false或null)。
  2. 按照在類聲明中出現的順序,依次執行所有域初始化語句和初始化塊。
  3. 如果構造器第一行調用了第二個構造器,則執行第二個構造器主體。
  4. 執行這個構造器的主題。
public class Main {
    {
        a = 2;
    }
    private int a = 3;
    
    public int getA() {
        return a;
    }
}

// Main m = new Main(); 問,m.getA() 的值?
           

對象析構與 finalize 方法

在實際應用中,不要依賴使用 finalize 方法回收任何短缺的資源,這是因為很難知道這個方法什麼時候才能夠調用。

我一直覺得,final,finally,finalize有啥差別,這個問題很傻×,因為他們毛關系沒有,差別從何談起。問問 final 與 volitile的差別吧!!!

将類放入包中

假定有一個源檔案開頭有下列語句:

編譯器在編譯源檔案的時候不檢查目錄結構,即使這個源檔案沒有在子目錄 com/aprz 下,也可以進行編譯。但是,最終的程式将無法運作。如果包與目錄不比對,虛拟機就找不到類。

包作用域

如果,把一個類檔案放置在類路徑的某處的 java/awt 子目錄下,那麼我們就可以通路 java.awt 包的内部了。非常危險!

從 1.2 版開始,JDK 的實作者修改了類加載器,明确禁止加載使用者自定義的、包名以“java”開始的類!

類路徑

javac編譯器總是在目前的目錄中查找檔案,但Java虛拟機僅在類路徑中有“.”目錄的時候才檢視目前目錄。如果沒有設定類路徑,那也并不會産生什麼問題,預設的類路徑包含“.”目錄。然而如果設定了類路徑卻忘記了包含“.”目錄,則程式仍然可以通過編譯,但不能運作。

下面看一個類路徑示例:

/home/user/classdir:.:/home/user/archives/archive.jar
           

假定虛拟機要搜尋

com.horstmann.corejava.Employee

類檔案。它首先要檢視儲存在jre/lib和jre/lib/ext目錄下的歸檔檔案中所存放的系統類檔案。顯然,在那裡找不到相應的類檔案,然後再檢視類路徑。然後查找一下檔案:

/home/user/classdir/com/horstmann/corejava/Employee.class
com/horstmann/corejava/Employee.class從目前目錄開始
com/horstmann/corejava/Employee.class inside /home/user/archives/archive.jar
           

編譯器定位檔案要比虛拟機複雜得多。如果引用了一個類,而沒有指出這個類所在的包,那麼編譯器将首先查找包含這個類的包,并詢查所有的import指令,确定其中是否包含了被引用的類。例如,假定源檔案包含指令:

import java.util.*;
import com.horstmann.corejava.*;
           

并且源代碼引用了Employe類。**編譯器将試圖查找jva.lang.Employee (因為java lang包被預設入)、java.util.Employee、com.hostmann.corejava.Employee和目前包中的Employee。**對這個類路徑的所有位置中所列出的每個類進行逐檢視。 如果找到了一個以上的類,就會産生編譯錯誤(因為類必須是唯一的, 而import語句的次序卻無關緊要)。

編譯器的任務不止這些,它還要檢視源檔案( Source files) 是否比類檔案新。如果是這樣的話,那麼源檔案就會自動地重新編譯。

在前面已經知道,僅可以導入其他包中的公有類。一個源檔案隻能包含一個公有類,并且檔案名必須與公有類比對。是以,編譯器很容易定位公有類所在的源檔案。當然,也可以從目前包中導入非公有類。這些類有可能定義在與類名不同的源檔案中。如果從目前包中導入一個類,編譯器就要搜尋目前包中的所有源檔案,以便确定哪個源檔案定義了這個類。

這一段很長,我隻能說QQ的圖檔文字識别真的牛逼。

第五章 繼承

類、超類和子類

字首“超”與“子”來源于計算機科學和數學理論中的集合語言的術語。

覆寫方法

盡管子類對象有父類的私有域,但是卻無法在子類中通路這個域。(這句話是我總結的,可能并不嚴謹)

有些人(包括我)認為super與this應用是類似的概念,實際上,這樣比較并不太恰當。這是因為super不是一個對象的引用,不能将super賦給另一個對象變量,它隻是一個隻是編譯器調用超類方法的特殊關鍵字。

動态綁定

虛拟機知道一個引用的對象類型,是以能夠正确的調用相應的方法。

多态

在Java中,子類數組的引用可以轉換成超類數組的引用,而不需要采用強制類型轉換。例如:

public class A {}
public class B extends A {}

B[] bs = new B[8];
A[] as = bs;
           

但是這樣會有一個問題,如下:

編譯器是會接納這個指派操作的。由于bs與as指向同一個數組,當調用B中特有的方法時,就會出現錯誤。而且在運作時還會報出 ArrayStoreException 錯誤。

了解方法調用

  1. 編譯器檢視對象的聲明類型和方法名。假設調用

    x.f(param)

    ,且隐私參數

    x

    聲明為

    C

    類的對象。編譯器會一一列舉所有

    C

    類中名為

    f

    的方法和其超類中通路屬性為

    public

    且名為

    f

    的方法。
  2. 接下來,編譯器将檢視調用方法時提供的參數類型。如果在所有名為f的方法中存在一個與提供的參數類型完全比對,就選擇這個方法。這個過程被稱為重載解析。

允許子類将覆寫方法的傳回類型定義為原傳回類型的子類型。

// A.java
public Father find() {...}
--------------------------------
// B.java
@Override
public Son find() {...}
           
  1. 如果是 private方法,static方法,final方法或者構造器,那麼編譯器可以準确的知道應該調用哪個方法,這種調用方式就是靜态綁定。
  2. 當程式運作,并且采用動态綁定調用方法時,虛拟機一定調用與x所引用對象的實際類型最合适的那個類的方法。每次調用方法都要進行搜尋,時間開銷相當大。是以,虛拟機預先為每個類建立了一個方法表,其中列出了所有方法的簽名和實際調用的方法。這樣一來,在真正調用的時候,虛拟機僅查找這個表就行了。

在覆寫一個方法的時候,子類不能低于超類方法的可見性。

阻止繼承:final類和方法

在早期的Java中,有些程式員為了避免動态綁定帶來的系統開銷而使用final關鍵字。如果一個方法沒有被覆寫而且很短,編譯器就能夠對它進行優化處理,這個過程為稱為内聯。例如,内聯調用

e.getName()

将被替換為通路

e.name

域。這是一項很有意義的改進,這是由于CPU在處理調用方法的指令時,使用的分支轉移會擾亂預取指令的政策。然面,如果

getName

在另外個類中被覆寫, 那麼編譯器就無法知道覆寫的代碼将會做什麼操作,是以也就不能對它進行内聯處理了。

幸運的是,虛拟機中的即時編譯器比傳統編譯器的處理能力強得多。這種編譯器可以準确地知道類之間的繼承關系,并能夠檢測出類中是否真正地存在覆寫給定的方法。如果方法很簡短、被頻繁調用且沒有真正被覆寫,那麼即時編譯器就會将這個方法進行内聯處理。如果虛拟機加載了另外一個子類,而在這個子類中包含了對内聯方法的覆寫,那麼将會發生什麼情況呢?優化器将取消對覆寫方法的内聯。這個過程很慢,但卻很少發生。

Object:所有類的超類

所有的數組類型,不管時對象數組還是基本類型的數組都擴充了Object類。

equals方法

在子類中定義 equals 方法時,首先調用超類的 equals。如果檢測失敗,對象就不可能相等。如果超類中的域都相等,就需要比較子類中的執行個體域。

相等測試與繼承

如果隐私和顯式的參數不屬于同一個類,equals方法将如何處理呢?這是一個很有争議的話題!

許多程式員喜歡使用 instanceof 進行檢測:

if(!(otherObject instanceof Person)) {
    return false;
}
           

這樣做不但沒有解決 otherObject 是子類的情況,并且還有可能會招來一些額外的麻煩。

Java語言規範要求 equals 方法具有下面的特性:

  1. 自反性
  2. 對稱性
  3. 傳遞性
  4. 一緻性
  5. 對于任意非空引用x,x.equals(null),應該傳回false。

就對稱性來說,當參數不屬于同一個類的時候需要仔細思考一下。

e.quals(m);
           

e 是父類,m是子類。如果這兩個對象的執行個體域都一樣,當使用 instanceof 操作符的時候,會傳回 true,那麼意味着,m.equals(e),也會傳回true。但是實際上,反過來調用是無法通過 instanceof 操作符的。

建議的規則:

  • 如果子類能夠擁有自己的相等概念,則對稱性需求将強制采用 getClass進行檢測
  • 如果由超類決定相等的概念,那麼就可以使用 instanceof 進行檢測,這樣可以在不同子類的對象之間進行相等的比較。

一個完美equals方法的編寫模闆:

  1. 顯示參數命名為 otherObject,稍後需要将它轉換成另一個叫做 other 的變量。
  2. 檢測 this 與 otherObject 是否引用同一個對象。
    if (this == otherObject) {
        return true;
    }
               
  3. 檢測otheObject是否為null,如果為null,傳回false。
    if(otherObject == null) {
        return false;
    }
               
  4. 比較this與otherObject是否屬于同一個類。如果equals的語義在每個子類中有所改變,就使用getClass檢測。如果所有的子類擁有統一的語義,就使用instanceof檢測。
  5. 将otherObject轉換為相應類型的變量

    ClassName other = (ClassName) otherObject;

  6. 開始對所有需要比較的域進行比較,如果在子類中重新定義equals,就要在其中包含調用 super.equals(other)。

一種常見的錯誤是将equals方法的參數類型改為具體需要比較的類型:

注意上面的方法不是覆寫,因為參數類型不一樣,Object類中equal方法的參數是Object。

hashCode

StringBuilder sb = new StringBuilder("ok");
StringBuffer tb = new StringBuffer("ok");
           

注意,sb與tb的hashCode不一樣,這是因為StringBuffer沒有定義自己的hashCode方法。

equals方法相等則hashCode必須一緻。

泛型數組清單

一旦确定數組清單的大小不再發生變化,就可以調用 trimToSize 方法。這個方法将儲存區域的大小調整為當權元素數量所需要的儲存空間數目。垃圾回收器将回收多餘的儲存空間。

對象包裝器與自動裝箱

對象包裝器類是不可變的,即一旦構造了包裝器,就不允許更改包裝在其中的值。同時,對象包裝器類還是final,是以不能定義它們的子類。

自動裝箱規範要求 boolean、byte、char <= 127,介于-128~127之間的short和int被包裝到固定的對象中。這句話乍一看很詭異,其實都是同一個意思。

char 的範圍是從 0 ~ 65535。

在運作時使用反射分析對象

setAccessible方法時AccessibleObject類中的一個方法,它是Field、Method和Constructor類的公共超類。

利用get方法可以通路域的值,但是有一個需要解決的問題。如果域是一個String類型,把它當作Object傳回沒有什麼問題,但是,假設這個域是double類型的,而Java中數值類型不是對象,該怎麼辦呢?其實反射機制會自動地将這個域值打包到相應的對象包裝器中。invoke 方法也是如此。

繼承的設計技巧

  1. 将公共操作和域放在超類
  2. 不要使用受保護的域

    protect 在某種程度上破壞了封裝,因為同一個包類的代碼也可以通路該域。

    子類也可以随便通路超類的protect域。

  3. 使用繼承實作“is-a”關系
  4. 除非所有繼承的方法都有意義,否則不要使用繼承
  5. 在覆寫方法時,不要改變預期的行為
  6. 使用多态,而非類型資訊
  7. 不要過多的使用反射

第六章 接口、lambda表達式與内部類

Comparable<T> 接口

  • 如果子類之間的比較含義不一樣,那就屬于不同類對象的非法比較。每個compareTo方法都應該在開始時進行下列檢測:
    if (getClass() != other.getClass) {
        throw new ClassCastException();
    }
               
  • 如果存在一種通用算法,它能夠對兩個不同的子類對象進行比較,則應該在超類中提供一個compareTo方法,并将這個方法聲明為final。

靜态方法

在Java SE8中,允許在接口中增加靜态方法。隻是有違于将接口作為抽象規範的初衷。

目前為止,通常的做飯都是将靜态方法放在伴随類中。Collection/Collections…

預設方法

可以為接口提供一個預設方法。必須用default修飾符标記這樣的一個方法。

一般來說,這并沒有太大的用處。但是當一個接口的方法特别多是就可以很有用。

public interface Listener {
    void fa();
    void fb();
    void fc();
    void fd();
    void fe();
}
           

大多數情況下,我們隻關心其中的一兩個方法。在Java SE8 中我們就可以将它聲明為預設方法,什麼也不做。

public interface Listener {
    default void fa() {};
    default void fb() {};
    default void fc() {};
    default void fd() {};
    default void fe() {};
}
           

這樣一來,使用者就隻需要覆寫真正關心的方法。

預設方法的一個重要作用是“接口演化”。以Collection接口為例,假設你有一個類實作了這個接口:

後來,在Java SE8中,又為這個接口增加了一個stream方法。

假設steam不是預設方法。那麼Bag類将無法編譯,因為它沒有實作這個方法。為接口增加一個非預設方法不能保證源代碼相容。

不過,如果不重新編譯這個類,而是使用原來的包含這個類的JAR檔案,這個類仍然可以正常加載。**為接口增加方法可以保證二進制相容。**不過,如果調用了steam方法,就會抛出一個AbstractMethodError。

解決預設方法沖突

如果先在一個接口中将一個方法定義為預設方法,然後又在超類或另一個接口中定義了同樣的方法,會發生什麼情況呢?

  1. 超類優先。如果超類提供了具體的方法,會忽略接口的預設方法。可以保證與Java SE7的相容性。
  2. 接口沖突。如果一個接口提供了預設方法,另一個接口提供了一個同名且參數類型相同的方法(不管是不是預設的),必須覆寫這個方法來解決沖突。

千萬不要讓一個預設方法重新定義Object類中方法!!!

對象克隆

clone方法是Object的一個protected方法,這說明你的代碼不能直接調用這個方法。

預設的克隆操作是“淺拷貝”,并沒有克隆對象中引用的其他對象。

Cloneable接口并沒有什麼作用,它隻是一個标記,訓示類設計者了解克隆過程,clone方法是從Object類中繼承過來的。

必須當心子類的克隆。

為什麼引入lambda表達式

lambda表達式是一個可傳遞的代碼塊,可以在以後執行一次或多次。

lambda表達式的文法

如果可以推導出一個lambda表達式的參數類型,則可以忽略其類型。例如:

Comparator<String> comp = 
    (first, second) -> first.length() - second.length();
           

無需指定lambda表達式的傳回類型。lambda表達式的傳回類型總是會由上下文推導得出。

如果,一個lambda表達式隻在某些分支傳回一個值,而在另外一些分支不傳回值,這是不合法的。

函數式表達式

對于隻有一個抽象方法的接口,需要這種接口的對象時,就可以提供一個lambda表達式。這種接口成為函數式接口。

最好把lambda表達式看作是一個函數,而不是一個對象,另外要接受lambda表達式可以傳遞到函數式接口。

不過,Java現在對lambda表達式能做的也隻是轉換為函數式接口。

Java API 在 java.util.function 包中定義了很多通用的函數式接口。例如:BiFunction<T,U,R>。

方法引用

有時,可能已經有現成的方法可以完成你想要傳遞到其他代碼的某個動作。例如:

這樣,看起來很簡潔了,但是如果能把 println 方法傳遞到 Timer 的構造器就更好了,如下:

表達式 System.out::println 是一個方法引用,它等價于 x-> System.out.println(x);

方法引用的寫法有3中:

  • objcet::instanceMethod
  • Class::staticMethod
  • Class::instanceMethod

前兩種情況,方法引用等價于提供方法參數的lambda表達式。

對于第三種情況,第一個參數會成為方法的目标。例如:

String::compareToIgnoreCase 等價于
(x, y) -> x.compareToIgnoreCase(y);
           

可以在方法引用種使用 this 與 super。

構造器引用

構造器引用與方法引用類似,隻不過方法名為new。例如,Person::new是Person構造器的一個引用。但是它具體引用的是哪個構造器與上下文有關。

ArrayList<String> names = ..;
Stream<Person> stream = names.steam().map(Person::new);
           

map方法會調用Person(String) 構造器。

可以用數組類型建立構造器引用。例如,int[]::new 是一個構造器引用,它有一個參數:即數組的長度。

Java有一個限制,無法構造泛型類型T的數組。new T[] 會産生錯誤。

變量作用域

有時候,我們會在lambda表達式種通路外圍方法或類中的變量。例如:

String text = "ok";
ActionListener listener = event -> {
  	Sysout.out.println(text);  
};

// 1000年之後
new Timer(delay, listener);
           

仔細想想,這裡會有問題。lambda表達式的代碼可能會在很久之後才運作,那個時候,text可能已經不存在了。要了解發生了什麼,我們先來了解lambda表達式的組成:

  1. 一個代碼塊
  2. 參數
  3. 自由變量的值,這是指非參數而且不在代碼中定義的變量。

lambda表達式的資料結構必須存儲自由變量的值。 關于代碼塊與自由變量在别的語言裡面有一個術語叫閉包。

lambda表達式引用了自由變量,無法對這個自由變量進行更改,因為會引發多線程問題。

在一個lambda表達式中使用this關鍵字時,是指建立這個lambda表達式的方法的this參數。

public class Application {
    public void init {
        ActionListener listener = event -> {
            Sysout.out.println(this.toString());
        }
    }
}
           

表達式會調用Application的toString,而不是 ActionListener的toString。

處理lambda表達式

如果設計你自己的接口,其中隻有一個抽象方法,可以用 @FunctionalInterface 注解來标記這個接口。

再談Comparator

靜态 comparing 方法取一個“鍵提取器”函數,它将類型T映射為一個可比較的類型。對要比較的對象應用這個函數,然後對傳回的鍵完成比較。

還有很多其他的方法,需要自己去戳源碼。

内部類的特殊規則文法

可以通過顯示的命名将外圍類引用設定為其他的對象。

public class Out{
    public class In {
        
    }
}
-----------------------------
Out o = new Out();
Out.In in = o.new In();
           

非靜态内部類的所有靜态域都必須是final的。

非靜态内部類不能有static方法。

非static的内部類,在外部類加載的時候,并不會加載它,是以它裡面不能有靜态變量或者靜态方法。

  • static類型的屬性和方法,在類加載的時候就會存在于記憶體中。
  • 要使用某個類的static屬性或者方法,那麼這個類必須要加載到jvm中。

基于以上兩點,可以看出,如果一個非static的内部類如果具有static的屬性或者方法,那麼就會出現一種情況:内部類未加載,但是卻試圖在記憶體中建立static的屬性和方法,這當然是錯誤的。原因:類還不存在,但卻希望操作它的屬性和方法。

内部類是否有用、必要和安全

内部類是一種編譯器現象,與虛拟機無關。編譯器将會把内部類翻譯成用 $ 符号分隔外部類名與内部類名的正常類檔案,而虛拟機對此一無所知。

内部類是如何通路外部的?

class Out {
    
    private int a;
    
    class In {

    }
    
    // 這是編譯器自動生成的方法,我們在内部類中調用a,實際上是使用了這個方法
    static int access$0(Out);
    
}
           

匿名内部類

由于構造器的名字必須與類名相同,而匿名類沒有類名,是以,匿名類不能有構造器。取而代之的是将構造參數傳遞給超類構造器。

對于靜态方法的日志問題,如果我們希望在靜态方法中輸出目前類的類名,但是靜态方法沒有this,可以使用如下方法:

代理的特性

所有的代理類都覆寫了Object類中的方法 toString、equals和hashCode。

對于特定的類加載器和預設的一組接口來說,隻能有一個代理類。如果使用同一個類加載器和接口數組調用兩次 newProxyInstance 方法的話,那麼隻能得到同一個類的兩個對象。

如果代理類實作的所有接口都是public的,那麼代理類不屬于某個特定的包。否則。所有非公有的接口都必須屬于同一個包,代理類也屬于這個包。

第七章 異常、斷言和日志

異常分類

Java核心技術(基礎知識)筆記

Error類層次結構描述了Java運作時系統的内部錯誤和資源耗盡錯誤。

Exception分為兩個分支:

一個分支派生于RuntimeException,另一個包含其他異常。

劃分規則是:由程式錯誤導緻的異常屬于RuntimeException;而程式本身沒有問題,但由于像IO錯誤這類問題的異常屬于其他異常。

聲明受查異常

如果在子類中覆寫了超類的一個方法,子類方法中聲明的受查異常不能比超類方法中聲明的異常更通用。

如果超類方法沒有抛出任何受查異常,子類也不能抛出任何異常。

捕獲異常

通常,應該捕獲那些知道如何處理的異常,而将那些不知道怎樣處理的異常繼續傳遞。

捕獲多個異常

在Java SE 7中,同一個catch子句中可以捕獲多個異常類型。

隻有當捕獲的異常類型彼此之間不存在子類關系時才需要這個特性。

捕獲多個異常類型時,異常變量隐含為final。

再次抛出異常與異常鍊

在catch子句中可以抛出一個異常,這樣做的目的是改變異常的類型。

try {
    ...
} catch (AException a) {
    throw new BException("msg");
}
           

不過,有一種更好的處理方法,并且将原始異常設定為新異常的“原因”:

try {
    ...
} catch (AException a) {
    Throwable t = new BException("msg"); 
    t.initCause(a);
    throw t;
}
           

這樣我們調用 e.getCause() 就可以拿到原始異常。強烈推薦使用這種包裝技術。這樣可以讓使用者抛出子系統中的進階異常,而不會丢失原始異常的細節。

在Java SE 7之前,無法抛出聲明異常之外的類型(雖然現在也是,但是編譯器的行為不一樣了):

public void update() throws SQLException {
    try {
        ...
    } catch(Exception e) {
        throw e;
    }
}
           

在Java SE 7之前,會有一個問題,編譯器會指出這個方法可以抛出任何異常,而不僅僅是SQLException。

現在編譯器會跟蹤到 e 來自 try 塊。假設這個try中僅有的已檢查異常是 SQLException的執行個體,另外,e沒有被改變,那麼這個方法就是合法的。

finally子句

強烈建議解耦 try/catch 和 try/finally 語句塊。這樣可以提高代碼的清晰度。

InputStream in = ...;
try {
    try {
        ...
    } finally {
        ...
    }
} catch (Exception e) {
    ...
}
           

這種設計方式不僅清楚,而且還有一個功能,可以捕獲 finally 裡面的異常。

當 finally 子句包含return語句時:

try {
    int r = n * n;
    return r;
} finally {
    if (n == 2) {
        return 0;
    }
}
           

在方法傳回之前,finally 子句的内容将會執行。如果finally子句中也有一個return語句,這傳回值将會覆寫原始的傳回值。

finally 子句的異常可能會覆寫try中的異常:

InputStream in = ...;
try {
    ...
} finally {
    in.close();
}
           

假設try塊中抛出了非IOException,而close方法也出了異常,那麼最後抛出的是 close 方法的 IOException。一般我們對 try 塊中的異常更感興趣,但是這時異常已經被丢失了,除非給 close 也加上 try 語句,這樣就會非常繁瑣。

帶資源的 try 語句

如果資源屬于一個實作了 AutoCloseable 接口的類,Java SE 7 為這種代碼模式提供了一個很有用的快捷方式。

try (Resource res = ...) {
    ...
}
           

try 塊退出時,或者存在一個異常,都會自動調用

res.close();

,資源會被關閉。

這種 try 語句自身也可以帶 catch 和 finally 語句,但是一般不用。

第八章 泛型程式設計

定義簡單泛型類

泛型類可以看作普通類的工廠。

類型變量的限定

一個類型變量或通配符可以有多個限定:

T extends Comparable & Serializable
           

限定類型用 & 分隔,而逗号用來分隔類型變量。如果用一個類來做限定,它必須是限定清單中的第一個。

類型擦除

虛拟機沒有泛型對象,它會擦除類型變量,并替換為限定類型(無限定類型的變量用Object)。

如果有多個限定,會怎麼樣呢?

原始類型用 Serialzable 替換 T,編譯器會在必要的時候将其強制轉換為 Comparable。為了提高效率,應該将标簽接口(沒有方法的接口)放在邊界清單的末尾。

當程式調用泛型方法時,如果擦除傳回類型,編譯器插入強制類型轉換。

翻譯泛型方法

假設我們有這樣的一個類:

class Pair<T> {
    private T first;
    private T second;
    
    ...
        
	public void setSecond(T second) {
        this.second = second;
    }
    
}
-----------------------------------
// 擦除之後,這裡 T 擦除之後是 Object
class Pair {
    private Object first;
    private Object second;
    
    ...
        
	public void setSecond(Object second) {
        this.second = second;
    }
    
}
           

使用一個類繼承它:

class Date extend Pair {
    
}
-----------------------------------------------
class Date extend Pair<LocalDate> {
    // 這裡不是重寫,隻是展示有一個從父類繼承過來的方法
	public void setSecond(LocalDate second) {
        ...
    }
}
           

于是問題就來了,假設我使用父類引用子類的變量,然後調用 setSecond 方法,那麼它本來應該走到子類的方法裡面去,但是由于泛型的擦除,導緻子類方法簽名不一緻了(父類是 Object,子類是 LocalDate)。

Date date = new Date();
Pare<LocalDate> pair = date;
// 猜猜它會調用那個方法
pair.setSecond(aDate);
           

現在多态與泛型擦除出現了沖突,解決方法是需要編譯器在 Date類中生成一個橋方法。

public void setSecond(Object second) {
    setSecond((Date)second);
}
           

然而,橋方法也會引出别的問題!

假設 Date 類覆寫了 getSecond 方法:

class Date extend Pair {
    // 這裡是重寫
    @Override
    public LocalDate getSecond() {
        ...
    }
}
           

那麼,Date 類裡面就有兩個同名方法了,參數一樣,隻有傳回值不一樣。編譯器是不允許這樣的,但是,在Java虛拟機中,用參數類型和傳回類型确定一個方法。是以,編譯器可能産生兩個僅傳回類型不同的方法位元組碼,虛拟機可以正确處理這樣的情況。

如果你記憶力比較好的話,前面也提到過在覆寫父類的方法時,可以傳回更加嚴格的類型,這也是利用的橋方法。

Java泛型轉換的事實:

  • 虛拟機中沒有泛型,隻有普通的類和方法
  • 所有的類型參數都用它們的限定類型替換
  • 橋方法被合成類保持多态
  • 為保持類型安全性,必要時插入強制類型轉換

不能用基本類型執行個體化類型參數

其原因是類型擦除,擦除之後,沒有限定類型的使用 Object 代替,而 Object 不能引用基本類型。

運作時類型查詢隻适用于原始類型

這裡隻是測試了 a 是否時一個 Pair 對象,與 String 毫無關系。

同樣的道理,getClass 也總是傳回原始類型。

Varargs 警告

Java 不允許建立泛型數組,其原因可以自己研究研究(泛型擦除)。假設我們有這樣方法:

ts 實際上時一個數組,考慮一下調用:

Collection<Pair<String>> table = ...;
Pair<String> p1 = ...;
Pair<String> p2 = ...;
addAll(table, p1, p2);
           

是以,Java虛拟機必須建立一個Pair<String> 數組,這就違反了不循序建立泛型數組的規定。不過對于這種情況,隻是會顯示一個警告。

不能構造泛型數組

最好讓使用者提供一個數組構造器的表達式。

泛型類的靜态上下文中類型變量無效

不能在靜态域中引用類型變量。

public class Singleton<T> {
    private static T singleInstance; // ERROR
}
           

因為如果能使用的話,不同的執行個體會有不同的類型。

不能抛出或捕獲泛型類的執行個體

泛型類擴充 Throwable 都是不合法的。

在異正常範中使用類型變量是合法的。

可以消除對受查異常的檢查

@SuppressWarnings("unchecked")
public static <T extends Throwable> void throwAs(Throwable t) throws T {
    throw (T)t;
}
           

編譯器會認為t是一個非受查異常。

再利用下面的代碼就會把所有異常轉換為編譯器所認為的非受查異常:

try {
    do work
} catch (Throwable t) {
    throwAs(t);
}
           

這樣,在某些不允許抛出任何受查異常的方法中,就可以使用這個方法來抛出受查異常。

注意擦除後的沖突

public class Pair<T> {
    public boolean equals(T t);
}
           

擦除之後,就是

public boolean equals(Object t);

與 Object 的方法重複了,會引發錯誤。

通配符概念

這個 set 方法不能傳遞任何參數,因為編譯器隻知道需要某個 Person 的子類,但是不知道具體是什麼類型。

void set(? super Person) {...}
           

該方法隻能傳遞Person或者Person的子對象。

直覺的講,帶有超類型限定的通配符可以向泛型寫入,帶有子類型限定的通配符可以從泛型對象讀取。

舉一個 super 限定符的應用:

LocalDate 實作了 ChronoLocalDate,而 ChronoLocalDate 擴充了 Comparable<ChronoLocalDate>。
是以,LocalDate 實作的是 Comparable<ChronoLocalDate>,而不是 Comparable<LocalDate>。

在這種情況下,可以使用超類限定符來救助(要注意泛型的 extends 與類的 extends 的不同意義):

public static <T extends Comparable<? super T>> T min(T[] a) {...}
           

上個圖來意思意思:

Java核心技術(基礎知識)筆記

無限定通配符

? getFirst()

void setFirst(?)
           

getFirst 的傳回值隻能指派給 Object。setFirst不能被調用,Object 也不行。

通配符捕獲

public static void swap(Pair<?> p)

public staic <T> void swapHelper(Pair<T> p)
           

比較有趣的是,可以在 swap 裡面調用 swapHelper。這種情況下,參數 T 捕獲通配符。

通配符捕獲隻有在有許多限制的情況下才是合法的,編譯器必須能夠确信通配符表達的是單個、确定的類型。

第九章 集合

疊代器

對 next 方法和 remove 方法的調用具有互相依賴性。

集合架構中的接口

List接口定義了多個用于随機通路的方法:

void add(int index, E element);
void remove(int index);
E get(int index);
E set(int index, E element);
           

坦率的講,集合架構的這個方面設計的很不好。

集合架構中有兩種類型的集合,其性能開銷有很大差異。由數組支援的有序集合可以快速地随機通路,是以适合使用List方法并提供一個整數索引來通路。而連結清單盡管也是有序的,但是随機通路會很慢,是以最好使用疊代器來周遊。是以如果原先就提供了兩個接口就會容易了解些了。

為了避免對連結清單完成随機通路操作,Java SE 1.4 引入了一個标記接口 RandomAccess。用來測試一個特定的集合是否支援高效的随機通路:

if (c instanceof RandomAccess) {
    // 支援
} else {
    // 不支援
}
           

Set接口等同于Collection接口,不過其方法的行為有更嚴謹的定義。

  • add 方法不允許增加重複的元素
  • equals 方法:隻要兩個集合包含相同的元素就認為是相等的,而不要求這些元素有相同的順序
  • hashCode 方法:要保證含相同元素的兩個集會得到相同的散列碼

既然兩個接口的方法簽名是一樣的,為什麼還要建立一個單獨的接口呢?

從概念上講,并不是所有集合都是集。建立一個Set接口可以讓程式員編寫隻接收集的方法。

連結清單

在 Java 程式設計語言中,所有連結清單實際上都是雙向連結的。

ListIterator 是 Iterator 的一個子接口,它新增了一些方法。LinkedList 的 listIterator 方法會傳回一個 ListIterator 的執行個體。注意,在使用 ListIterator 的 remove 方法時需要謹慎。

在調用 next 之後,remove 方法會删除疊代器左側的元素,但是,如果調用 previous 會删除疊代器右側的元素。

ConcurrentModificationException 異常的檢測有一個特例:

連結清單隻負責跟蹤對清單的結構性修改,例如,添加元素,删除元素。set方法不被視為結構性修改。可以将多個疊代器附加給一個連結清單,所有的疊代器都調用set方法對現有的結點的内容進行修改。

不要使用下面的方法來周遊連結清單:

for (int i=0; i<list.size(); i++) {
    Element e = get(i);
}
           

雖然 get 方法做了微小的優化(如果 i 大于 size()/2,會從後面開始周遊),但是這樣寫每次循環都要周遊一次。

散清單

在 Java中,散清單用連結清單數組實作。

樹集

Java SE 8 中使用的是紅黑樹。

将一個元素添加到樹中要比添加到散清單中慢,不過與檢查數組或連結清單中重複元素相比還是快很多。

優先級隊列

優先級隊列并沒有對所有的元素進行排序。它使用了一個優雅且高效的資料結構——堆。

映射 (Map)

總感覺翻譯有點奇怪!!!

更新映射項

看一個例子,統計單詞出現的頻率:

這會有一個問題,就是 get 可能會傳回 null。于是可以這樣寫,給一個預設值:

另一種方法就是先調用 putIfAbsent:

counts.putIfAbsent(word, 0);
counts.put(word, counts.get(word) + 1);
           

不過,還可以有更簡單的方式,使用 merge 方法,可以簡化這個常見的操作:

如果鍵值不存在,則将 word 置為 1,否則使用 Integer::sum 函數組合原值和 1。

映射視圖

集合架構不認為Map本身是一個集合。

Map提供了方法用來擷取映射視圖,映射視圖是實作了Collection接口或某個子接口的對象。

Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K,V>> entrySet();
           

這3個方法會分别傳回3個視圖。要注意這些視圖并不是 TreeSet 或者 HashSet 的執行個體,而是實作了 Set 接口的另外某個類的執行個體。

視圖與包裝器

再來說說 keySet 方法。初看起來,好像這個方法建立了一個新集,并将映射中的所有鍵都填進去,然後傳回這個集。但是,情況并非如此。取而代之的是:keySet 方法傳回一個實作 Set 接口的類對象,這個類的方法對原映射進行操作。這種集合稱為視圖。

輕量級集合包裝器

asList 傳回的并不是一個 ArrayList。它是一個視圖對象,帶有通路底層數組的 get 和 set 方法。改變數組大小的所有方法都會抛出一個 UnsupportedOperationException 異常。

子範圍

List group2 = staff.subList(10, 20);
group2.clear();
           

元素自動的從 staff 清單中清除,并且 group2 為空。

不可修改的視圖

簡而言之,就是對所有更改器方法直接抛出一個異常。

由于視圖隻是包裝了接口而不是實際的集合對象,是以隻能通路接口中定義的方法。

注意,視圖并沒有重新定義 Object 的 equals 和 hashCode 方法(内容是否相等的檢測在分層結構的這一層上沒有定義妥當)。

受查視圖

“受查”視圖用來對泛型類型發生問題時提供調試支援。

ArrayList<String> strings = new ArrayList<>();
ArrayList rawList = strings;
rawList.add(new Date());
           

這個錯誤在 add 的時候檢測不到。相反,隻有在調用 get 方法的時候才會抛出異常。受查視圖可以探測這類問題。

List<String> safeStrings = Collections.checkList(strings, String.class);
ArrayList rawList = safeStrings;
rawList.add(new Date()); // Error
           

虛拟機在運作到 add 方法時,就會抛出異常。

排序與混排

可以使用歸并排序對連結清單進行高效的排序。但是Java中不是這樣做的。它直接将所有元素轉入一個數組,對數組進行排序,然後,再将排序後的序列複制會連結清單。

集合類庫中使用的排序算法比快速排序要慢一些,快速排序時通用排序算法的傳統選擇。但是,歸并排序有一個主要的優點:穩定,即不需要交換相同的元素。

二分查找

隻有采用随機通路,二分查找才有意義。

第十四章 并發

中斷線程

沒有可以強制線程終止的方法。然而,interrupt 方法可以用來請求線程終止。

但是,如果線程被阻塞,就無法檢測中斷狀态。

當在一個被阻塞的線程(sleep或者wait)上調用 interrupt 方法時,阻塞調用将會被 Interrupted Exception 異常中斷。

如果在每次工作疊代之後都調用 sleep 方法(或者其他可中斷方法),isInterrupted 檢測既沒有必要也沒有用處。如果在中斷狀态被置位時調用 sleep 方法,它不會休眠。相反,它将清除這一狀态并抛出InterruptedException。

不要将 InterruptedException 捕獲在低層次上!

要麼捕獲然後再次設定中斷狀态,要麼直接抛出。

可運作線程

在任何給定時刻,一個可運作的線程可能在運作也可能沒有運作(這就是為什麼将這個狀态稱為可運作而不是運作)。

被阻塞線程和等待線程

被阻塞狀态與等待狀态是有很大不同的。其實這句話我還不太能夠了解,是本質上不同,還是Java行為上不同?

線程優先級

每一個線程有一個優先級。預設情況下,一個線程繼承它的父線程的優先級。

守護線程

守護線程應該永遠不去通路固有資源,如檔案,資料庫,因為它會在任何時候甚至在一個操作的中間發生中斷。

setDaemon 必須線上程啟動之前調用。

未捕獲異常處理器

線程的 run 方法不會抛出任何受查異常,非受查異常會導緻線程終止。線上程死亡之前,異常被傳遞到一個用于未捕獲異常的處理器。

setUncaughtExceptionHandler 方法會未任何線程安裝一個預設的處理器。

也可以用Thread類的靜态方法 setDefaultUncaughtExceptionHandler 為所有的線程安裝一個預設的處理器。

如果不安裝預設的處理器,預設的處理器為空。但是,如果不為獨立的線程安裝處理器,此時的處理器就是該線程的 ThreadGroup 對象。

ThreadGroup 類實作 Thread.UncaughtExceptionHanlder 接口。它的 uncaughtException 方法做如下操作:

  1. 如果該線程組有父線程組,那麼父線程組的 uncaughtException 方法被調用。
  2. 否則,如果 Thread.getDefaultExceptionHandler 方法傳回一個非空的處理器,則調用該處理器。
  3. 否則,如果 Throwable 是 ThreadDeath 的一個執行個體,什麼都不做。
  4. 否則,線程的名字以及 Throwable 的棧軌迹被輸出到 System.err 上。

鎖對象 (ReentrantLock)

如果使用鎖,就不能使用帶資源的 try 語句。

一是無法釋放鎖,二是會新建立一個變量。

鎖是可重入的,因為線程可以重複地獲得已經持有的鎖。鎖保持一個持有計數來跟蹤對lock方法的嵌套調用。

條件對象

使用一個條件對象來管理那些已經獲得了一個鎖但是卻不能做有用工作的線程。

一個鎖對象可以有一個或多個相關的條件對象。你可以用 newCondition 方法獲得一個條件對象。

signalAll 方法不會立即激活一個等待線程。它僅僅解除等待線程的阻塞,以便這些線程可以在目前線程退出同步方法之後,通過競争實作對對象的通路。

每一個對象有一個内部鎖,并且該鎖有一個内部條件。初學者常常對條件感到困惑,推薦先學習ReentrantLock 的 Condition。

wait、nofity、notifyAll 方法都需要目前線程持有鎖,否則會抛出異常。

同步阻塞

舉一個有趣的例子:

public class Sync {
    
    private Map<String, Person> pList = Collections.synchronizedList(new HashMap<>());
    
    public synchronized void putIfAbsent(String key, Person p) {
        if(!pList.contains(p)) {
            pList.put(p);
        }
    }
    
    public Person get(String key) {
        pList.get(key);
    }
    
}
           

先不管這個程式有什麼意義,隻問一個問題,這個類是線程安全的嗎?

雖然看起來很像是線程安全的,但是實際上不是,因為 Collections.synchronizedList 使用的鎖,肯定不是 Sync 的執行個體。

Volatile 域

僅僅為了讀寫一個或兩個執行個體域就使用同步,顯得開銷過大了。Volatile 可以幫助我們在這種情況下避免使用鎖。

先來看看多個線程為什麼會出現值不一緻的原因:

  • 多處理器的計算機能夠暫時在寄存器或本地記憶體緩存區中儲存記憶體的值。結果是,運作在不同處理器上的線程可能在同一個記憶體位置取到不同的值。
  • 編譯器可以改變指令執行的順序以使吞吐量最大化。這種順序上的變化不會改變代碼語義,但是編譯器假定記憶體的值僅僅在代碼中有顯示的修改指令時才會改變。然而,記憶體的值可以被另一個線程改變!

早期的CPU使用的是總線鎖的方式來保證 Volatile 域的一緻性,現在都使用的是緩存一緻性。

final 變量

如果在某個類中,将一個域聲明為 final 類型,那麼會起到這樣的一個效果:

其他的線程會在該類的構造函數執行完畢之後才能看到這個 final 域的值。

原子性

假設對共享變量除了指派之外并不完成其他操作,那麼可以将這些共享變量聲明為 volatile。

java.util.concurrent.atomic 包中有很多類使用了很進階的機器級指令(不是使用鎖)來保證其他操作的原子性。

稍微提一下,使用 compareAndSet 實作樂觀鎖的常用寫法:

do {
    oldValue = largest.get();
    newValue = Math.max(oldValue, observed);
} while (!largest.compareAndSet(oldValue, newValue));
           

compareAndSet 的工作原理:期望記憶體中的值是 oldValue,是則用 newValue 替換它,傳回 true,不是則傳回 false。

如果有大量線程要通路相同的原子值,函數性能會大幅下降,因為樂觀更新需要太多次重試。

鎖測試與逾時

lock 方法不能被中斷,在獲得鎖之前會一直阻塞,如果出現死鎖,則 lock 方法無法終止。可以使用 tryLock 來響應中斷。

tryLock 還有一個隐藏特性:這個方法會搶奪可用的鎖,即使該鎖有公平加鎖政策,即便其他線程已經等待很久也是如此。

為什麼棄用 stop 和 suspend 方法

stop 方法:該方法終止所有未結束的方法,包括 run 方法。當線程被終止,立即釋放被它鎖住的所有對象的鎖。這會導緻對象處于不一緻的狀态。

例如:從 A 轉賬到 B,線程突然被終止,錢已經轉出去了,卻沒有進入 B 賬戶,那麼 Bank 對象就被破壞了。

suspend 方法:如果用該方法挂起一個持有鎖的線程,那麼該鎖在恢複之前是不可用的。如果調用 suspend 方法的線程試圖獲得同一個鎖,那麼程式死鎖:被挂起的線程等着被恢複,而将其挂起的線程等待獲得鎖。

ConcurrentHashMap

// map 是 ConcurrentHashMap 的執行個體
Long oldValue = map.get(word);
Long newValue = oldValue == null ? 1 : oldValue + 1;
map.put(word, newValue);
           

在上面的例子中,由于操作不是原子的,是以最終的結果不可預料。但是,要了解 ConcurrentHashMap 與 HashMap 的差別,這裡的 get 與 put 都是原子操作,在多線程情況下不會破壞 map 的結構,而 HashMap 在多線程情況下會出現循環連結清單等問題。

ConcurrentHashMap 傳回的疊代器具有弱一緻性。這意味着疊代器不一定能反映出它們被構造之後的所有的修改(可以認為是某一特定時刻的快照),它們不會将同一個值傳回兩次,不會抛出 ConcurrentModificationException。

CopyOnWriteArrayList 和 CopyOnWriteArraySet

CopyOnWriteArrayList 和 CopyOnWriteArraySet 是線程安全的集合,其中所有的修改線程對底層數組進行複制。

線程池

調用 shutdown 方法,該線程池不再接收新任務。當所有任務完成後,線程池死亡。

調用 shutdownNow 方法,該線程池取消尚未開始的所有任務,并視圖中斷正在運作的線程。

ExecutorCompletionService

如果有大量的 Callable 要執行,可以使用這個類。

Fork-Join 架構

這個架構用來分解子任務,提高線程使用率。

class Counter extends RecursiveTasks<Integer> {
    protected Integer compute() {
        if (to - from < THRESHOLD) {
            // .....
        } else {
            int mid = (from + to) / 2;
            Counter first = new Counter(values, from, mid, filter);
            Counter second = new Counter(values, mid, to, filter);
            // 阻塞
            invokeAll(first, second);
            // 合并
            return first.join() + second.join();
        }
    }
}
           

信号量

任何線程可以釋放任何數量的許可,這可能會增加許可數目以至于超出初始數目。