1. 程式人生 > >深入理解static關鍵字

深入理解static關鍵字

在開始講static之前,我想讓各位看一段有意思的程式碼:

public class Test {
     
    static{
        System.out.println("test static 1");
    }
  
    static{
        System.out.println("test static 2");
    }
    
    public static void main(String[] args) {
         
    }
}

看完程式,小白童鞋發話了:啥玩意?main方法中啥都沒有,能執行啥?博主你個星星星...

執行結果:
test static 1
test static 2

小白童鞋:那啥...那啥...博主我說啥了,我啥都沒說...

其實,上面的程式碼懂的自然懂,不懂的自然就不懂了,因為上面的程式碼涉及到JVM的類載入了!當然不在本篇部落格文章的範疇內,如果有興趣理解上面的程式,這篇文章可能會對你有所幫助

別翻了,這篇文章絕對讓你深刻理解java類的載入以及ClassLoader原始碼分析【JVM篇二】

1、static存在的主要意義

static的主要意義是在於建立獨立於具體物件的域變數或者方法。以致於即使沒有建立物件,也能使用屬性和呼叫方法!

static關鍵字還有一個比較關鍵的作用就是 用來形成靜態程式碼塊以優化程式效能。static塊可以置於類中的任何地方,類中可以有多個static塊。在類初次被載入的時候,會按照static塊的順序來執行每個static塊,並且只會執行一次。

  為什麼說static塊可以用來優化程式效能,是因為它的特性:只會在類載入的時候執行一次。因此,很多時候會將一些只需要進行一次的初始化操作都放在static程式碼塊中進行。

2、static的獨特之處

1、被static修飾的變數或者方法是獨立於該類的任何物件,也就是說,這些變數和方法不屬於任何一個例項物件,而是被類的例項物件所共享。

怎麼理解 “被類的例項物件所共享” 這句話呢?就是說,一個類的靜態成員,它是屬於大夥的【大夥指的是這個類的多個物件例項,我們都知道一個類可以建立多個例項!】,所有的類物件共享的,不像成員變數是自個的【自個指的是這個類的單個例項物件】...我覺得我已經講的很通俗了,你明白了咩?

2、在該類被第一次載入的時候,就會去載入被static修飾的部分,而且只在類第一次使用時載入並進行初始化,注意這是第一次用就要初始化,後面根據需要是可以再次賦值的。

3、static變數值在類載入的時候分配空間,以後建立類物件的時候不會重新分配。賦值的話,是可以任意賦值的!

4、被static修飾的變數或者方法是優先於物件存在的,也就是說當一個類載入完畢之後,即便沒有建立物件,也可以去訪問。

3、static應用場景

因為static是被類的例項物件所共享,因此如果某個成員變數是被所有物件所共享的,那麼這個成員變數就應該定義為靜態變數。

因此比較常見的static應用場景有:

1、修飾成員變數
2、修飾成員方法
3、靜態程式碼塊
4、修飾類【只能修飾內部類也就是靜態內部類】
5、靜態導包

以上的應用場景將會在下文陸續講到...

4、靜態變數和例項變數的概念

靜態變數:
static修飾的成員變數叫做靜態變數【也叫做類變數】,靜態變數是屬於這個類,而不是屬於是物件。

例項變數:
沒有被static修飾的成員變數叫做例項變數,例項變數是屬於這個類的例項物件。

還有一點需要注意的是:static是不允許用來修飾區域性變數,不要問我問什麼,因為java規定的!

5、靜態變數和例項變數區別【重點常用】

靜態變數:
靜態變數由於不屬於任何例項物件,屬於類的,所以在記憶體中只會有一份,在類的載入過程中,JVM只為靜態變數分配一次記憶體空間。

例項變數:
每次建立物件,都會為每個物件分配成員變數記憶體空間,例項變數是屬於例項物件的,在記憶體中,建立幾次物件,就有幾份成員變數。

我相信各位智商都比宜春智商要高,應該都能理解上面的話。下面舉了例子完全出於娛樂,理解了大可不必看,下面的例子僅供參考,僅供娛樂一下下氣氛,趕時間的熊dei大可略過!

