## 03 Java 常用關鍵字理解
## 引導語
Java 中的關鍵字很多,大約有 50+,在命名上我們不能和這些關鍵字沖突的,編譯會報錯,每個關鍵字都代表著不同場景下的不同含義,接下來我們挑選 6 個比較重要的關鍵字,深入學習一下。
### 1 static
意思是靜態的、全局的,一旦被修飾,說明被修飾的東西在一定范圍內是共享的,誰都可以訪問,這時候需要注意并發讀寫的問題。
1.1 修飾的對象
static 只能修飾類變量、方法和方法塊。補充:還有class。
當 static 修飾類變量時,如果該變量是 public 的話,表示該變量任何類都可以直接訪問,而且無需初始化類,直接使用 類名.static 變量 這種形式訪問即可。
這時候我們非常需要注意的一點就是線程安全的問題了,因為當多個線程同時對共享變量進行讀寫時,很有可能會出現并發問題,如我們定義了:public static List list = new ArrayList();這樣的共享變量。這個 list 如果同時被多個線程訪問的話,就有線程安全的問題,這時候一般有兩個解決辦法:
1. 把線程不安全的 ArrayList 換成 線程安全的 CopyOnWriteArrayList;
2. 每次訪問時,手動加鎖。
所以在使用 static 修飾類變量時,如何保證線程安全是我們常常需要考慮的。
當 static 修飾方法時,代表該方法和當前類是無關的,任意類都可以直接訪問(如果權限是 public 的話)。
有一點需要注意的是,該方法內部只能調用同樣被 static 修飾的方法,不能調用普通方法,我們常用的 util 類里面的各種方法,我們比較喜歡用 static 修飾方法,好處就是調用特別方便。
static 方法內部的變量在執行時是沒有線程安全問題的。方法執行時,數據運行在棧里面,棧的數據每個線程都是隔離開的,所以不會有線程安全的問題,所以 util 類的各個 static 方法,我們是可以放心使用的。
當 static 修飾方法塊時,我們叫做靜態塊,靜態塊常常用于在類啟動之前,初始化一些值,比如:
```
public static List<String> list = new ArrayList(); // 進行一些初始化的工作
static {
list.add("1");
}
```
這段代碼演示了靜態塊做一些初始化的工作,但需要注意的是,靜態塊只能調用同樣被 static 修飾的變量,并且 static 的變量需要寫在靜態塊的前面,不然編譯也會報錯。
1.2 初始化時機
對于被 static 修飾的類變量、方法塊和靜態方法的初始化時機,我們寫了一個測試 demo,如下圖:

打印出來的結果是:
父類靜態變量初始化 父類靜態塊初始化 子類靜態變量初始化 子類靜態塊初始化 main 方法執行 父類構造器初始化 子類構造器初始化
從結果中,我們可以看出兩點:
1. 父類的靜態變量和靜態塊比子類優先初始化;
2. 靜態變量和靜態塊比類構造器優先初始化。
被 static 修飾的方法,在類初始化的時候并不會初始化,只有當自己被調用時,才會被執行。
### 2 final
final 的意思是不變的,一般來說用于以下三種場景:
1. 被 final 修飾的類,表明該類是無法繼承的;
2. 被 final 修飾的方法,表明該方法是無法覆寫的;
3. 被 final 修飾的變量,說明該變量在聲明的時候,就必須初始化完成,而且以后也不能修改其內存地址。
第三點注意下,我們說的是無法修改其內存地址,并沒有說無法修改其值。因為對于 List、Map 這些集合類來說,被 final 修飾后,是可以修改其內部值的,但卻無法修改其初始化時的內存地址。
例子我們就不舉了,1-1 小節 String 的不變性就是一個很好的例子。
### 3 try、catch、finally
這三個關鍵字常用于我們捕捉異常的一整套流程,try 用來確定代碼執行的范圍,catch 捕捉可能會發生的異常,finally 用來執行一定要執行的代碼塊,除了這些,我們還需要清楚,每個地方如果發生異常會怎么辦,我們舉一個例子來演示一下:
```
public void testCatchFinally() {
try {
log.info("try is run");
if (true) {
throw new RuntimeException("try exception");
}
} catch (Exception e) {
log.info("catch is run");
if (true) {
throw new RuntimeException("catch exception");
}
} finally {
log.info("finally is run");
}
}
```
這個代碼演示了在 try、catch 中都遇到了異常,代碼的執行順序為:try -> catch -> finally,輸出的結果如下:

可以看到兩點:
1. finally 先執行后,再拋出 catch 的異常;
2. 最終捕獲的異常是 catch 的異常,try 拋出來的異常已經被 catch 吃掉了,所以當我們遇見 catch 也有可能會拋出
異常時,我們可以先打印出 try 的異常,這樣 try 的異常在日志中就會有所體現。
### 4 volatile
volatile 的意思是可見的,常用來修飾某個共享變量,意思是當共享變量的值被修改后,會及時通知到其它線程上,其它線程就能知道當前共享變量的值已經被修改了。
我們再說原理之前,先說下基礎知識。就是在多核 CPU 下,為了提高效率,線程在拿值時,是直接和 CPU 緩存打交道的,而不是內存。主要是因為 CPU 緩存執行速度更快,比如線程要拿值 C,會直接從 CPU 緩存中拿, CPU 緩存中沒有,就會從內存中拿,所以線程讀的操作永遠都是拿 CPU 緩存的值。
這時候會產生一個問題,CPU 緩存中的值和內存中的值可能并不是時刻都同步,導致線程計算的值可能不是最新的,共享變量的值有可能已經被其它線程所修改了,但此時修改是機器內存的值,CPU 緩存的值還是老的,導致計算會出現問題。
這時候有個機制,就是內存會主動通知 CPU 緩存。當前共享變量的值已經失效了,你需要重新來拉取一份,CPU 緩存就會重新從內存中拿取一份最新的值。
volatile 關鍵字就會觸發這種機制,加了 volatile 關鍵字的變量,就會被識別成共享變量,內存中值被修改后,會通知到各個 CPU 緩存,使 CPU 緩存中的值也對應被修改,從而保證線程從 CPU 緩存中拿取出來的值是最新的。
我們畫了一個圖來說明一下:

從圖中我們可以看到,線程 1 和線程 2 一開始都讀取了 C 值,CPU 1 和 CPU 2 緩存中也都有了 C 值,然后線程 1 把 C 值修改了,這時候內存的值和 CPU 2 緩存中的 C 值就不等了,內存這時發現 C 值被 volatile 關鍵字修飾,發現其是共享變量,就會使 CPU 2 緩存中的 C 值狀態置為無效,CPU 2 會從內存中重新拉取最新的值,這時候線程 2 再來讀取 C 值時,讀取的已經是內存中最新的值了。
### 5 transient
transient 關鍵字我們常用來修飾類變量,意思是當前變量是無需進行序列化的。在序列化時,就會忽略該變量,這些在序列化工具底層,就已經對 transient 進行了支持。
### 6 default
default 關鍵字一般會用在接口的方法上,意思是對于該接口,子類是無需強制實現的,但自己必須有默認實現,我們舉個例子如下: default 關鍵字被很多源碼使用,我們后面會說。

