static、final、transient
Java中的static關鍵字解析
static關鍵字是很多朋友在編寫程式碼和閱讀程式碼時碰到的比較難以理解的一個關鍵字,也是各大公司的面試官喜歡在面試時問到的知識點之一。下面就先講述一下static關鍵字的用法和平常容易誤解的地方,最後列舉了一些面試筆試中常見的關於static的考題。以下是本文的目錄大綱:
一.static關鍵字的用途
二.static關鍵字的誤區
三.常見的筆試面試題
若有不正之處,希望諒解並歡迎批評指正。
一.static關鍵字的用途
在《Java程式設計思想》P86頁有這樣一段話:
“static方法就是沒有this的方法。在static方法內部不能呼叫非靜態方法,反過來是可以的。而且可以在沒有建立任何物件的前提下,僅僅通過類本身來呼叫static方法。這實際上正是static方法的主要用途。”
這段話雖然只是說明了static方法的特殊之處,但是可以看出static關鍵字的基本作用,簡而言之,一句話來描述就是:
方便在沒有建立物件的情況下來進行呼叫(方法/變數)。
很顯然,被static關鍵字修飾的方法或者變數不需要依賴於物件來進行訪問,只要類被載入了,就可以通過類名去進行訪問。
static可以用來修飾類的成員方法、類的成員變數,另外可以編寫static程式碼塊來優化程式效能。
1)static方法
static方法一般稱作靜態方法,由於靜態方法不依賴於任何物件就可以進行訪問,因此對於靜態方法來說,是沒有this的,因為它不依附於任何物件,既然都沒有物件,就談不上this了。並且由於這個特性,在靜態方法中不能訪問類的非靜態成員變數和非靜態成員方法,因為非靜態成員方法/變數都是必須依賴具體的物件才能夠被呼叫。
但是要注意的是,雖然在靜態方法中不能訪問非靜態成員方法和非靜態成員變數,但是在非靜態成員方法中是可以訪問靜態成員方法/變數的。舉個簡單的例子:
在上面的程式碼中,由於print2方法是獨立於物件存在的,可以直接用過類名呼叫。假如說可以在靜態方法中訪問非靜態方法/變數的話,那麼如果在main方法中有下面一條語句:
MyObject.print2();
此時物件都沒有,str2根本就不存在,所以就會產生矛盾了。同樣對於方法也是一樣,由於你無法預知在print1方法中是否訪問了非靜態成員變數,所以也禁止在靜態成員方法中訪問非靜態成員方法。
而對於非靜態成員方法,它訪問靜態成員方法/變數顯然是毫無限制的。
因此,如果說想在不建立物件的情況下呼叫某個方法,就可以將這個方法設定為static。我們最常見的static方法就是main方法,至於為什麼main方法必須是static的,現在就很清楚了。因為程式在執行main方法的時候沒有建立任何物件,因此只有通過類名來訪問。
另外記住,即使沒有顯示地宣告為static,類的構造器實際上也是靜態方法。
2)static變數
static變數也稱作靜態變數,靜態變數和非靜態變數的區別是:靜態變數被所有的物件所共享,在記憶體中只有一個副本,它當且僅當在類初次載入時會被初始化。而非靜態變數是物件所擁有的,在建立物件的時候被初始化,存在多個副本,各個物件擁有的副本互不影響。
static成員變數的初始化順序按照定義的順序進行初始化。
3)static程式碼塊
static關鍵字還有一個比較關鍵的作用就是 用來形成靜態程式碼塊以優化程式效能。static塊可以置於類中的任何地方,類中可以有多個static塊。在類初次被載入的時候,會按照static塊的順序來執行每個static塊,並且只會執行一次。
為什麼說static塊可以用來優化程式效能,是因為它的特性:只會在類載入的時候執行一次。下面看個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class
Person{
private
Date birthDate;
public
Person(Date birthDate) {
this .birthDate = birthDate;
}
boolean
isBornBoomer() {
Date startDate = Date.valueOf( "1946" );
Date endDate = Date.valueOf( "1964" );
return
birthDate.compareTo(startDate)>= 0
&& birthDate.compareTo(endDate) < 0 ;
}
}
|
isBornBoomer是用來這個人是否是1946-1964年出生的,而每次isBornBoomer被呼叫的時候,都會生成startDate和birthDate兩個物件,造成了空間浪費,如果改成這樣效率會更好:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class
Person{
private
Date birthDate;
private
static Date startDate,endDate;
static {
startDate = Date.valueOf( "1946" );
endDate = Date.valueOf( "1964" );
}
public
Person(Date birthDate) {
this .birthDate = birthDate;
}
boolean
isBornBoomer() {
return
birthDate.compareTo(startDate)>= 0
&& birthDate.compareTo(endDate) < 0 ;
}
}
|
因此,很多時候會將一些只需要進行一次的初始化操作都放在static程式碼塊中進行。
二.static關鍵字的誤區
1.static關鍵字會改變類中成員的訪問許可權嗎?
有些初學的朋友會將java中的static與C/C++中的static關鍵字的功能混淆了。在這裡只需要記住一點:與C/C++中的static不同,Java中的static關鍵字不會影響到變數或者方法的作用域。在Java中能夠影響到訪問許可權的只有private、public、protected(包括包訪問許可權)這幾個關鍵字。看下面的例子就明白了:
提示錯誤"Person.age 不可視",這說明static關鍵字並不會改變變數和方法的訪問許可權。
2.能通過this訪問靜態成員變數嗎?
雖然對於靜態方法來說沒有this,那麼在非靜態方法中能夠通過this訪問靜態成員變數嗎?先看下面的一個例子,這段程式碼輸出的結果是什麼?
1 2 3 4 5 6 7 8 9 10 11 12 |
public
class Main {
static
int value = 33 ;
public
static void
main(String[] args) throws
Exception{
new
Main().printValue();
}
private
void printValue(){
int
value = 3 ;
System.out.println( this .value);
}
}
|
這裡面主要考察隊this和static的理解。this代表什麼?this代表當前物件,那麼通過new Main()來呼叫printValue的話,當前物件就是通過new Main()生成的物件。而static變數是被物件所享有的,因此在printValue中的this.value的值毫無疑問是33。在printValue方法內部的value是區域性變數,根本不可能與this關聯,所以輸出結果是33。在這裡永遠要記住一點:靜態成員變數雖然獨立於物件,但是不代表不可以通過物件去訪問,所有的靜態方法和靜態變數都可以通過物件訪問(只要訪問許可權足夠)。
3.static能作用於區域性變數麼?
在C/C++中static是可以作用域區域性變數的,但是在Java中切記:static是不允許用來修飾區域性變數。不要問為什麼,這是Java語法的規定。
三.常見的筆試面試題
下面列舉一些面試筆試中經常遇到的關於static關鍵字的題目,僅供參考,如有補充歡迎下方留言。
1.下面這段程式碼的輸出結果是什麼?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public
class Test extends
Base{
static {
System.out.println( "test static" );
}
public
Test(){
System.out.println( "test constructor" );
}
public
static void
main(String[] args) {
new
Test();
}
}
class
Base{
static {
System.out.println( "base static" );
}
public
Base(){
System.out.println( "base constructor" );
}
}
|
至於為什麼是這個結果,我們先不討論,先來想一下這段程式碼具體的執行過程,在執行開始,先要尋找到main方法,因為main方法是程式的入口,但是在執行main方法之前,必須先載入Test類,而在載入Test類的時候發現Test類繼承自Base類,因此會轉去先載入Base類,在載入Base類的時候,發現有static塊,便執行了static塊。在Base類載入完成之後,便繼續載入Test類,然後發現Test類中也有static塊,便執行static塊。在載入完所需的類之後,便開始執行main方法。在main方法中執行new Test()的時候會先呼叫父類的構造器,然後再呼叫自身的構造器。因此,便出現了上面的輸出結果。
2.這段程式碼的輸出結果是什麼?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public
class Test {
Person person =
new Person( "Test" );
static {
System.out.println( "test static" );
}
public
Test() {
System.out.println( "test constructor" );
}
public
static void
main(String[] args) {
new
MyClass();
}
}
class
Person{
static {
System.out.println( "person static" );
}
public
Person(String str) {
System.out.println( "person " +str);
}
}
class
MyClass extends
Test {
Person person =
new Person( "MyClass" );
static {
System.out.println( "myclass static" );
}
public
MyClass() {
System.out.println( "myclass constructor" );
}
}
|
類似地,我們還是來想一下這段程式碼的具體執行過程。首先載入Test類,因此會執行Test類中的static塊。接著執行new MyClass(),而MyClass類還沒有被載入,因此需要載入MyClass類。在載入MyClass類的時候,發現MyClass類繼承自Test類,但是由於Test類已經被載入了,所以只需要載入MyClass類,那麼就會執行MyClass類的中的static塊。在載入完之後,就通過構造器來生成物件。而在生成物件的時候,必須先初始化父類的成員變數,因此會執行Test中的Person person = new Person(),而Person類還沒有被載入過,因此會先載入Person類並執行Person類中的static塊,接著執行父類的構造器,完成了父類的初始化,然後就來初始化自身了,因此會接著執行MyClass中的Person person = new Person(),最後執行MyClass的構造器。
3.這段程式碼的輸出結果是什麼?
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public
class Test {
static {
System.out.println( "test static 1" );
}
public
static void
main(String[] args) {
}
static {
System.out.println( "test static 2" );
}
}
|
雖然在main方法中沒有任何語句,但是還是會輸出,原因上面已經講述過了。另外,static塊可以出現類中的任何地方(只要不是方法內部,記住,任何方法內部都不行),並且執行是按照static塊的順序執行的。
參考資料:
《Java程式設計思想》