我們說Java是一種面向對象編程的語言,而對象是把數據及對數據的操作方法放在一起,作為一個相互依存的整體,對同類對象抽象出其共性,便是Java中的類,我們可以用類描述世間萬物,也可以說萬物皆對象。但是這里有個特殊的東西——static,它不屬於對象,那么為什么呢?
static 是Java的一個關鍵字,可以用來修飾成員變量、修飾成員方法、構造靜態代碼塊、實現靜態導包以及實現靜態內部類,下面我們來分別介紹。
1、修飾成員變量
用 static 修飾成員變量可以說是該關鍵字最常用的一個功能,通常將用 static 修飾的成員變量稱為類成員或者靜態成員,那么靜態成員和不用 static 修飾的非靜態成員有什么區別呢?
我們先看看不用 static 修飾的成員變量在內存中的構造。

1 package com.ys.bean; 2 3 /** 4 * Create by YSOcean 5 */ 6 public class Person { 7 private String name; 8 private Integer age; 9 10 public Person(String name, Integer age) { 11 this.name = name; 12 this.age = age; 13 } 14 15 @Override 16 public String toString() { 17 return "Person{" + 18 "name='" + name + '\'' + 19 ", age=" + age + 20 '}'; 21 } 22 //get和set方法省略 23 }
首先,我們創建一個實體類 Person,有兩個屬性 name 和 age,都是普通成員變量(沒有用 static 關鍵字修飾),接着我們通過其構造方法創建兩個對象:
1 Person p1 = new Person("Tom",21); 2 Person p2 = new Person("Marry",20); 3 System.out.println(p1.toString());//Person{name='Tom', age=21} 4 System.out.println(p2.toString());//Person{name='Marry', age=20}
這兩個對象在內存中的存儲結構如下:
由上圖可知,我們創建的兩個對象 p1 和 p2 存儲在堆中,但是其引用地址是存放在棧中的,而且這兩個對象的兩個變量互相獨立,我們修改任何一個對象的屬性值,是不改變另外一個對象的屬性值的。
下面我們將 Person 類中的 age 屬性改為由 static 關鍵字修飾:

1 package com.ys.bean; 2 3 /** 4 * Create by YSOcean 5 */ 6 public class Person { 7 private String name; 8 private static Integer age; 9 10 public Person(String name, Integer age) { 11 this.name = name; 12 this.age = age; 13 } 14 15 @Override 16 public String toString() { 17 return "Person{" + 18 "name='" + name + '\'' + 19 ", age=" + age + 20 '}'; 21 } 22 //get和set方法省略 23 24 }
同樣我們還是向上面一樣,創建 p1 和 p2 兩個對象,並打印這兩個對象,看看和上面打印的有啥區別呢?
1 Person p1 = new Person("Tom",21); 2 Person p2 = new Person("Marry",20); 3 System.out.println(p1.toString());//Person{name='Tom', age=20} 4 System.out.println(p2.toString());//Person{name='Marry', age=20}
我們發現第三行代碼打印的 p1 對象 age 屬性變為 20了,這是為什么呢?
這是因為用在 jvm 的內存構造中,會在堆中開辟一塊內存空間,專門用來存儲用 static 修飾的成員變量,稱為靜態存儲區,無論我們創建多少個對象,用 static 修飾的成員變量有且只有一份存儲在靜態存儲區中,所以該靜態變量的值是以最后創建對象時設置該靜態變量的值為准,也就是由於 p1 先設置 age = 21,后來創建了 p2 對象,p2將 age 改為了20,那么該靜態存儲區的 age 屬性值也被修改成了20。
PS:在 JDK1.8 以前,靜態存儲區是存放在方法區的,而方法區不屬於堆,在 JDK1.8 之后,才將方法區干掉了,方法區中的靜態存儲區改為到堆中存儲。
總結:static 修飾的變量被所有對象所共享,在內存中只有一個副本。由於與對象無關,所以我們可以直接通過 類名.靜態變量 的方式來直接調用靜態變量。對應的非靜態變量是對象所擁有的,多少個對象就有多少個非靜態變量,各個對象所擁有的副本不受影響。
2、修飾修飾成員方法
用 static 關鍵字修飾成員方法也是一樣的道理,我們可以直接通過 類名.靜態方法名() 的方式來調用,而不用創建對象。

1 public class Person { 2 private String name; 3 private static Integer age; 4 5 public static void printClassName(){ 6 System.out.println("com.ys.bean.Person"); 7 } 8 public Person(String name, Integer age) { 9 this.name = name; 10 this.age = age; 11 } 12 13 @Override 14 public String toString() { 15 return "Person{" + 16 "name='" + name + '\'' + 17 ", age=" + age + 18 '}'; 19 } 20 //get和set方法省略 21 22 }
調用靜態方法:
1 Person.printClassName();//com.ys.bean.Person
3、靜態代碼塊
用 static 修飾的代碼塊稱為靜態代碼塊,靜態代碼塊可以置於類的任意一個地方(和成員變量成員方法同等地位,不可放入方法中),並且一個類可以有多個靜態代碼塊,在類初次載入內存時加載靜態代碼塊,並且按照聲明靜態代碼塊的順序來加載,且僅加載一次,優先於各種代碼塊以及構造函數。
關於靜態代碼塊、構造代碼塊、構造函數、普通代碼塊的區別可以參考我的這篇博客。
1 public class CodeBlock { 2 static{ 3 System.out.println("靜態代碼塊"); 4 } 5 }
由於靜態代碼塊只在類載入內存時加載一次的特性,我們可以利用靜態代碼塊來優化程序性能,比如某個比較大配置文件需要在創建對象時加載,這時候為了節省內存,我們可以將該配置文件的加載時機放入到靜態代碼塊中,那么我們無論創建多少個對象時,該配置文件也只加載了一次。
4、靜態導包
用 static 來修飾成員變量,成員方法,以及靜態代碼塊是最常用的三個功能,靜態導包是 JDK1.5以后的新特性,用 import static 包名 來代替傳統的 import 包名 方式。那么有什么用呢?
比如我們創建一個數組,然后用 JDK 自帶的 Arrays 工具類的 sort 方法來對數組進行排序:
1 package com.ys.test; 2 3 import java.util.Arrays; 4 /** 5 * Create by YSOcean 6 */ 7 public class StaticTest { 8 9 public static void main(String[] args) { 10 int[] arrays = {3,4,2,8,1,9}; 11 Arrays.sort(arrays); 12 } 13 }
我們可以看到,調用 sort 方法時,需要進行 import java.util.Arrays 的導包操作,那么變為靜態導包呢?
1 package com.ys.test; 2 3 import static java.util.Arrays.*; 4 /** 5 * Create by YSOcean 6 */ 7 public class StaticTest { 8 9 public static void main(String[] args) { 10 int[] arrays = {3,4,2,8,1,9}; 11 sort(arrays); 12 } 13 }
我們可以看到第三行代碼的 import java.util.Arrays 變為了 import static java.util.Arrays.*,意思是導入 Arrays 類中的所有靜態方法,當然你也可以將 * 變為某個方法名,也就是只導入該方法,那么我們在調用該方法時,就可以不帶上類名,直接通過方法名來調用(第 11 行代碼)。
靜態導包只會減少程序員的代碼編寫量,對於性能是沒有任何提升的(也不會降低性能,Java核心技術第10版卷1第148頁4.7.1章節類的導入有介紹),反而會降低代碼的可讀性,所以實際如何使用需要權衡。
5、靜態內部類
首先我們要知道什么是內部類,定義在一個類的內部的類叫內部類,包含內部類的類叫外部類,內部類用 static 修飾便是我們所說的靜態內部類。
定義內部類的好處是外部類可以訪問內部類的所有方法和屬性,包括私有方法和私有屬性。
訪問普通內部類,我們需要先創建外部類的對象,然后通過外部類名.new 創建內部類的實例。
1 package com.ys.bean; 2 3 /** 4 * Create by hadoop 5 */ 6 public class OutClass { 7 8 public class InnerClass{ 9 10 } 11 }
1 * OuterClass oc = new OuterClass(); 2 * OuterClass.InnerClass in = oc.new InnerClass();
訪問靜態內部類,我們不需要創建外部類的對象,可以直接通過 外部類名.內部類名 來創建實例。
1 package com.ys.bean; 2 3 /** 4 * Create by hadoop 5 */ 6 public class OutClass { 7 8 public static class InnerClass{ 9 10 } 11 }
1 OuterClass.StaticInnerClass sic = new OuterClass.StaticInnerClass();
6、常見問題
①、靜態變量能存在於普通方法中嗎?
能。很明顯,普通方法必須通過對象來調用,靜態變量都可以直接通過類名來調用了,更不用說通過對象來調用,所以是可以存在於普通方法中的。
②、靜態方法能存在普通變量嗎?
不能。因為靜態方法可以直接通過類名來直接調用,不用創建對象,而普通變量是必須通過對象來調用的。那么將普通變量放在靜態方法中,在直接通過類來調用靜態方法時就會報錯。所以不能。
③、靜態代碼塊能放在方法體中嗎?
不能。首先我們要明確靜態代碼塊是在類加載的時候自動運行的。
普通方法需要我們創建對象,然后手工去調用方法,所靜態代碼塊不能聲明在普通方法中。
那么對於用 static 修飾的靜態方法呢?同樣也是不能的。因為靜態方法同樣也需要我們手工通過類名來調用,而不是直接在類加載的時候就運行了。
也就是說靜態代碼塊能夠自動執行,而不管是普通方法還是靜態方法都是需要手工執行的。
④、靜態導包會比普通導包消耗更多的性能?
不會。靜態導包實際上在編譯期間都會被編譯器進行處理,將其轉換成普通按需導包的形式,所以在程序運行期間是不影響性能的。
⑤、static 可以用來修飾局部變量嗎?
不能。不管是在普通方法還是在靜態方法中,static 關鍵字都不能用來修飾局部變量,這是Java的規定。稍微想想也能明白,局部變量的聲明周期是隨着方法的結束而結束的,因為static 修飾的變量是全局的,不與對象有關的,如果用 static 修飾局部變量容易造成理解上的沖突,所以Java規定 static 關鍵字不能用來修飾局部變量。