之前講到final關鍵字的作用是每次面試的時候我必問求職者的兩個問題之一,另外一個問題就是文本會寫到的static。final和static一樣,都是一個小問題可以看到一個人的基礎是否扎實以及平時是否有鑽研精神。
靜態變量和靜態方法
static關鍵字最基本的用法是:
1、被static修飾的變量屬於類變量,可以通過類名.變量名直接引用,而不需要new出一個類來
2、被static修飾的方法屬於類方法,可以通過類名.方法名直接引用,而不需要new出一個類來
被static修飾的變量、被static修飾的方法統一屬於類的靜態資源,是類實例之間共享的,換言之,一處變、處處變。JDK把不同的靜態資源放在了不同的類中而不把所有靜態資源放在一個類裡面,很多人可能想當然認為當然要這麼做,但是是否想過為什麼要這麼做呢?個人認為主要有三個好處:
1、不同的類有自己的靜態資源,這可以實現靜態資源分類。比如和數學相關的靜態資源放在java.lang.Math中,和日歷相關的靜態資源放在java.util.Calendar中,這樣就很清晰了
2、避免重名。不同的類之間有重名的靜態變量名、靜態方法名也是很正常的,如果所有的都放在一起不可避免的一個問題就是名字重復,這時候怎麼辦?分類放置就好了。
3、避免靜態資源類無限膨脹,這很好理解。
OK,再微微深入一下,也是有些人容易混淆的一個問題:靜態方法能不能引用非靜態資源?靜態方法裡面能不能引用靜態資源?非靜態方法裡面能不能引用靜態資源?比如就以這段代碼為例,是否有錯?
1 public class A 2 { 3 private int i = 1; 4 5 public static void main(String[] args) 6 { 7 i = 1; 8 } 9 }
當然有錯,在第7行的地方。不妨這麼思考這個問題:
靜態資源屬於類,但是是獨立於類存在的。從JVM的類加載機制的角度講,靜態資源是類初始化的時候加載的,而非靜態資源是類new的時候加載的。 類的初始化早於類的new,比如Class.forName(“xxx”)方法,就是初始化了一個類,但是並沒有new它,只是加載這個類的靜態資源罷 了。所以對於靜態資源來說,它是不可能知道一個類中有哪些非靜態資源的;但是對於非靜態資源來說就不一樣了,由於它是new出來之後產生的,因此屬於類的 這些東西它都能認識。所以上面的幾個問題答案就很明確了:
1、靜態方法能不能引用非靜態資源?不能,new的時候才會產生的東西,對於初始化後就存在的靜態資源來說,根本不認識它。
2、靜態方法裡面能不能引用靜態資源?可以,因為都是類初始化的時候加載的,大家相互都認識。
3、非靜態方法裡面能不能引用靜態資源?可以,非靜態方法就是實例方法,那是new之後才產生的,那麼屬於類的內容它都認識。
靜態塊
靜態塊也是static的重要應用之一。也是用於初始化一個類的時候做操作用的,和靜態變量、靜態方法一樣,靜態塊裡面的代碼只執行一次,且只在初始化類的時候執行。靜態塊很簡單,不過提三個小細節:
1 public class A 2 { 3 private static int a = B(); 4 5 static 6 { 7 System.out.println("Enter A.static block"); 8 } 9 10 public static void main(String[] args) 11 { 12 new A(); 13 } 14 15 public static int B() 16 { 17 System.out.println("Enter A.B()"); 18 return 1; 19 } 20 }
打印結果是
Enter A.B() Enter A.static block
得出第一個結論:靜態資源的加載順序是嚴格按照靜態資源的定義順序來加載的。這和周志明老師《深入理解Java虛擬機:JVM高級特性與最佳實踐》中類初始化中的說法“<clinit>()方法是由編譯器自動收集類中所有類變量的賦值動作和靜態語句塊(static{}塊)中的語句合並產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的”是一致的。
再看一個例子:
1 public class A 2 { 3 static 4 { 5 c = 3; 6 System.out.println(c); 7 } 8 9 private static int c; 10 }
這段代碼的第6行是有錯誤的“Cannot reference a field before it is defined”。從這個例子得出第二個結論:靜態代碼塊對於定義在它之後的靜態變量,可以賦值,但是不能訪問。
最後一個小例子:
1 public class A 2 { 3 static 4 { 5 System.out.println("A.static block"); 6 } 7 8 public A() 9 { 10 System.out.println("A.constructor()"); 11 } 12 }
1 public class B extends A 2 { 3 static 4 { 5 System.out.println("B.static block"); 6 } 7 8 public B() 9 { 10 System.out.println("B.constructor()"); 11 } 12 13 public static void main(String[] args) 14 { 15 new B(); 16 new B(); 17 } 18 }
結果是
A.static block B.static block A.constructor() B.constructor() A.constructor() B.constructor()
這個例子得出第三個結論:靜態代碼塊是嚴格按照父類靜態代碼塊->子類靜態代碼塊的順序加載的,且只加載一次。
static修飾類
這個用得相對比前面的用法少多了,static一般情況下來說是不可以修飾類的, 如果static要修飾一個類,說明這個類是一個靜態內部類(注意static只能修飾一個內部類),也就是匿名內部類。像線程池 ThreadPoolExecutor中的四種拒絕機制CallerRunsPolicy、AbortPolicy、DiscardPolicy、 DiscardOldestPolicy就是靜態內部類。靜態內部類相關內容會在寫內部類的時候專門講到。
import static
這個比較冷門,基本很少看見有地方用,使用JUnit可能會用到,寫assert的時候會方便些。import static是JDK1.5之後的新特性,這兩個關鍵字連用可以指定導入某個類中的指定靜態資源,並且不需要使用類名.資源名,可以直接使用資源名。注意一下,import static必須這麼寫,而不能寫成static import。舉個例子來看一下:
1 import static java.lang.Math.*; 2 3 public class A 4 { 5 public static void main(String[] args) 6 { 7 System.out.println(sin(2.2)); 8 } 9 }
這麼寫意味著我導入了Math下的所有靜態資源,main函數裡面我就可以直接用sin(2.2)而不需要使用Math.sin(2.2)了。注意一下,要寫import static java.lang.Math.*, 最後的“.*”不可少,有了這兩個字符才意味著導入的是Math下的所有靜態資源,寫成import static java.lang.Math是有問題的。當然,我們也可以指定只導入某個靜態資源,比如只導入Math下sin這個方法而不導入Math下的所有靜態資 源:
1 import static java.lang.Math.sin; 2 3 public class A 4 { 5 public static void main(String[] args) 6 { 7 System.out.println(sin(2.2)); 8 } 9 }
這麼寫也是沒問題的。導入靜態變量也是一樣,有興趣的可以自己試一下。對於import static,個人的態度是:
1、簡化了一些操作,比如靜態導入Math下的所有靜態資源,在頻繁使用Math類下靜態資源的地方,可以少些很多“Math.”
2、降低了代碼的可讀性
建議在某些場景下導入特定的靜態資源,不建議使用“.*”的導入方式。