一個(gè) static 還能難得住我?
點(diǎn)擊藍(lán)色“程序員cxuan?”關(guān)注我喲
加個(gè)“星標(biāo)”,歡迎來(lái)撩

static 是我們?nèi)粘I钪薪?jīng)常用到的關(guān)鍵字,也是 Java 中非常重要的一個(gè)關(guān)鍵字,static 可以修飾變量、方法、做靜態(tài)代碼塊、靜態(tài)導(dǎo)包等,下面我們就來(lái)具體聊一聊這個(gè)關(guān)鍵字,我們先從基礎(chǔ)開(kāi)始,從基本用法入手,然后分析其原理、優(yōu)化等。

1
?初識(shí)?static 關(guān)鍵字
static 修飾變量
static 關(guān)鍵字表示的概念是 全局的、靜態(tài)的,用它修飾的變量被稱為靜態(tài)變量。
public?class?TestStatic?{
????
????static?int?i?=?10;?//?定義了一個(gè)靜態(tài)變量?i?
}
靜態(tài)變量也被稱為類變量,靜態(tài)變量是屬于這個(gè)類所有的。什么意思呢?這其實(shí)就是說(shuō),static 關(guān)鍵字只能定義在類的 {} 中,而不能定義在任何方法中。

就算把方法中的 static 關(guān)鍵字去掉也是一樣的。

static 屬于類所有,由類來(lái)直接調(diào)用 static 修飾的變量,它不需要手動(dòng)實(shí)例化類進(jìn)行調(diào)用
public?class?TestStatic?{
????static?int?i?=?10;
????public?static?void?main(String[]?args)?{
????????System.out.println(TestStatic.i);
????}
}
這里你需要理解幾個(gè)變量的概念
定義在構(gòu)造方法、代碼塊、方法 外的變量被稱為成員變量,成員變量的副本數(shù)量和實(shí)例的數(shù)量一樣。定義在方法、構(gòu)造方法、代碼塊 內(nèi)的變量被稱為局部變量;定義在方法參數(shù) 中的變量被稱為參數(shù)。
詳情參考
都說(shuō)變量有七八種,到底誰(shuí)是 Java 的親兒子
static 修飾方法
static 可以修飾方法,被 static 修飾的方法被稱為靜態(tài)方法,其實(shí)就是在一個(gè)方法定義中加上 static 關(guān)鍵字進(jìn)行修飾,例如下面這樣
static?void?sayHello(){}
《Java 編程思想》在 P86 頁(yè)有一句經(jīng)典的描述
static 方法就是沒(méi)有 this 的方法,在 static 內(nèi)部不能調(diào)用非靜態(tài)方法,反過(guò)來(lái)是可以的。而且可以在沒(méi)有創(chuàng)建任何對(duì)象的前提下,僅僅通過(guò)類本身來(lái)調(diào)用 static 方法,這實(shí)際上是 static 方法的主要用途。
其中有一句非常重要的話就是 static 方法就是沒(méi)有 this 的方法,也就是說(shuō),可以在不用創(chuàng)建對(duì)象的前提下就能夠訪問(wèn) static 方法,如何做到呢?看下面一段代碼

在上面的例子中,由于 staticMethod 是靜態(tài)方法,所以能夠使用 類名.變量名進(jìn)行調(diào)用。
因此,如果說(shuō)想在不創(chuàng)建對(duì)象的情況下調(diào)用某個(gè)方法,就可以將這個(gè)方法設(shè)置為 static。平常我們見(jiàn)的最多的 static 方法就是 main方 法,至于為什么 main 方法必須是 static 的,現(xiàn)在應(yīng)該很清楚了。因?yàn)槌绦蛟趫?zhí)行 main 方法的時(shí)候沒(méi)有創(chuàng)建任何對(duì)象,因此只有通過(guò)類名來(lái)訪問(wèn)。
static 修飾方法的注意事項(xiàng)
首先第一點(diǎn)就是最常用的,不用創(chuàng)建對(duì)象,直接 類名.變量名即可訪問(wèn);static 修飾的方法內(nèi)部不能調(diào)用非靜態(tài)方法;

