一個 static 還能可貴住我?

static 是咱們平常生活中常常用到的關鍵字,也是 Java 中很是重要的一個關鍵字,static 能夠修飾變量、方法、作靜態代碼塊、靜態導包等,下面咱們就來具體聊一聊這個關鍵字,咱們先從基礎開始,從基本用法入手,而後分析其原理、優化等。html

初識 static 關鍵字

static 修飾變量

static 關鍵字表示的概念是 全局的、靜態的,用它修飾的變量被稱爲靜態變量java

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

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

image.png

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

image.png

static 屬於類全部,由類來直接調用 static 修飾的變量,它不須要手動實例化類進行調用oracle

public class TestStatic {

    static int i = 10;

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

這裏你須要理解幾個變量的概念jvm

  • 定義在構造方法、代碼塊、方法的變量被稱爲實例變量,實例變量的副本數量和實例的數量同樣。
  • 定義在方法、構造方法、代碼塊的變量被稱爲局部變量;
  • 定義在方法參數的變量被稱爲參數。

static 修飾方法

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

static void sayHello(){}

《Java 編程思想》在 P86 頁有一句經典的描述優化

static 方法就是沒有 this 的方法,在 static 內部不能調用非靜態方法,反過來是能夠的。並且能夠在沒有建立任何對象的前提下,僅僅經過類自己來調用 static 方法,這其實是 static 方法的主要用途this

其中有一句很是重要的話就是 static 方法就是沒有 this 的方法,也就是說,能夠在不用建立對象的前提下就可以訪問 static 方法,如何作到呢?看下面一段代碼spa

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 是專門用來執行實例方法的指令;從這個角度來說,構造器也不是靜態的。

相關文章
相關標籤/搜索