一個 static 還能難得住我?
阿新 • • 發佈:2020-05-31
static 是我們日常生活中經常用到的關鍵字,也是 Java 中非常重要的一個關鍵字,static 可以修飾變數、方法、做靜態程式碼塊、靜態導包等,下面我們就來具體聊一聊這個關鍵字,我們先從基礎開始,從基本用法入手,然後分析其原理、優化等。
## 初識 static 關鍵字
### static 修飾變數
`static` 關鍵字表示的概念是 `全域性的、靜態的`,用它修飾的變數被稱為`靜態變數`。
```java
public class TestStatic {
static int i = 10; // 定義了一個靜態變數 i
}
```
靜態變數也被稱為類變數,靜態變數是屬於這個類所有的。什麼意思呢?這其實就是說,static 關鍵字只能定義在類的 `{}` 中,而不能定義在任何方法中。
![](https://img2020.cnblogs.com/blog/1515111/202005/1515111-20200531112615469-3463548.png)
就算把方法中的 static 關鍵字去掉也是一樣的。
![](https://img2020.cnblogs.com/blog/1515111/202005/1515111-20200531112621569-1106286631.png)
static 屬於類所有,由類來直接呼叫 static 修飾的變數,它不需要手動例項化類進行呼叫
```java
public class TestStatic {
static int i = 10;
public static void main(String[] args) {
System.out.println(TestStatic.i);
}
}
```
**這裡你需要理解幾個變數的概念**
* 定義在構造方法、程式碼塊、方法`外`的變數被稱為例項變數,例項變數的副本數量和例項的數量一樣。
* 定義在方法、構造方法、程式碼塊`內`的變數被稱為區域性變數;
* 定義在方法引數`中`的變數被稱為引數。
詳情參考
### static 修飾方法
static 可以修飾方法,被 static 修飾的方法被稱為`靜態方法`,其實就是在一個方法定義中加上 `static` 關鍵字進行修飾,例如下面這樣
```java
static void sayHello(){}
```
《Java 程式設計思想》在 P86 頁有一句經典的描述
**static 方法就是沒有 this 的方法,在 static 內部不能呼叫非靜態方法,反過來是可以的。而且可以在沒有建立任何物件的前提下,僅僅通過類本身來呼叫 static 方法,這實際上是 static 方法的主要用途**。
其中有一句非常重要的話就是 **static 方法就是沒有 this 的方法**,也就是說,可以在不用建立物件的前提下就能夠訪問 static 方法,如何做到呢?看下面一段程式碼
![](https://img2020.cnblogs.com/blog/1515111/202005/1515111-20200531112631544-1221882463.png)
在上面的例子中,由於 `staticMethod` 是靜態方法,所以能夠使用 類名.變數名進行呼叫。
因此,如果說想在不建立物件的情況下呼叫某個方法,就可以將這個方法設定為 static。平常我們見的最多的 static 方法就是 main方 法,至於為什麼 main 方法必須是 static 的,現在應該很清楚了。因為程式在執行 main 方法的時候沒有建立任何物件,因此只有通過類名來訪問。
**static 修飾方法的注意事項**
* 首先第一點就是最常用的,不用建立物件,直接`類名.變數名` 即可訪問;
* static 修飾的方法內部不能呼叫非靜態方法;
![](https://img2020.cnblogs.com/blog/1515111/202005/1515111-20200531112642836-419444156.png)
* 非靜態方法內部可以呼叫 static 靜態方法。
![](https://img2020.cnblogs.com/blog/1515111/202005/1515111-20200531112649976-2042624207.png)
### static 修飾程式碼塊
static 關鍵字可以用來修飾程式碼塊,程式碼塊分為兩種,一種是使用 `{}` 程式碼塊;一種是 `static {}` 靜態程式碼塊。static 修飾的程式碼塊被稱為靜態程式碼塊。靜態程式碼塊可以置於類中的任何地方,類中可以有多個 static 塊,在類初次被載入的時候,會按照 static 程式碼塊的順序來執行,每個 static 修飾的程式碼塊只能執行一次。我們會面會說一下程式碼塊的載入順序。下面是靜態程式碼塊的例子
![](https://img2020.cnblogs.com/blog/1515111/202005/1515111-20200531112657178-851431268.png)
static 程式碼塊可以用來**優化程式執行順序**,是因為它的特性:只會在類載入的時候執行一次。
### static 用作靜態內部類
內部類的使用場景比較少,但是內部類還有具有一些比較有用的。在瞭解靜態內部類前,我們先看一下內部類的分類
* 普通內部類
* 區域性內部類
* 靜態內部類
* 匿名內部類
`靜態內部類`就是用 static 修飾的內部類,靜態內部類可以包含靜態成員,也可以包含非靜態成員,但是在非靜態內部類中不可以宣告靜態成員。
靜態內部類有許多作用,由於非靜態內部類的例項建立需要有外部類物件的引用,所以非靜態內部類物件的建立必須依託於外部類的例項;而靜態內部類的例項建立只需依託外部類;
並且由於非靜態內部類物件持有了外部類物件的引用,因此非靜態內部類可以訪問外部類的非靜態成員;而靜態內部類只能訪問外部類的靜態成員;
* 內部類需要脫離外部類物件來建立例項
* 避免內部類使用過程中出現記憶體溢位
```java
public class ClassDemo {
private int a = 10;
private static int b = 20;
static class StaticClass{
public static int c = 30;
public int d = 40;
public static void print(){
//下面程式碼會報錯,靜態內部類不能訪問外部類例項成員
//System.out.println(a);
//靜態內部類只可以訪問外部類類成員
System.out.println("b = "+b);
}
public void print01(){
//靜態內部內所處的類中的方法,呼叫靜態內部類的例項方法,屬於外部類中呼叫靜態內部類的例項方法
StaticClass sc = new StaticClass();
sc.print();
}
}
}
```
### 靜態導包
不知道你注意到這種現象沒有,比如你使用了 `java.util` 內的工具類時,你需要匯入 java.util 包,才能使用其內部的工具類,如下
![](https://img2020.cnblogs.com/blog/1515111/202005/1515111-20200531112708060-1225257533.png)
但是還有一種導包方式是使用`靜態導包`,靜態匯入就是使用 `import static` 用來匯入某個類或者某個包中的靜態方法或者靜態變數。
```java
import static java.lang.Integer.*;
public class StaticTest {
public static void main(String[] args) {
System.out.println(MAX_VALUE);
System.out.println(toHexString(111));
}
}
```
## static 進階知識
我們在瞭解了 static 關鍵字的用法之後,來看一下 static 深入的用法,也就是由淺入深,慢慢來,前戲要夠~
### 關於 static 的所屬類
static 所修飾的屬性和方法都屬於類的,不會屬於任何物件;它們的呼叫方式都是 `類名.屬性名/方法名`,而例項變數和區域性變數都是屬於具體的物件例項。
### static 修飾變數的儲存位置
首先,先來認識一下 JVM 的不同儲存區域。
![](https://img2020.cnblogs.com/blog/1515111/202005/1515111-20200531112716049-785669819.png)
* `虛擬機器棧` : Java 虛擬機器棧是執行緒私有的資料區,Java 虛擬機器棧的生命週期與執行緒相同,虛擬機器棧也是區域性變數的儲存位置。方法在執行過程中,會在虛擬機器棧種建立一個 `棧幀(stack frame)`。
* `本地方法棧`: 本地方法棧也是執行緒私有的資料區,本地方法棧儲存的區域主要是 Java 中使用 `native` 關鍵字修飾的方法所儲存的區域
* `程式計數器`:程式計數器也是執行緒私有的資料區,這部分割槽域用於儲存執行緒的指令地址,用於判斷執行緒的分支、迴圈、跳轉、異常、執行緒切換和恢復等功能,這些都通過程式計數器來完成。
* `方法區`:方法區是各個執行緒共享的記憶體區域,它用於儲存虛擬機器載入的 類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料,也就是說,**static 修飾的變數儲存在方法區中**
* `堆`: 堆是執行緒共享的資料區,堆是 JVM 中最大的一塊儲存區域,所有的物件例項,包括**例項變數都在堆上**進行相應的分配。
### static 變數的生命週期
static 變數的生命週期與類的生命週期相同,隨類的載入而建立,隨類的銷燬而銷燬;普通成員變數和其所屬的生命週期相同。
### static 序列化
我們知道,序列化的目的就是為了 **把 Java 物件轉換為位元組序列**。物件轉換為有序位元組流,以便其能夠在網路上傳輸或者儲存在本地檔案中。
宣告為 static 和 transient 型別的變數不能被序列化,因為 static 修飾的變數儲存在方法區中,只有堆記憶體才會被序列化。而 `transient` 關鍵字的作用就是防止物件進行序列化操作。
### 類載入順序
我們前面提到了類載入順序這麼一個概念,static 修飾的變數和靜態程式碼塊在使用前已經被初始化好了,類的初始化順序依次是
載入父類的靜態欄位 -> 父類的靜態程式碼塊 -> 子類靜態欄位 -> 子類靜態程式碼塊 -> 父類成員變數(非靜態欄位)
-> 父類非靜態程式碼塊 -> 父類構造器 -> 子類成員變數 -> 子類非靜態程式碼塊 -> 子類構造器
### static 經常用作日誌列印
我們在開發過程中,經常會使用 `static` 關鍵字作為日誌列印,下面這行程式碼你應該經常看到
```java
private static final Logger LOGGER = LogFactory.getLoggger(StaticTest.class);
```
然而把 static 和 final 去掉都可以列印日誌
```java
private final Logger LOGGER = LogFactory.getLoggger(StaticTest.class);
private Logger LOGGER = LogFactory.getLoggger(StaticTest.class);
```
但是這種列印日誌的方式存在問題
對於每個 StaticTest 的例項化物件都會擁有一個 LOGGER,如果建立了1000個 StaticTest 物件,則會多出1000個Logger 物件,造成資源的浪費,因此通常會將 Logger 物件宣告為 static 變數,這樣一來,能夠減少對記憶體資源的佔用。
### static 經常用作單例模式
由於單例模式指的就是對於不同的類來說,它的副本只有一個,因此 static 可以和單例模式完全匹配。
下面是一個經典的雙重校驗鎖實現單例模式的場景
```java
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
```
來對上面程式碼做一個簡單的描述
使用 `static` 保證 singleton 變數是靜態的,使用 `volatile` 保證 singleton 變數的可見性,使用私有構造器確保 Singleton 不能被 new 例項化。
使用 `Singleton.getInstance()` 獲取 singleton 物件,首先會進行判斷,如果 singleton 為空,會鎖住 Singletion 類物件,這裡有一些小夥伴們可能不知道為什麼需要兩次判斷,這裡來解釋下
如果執行緒 t1 執行到 singleton == null 後,判斷物件為 null,此時執行緒把執行權交給了 t2,t2 判斷物件為 null,鎖住 Singleton 類物件,進行下面的判斷和例項化過程。如果不進行第二次判斷的話,那麼 t1 在進行第一次判空後,也會進行例項化過程,此時仍然會建立多個物件。
## 類的構造器是否是 static 的
這個問題我相信大部分小夥伴都沒有考慮過,在 Java 程式設計思想中有這麼一句話 **類的構造器雖然沒有用 static 修飾,但是實際上是 static 方法**,但是並沒有給出實際的解釋,但是這個問題可以從下面幾個方面來回答
* static 最簡單、最方便記憶的規則就是沒有 this 引用。而在類的構造器中,是有隱含的 this 繫結的,因為構造方法是和類繫結的,從這個角度來看,構造器不是靜態的。
* 從類的方法這個角度來看,因為 `類.方法名`不需要新建立物件就能夠訪問,所以從這個角度來看,構造器也不是靜態的
* 從 JVM 指令角度去看,我們來看一個例子
```java
public class StaticTest {
public StaticTest(){}
public static void test(){
}
public static void main(String[] args) {
StaticTest.test();
StaticTest staticTest = new StaticTest();
}
}
```
我們使用 javap -c 生成 StaticTest 的位元組碼看一下
```assembly
public class test.StaticTest {
public test.StaticTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public static void test();
Code:
0: return
public static void main(java.lang.String[]);
Code:
0: invokestatic #2 // Method test:()V
3: new #3 // class test/StaticTest
6: dup
7: invokespecial #4 // Method "":()V
10: astore_1
11: return
}
```
我們發現,在呼叫 static 方法時是使用的 `invokestatic` 指令,new 物件呼叫的是 `invokespecial` 指令,而且在 JVM 規範中 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokestatic 說到
![](https://img2020.cnblogs.com/blog/1515111/202005/1515111-20200531112727532-1542376786.png)
![](https://img2020.cnblogs.com/blog/1515111/202005/1515111-20200531112733139-60373309.png)
從這個角度來講,`invokestatic` 指令是專門用來執行 static 方法的指令;`invokespecial` 是專門用來執行例項方法的指令;從這個角度來講,構造器也不是靜態的。
![](https://img2020.cnblogs.com/blog/1515111/202005/1515111-20200531112752970-1888014