怎麼理解呢?打個比喻吧...就比方說程式設計師小王是一個比較溫柔陽光的男孩子,這1024的這一天,老闆閒的沒事,非要拉著程式設計師小王來玩耍,怎麼個玩法呢?老闆和小王一人拿著一把菜刀,規則很簡單,互相傷害,一人一刀,你一刀,我一刀....遊戲一開始,老闆二話不說,跳起來就是一刀,程式設計師小王二話也沒說反手就是一菜刀回去,這個時候老闆發飆了,雙眼瞪得忒大,跳起來又是一刀,這個時候程式設計師小王不敢還手了,就沒動手。沒想到老闆越來越生猛,左一刀右一刀全程下來差不多砍個半個時....程式設計師小王一直沒有還過手,因為小王知道他是老闆...

這個程式設計師小王只會在老闆第一次揮刀的時候,回老闆一刀,之後就不還手了,這個時候我們把程式設計師小王看做是靜態變數,把老闆第一次向小王揮刀看做是類載入,把小王回老闆一刀看出是分配記憶體空間,而一人一刀這個回合過程看成是類載入的過程,之後老闆的每一刀都看成是建立一次物件。

連貫起來就是static變數值在類第一次載入的時候分配空間,以後建立類物件的時候不會重新分配

之後這個老闆捱了一刀之後躺醫院了一年,一出院回到公司第一件事就是拉程式設計師宜春出來玩耍,老闆殊不知其然,這個博主程式設計師宜春性格異常暴躁,老闆遞給程式設計師宜春一把菜刀,博主宜春一接過菜刀,猝不及防的被老闆跳起來就是一刀,程式設計師宜春痛的嗷了一聲,暴躁的程式設計師宜春還沒嗷完,在嗷的同時跳起來就是給老闆一刀,接著老闆跳起來又是一刀,程式設計師宜春嗷的一聲又是回一刀,老闆跳起來又一刀,程式設計師宜春嗷的一聲又是回一刀,只要老闆沒停程式設計師宜春就沒停,因為程式設計師宜春知道,就自己這曝脾氣,暴躁起來si都敢摸,肯定有幾個老鐵知道....

程式設計師宜春就類似例項變數,每次建立物件,都會為每個物件分配成員變數記憶體空間,就像老闆來一刀,程式設計師宜春都會回一刀這樣子的...

6、訪問靜態變數和例項變數的兩種方式

我們都知道靜態變數是屬於這個類,而不是屬於是物件,static獨立於物件。

但是各位有木有想過:靜態成員變數雖然獨立於物件,但是不代表不可以通過物件去訪問,所有的靜態方法和靜態變數都可以通過物件訪問【只要訪問許可權足夠允許就行】,不理解沒關係,來個程式碼就理解了

public class StaticDemo {

        static int value = 666;

        public static void main(String[] args) throws Exception{
            new StaticDemo().method();
        }

        private void method(){
            int value = 123;
            System.out.println(this.value);
        }

}

猜想一下結果,我猜你的結果是123,哈哈是咩?其實

執行結果: 666

回過頭再去品味一下上面的那段話,你就能非常客觀明瞭了,這個思想概念要有隻是這種用法不推薦!

因此小結一下訪問靜態變數和例項變數的兩種方法:

靜態變數:

類名.靜態變數

物件.靜態變數(不推薦)

靜態方法:

類名.靜態方法

物件.靜態方法(不推薦)

7、static靜態方法

static修飾的方法也叫做靜態方法,不知道各位發現咩有,其實我們最熟悉的static靜態方法就是main方法了~小白童鞋:喔好像真的是哦~。由於對於靜態方法來說是不屬於任何例項物件的,this指的是當前物件,因為static靜態方法不屬於任何物件,所以就談不上this了。

還有一點就是:構造方法不是靜態方法!

8、static靜態程式碼塊

先看個程式吧,看看自個是否掌握了static程式碼塊,下面程式程式碼繼承關係為 BaseThree——> BaseTwo——> BaseOne

BaseOne類

package com.gx.initializationblock;

public class BaseOne {

    public BaseOne() {
        System.out.println("BaseOne構造器");
    }

    {
        System.out.println("BaseOne初始化塊");
        System.out.println();
    }

    static {
        System.out.println("BaseOne靜態初始化塊");

    }

}

BaseTwo類

