注:源碼系列文章主要是對某付費專欄的總結記錄吭敢。如有侵權剪廉,請聯(lián)系刪除质帅。
1 static
1.1 靜態(tài)變量
- 靜態(tài)變量:又稱為類變量烈疚,也就是說這個變量屬于類黔牵,類所有的實例都共享靜態(tài)變量,可以直接通過類名來訪問它爷肝。靜態(tài)變量在內存中只存在一份猾浦;
- 實例變量:每創(chuàng)建一個實例就會產生一個實例變量,它與該實例共生共死灯抛。
public class StaticExample {
// 靜態(tài)變量
public static int ID = 1;
// 實例變量
private String name;
public StaticExample() {
this.name = "李雷";
}
// TEST
public static void main(String[] args) {
// 靜態(tài)變量直接通過類名訪問
System.out.println(StaticExample.ID);
// 實例變量
StaticExample example = new StaticExample();
System.out.println(example.name);
}
}
1.2 靜態(tài)方法
靜態(tài)方法在類加載的時候就存在了金赦,它不依賴于任何實例,所有靜態(tài)方法必須有實現(xiàn)对嚼,也就是說它不能是抽象方法夹抗。
靜態(tài)方法只能調用同樣被 static 修飾的方法或屬性,不能調用普通方法纵竖,我們常用的 util 類里面的各種方法漠烧,我們比較喜歡用 static 修飾方法,好處就是調用特別方便靡砌。
static 方法內部的變量在執(zhí)行時是沒有線程安全問題的已脓。方法執(zhí)行時,數(shù)據運行在棧里面乏奥,棧的數(shù)據每個線程都是隔離開的摆舟,所以不會有線程安全的問題,所以 util 類的各個 static 方法邓了,我們是可以放心使用的恨诱。
public static void print(String... args) {
for (String arg : args) {
System.out.print(arg + " ");
}
}
1.3 靜態(tài)代碼塊
靜態(tài)代碼塊在類初始化時只運行一次。
public class A {
static {
System.out.println("static code block");
}
public static void main(String[] args) {
A a1 = new A();
A a2 = new A();
}
}
執(zhí)行結果:
static code block
1.4 靜態(tài)內部類
略骗炉。
1.5 靜態(tài)導包
在使用靜態(tài)變量和方法時照宝,不用在指明 ClassName,從而簡化代碼句葵,但可讀性大大降低厕鹃。
import com.alibaba.fastjson.JSON;
import com.google.common.base.Splitter;
import java.util.List;
public class A {
public static void main(String[] args) {
// 不用指明 ClassName
System.out.println(StaticExample.ID);
// 非靜態(tài)需要導包, 也就是指明 ClassName
Splitter splitter = Splitter.on(",");
List<String> list = splitter.trimResults().omitEmptyStrings().splitToList("a,b,c,d");
System.out.println(JSON.toJSONString(list));
}
}
1.6 執(zhí)行順序
靜態(tài)變量和靜態(tài)代碼塊優(yōu)先于實例變量和普通代碼塊執(zhí)行,靜態(tài)變量和靜態(tài)代碼塊的初始化順序取決于它們在代碼中的順序乍丈。如下:
靜態(tài)變量/靜態(tài)代碼塊
-> 實例變量/普通代碼塊
-> 構造函數(shù)
存在繼承情況下剂碴,初始化順序為:
- 父類(靜態(tài)變量、靜態(tài)代碼塊)
- 子類(靜態(tài)變量轻专、靜態(tài)代碼塊)
- 父類(實例變量忆矛、普通代碼塊)
- 父類(構造函數(shù))
- 子類(實例變量、普通代碼塊)
- 子類(構造函數(shù))
被 static
修飾的方法请垛,在類初始化的時候并不會初始化催训,只有當自己被調用時洽议,才會被執(zhí)行。
2 final
2.1 數(shù)據
聲明數(shù)據為 final漫拭,可以是編譯時常量亚兄,也可以是在運行時被初始化后不能被改變的常量。
- 對于基本類型采驻,final 使數(shù)值不變审胚;
- 對于引用類型,final 使引用不變挑宠,也就不能引用其它對象菲盾,但是被引用的對象本身是可以修改的颓影。
public class FinalExample {
int a;
public static void main(String[] args) {
// 基本類型
final int b = 100;
// 改變基本類型
// b = 200; // Cannot assign a value to final variable 'b'
// 引用類型
final FinalExample example = new FinalExample();
System.out.println(example.a);
example.a = 1;
System.out.println(example.a);
}
}
一般用于以下三種場景:
- 被 final 修飾的類各淀,表明該類是無法繼承的;
- 被 final 修飾的方法诡挂,表明該方法是無法覆蓋的碎浇;
- 被 final 修飾的變量,說明該變量在聲明的時候璃俗,就必須初始化完成奴璃,而且以后也不能修改其內存地址。
第 3 點需要注意城豁,我們說的是無法修改其內存地址苟穆,并沒有說無法修改其值。因為對于 List唱星、Map雳旅、自定義Object 這些類來說,被 final 修飾后间聊,是可以修改其內部值的攒盈,但卻無法修改其初始化時的內存地址。如:String 被 final 修飾哎榴;String 存儲數(shù)據的 char 數(shù)組被 final 修飾型豁;上面的 FinalExample 等。
3 try尚蝌、catch迎变、finally
這三個關鍵字常用于捕獲異常的一整套流程,try 用來確定代碼范圍飘言,catch 捕獲可能發(fā)生的異常衣形,finally 用來執(zhí)行一定要執(zhí)行的代碼塊(如 IO流的關閉)。除了這些地方我們還需要清楚的知道热凹,每個地方如果發(fā)生異常會怎么辦泵喘。如:
public static void main(String[] args) {
try {
log.info("try run.");
if(true) {
throw new RuntimeException("try exception");
}
} catch (Exception e) {
log.info("catch run: " + e.getMessage());
if(true) {
throw new RuntimeException("catch exception");
}
} finally {
log.info("finally run.");
}
}
總結:
- finally 先執(zhí)行后泪电,再拋出 catch 的異常;
- 最終捕獲的異常是 catch 的異常纪铺,try 拋出來的異常已經被 catch 吃掉了相速,所以當我們遇見 catch 也有可能拋出異常時,可以先打印出 try 的異常信息鲜锚,這樣 try 的異常在日志中就會有所體現(xiàn)突诬。
4 volatile
volatile 的意思是可見的,常用來修飾某個共享變量芜繁,意思是當共享變量的值被改變后旺隙,會及時通知到其它線程上,其它線程就能知道當前共享變量的值已經被修改了骏令。
在多核 CPU 下蔬捷,為了提高效率,線程在拿值時榔袋,是直接和 CPU 緩存交互的周拐,而不是內存。主要是因為 CPU 緩存執(zhí)行速度更快凰兑,比如線程要拿值 C妥粟,會直接從 CPU 緩存中拿,CPU 緩存中沒有吏够,才會從內存中拿勾给,所以線程讀的操作永遠都是拿 CPU 緩存的值。
這個時候會產生一個問題锅知,CPU 緩存中的值和內存中的值可能并不是時刻都同步播急,導致線程計算的值可能不是最新的,共享變量的值有可能已經被其它線程所修改了喉镰,但此時修改的是機器內存的值旅择,CPU 緩存的值還是老的,導致計算會出現(xiàn)問題侣姆。
這時候有個機制生真,就是內存會主動通知 CPU 緩存。當前共享變量的值已經失效了捺宗,你需要重新來拉取一份柱蟀,CPU 緩存就會重新從內存中拿取一份最新的值。
volatile 關鍵字就會觸發(fā)這種機制蚜厉,加了 volatile 關鍵字的變量长已,就會被識別成共享變量,內存中的值一旦被修改后,就會通知各個 CPU 緩存术瓮,使 CPU 緩存中的值也對應被修改康聂,從而保證線程從 CPU 緩存中拿去出來的值是最新的。
從圖中看到胞四,線程 1 和線程 2 一開始都讀取了 C 值恬汁,CPU 1 和 CPU 2 緩存中也都有了 C 值,然后線程 1 把 C 值修改了辜伟,這時候內存中 C 值和 CPU 2 緩存中的 C 值就不等了氓侧,內存這是發(fā)現(xiàn) C 值被 volatile 關鍵字修飾,發(fā)現(xiàn)其實共享變量导狡,就會使 CPU 2 緩存中的 C 值狀態(tài)置為無效约巷,CPU 2 會從內存中重新拉取一份最新的 C 值到緩存,旱捧。這時候線程 2 再來讀取緩存中的 C 值独郎,讀取的已經是內存中最新的值了。
5 transient
transient 關鍵字用來修飾類變量廊佩,意思是當前變量是無需進行序列化的囚聚。在序列化時,會忽略該變量标锄。如:ArrayList 對內部存儲數(shù)據 elementData 使用 transient 修飾 transient Object[] elementData
,因為 elementData 數(shù)組并不會存儲滿茁计,所以無需全部序列化料皇,這里重寫了 writeObject
和 readObject
。
6 default
default 關鍵字一般會用在接口的方法上星压,意思是對于該方法践剂,子類是無需強制實現(xiàn)的,但自己必須有默認的實現(xiàn)娜膘,如:
public interface DefaultExample {
default void print() {
System.out.println("default method");
}
}
7 面試題
7.1 如何證明 static 靜態(tài)變量和類實例無關逊脯?
答:從三個方面就可以看出靜態(tài)變量和類無關。
- 我們不需要初始化類就可以直接使用靜態(tài)變量竣贪;
- 我們在類中執(zhí)行 main 方法军洼,即使不寫初始化類的代碼,靜態(tài)變量也會自動初始化演怎;
- 靜態(tài)變量只會初始化一次匕争,初始化完成之后,不管我們再 new 多少個類出來爷耀,靜態(tài)變量都不會再初始化了甘桑。
不僅僅是靜態(tài)變量,靜態(tài)方法塊也和類無關。
7.2 常撑芎迹看見變量和方法被 static 和 final 兩個關鍵字同時修飾铆帽,為什么這么做?
答:這么做有兩個目的:
- 變量和方法于類無關德谅,可以直接使用锄贼,使用比較方便;
- 強調變量內存地址不可變女阀,方法不可以被繼承覆寫宅荤,強調了方法內部的穩(wěn)定性。
7.3 catch 中發(fā)生了未知異常浸策,finally 還會執(zhí)行么冯键?
答:會的。catch 發(fā)生了異常庸汗,finally 還會執(zhí)行的惫确,并且 finally 執(zhí)行完之后,才會拋出 catch 中的異常蚯舱。
捕獲 catch 會吃掉 try 中拋出的異常改化,為了避免這種情況,在一些可以預見的 catch 中會發(fā)生異常的地方枉昏,先把 try 拋出的異常打印出來陈肛,這樣從日志中就可以看到完成的異常信息了。
7.4 vilatile 關鍵字的作用和原理
答:參考上文兄裂。
------------------------------------- END -------------------------------------