引入
?在前面我們已經(jīng)根據(jù)虛擬機的工作流程大致分析過類加載的過程和對象實例化的過程,本篇中我們將介紹這一塊中常用的幾個關鍵字,他們分別是static、super和this。
?這三個關鍵字在我們的工作中使用頻率相當高,我們也都比較熟悉了,所以本篇不會對這三個關鍵字作過多深層的介紹,僅針對常見的面試題作剖析。
?請注意,本篇文章的代碼片段都是基于jdk1.8編寫、編譯及調試的。
項目中必用的關鍵字static
(一)修飾成員變量
?如果你已經(jīng)看過類加載機制那一篇,那么你大概已經(jīng)知道這種用法了。這里我們簡單回顧一下,這種用法有以下特性:
- 這個成員變量是類變量,屬于該類所對應的java.lang.Class對象(和java.lang.Class對象一同存在方法區(qū)中),并不屬于這個類的實例;
- 這個成員變量的值在類加載時被賦初始值,并且可能會被賦值兩次。第一次賦值發(fā)生在類加載的準備階段,第二次發(fā)生在類加載的初始化階段;
- 訪問該成員變量可通過:類名.變量名(同一個類中訪問可不需要指定類名)。
?關于上面所描述的內容見下面的代碼片段所示:
public class Demo {
// 虛擬機第一次給demoInt賦初始值在準備階段,賦值為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修飾的方法也是屬于類的,與實例對象無關。同樣在類加載的時候作為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)代碼塊。在類加載機制一篇中有這部分相關的說明??偟膩碚f,靜態(tài)代碼塊有如下特性:
- 這個代碼塊屬于該類所對應的java.lang.Class對象(和java.lang.Class對象一同存在方法區(qū)中),并不屬于這個類的實例;
- 這個代碼塊只會被執(zhí)行一次,并且不能手動調用,在類加載的時候虛擬機會自動調用(和靜態(tài)變量一起被封裝在<clinit>()方法中);
?如果一個代碼塊沒有被static所修飾,那么這個代碼塊屬于實例,在實例化的時候被虛擬機自動調用并執(zhí)行,調用時機為:在成員變量賦值之后,構造方法執(zhí)行之前。
?不管有沒有被static所修飾,代碼塊均不能被手動調用,如果你有C++的基礎應該很好理解這一點,換句話說,代碼塊的設計理念實際上就是為了初始化成員變量。
?不同的是,靜態(tài)代碼塊是在類加載的時候被調用執(zhí)行的,在一個類的生命周期中,只會被執(zhí)行一次;而非靜態(tài)代碼塊,則有可能會被多次執(zhí)行,原因是在內存中一個類只會被加載一次,但是這個類所對應的實例有多個。從另外一個角度來說,一個對象的生命周期中,非靜態(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
(四)修飾類
?如果你有了解過單例模式,你應該會清楚單例模式中有一種特殊的寫法——靜態(tài)內部類。就長下面這個樣子的:
public class Singleton {
private Singleton(){}
private static class Inner{
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return Inner.instance;
}
}
?說實話,這個理解起來有點麻煩。我很想在這里貼一個鏈接,等到單例模式那一章再去說這個問題。但是不知道要到什么時候才去寫那一塊的內容,所以還是在這里講清楚吧,反正遲早都會說到這段代碼。
?首先,上面這段代碼為什么是單例的?
?所謂單例,說白了就是單個實例,再通俗點講就是說一個類在運行期間永遠只會產(chǎn)生一個實例,當然可以沒有,但是不能大于一個。上面的代碼中,我們把Singleton的構造函數(shù)私有化,就意味著我們不能通過new來創(chuàng)建實例對象,這是單例的大前提。
- 構造函數(shù)私有,便不能通過new來創(chuàng)建Singleton的實例,我們要獲取一個Singleton的實例,只能通過getInstance()方法;
- getInstance()為什么要用static修飾?因為我們無法new一個Singleton的實例,我們就無法用實例去調用它的非靜態(tài)的方法,無法調用getInstance()方法我們又怎么拿到Singleton的實例?...有點繞,盡量理解吧...
?其次,上面這段代碼中為什么是懶加載?
?懶加載是什么意思就不用解釋了吧。直接看為什么:
- 根據(jù)類加載機制,類加載時只會執(zhí)行靜態(tài)變量的賦值和靜態(tài)代碼塊,而上面的代碼中并沒有這兩個東東;
- 只有靜態(tài)的getInstance()方法被調用時,才會返回一個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)導包
?"靜態(tài)導包"這個詞我是在其他的文章中看到的,當然我個人覺得這個詞并不能準確的表達出這種用法的意義,但又找不到合適的詞來描述,既然大家都這么叫,那就這樣叫吧。
?實際上,這個用法是Java5開始才有的。用法就是在導入包的import關鍵字后緊跟static關鍵字。例如:
// 普通導包
// import static XXXX.util.SwResult;
// 靜態(tài)導入
import static XXXX.util.SwResult.*;
public class Main {
public static void main(String[] args){
// 未用static關鍵字時
// System.out.println(SwResult.Status.OK);
// 使用static關鍵字
System.out.println(Status.OK);
}
}
?講真的,我個人覺得這個東西不重要。說白了就是你在外部用一個類的靜態(tài)方法時,可以不用通過類名.靜態(tài)方法名()或者類名.靜態(tài)成員來訪問,可直接通過靜態(tài)方法名()和靜態(tài)成員來訪問。
?盡管已經(jīng)提供了這種用法,但我仍然不推薦大家使用。原因大家一眼就能看出來,這玩意會降低代碼的可閱讀性。
super關鍵字
?super關鍵字沒有static關鍵字那么多雜七雜八的用法,總的來說就一句話:super關鍵字用于從子類的實例方法(或者代碼塊)中訪問父類的實例成員、代碼塊、實例方法。例子一看便知:
public class Parent {
public int parentValue = 10;
{
System.out.println("Parent類的代碼塊開始...");
System.out.println("Parent類說Parent類實例的parentValue = " + parentValue);
System.out.println("Parent類的代碼塊結束...");
}
public void sayParent(){System.out.println("Parent說我的sayParent()被調用了...");}
}
public class Sub extends Parent {
{
System.out.println("Sub類的代碼塊開始...");
super.parentValue =20;
System.out.println("Sub類說Parent類實例的parentValue = " + super.parentValue);
System.out.println("Sub類的代碼塊結束...");
}
public void saySub(){
super.sayParent();
System.out.println("Sub說我的saySub()被調用了...");
}
}
public class Main {
public static void main(String[] args){
new Sub().saySub();
}
}
/**************************結果*************************/
Parent類的代碼塊開始...
Parent類說Parent類實例的parentValue = 10
Parent類的代碼塊結束...
Sub類的代碼塊開始...
Sub類說Parent類實例的parentValue = 20
Sub類的代碼塊結束...
Parent說我的sayParent()被調用了...
Sub說我的saySub()被調用了...
this關鍵字
?this關鍵字用于引用當前類的實例成員、代碼塊、實例方法。
public class Sub{
private int value = 7;
{
System.out.println("Sub類的代碼塊開始...");
System.out.println("Sub類的value = " + this.value);
this.saySub();
System.out.println("Sub類的代碼塊結束...");
}
public void saySub(){
System.out.println("Sub說我的saySub()被調用了...");
}
}
public class Main {
public static void main(String[] args){
new Sub().saySub();
}
}
/**************************結果*************************/
Sub類的代碼塊開始...
Sub類的value = 7
Sub說我的saySub()被調用了...
Sub類的代碼塊結束...
Sub說我的saySub()被調用了...
?實際情況是this關鍵字在代碼中如果不存在同名的情況(比如形參的名字與成員變量的名字重名)時可完全省略,而super關鍵字在子類和父類沒有重名的情況下也可省略。但在工作中,更建議大家不要省略這兩個關鍵字,以保證代碼的可讀性。
?注意:super和this關鍵字都不能用于靜態(tài)方法中,因為super和this都是針對類的實例的。
常見面試題
(一)靜態(tài)方法中可以調用本類的非靜態(tài)方法嗎?
?答:不可以。靜態(tài)方法屬于類,非靜態(tài)方法屬于實例。類對象(java.lang.Class對象)在類加載時產(chǎn)生,那時可能并沒有產(chǎn)生實例對象。
?你可以這樣想來幫助理解:一個類對應的Class對象只有一個,而實例對象可能有多個,那么我的靜態(tài)方法到底應該調用哪一個實例的非靜態(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關鍵字不允許出現(xiàn)在static方法中
擴展區(qū)域
擴展區(qū)域主體
這是一個沒有實現(xiàn)的擴展。