面試題:Java常用關鍵字解析

1 static

意思是靜態的、全局的,一旦被修飾,說明被修飾的東西在一定範圍內是共享的,誰都可以訪問,這時候需要注意併發讀寫的問題。


1.1 修飾的對象

static 只能修飾類變量、方法和方法塊。

當 static 修飾類變量時,如果該變量是 public 的話,表示該變量任何類都可以直接訪問,而且無需初始化類,直接使用 類名.static 變量 這種形式訪問即可。

這時候我們非常需要注意的一點就是線程安全的問題了,因為當多個線程同時對共享變量進行讀寫時,很有可能會出現併發問題,如我們定義了:public static List<string> list = new ArrayList();這樣的共享變量。這個 list 如果同時被多個線程訪問的話,就有線程安全的問題,這時候一般有兩個解決辦法:/<string>

  1. 把線程不安全的 ArrayList 換成 線程安全的 CopyOnWriteArrayList;
  2. 每次訪問時,手動加鎖。

所以在使用 static 修飾類變量時,如何保證線程安全是我們常常需要考慮的。

當 static 修飾方法時

,代表該方法和當前類是無關的,任意類都可以直接訪問(如果權限是 public 的話)。

有一點需要注意的是,該方法內部只能調用同樣被 static 修飾的方法,不能調用普通方法,我們常用的 util 類裡面的各種方法,我們比較喜歡用 static 修飾方法,好處就是調用特別方便。

static 方法內部的變量在執行時是沒有線程安全問題的。方法執行時,數據運行在棧裡面,棧的數據每個線程都是隔離開的,所以不會有線程安全的問題,所以 util 類的各個 static 方法,我們是可以放心使用的。

當 static 修飾方法塊時,我們叫做靜態塊,靜態塊常常用於在類啟動之前,初始化一些值,比如:

<code>public static List<string> list = new ArrayList();// 進行一些初始化的工作static {    list.add("1");}/<string>/<code>

這段代碼演示了靜態塊做一些初始化的工作,但需要注意的是,靜態塊只能調用同樣被 static 修飾的變量,並且 static 的變量需要寫在靜態塊的前面,不然編譯也會報錯。


1.2 初始化時機

對於被 static 修飾的類變量、方法塊和靜態方法的初始化時機,我們寫了一個測試 demo,如下圖:

面試題:Java常用關鍵字解析

打印出來的結果是:

父類靜態變量初始化父類靜態塊初始化子類靜態變量初始化子類靜態塊初始化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 用來執行一定要執行的代碼塊,除了這些,我們還需要清楚,每個地方如果發生異常會怎麼辦,我們舉一個例子來演示一下:

<code>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");  }}/<code>

這個代碼演示了在 try、catch 中都遇到了異常,代碼的執行順序為:try -> catch -> finally,輸出的結果如下:

面試題:Java常用關鍵字解析

可以看到兩點:

  1. finally 先執行後,再拋出 catch 的異常;
  2. 最終捕獲的異常是 catch 的異常,try 拋出來的異常已經被 catch 吃掉了,所以當我們遇見 catch 也有可能會拋出異常時,我們可以先打印出 try 的異常,這樣 try 的異常在日誌中就會有所體現。

4 transient

transient 關鍵字我們常用來修飾類變量,意思是當前變量是無需進行序列化的。在序列化時,就會忽略該變量,這些在序列化工具底層,就已經對 transient 進行了支持。


5 default

default 關鍵字一般會用在接口的方法上,意思是對於該接口,子類是無需強制實現的,但自己必須有默認實現,我們舉個例子如下:


面試題:Java常用關鍵字解析


6 面試題


6.1 如何證明 static 靜態變量和類無關?

答:從三個方面就可以看出靜態變量和類無關。

  1. 我們不需要初始化類就可直接使用靜態變量;
  2. 我們在類中寫個 main 方法運行,即便不寫初始化類的代碼,靜態變量都會自動初始化;
  3. 靜態變量只會初始化一次,初始化完成之後,不管我再 new 多少個類出來,靜態變量都不會再初始化了。

不僅僅是靜態變量,靜態方法塊也和類無關。


6.2 常常看見變量和方法被 static 和 final 兩個關鍵字修飾,為什麼這麼做?

答:這麼做有兩個目的:

  1. 變量和方法於類無關,可以直接使用,使用比較方便;
  2. 強調變量內存地址不可變,方法不可繼承覆寫,強調了方法內部的穩定性。


6.3 catch 中發生了未知異常,finally 還會執行麼?

答:會的,catch 發生了異常,finally 還會執行的,並且是 finally 執行完成之後,才會拋出 catch 中的異常。

不過 catch 會吃掉 try 中拋出的異常,為了避免這種情況,在一些可以預見 catch 中會發生異常的地方,先把 try 拋出的異常打印出來,這樣從日誌中就可以看到完整的異常了。


6.4 volatile 關鍵字的作用和原理

volatile 的意思是可見的,常用來修飾某個共享變量,意思是當共享變量的值被修改後,會及時通知到其它線程上,其它線程就能知道當前共享變量的值已經被修改了。

我們再說原理之前,先說下基礎知識。就是在多核 CPU 下,為了提高效率,線程在拿值時,是直接和 CPU 緩存打交道的,而不是內存。主要是因為 CPU 緩存執行速度更快,比如線程要拿值 C,會直接從 CPU 緩存中拿, CPU 緩存中沒有,就會從內存中拿,所以線程讀的操作永遠都是拿 CPU 緩存的值。

這時候會產生一個問題,CPU 緩存中的值和內存中的值可能並不是時刻都同步,導致線程計算的值可能不是最新的,共享變量的值有可能已經被其它線程所修改了,但此時修改是機器內存的值,CPU 緩存的值還是老的,導致計算會出現問題。

這時候有個機制,就是內存會主動通知 CPU 緩存。當前共享變量的值已經失效了,你需要重新來拉取一份,CPU 緩存就會重新從內存中拿取一份最新的值。

volatile 關鍵字就會觸發這種機制,加了 volatile 關鍵字的變量,就會被識別成共享變量,內存中值被修改後,會通知到各個 CPU 緩存,使 CPU 緩存中的值也對應被修改,從而保證線程從 CPU 緩存中拿取出來的值是最新的。

我們畫了一個圖來說明一下:

面試題:Java常用關鍵字解析

從圖中我們可以看到,線程 1 和線程 2 一開始都讀取了 C 值,CPU 1 和 CPU 2 緩存中也都有了 C 值,然後線程 1 把 C 值修改了,這時候內存的值和 CPU 2 緩存中的 C 值就不等了,內存這時發現 C 值被 volatile 關鍵字修飾,發現其是共享變量,就會使 CPU 2 緩存中的 C 值狀態置為無效,CPU 2 會從內存中重新拉取最新的值,這時候線程 2 再來讀取 C 值時,讀取的已經是內存中最新的值了。

總結

Java 的關鍵字屬於比較基礎的內容,我們需要清晰明確其含義,才能在後續源碼閱讀和工作中碰到這些關鍵字時瞭然於心,才能明白為什麼會在這裡使用這樣的關鍵字。比如 String 源碼是如何使用 final 關鍵字達到起不變性的,比如 Java 8 集合中 Map 是如何利用 default 關鍵字新增各種方法的


分享到:


相關文章: