引入
?在前面我們已經(jīng)根據(jù)虛擬機的工作流程大致分析過類加載的過程和對象實例化的過程责蝠,本篇中我們將介紹這一塊中常用的幾個關(guān)鍵字,他們分別是static护奈、super和this。
?這三個關(guān)鍵字在我們的工作中使用頻率相當(dāng)高,我們也都比較熟悉了乐尊,所以本篇不會對這三個關(guān)鍵字作過多深層的介紹,僅針對常見的面試題作剖析划址。
?請注意扔嵌,本篇文章的代碼片段都是基于jdk1.8編寫、編譯及調(diào)試的夺颤。
項目中必用的關(guān)鍵字static
(一)修飾成員變量
?如果你已經(jīng)看過類加載機制那一篇痢缎,那么你大概已經(jīng)知道這種用法了。這里我們簡單回顧一下世澜,這種用法有以下特性:
- 這個成員變量是類變量独旷,屬于該類所對應(yīng)的java.lang.Class對象(和java.lang.Class對象一同存在方法區(qū)中),并不屬于這個類的實例寥裂;
- 這個成員變量的值在類加載時被賦初始值嵌洼,并且可能會被賦值兩次。第一次賦值發(fā)生在類加載的準(zhǔn)備階段封恰,第二次發(fā)生在類加載的初始化階段麻养;
- 訪問該成員變量可通過:類名.變量名(同一個類中訪問可不需要指定類名)。
?關(guān)于上面所描述的內(nèi)容見下面的代碼片段所示:
public class Demo {
// 虛擬機第一次給demoInt賦初始值在準(zhǔn)備階段诺舔,賦值為0(零值)
// 由于demoInt后有"= 6"這樣的賦值語句鳖昌,所以在初始化時有第二次賦值,賦值為6
public static int demoInt = 6;
}
public class Main {
public static void main(String[] args){
System.out.println(Demo.demoInt);
}
}
?如果對于虛擬機的類加載機制不熟悉可參見:傳送門低飒。
(二)修飾成員方法
?和被static修飾的成員變量一樣许昨,被static修飾的方法也是屬于類的,與實例對象無關(guān)逸嘀。同樣在類加載的時候作為java.lang.Class對象的一部分存儲在方法區(qū)中车要,訪問該方法可通過:類名.方法名(參數(shù)列表)。例如下面的代碼段所示:
public class Demo {
private static int demoInt = 6;
public static void doSomething(){
demoInt++;
System.out.println("Demo.doSomething()中demoInt = "+demoInt);
}
}
public class Main {
public static void main(String[] args){
Demo.doSomething();
}
}
// 輸出:
// Demo.doSomething()中demoInt = 7
(三)修飾代碼塊
?在Java中崭倘,我們常把被static修飾的代碼塊叫做靜態(tài)代碼塊翼岁。在類加載機制一篇中有這部分相關(guān)的說明∷竟猓總的來說琅坡,靜態(tài)代碼塊有如下特性:
- 這個代碼塊屬于該類所對應(yīng)的java.lang.Class對象(和java.lang.Class對象一同存在方法區(qū)中),并不屬于這個類的實例残家;
- 這個代碼塊只會被執(zhí)行一次榆俺,并且不能手動調(diào)用,在類加載的時候虛擬機會自動調(diào)用(和靜態(tài)變量一起被封裝在<clinit>()方法中);
?如果一個代碼塊沒有被static所修飾茴晋,那么這個代碼塊屬于實例陪捷,在實例化的時候被虛擬機自動調(diào)用并執(zhí)行,調(diào)用時機為:在成員變量賦值之后诺擅,構(gòu)造方法執(zhí)行之前市袖。
?不管有沒有被static所修飾,代碼塊均不能被手動調(diào)用烁涌,如果你有C++的基礎(chǔ)應(yīng)該很好理解這一點苍碟,換句話說,代碼塊的設(shè)計理念實際上就是為了初始化成員變量撮执。
?不同的是微峰,靜態(tài)代碼塊是在類加載的時候被調(diào)用執(zhí)行的,在一個類的生命周期中抒钱,只會被執(zhí)行一次蜓肆;而非靜態(tài)代碼塊,則有可能會被多次執(zhí)行继效,原因是在內(nèi)存中一個類只會被加載一次症杏,但是這個類所對應(yīng)的實例有多個。從另外一個角度來說瑞信,一個對象的生命周期中,非靜態(tài)代碼塊也只會被執(zhí)行一次穴豫。
public class Demo {
private static int demoInt = 6;
public static void doSomething(){
demoInt++;
System.out.println("Demo.doSomething()中demoInt = "+demoInt);
}
static {
demoInt = 20;
System.out.println("Demo的第一個靜態(tài)代碼塊中demoInt = "+demoInt);
}
static {
demoInt = 40;
System.out.println("Demo的第二個靜態(tài)代碼塊中demoInt = "+demoInt);
}
}
public class Main {
public static void main(String[] args){
Demo.doSomething();
}
}
// 輸出:
// Demo的第一個靜態(tài)代碼塊中demoInt = 20
// Demo的第二個靜態(tài)代碼塊中demoInt = 40
// Demo.doSomething()中demoInt = 41
?事實上凡简,一個類中可能會有多個靜態(tài)代碼塊,但是如果你看過這個類生成的字節(jié)碼文件就能發(fā)現(xiàn):不管有多少個靜態(tài)代碼塊精肃,最終都會被編譯器合并成一個靜態(tài)代碼塊秤涩。例如,我們看一下上面Demo類中的靜態(tài)代碼塊編譯后是什么樣子的司抱?
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=3, locals=0, args_size=0
0: bipush 6
2: putstatic #2 // Field demoInt:I
5: bipush 20
7: putstatic #2 // Field demoInt:I
10: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
13: new #4 // class java/lang/StringBuilder
16: dup
17: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
20: ldc #11 // String Demo的第一個靜態(tài)代碼塊中demoInt =
22: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
25: getstatic #2 // Field demoInt:I
28: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
31: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
37: bipush 40
39: putstatic #2 // Field demoInt:I
42: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
45: new #4 // class java/lang/StringBuilder
48: dup
49: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
52: ldc #12 // String Demo的第二個靜態(tài)代碼塊中demoInt =
54: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
57: getstatic #2 // Field demoInt:I
60: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
63: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
66: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
69: return
LineNumberTable:
line 4: 0
line 10: 5
line 11: 10
line 14: 37
line 15: 42
line 16: 69
(四)修飾類
?如果你有了解過單例模式筐眷,你應(yīng)該會清楚單例模式中有一種特殊的寫法——靜態(tài)內(nèi)部類。就長下面這個樣子的:
public class Singleton {
private Singleton(){}
private static class Inner{
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return Inner.instance;
}
}
?說實話习柠,這個理解起來有點麻煩匀谣。我很想在這里貼一個鏈接,等到單例模式那一章再去說這個問題资溃。但是不知道要到什么時候才去寫那一塊的內(nèi)容武翎,所以還是在這里講清楚吧,反正遲早都會說到這段代碼溶锭。
?首先宝恶,上面這段代碼為什么是單例的?
?所謂單例,說白了就是單個實例垫毙,再通俗點講就是說一個類在運行期間永遠只會產(chǎn)生一個實例霹疫,當(dāng)然可以沒有,但是不能大于一個综芥。上面的代碼中丽蝎,我們把Singleton的構(gòu)造函數(shù)私有化,就意味著我們不能通過new來創(chuàng)建實例對象毫痕,這是單例的大前提征峦。
- 構(gòu)造函數(shù)私有,便不能通過new來創(chuàng)建Singleton的實例消请,我們要獲取一個Singleton的實例栏笆,只能通過getInstance()方法;
- getInstance()為什么要用static修飾臊泰?因為我們無法new一個Singleton的實例蛉加,我們就無法用實例去調(diào)用它的非靜態(tài)的方法,無法調(diào)用getInstance()方法我們又怎么拿到Singleton的實例缸逃?...有點繞针饥,盡量理解吧...
?其次,上面這段代碼中為什么是懶加載需频?
?懶加載是什么意思就不用解釋了吧丁眼。直接看為什么:
- 根據(jù)類加載機制,類加載時只會執(zhí)行靜態(tài)變量的賦值和靜態(tài)代碼塊昭殉,而上面的代碼中并沒有這兩個東東苞七;
- 只有靜態(tài)的getInstance()方法被調(diào)用時,才會返回一個Singleton的實例挪丢,這樣就能解釋是懶加載了蹂风;
?推薦大家去生成上面這段代碼的字節(jié)碼看一下。我相信乾蓬,一看你就明白了惠啄,這段代碼會生成兩個.class文件,一個叫Singleton$1.class任内,另一個叫Singleton$Inner.class撵渡,用javap命令去看一下Singleton$1.class這個文件的字節(jié)碼,你就會發(fā)現(xiàn)這個Singleton類壓根沒有<clinit>()方法族奢,所以也就不存在類加載的時候就生成了實例姥闭。
?拓展一下,如果你嘗試把getInstance()方法體中的代碼搬到靜態(tài)代碼塊中去的話越走,那么這個就便不是懶加載了棚品,因為在加載Singleton類時就生成了<clinit>()方法靠欢,就會自動生成Singleton的實例。
?再次铜跑,上面這段代碼為什么能保證線程安全门怪?
?上面代碼片段的線程安全保證實際上是依托于類加載機制所提供的天然的線程安全性。因為類加載的過程中锅纺,如果多個線程同時去加載一個類掷空,那么最終只會有一個線程拿到鎖并執(zhí)行<clinit>()方法,其他的線程都處于阻塞的狀態(tài)囤锉,直到這個線程執(zhí)行<clinit>()完畢坦弟。大家可以看下ClassLoader類的源碼,里面loadClass()方法實際上一上來就用synchronized加了鎖官地。
(四)靜態(tài)導(dǎo)包
?"靜態(tài)導(dǎo)包"這個詞我是在其他的文章中看到的酿傍,當(dāng)然我個人覺得這個詞并不能準(zhǔn)確的表達出這種用法的意義,但又找不到合適的詞來描述驱入,既然大家都這么叫赤炒,那就這樣叫吧。
?實際上亏较,這個用法是Java5開始才有的莺褒。用法就是在導(dǎo)入包的import關(guān)鍵字后緊跟static關(guān)鍵字。例如:
// 普通導(dǎo)包
// import static XXXX.util.SwResult;
// 靜態(tài)導(dǎo)入
import static XXXX.util.SwResult.*;
public class Main {
public static void main(String[] args){
// 未用static關(guān)鍵字時
// System.out.println(SwResult.Status.OK);
// 使用static關(guān)鍵字
System.out.println(Status.OK);
}
}
?講真的雪情,我個人覺得這個東西不重要遵岩。說白了就是你在外部用一個類的靜態(tài)方法時,可以不用通過類名.靜態(tài)方法名()或者類名.靜態(tài)成員來訪問巡通,可直接通過靜態(tài)方法名()和靜態(tài)成員來訪問旷余。
?盡管已經(jīng)提供了這種用法,但我仍然不推薦大家使用扁达。原因大家一眼就能看出來,這玩意會降低代碼的可閱讀性蠢熄。
super關(guān)鍵字
?super關(guān)鍵字沒有static關(guān)鍵字那么多雜七雜八的用法跪解,總的來說就一句話:super關(guān)鍵字用于從子類的實例方法(或者代碼塊)中訪問父類的實例成員、代碼塊签孔、實例方法叉讥。例子一看便知:
public class Parent {
public int parentValue = 10;
{
System.out.println("Parent類的代碼塊開始...");
System.out.println("Parent類說Parent類實例的parentValue = " + parentValue);
System.out.println("Parent類的代碼塊結(jié)束...");
}
public void sayParent(){System.out.println("Parent說我的sayParent()被調(diào)用了...");}
}
public class Sub extends Parent {
{
System.out.println("Sub類的代碼塊開始...");
super.parentValue =20;
System.out.println("Sub類說Parent類實例的parentValue = " + super.parentValue);
System.out.println("Sub類的代碼塊結(jié)束...");
}
public void saySub(){
super.sayParent();
System.out.println("Sub說我的saySub()被調(diào)用了...");
}
}
public class Main {
public static void main(String[] args){
new Sub().saySub();
}
}
/**************************結(jié)果*************************/
Parent類的代碼塊開始...
Parent類說Parent類實例的parentValue = 10
Parent類的代碼塊結(jié)束...
Sub類的代碼塊開始...
Sub類說Parent類實例的parentValue = 20
Sub類的代碼塊結(jié)束...
Parent說我的sayParent()被調(diào)用了...
Sub說我的saySub()被調(diào)用了...
this關(guān)鍵字
?this關(guān)鍵字用于引用當(dāng)前類的實例成員、代碼塊饥追、實例方法图仓。
public class Sub{
private int value = 7;
{
System.out.println("Sub類的代碼塊開始...");
System.out.println("Sub類的value = " + this.value);
this.saySub();
System.out.println("Sub類的代碼塊結(jié)束...");
}
public void saySub(){
System.out.println("Sub說我的saySub()被調(diào)用了...");
}
}
public class Main {
public static void main(String[] args){
new Sub().saySub();
}
}
/**************************結(jié)果*************************/
Sub類的代碼塊開始...
Sub類的value = 7
Sub說我的saySub()被調(diào)用了...
Sub類的代碼塊結(jié)束...
Sub說我的saySub()被調(diào)用了...
?實際情況是this關(guān)鍵字在代碼中如果不存在同名的情況(比如形參的名字與成員變量的名字重名)時可完全省略,而super關(guān)鍵字在子類和父類沒有重名的情況下也可省略但绕。但在工作中救崔,更建議大家不要省略這兩個關(guān)鍵字惶看,以保證代碼的可讀性。
?注意:super和this關(guān)鍵字都不能用于靜態(tài)方法中六孵,因為super和this都是針對類的實例的纬黎。
常見面試題
(一)靜態(tài)方法中可以調(diào)用本類的非靜態(tài)方法嗎?
?答:不可以劫窒。靜態(tài)方法屬于類本今,非靜態(tài)方法屬于實例。類對象(java.lang.Class對象)在類加載時產(chǎn)生主巍,那時可能并沒有產(chǎn)生實例對象冠息。
?你可以這樣想來幫助理解:一個類對應(yīng)的Class對象只有一個,而實例對象可能有多個孕索,那么我的靜態(tài)方法到底應(yīng)該調(diào)用哪一個實例的非靜態(tài)方法呢逛艰?
(二)下面的代碼片段中,分別插入以下語句后能編譯通過的選項有檬果?
public class Main {
private String aName = "----";
private static String bName = "====";
public void trans(){
String tempName;
// 插入代碼
}
public static void main(String[] args) {
new Main().trans();
}
}
// A. tempName = this.aName;
// B. tempName = this.bName;
// C. this.bName = aName;
// D. this.aName = this.bName;
/*************************答案*************************/
// ABCD
/*************************分析*************************/
// 需注意一點:this.bName是通過類的實例去獲取類的變量瓮孙,可以編譯通過,等效于Main.bName
// 如果trans()方法用static修飾选脊,那么這道題沒有正確答案杭抠;因為this關(guān)鍵字不允許出現(xiàn)在static方法中
擴展區(qū)域
擴展區(qū)域主體
這是一個沒有實現(xiàn)的擴展。