非靜態(tài)方法內(nèi)部可以調(diào)用 static 靜態(tài)方法。 
static 修飾代碼塊
static 關(guān)鍵字可以用來(lái)修飾代碼塊,代碼塊分為兩種,一種是使用 {} 代碼塊;一種是 static {} 靜態(tài)代碼塊。static 修飾的代碼塊被稱為靜態(tài)代碼塊。靜態(tài)代碼塊可以置于類中的任何地方,類中可以有多個(gè) static 塊,在類初次被加載的時(shí)候,會(huì)按照 static 代碼塊的順序來(lái)執(zhí)行,每個(gè) static 修飾的代碼塊只能執(zhí)行一次。我們會(huì)面會(huì)說(shuō)一下代碼塊的加載順序。下面是靜態(tài)代碼塊的例子

static 代碼塊可以用來(lái)優(yōu)化程序執(zhí)行順序,是因?yàn)樗奶匦裕褐粫?huì)在類加載的時(shí)候執(zhí)行一次。
static 用作靜態(tài)內(nèi)部類
內(nèi)部類的使用場(chǎng)景比較少,但是內(nèi)部類還有具有一些比較有用的。在了解靜態(tài)內(nèi)部類前,我們先看一下內(nèi)部類的分類
普通內(nèi)部類 局部?jī)?nèi)部類 靜態(tài)內(nèi)部類 匿名內(nèi)部類
靜態(tài)內(nèi)部類就是用 static 修飾的內(nèi)部類,靜態(tài)內(nèi)部類可以包含靜態(tài)成員,也可以包含非靜態(tài)成員,但是在非靜態(tài)內(nèi)部類中不可以聲明靜態(tài)成員。
靜態(tài)內(nèi)部類有許多作用,由于非靜態(tài)內(nèi)部類的實(shí)例創(chuàng)建需要有外部類對(duì)象的引用,所以非靜態(tài)內(nèi)部類對(duì)象的創(chuàng)建必須依托于外部類的實(shí)例;而靜態(tài)內(nèi)部類的實(shí)例創(chuàng)建只需依托外部類;
并且由于非靜態(tài)內(nèi)部類對(duì)象持有了外部類對(duì)象的引用,因此非靜態(tài)內(nèi)部類可以訪問(wèn)外部類的非靜態(tài)成員;而靜態(tài)內(nèi)部類只能訪問(wèn)外部類的靜態(tài)成員;
內(nèi)部類需要脫離外部類對(duì)象來(lái)創(chuàng)建實(shí)例 避免內(nèi)部類使用過(guò)程中出現(xiàn)內(nèi)存溢出
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(){
????????????//下面代碼會(huì)報(bào)錯(cuò),靜態(tài)內(nèi)部類不能訪問(wèn)外部類實(shí)例成員
????????????//System.out.println(a);
?????
????????????//靜態(tài)內(nèi)部類只可以訪問(wèn)外部類類成員
????????????System.out.println("b?=?"+b);
????????????
????????}
??????
????????public?void?print01(){
????????????//靜態(tài)內(nèi)部?jī)?nèi)所處的類中的方法,調(diào)用靜態(tài)內(nèi)部類的實(shí)例方法,屬于外部類中調(diào)用靜態(tài)內(nèi)部類的實(shí)例方法
????????????StaticClass?sc?=?new?StaticClass();
????????????sc.print();
????????}???
????}
}
靜態(tài)導(dǎo)包
不知道你注意到這種現(xiàn)象沒(méi)有,比如你使用了 java.util 內(nèi)的工具類時(shí),你需要導(dǎo)入 java.util 包,才能使用其內(nèi)部的工具類,如下

