一個 static 還能難得住我?

程式設計師cxuan發表於2020-05-31

static 是我們日常生活中經常用到的關鍵字,也是 Java 中非常重要的一個關鍵字,static 可以修飾變數、方法、做靜態程式碼塊、靜態導包等,下面我們就來具體聊一聊這個關鍵字,我們先從基礎開始,從基本用法入手,然後分析其原理、優化等。

初識 static 關鍵字

static 修飾變數

static 關鍵字表示的概念是 全域性的、靜態的,用它修飾的變數被稱為靜態變數

public class TestStatic {
    
    static int i = 10; // 定義了一個靜態變數 i 
}

靜態變數也被稱為類變數,靜態變數是屬於這個類所有的。什麼意思呢?這其實就是說,static 關鍵字只能定義在類的 {} 中,而不能定義在任何方法中。

image.png

就算把方法中的 static 關鍵字去掉也是一樣的。

image.png

static 屬於類所有,由類來直接呼叫 static 修飾的變數,它不需要手動例項化類進行呼叫

public class TestStatic {

    static int i = 10;

    public static void main(String[] args) {
        System.out.println(TestStatic.i);
    }
}

這裡你需要理解幾個變數的概念

  • 定義在構造方法、程式碼塊、方法的變數被稱為例項變數,例項變數的副本數量和例項的數量一樣。
  • 定義在方法、構造方法、程式碼塊的變數被稱為區域性變數;
  • 定義在方法引數的變數被稱為引數。

static 修飾方法

static 可以修飾方法,被 static 修飾的方法被稱為靜態方法,其實就是在一個方法定義中加上 static 關鍵字進行修飾,例如下面這樣

static void sayHello(){}

《Java 程式設計思想》在 P86 頁有一句經典的描述

static 方法就是沒有 this 的方法,在 static 內部不能呼叫非靜態方法,反過來是可以的。而且可以在沒有建立任何物件的前提下,僅僅通過類本身來呼叫 static 方法,這實際上是 static 方法的主要用途

其中有一句非常重要的話就是 static 方法就是沒有 this 的方法,也就是說,可以在不用建立物件的前提下就能夠訪問 static 方法,如何做到呢?看下面一段程式碼

image.png

在上面的例子中,由於 staticMethod 是靜態方法,所以能夠使用 類名.變數名進行呼叫。

因此,如果說想在不建立物件的情況下呼叫某個方法,就可以將這個方法設定為 static。平常我們見的最多的 static 方法就是 main方 法,至於為什麼 main 方法必須是 static 的,現在應該很清楚了。因為程式在執行 main 方法的時候沒有建立任何物件,因此只有通過類名來訪問。

static 修飾方法的注意事項

  • 首先第一點就是最常用的,不用建立物件,直接類名.變數名 即可訪問;
  • static 修飾的方法內部不能呼叫非靜態方法;

image.png

  • 非靜態方法內部可以呼叫 static 靜態方法。

image.png

static 修飾程式碼塊

static 關鍵字可以用來修飾程式碼塊,程式碼塊分為兩種,一種是使用 {} 程式碼塊;一種是 static {} 靜態程式碼塊。static 修飾的程式碼塊被稱為靜態程式碼塊。靜態程式碼塊可以置於類中的任何地方,類中可以有多個 static 塊,在類初次被載入的時候,會按照 static 程式碼塊的順序來執行,每個 static 修飾的程式碼塊只能執行一次。我們會面會說一下程式碼塊的載入順序。下面是靜態程式碼塊的例子

image.png

static 程式碼塊可以用來優化程式執行順序,是因為它的特性:只會在類載入的時候執行一次。

static 用作靜態內部類

內部類的使用場景比較少,但是內部類還有具有一些比較有用的。在瞭解靜態內部類前,我們先看一下內部類的分類

  • 普通內部類
  • 區域性內部類
  • 靜態內部類
  • 匿名內部類

靜態內部類就是用 static 修飾的內部類,靜態內部類可以包含靜態成員,也可以包含非靜態成員,但是在非靜態內部類中不可以宣告靜態成員。