package com.gx.initializationblock;

public class BaseTwo extends BaseOne {
    public BaseTwo() {
        System.out.println("BaseTwo構造器");
    }

    {
        System.out.println("BaseTwo初始化塊");
    }

    static {
        System.out.println("BaseTwo靜態初始化塊");
    }
}

BaseThree 類

package com.gx.initializationblock;

public class BaseThree extends BaseTwo {
    public BaseThree() {
        System.out.println("BaseThree構造器");
    }

    {
        System.out.println("BaseThree初始化塊");
    }

    static {
        System.out.println("BaseThree靜態初始化塊");
    }
}

測試demo2類

package com.gx.initializationblock;

/*
     注:這裡的ABC對應BaseOne、BaseTwo、BaseThree 
 * 多個類的繼承中初始化塊、靜態初始化塊、構造器的執行順序
     在繼承中,先後執行父類A的靜態塊,父類B的靜態塊,最後子類的靜態塊,
     然後再執行父類A的非靜態塊和構造器,然後是B類的非靜態塊和構造器,最後執行子類的非靜態塊和構造器
 */
public class Demo2 {
    public static void main(String[] args) {
        BaseThree baseThree = new BaseThree();
        System.out.println("-----");
        BaseThree baseThree2 = new BaseThree();

    }
}

執行結果

BaseOne靜態初始化塊
BaseTwo靜態初始化塊
BaseThree靜態初始化塊
BaseOne初始化塊

BaseOne構造器
BaseTwo初始化塊
BaseTwo構造器
BaseThree初始化塊
BaseThree構造器
-----
BaseOne初始化塊

BaseOne構造器
BaseTwo初始化塊
BaseTwo構造器
BaseThree初始化塊
BaseThree構造器

至於static程式碼塊執行結果不是很清晰的童鞋,詳細講解請看這篇Static靜態程式碼塊以及各程式碼塊之間的執行順序

以上僅僅是讓各位明確程式碼塊之間的執行順序,顯然還是不夠的,靜態程式碼塊通常用來對靜態變數進行一些初始化操作,比如定義列舉類,程式碼如下:

public enum WeekDayEnum {
    MONDAY(1,"週一"),
    TUESDAY(2, "週二"),
    WEDNESDAY(3, "週三"),
    THURSDAY(4, "週四"),
    FRIDAY(5, "週五"),
    SATURDAY(6, "週六"),
    SUNDAY(7, "週日");
 
    private int code;
    private String desc;
 
    WeekDayEnum(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }
 
    private static final Map<Integer, WeekDayEnum> WEEK_ENUM_MAP = new HashMap<Integer, WeekDayEnum>();
 
    // 對map進行初始化
    static {
        for (WeekDayEnum weekDay : WeekDayEnum.values()) {
            WEEK_ENUM_MAP.put(weekDay.getCode(), weekDay);
        }
    }
 
    public static WeekDayEnum findByCode(int code) {
        return WEEK_ENUM_MAP.get(code);
    }
 
    public int getCode() {
        return code;
    }
 
    public void setCode(int code) {
        this.code = code;
    }
 
    public String getDesc() {
        return desc;
    }
 
    public void setDesc(String desc) {
        this.desc = desc;
    }
} 

當然不僅僅是列舉這一方面,還有我們熟悉的單例模式同樣也用到了靜態程式碼塊,如下:

public class Singleton {
    private static Singleton instance;
 
    static {
        instance = new Singleton();
    }
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        return instance;
    }
}

9、static變數與普通變數區別

static變數也稱作靜態變數,靜態變數和非靜態變數的區別是:靜態變數被所有的物件所共享,在記憶體中只有一個副本,它當且僅當在類初次載入時會被初始化。而非靜態變數是物件所擁有的,在建立物件的時候被初始化,存在多個副本,各個物件擁有的副本互不影響。

還有一點就是static成員變數的初始化順序按照定義的順序進行初始化。

10、靜態內部類

靜態內部類與非靜態內部類之間存在一個最大的區別,我們知道非靜態內部類在編譯完成之後會隱含地儲存著一個引用,該引用是指向建立它的外圍內,但是靜態內部類卻沒有。沒有這個引用就意味著:

1、它的建立是不需要依賴外圍類的建立。
2、它不能使用任何外圍類的非static成員變數和方法。

程式碼舉例(靜態內部類實現單例模式)

public class Singleton {
    
   // 宣告為 private 避免呼叫預設構造方法建立物件
    private Singleton() {
    }
    
   // 宣告為 private 表明靜態內部該類只能在該 Singleton 類中被訪問
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getUniqueInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Singleton 類載入時,靜態內部類 SingletonHolder 沒有被載入進記憶體。只有當呼叫 getUniqueInstance()方法從而觸發 SingletonHolder.INSTANCESingletonHolder 才會被載入,此時初始化 INSTANCE 例項,並且 JVM 能確保 INSTANCE 只被例項化一次。

這種方式不僅具有延遲初始化的好處,而且由 JVM 提供了對執行緒安全的支援。

11、靜態導包

靜態導包格式:import static

這兩個關鍵字連用可以指定匯入某個類中的指定靜態資源,並且不需要使用類名呼叫類中靜態成員,可以直接使用類中靜態成員變數和成員方法

//  Math. --- 將Math中的所有靜態資源匯入,這時候可以直接使用裡面的靜態方法,而不用通過類名進行呼叫
//  如果只想匯入單一某個靜態方法,只需要將換成對應的方法名即可
 
import static java.lang.Math.;
//  換成import static java.lang.Math.max;具有一樣的效果
 
public class Demo {
    public static void main(String[] args) {
 
        int max = max(1,2);
        System.out.println(max);
    }
}

靜態導包在書寫程式碼的時候確實能省一點程式碼,可以直接呼叫裡面的靜態成員,但是會影響程式碼可讀性,所以開發中一般情況下不建議這麼使用。

12、static注意事項

1、靜態只能訪問靜態。
2、非靜態既可以訪問非靜態的,也可以訪問靜態的。

13、final與static的藕斷絲連

到這裡文章本該結束了的,但是static的使用始終離不開final字眼,二者可謂藕斷絲連,常常繁見,我覺得還是很有必要講講,那麼一起來看看下面這個程式吧。

package Demo;

class FinalDemo {
    public final double i = Math.random();
    public static double t = Math.random();
}

public class DemoDemo {
    public static void main(String[] args) {

        FinalDemo demo1 = new FinalDemo();
        FinalDemo demo2 = new FinalDemo();
        System.out.println("final修飾的  i=" + demo1.i);
        System.out.println("static修飾的 t=" + demo1.t);
        System.out.println("final修飾的  i=" + demo2.i);
        System.out.println("static修飾的 t=" + demo2.t);

        System.out.println("t+1= "+ ++demo2.t );
//      System.out.println( ++demo2.i );//編譯失敗
      }
}
執行結果:
    final修飾的  i=0.7282093281367935
    static修飾的 t=0.30720545678577604
    final修飾的  i=0.8106990945706758
    static修飾的 t=0.30720545678577604
    t+1= 1.307205456785776

static修飾的變數沒有發生變化是因為static作用於成員變數只是用來表示儲存一份副本,其不會發生變化。怎麼理解這個副本呢?其實static修飾的在類載入的時候就載入完成了(初始化),而且只會載入一次也就是說初始化一次,所以不會發生變化!

至於final修飾的反而發生變化了?是不是巔覆你對final的看法?關於final詳細講解博主也準備好了一篇文章程式設計師你真的理解final關鍵字嗎?

ok,文章就先到這裡了,希望這篇文章能夠幫助到你對static的認識,若有不足或者不正之處,希望諒解並歡迎批評指正!

如果本文章對你有幫助,哪怕是一點點,那就請點一個讚唄,謝謝~

參考:
《java程式設計思想》
http://baijiahao.baidu.com/s?id=1601254463089390982&wfr=spider&for=pc
https://blog.csdn.net/qq_34337272/article/details/82766943
https://www.cnblogs.com/dolphin0520/p/3799052.html

如果本文對你有一點點幫助,那麼請點個讚唄,謝謝~

最後,若有不足或者不正之處,歡迎指正批評,感激不盡!如果有疑問歡迎留言,絕對第一時間回覆!

歡迎各位關注我的公眾號,一起探討技術,嚮往技術,追求技術,說好了來了就是盆友喔...