但是還有一種導(dǎo)包方式是使用靜態(tài)導(dǎo)包,靜態(tài)導(dǎo)入就是使用 import static 用來(lái)導(dǎo)入某個(gè)類或者某個(gè)包中的靜態(tài)方法或者靜態(tài)變量。
import?static?java.lang.Integer.*;
public?class?StaticTest?{
????public?static?void?main(String[]?args)?{
????????System.out.println(MAX_VALUE);
????????System.out.println(toHexString(111));
????}
}
2
static 進(jìn)階知識(shí)
我們?cè)诹私饬?static 關(guān)鍵字的用法之后,來(lái)看一下 static 深入的用法,也就是由淺入深,慢慢來(lái),前戲要夠~????

關(guān)于 static 的所屬類
static 所修飾的屬性和方法都屬于類的,不會(huì)屬于任何對(duì)象;它們的調(diào)用方式都是 類名.屬性名/方法名,而實(shí)例變量和局部變量都是屬于具體的對(duì)象實(shí)例。
static 修飾變量的存儲(chǔ)位置
首先,先來(lái)認(rèn)識(shí)一下 JVM 的不同存儲(chǔ)區(qū)域。

虛擬機(jī)棧: Java 虛擬機(jī)棧是線程私有的數(shù)據(jù)區(qū),Java 虛擬機(jī)棧的生命周期與線程相同,虛擬機(jī)棧也是局部變量的存儲(chǔ)位置。方法在執(zhí)行過(guò)程中,會(huì)在虛擬機(jī)棧中創(chuàng)建一個(gè)棧幀(stack frame)。本地方法棧: 本地方法棧也是線程私有的數(shù)據(jù)區(qū),本地方法棧存儲(chǔ)的區(qū)域主要是 Java 中使用native關(guān)鍵字修飾的方法所存儲(chǔ)的區(qū)域程序計(jì)數(shù)器:程序計(jì)數(shù)器也是線程私有的數(shù)據(jù)區(qū),這部分區(qū)域用于存儲(chǔ)線程的指令地址,用于判斷線程的分支、循環(huán)、跳轉(zhuǎn)、異常、線程切換和恢復(fù)等功能,這些都通過(guò)程序計(jì)數(shù)器來(lái)完成。方法區(qū):方法區(qū)是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)虛擬機(jī)加載的 類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù),也就是說(shuō),static 修飾的變量存儲(chǔ)在方法區(qū)中堆:堆是線程共享的數(shù)據(jù)區(qū),堆是 JVM 中最大的一塊存儲(chǔ)區(qū)域,所有的對(duì)象實(shí)例,包括實(shí)例變量都在堆上進(jìn)行相應(yīng)的分配。
static 變量的生命周期
static 變量的生命周期與類的生命周期相同,隨類的加載而創(chuàng)建,隨類的銷毀而銷毀;普通成員變量和其所屬的生命周期相同。
static 序列化
我們知道,序列化的目的就是為了 把 Java 對(duì)象轉(zhuǎn)換為字節(jié)序列。對(duì)象轉(zhuǎn)換為有序字節(jié)流,以便其能夠在網(wǎng)絡(luò)上傳輸或者保存在本地文件中。
聲明為 static 和 transient 類型的變量不能被序列化,因?yàn)?static 修飾的變量保存在方法區(qū)中,只有堆內(nèi)存才會(huì)被序列化。而 transient 關(guān)鍵字的作用就是防止對(duì)象進(jìn)行序列化操作。
類加載順序
我們前面提到了類加載順序這么一個(gè)概念,static 修飾的變量和靜態(tài)代碼塊在使用前已經(jīng)被初始化好了,類的初始化順序依次是
加載父類的靜態(tài)字段 -> 父類的靜態(tài)代碼塊 -> 子類靜態(tài)字段 -> 子類靜態(tài)代碼塊 -> 父類成員變量(非靜態(tài)字段)
-> 父類非靜態(tài)代碼塊 -> 父類構(gòu)造器 -> 子類成員變量 ?-> 子類非靜態(tài)代碼塊 -> 子類構(gòu)造器
static 經(jīng)常用作日志打印
我們?cè)陂_(kāi)發(fā)過(guò)程中,經(jīng)常會(huì)使用 static 關(guān)鍵字作為日志打印,下面這行代碼你應(yīng)該經(jīng)常看到
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);
但是這種打印日志的方式存在問(wèn)題
對(duì)于每個(gè) StaticTest 的實(shí)例化對(duì)象都會(huì)擁有一個(gè) LOGGER,如果創(chuàng)建了1000個(gè) StaticTest 對(duì)象,則會(huì)多出1000個(gè)Logger 對(duì)象,造成資源的浪費(fèi),因此通常會(huì)將 Logger 對(duì)象聲明為 static 變量,這樣一來(lái),能夠減少對(duì)內(nèi)存資源的占用。
static 經(jīng)常用作單例模式
由于單例模式指的就是對(duì)于不同的類來(lái)說(shuō),它的副本只有一個(gè),因此 static 可以和單例模式完全匹配。
下面是一個(gè)經(jīng)典的雙重校驗(yàn)鎖實(shí)現(xiàn)單例模式的場(chǎng)景
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;
????}
}
來(lái)對(duì)上面代碼做一個(gè)簡(jiǎn)單的描述
使用 static 保證 singleton 變量是靜態(tài)的,使用 volatile 保證 singleton 變量的可見(jiàn)性,使用私有構(gòu)造器確保 Singleton 不能被 new 實(shí)例化。
使用 Singleton.getInstance() 獲取 singleton 對(duì)象,首先會(huì)進(jìn)行判斷,如果 singleton 為空,會(huì)鎖住 Singletion 類對(duì)象,這里有一些小伙伴們可能不知道為什么需要兩次判斷,這里來(lái)解釋下
如果線程 t1 執(zhí)行到 singleton == null 后,判斷對(duì)象為 null,此時(shí)線程把執(zhí)行權(quán)交給了 t2,t2 判斷對(duì)象為 null,鎖住 Singleton 類對(duì)象,進(jìn)行下面的判斷和實(shí)例化過(guò)程。如果不進(jìn)行第二次判斷的話,那么 t1 在進(jìn)行第一次判空后,也會(huì)進(jìn)行實(shí)例化過(guò)程,此時(shí)仍然會(huì)創(chuàng)建多個(gè)對(duì)象。