靜態內部類有許多作用,由於非靜態內部類的例項建立需要有外部類物件的引用,所以非靜態內部類物件的建立必須依託於外部類的例項;而靜態內部類的例項建立只需依託外部類;

並且由於非靜態內部類物件持有了外部類物件的引用,因此非靜態內部類可以訪問外部類的非靜態成員;而靜態內部類只能訪問外部類的靜態成員;

  • 內部類需要脫離外部類物件來建立例項
  • 避免內部類使用過程中出現記憶體溢位
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 包,才能使用其內部的工具類,如下

image.png

但是還有一種導包方式是使用靜態導包,靜態匯入就是使用 import static 用來匯入某個類或者某個包中的靜態方法或者靜態變數。

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 的不同儲存區域。

image.png

  • 虛擬機器棧 : Java 虛擬機器棧是執行緒私有的資料區,Java 虛擬機器棧的生命週期與執行緒相同,虛擬機器棧也是區域性變數的儲存位置。方法在執行過程中,會在虛擬機器棧種建立一個 棧幀(stack frame)
  • 本地方法棧: 本地方法棧也是執行緒私有的資料區,本地方法棧儲存的區域主要是 Java 中使用 native 關鍵字修飾的方法所儲存的區域
  • 程式計數器:程式計數器也是執行緒私有的資料區,這部分割槽域用於儲存執行緒的指令地址,用於判斷執行緒的分支、迴圈、跳轉、異常、執行緒切換和恢復等功能,這些都通過程式計數器來完成。
  • 方法區:方法區是各個執行緒共享的記憶體區域,它用於儲存虛擬機器載入的 類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料,也就是說,static 修飾的變數儲存在方法區中
  • : 堆是執行緒共享的資料區,堆是 JVM 中最大的一塊儲存區域,所有的物件例項,包括例項變數都在堆上進行相應的分配。

static 變數的生命週期

static 變數的生命週期與類的生命週期相同,隨類的載入而建立,隨類的銷燬而銷燬;普通成員變數和其所屬的生命週期相同。

static 序列化

我們知道,序列化的目的就是為了 把 Java 物件轉換為位元組序列。物件轉換為有序位元組流,以便其能夠在網路上傳輸或者儲存在本地檔案中。

宣告為 static 和 transient 型別的變數不能被序列化,因為 static 修飾的變數儲存在方法區中,只有堆記憶體才會被序列化。而 transient 關鍵字的作用就是防止物件進行序列化操作。

類載入順序

我們前面提到了類載入順序這麼一個概念,static 修飾的變數和靜態程式碼塊在使用前已經被初始化好了,類的初始化順序依次是

載入父類的靜態欄位 -> 父類的靜態程式碼塊 -> 子類靜態欄位 -> 子類靜態程式碼塊 -> 父類成員變數(非靜態欄位)

-> 父類非靜態程式碼塊 -> 父類構造器 -> 子類成員變數 -> 子類非靜態程式碼塊 -> 子類構造器

static 經常用作日誌列印

我們在開發過程中,經常會使用 static 關鍵字作為日誌列印,下面這行程式碼你應該經常看到

private static final Logger LOGGER = LogFactory.getLoggger(StaticTest.class);

然而把 static 和 final 去掉都可以列印日誌

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 可以和單例模式完全匹配。

下面是一個經典的雙重校驗鎖實現單例模式的場景

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 指令角度去看,我們來看一個例子
public class StaticTest {

    public StaticTest(){}

    public static void test(){

    }

    public static void main(String[] args) {
        StaticTest.test();
        StaticTest staticTest = new StaticTest();
    }
}

我們使用 javap -c 生成 StaticTest 的位元組碼看一下

public class test.StaticTest {
  public test.StaticTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()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 "<init>":()V
      10: astore_1
      11: return
}

我們發現,在呼叫 static 方法時是使用的 invokestatic 指令,new 物件呼叫的是 invokespecial 指令,而且在 JVM 規範中 https://docs.oracle.com/javas... 說到

image.png

image.png

從這個角度來講,invokestatic 指令是專門用來執行 static 方法的指令;invokespecial 是專門用來執行例項方法的指令;從這個角度來講,構造器也不是靜態的。

相關文章