### 7 面試題
#### 7.1 如何證明 static 靜態變量和類無關?
答:從三個方面就可以看出靜態變量和類無關。
1. 我們不需要初始化類就可直接使用靜態變量;
2. 我們在類中寫個 main 方法運行,即便不寫初始化類的代碼,靜態變量都會自動初始化;
3. 靜態變量只會初始化一次,初始化完成之后,不管我再 new 多少個類出來,靜態變量都不會再初始化了。
不僅僅是靜態變量,靜態方法塊也和類無關。
#### 7.2 常常看見變量和方法被 static 和 final 兩個關鍵字修飾,為什么這么做?
答:這么做有兩個目的:
1. 變量和方法于類無關,可以直接使用,使用比較方便;
2. 強調變量內存地址不可變,方法不可繼承覆寫,強調了方法內部的穩定性。
#### 7.3 catch 中發生了未知異常,finally 還會執行么?
答:會的,catch 發生了異常,finally 還會執行的,并且是 finally 執行完成之后,才會拋出 catch 中的異常。
不過 catch 會吃掉 try 中拋出的異常,為了避免這種情況,在一些可以預見 catch 中會發生異常的地方,先把 try 拋出的異常打印出來,這樣從日志中就可以看到完整的異常了。
#### 7.4 volatile 關鍵字的作用和原理
答:這個上文說的比較清楚,可以參考上文。
### 總結
Java 的關鍵字屬于比較基礎的內容,我們需要清晰明確其含義,才能在后續源碼閱讀和工作中碰到這些關鍵字時了然于心,才能明白為什么會在這里使用這樣的關鍵字。比如 String 源碼是如何使用 final 關鍵字達到起不變性的,比如 Java 8 集合中 Map 是如何利用 default 關鍵字新增各種方法的,這些我們在后續內容都會提到。
- 前言
- 第1章 基礎
- 01 開篇詞:為什么學習本專欄
- 02 String、Long 源碼解析和面試題
- 03 Java 常用關鍵字理解
- 04 Arrays、Collections、Objects 常用方法源碼解析
- 第2章 集合
- 05 ArrayList 源碼解析和設計思路
- 06 LinkedList 源碼解析
- 07 List 源碼會問哪些面試題
- 08 HashMap 源碼解析
- 09 TreeMap 和 LinkedHashMap 核心源碼解析
- 10 Map源碼會問哪些面試題
- 11 HashSet、TreeSet 源碼解析
- 12 彰顯細節:看集合源碼對我們實際工作的幫助和應用
- 13 差異對比:集合在 Java 7 和 8 有何不同和改進
- 14 簡化工作:Guava Lists Maps 實際工作運用和源碼
- 第3章 并發集合類
- 15 CopyOnWriteArrayList 源碼解析和設計思路
- 16 ConcurrentHashMap 源碼解析和設計思路
- 17 并發 List、Map源碼面試題
- 18 場景集合:并發 List、Map的應用場景
- 第4章 隊列
- 19 LinkedBlockingQueue 源碼解析
- 20 SynchronousQueue 源碼解析
- 21 DelayQueue 源碼解析
- 22 ArrayBlockingQueue 源碼解析
- 23 隊列在源碼方面的面試題
- 24 舉一反三:隊列在 Java 其它源碼中的應用
- 25 整體設計:隊列設計思想、工作中使用場景
- 26 驚嘆面試官:由淺入深手寫隊列
- 第5章 線程
- 27 Thread 源碼解析
- 28 Future、ExecutorService 源碼解析
- 29 押寶線程源碼面試題
- 第6章 鎖
- 30 AbstractQueuedSynchronizer 源碼解析(上)
- 31 AbstractQueuedSynchronizer 源碼解析(下)
- 32 ReentrantLock 源碼解析
- 33 CountDownLatch、Atomic 等其它源碼解析
- 34 只求問倒:連環相扣系列鎖面試題
- 35 經驗總結:各種鎖在工作中使用場景和細節
- 36 從容不迫:重寫鎖的設計結構和細節
- 第7章 線程池
- 37 ThreadPoolExecutor 源碼解析
- 38 線程池源碼面試題
- 39 經驗總結:不同場景,如何使用線程池
- 40 打動面試官:線程池流程編排中的運用實戰
- 第8章 Lambda 流
- 41 突破難點:如何看 Lambda 源碼
- 42 常用的 Lambda 表達式使用場景解析和應用
- 第9章 其他
- 43 ThreadLocal 源碼解析
- 44 場景實戰:ThreadLocal 在上下文傳值場景下的實踐
- 45 Socket 源碼及面試題
- 46 ServerSocket 源碼及面試題
- 47 工作實戰:Socket 結合線程池的使用
- 第10章 專欄總結
- 48 一起看過的 Java 源碼和面試真題