3
?類的構(gòu)造器是否是 static 的?
這個(gè)問(wèn)題我相信大部分小伙伴都沒(méi)有考慮過(guò),在 Java 編程思想中有這么一句話 類的構(gòu)造器雖然沒(méi)有用 static 修飾,但是實(shí)際上是 static 方法,但是并沒(méi)有給出實(shí)際的解釋,但是這個(gè)問(wèn)題可以從下面幾個(gè)方面來(lái)回答
static 最簡(jiǎn)單、最方便記憶的規(guī)則就是沒(méi)有 this 引用。而在類的構(gòu)造器中,是有隱含的 this 綁定的,因?yàn)闃?gòu)造方法是和類綁定的,從這個(gè)角度來(lái)看,構(gòu)造器不是靜態(tài)的。 從類的方法這個(gè)角度來(lái)看,因?yàn)? 類.方法名不需要新創(chuàng)建對(duì)象就能夠訪問(wèn),所以從這個(gè)角度來(lái)看,構(gòu)造器也不是靜態(tài)的從 JVM 指令角度去看,我們來(lái)看一個(gè)例子
public?class?StaticTest?{
????public?StaticTest(){}
????public?static?void?test(){
????}
????public?static?void?main(String[]?args)?{
????????StaticTest.test();
????????StaticTest?staticTest?=?new?StaticTest();
????}
}
我們使用 javap -c 生成 StaticTest 的字節(jié)碼看一下
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
}
我們發(fā)現(xiàn),在調(diào)用 static 方法時(shí)是使用的 invokestatic 指令,new 對(duì)象調(diào)用的是 invokespecial 指令,而且在 JVM 規(guī)范中 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokestatic 說(shuō)到


從這個(gè)角度來(lái)講,invokestatic 指令是專門用來(lái)執(zhí)行 static 方法的指令;invokespecial 是專門用來(lái)執(zhí)行實(shí)例方法的指令;從這個(gè)角度來(lái)講,構(gòu)造器也不是靜態(tài)的。
