Java關鍵字final&static

Java關鍵字final

在設計程序時,出于效率或者設計的原因,有時候希望某些數據是不可改變的。這時候可以使用final關鍵字,修飾這部分是無法修改的,達到了終態。final可以修飾非抽象類,非抽象類成員變量和方法。

final常量

在Java中,利用關鍵字final指示常量。而常量有兩種:

  • final修飾實例域,final+類型
  • final修飾的類常量,static final+類型

類型可以是基本數據類型,也可以是引用數據類型。

如果根據初始化時機分:

  • 編譯期常量,static final修飾的基本數據類型或者String類型。需要注意的是這種情況必須是在聲明時就顯示的賦值(基本類型賦予直接量,String類型直接用字符串字面量聲明)。因為它們在類加載的加載階段被放入方法區中的運行時常量池中,能夠存放的只有聲明為final的常量值,字符串字面量。一旦類加載完畢,就不能在更改。編譯期可以將它代入到任何用到它的計算式中,也就是說可以在編譯期執行計算式。
  • 運行期常量,final修飾的基本數據類型或者引用數據類型。它們是在實例化過程中依據不同對象的要求進行不同的初始化。同時由于final的特性一旦被初始化就不會改變。這是由于final空白特性。在聲明final常量時,可以不賦初值,但是編譯器必須確保使用該空白final常量時,已經被賦值(初始化)。所以必須在執行完構造函數之后必須已經被初始化。
  • 介于編譯期和運行期,static final修飾的其他引用數據類型(包括不使用字符串字面量聲明的String對象)或者在聲明中未賦值的基本類型,必須在類加載的初始化階段被初始化(在static代碼塊中)。

注意一旦給final變量初值后,值就不能再改變了。但是有一個誤區,當修飾引用數據類型時,而且類型是可變類,那么不可變的是引用地址,而對象的內容是可變的。

import java.util.*;
class BaseLoader {
    static final int i = new Random(47).nextInt(20);
    static {
        System.out.println("Inititalization!");
        System.out.println("i is " + i);
    }
}

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

執行Test.java后,Console輸出:Inititalization! i is 18 18。說明只有在聲明時賦值的static final修飾的常量才屬于編譯期常量。而static final int i = new Random(47).nextInt(20)是在類加載的初始化階段初始化的。

final方法

如果一個方法被final修飾。那么其子類不能覆寫該方法。這樣做的原因出于兩個方面的考慮:

  1. 把方法鎖定,防止子類修改它的意義和實現
  2. 高效。編譯器在遇到調用final方法時候會轉入內嵌機制,大大提高執行效率。

在java的早期實現中,如果將一個方法指明為final,就是同意編譯器將針對該方法的所有調用都轉為內嵌調用。當編譯器發現一個final方法調用命令時,它會根據自己的謹慎判斷,跳過插入程序代碼這種正常的調用方式而執行方法調用機制(將參數壓入棧,跳至方法代碼處執行,然后跳回并清理棧中的參數,處理返回值),并且以方法體中的實際代碼的副本來代替方法調用。這將消除方法調用的開銷。當然,如果一個方法很大,你的程序代碼會膨脹,因而可能看不到內嵌所帶來的性能上的提高,因為所帶來的性能會花費于方法內的時間量而被縮減(不是很理解)。

final類

在設計類的時候,出于某些因素的考慮,這個類的實現細節不允許隨意修改,而且不需要子類,確定它不會要被擴展。那么設計時使用final修飾。final類是不允許被繼承的,表明該類事最終類。由于final類是無法繼承的,所以類方法會默認加上final修飾。而它的成員變量并沒有強制規定被final修飾。

final參數

final可以修飾方法參數列表中的參數,一旦調用方法傳遞參數后,方法內不可以修改參數(基本數據類型不能修改值,引用類型的可變類不能修改地址,不可變類完全不可變)。最常見的就是方法中將參數傳遞給匿名內部類使用,此時該參數必須為final。

那么為什么匿名內部類在使用方法中的局部變量或者方法的參數時,需要使用final修飾?首先來了解一個基本概念:

內部類被編譯時,字節碼會單獨放在一個.class文件中,與外部類的字節碼文件分開。

匿名內部類使用方法局部變量

public class OuterClass{

    public void test() {
        final int a = 10;
        new Thread() {
            public void run() {
                System.out.println(a);
            }
        }.start();
    }
}

如果執行test()完成后,那么在站內存中的變量a就會被回收,而此時如果匿名內部類(Thread)生命周期沒有結束,那么在run()方法中訪問變量a就無法實現。所以Java通過復制的手段來避免這個問題。

這個過程是在編譯期間由編譯器默認進行,如果這個變量的值在編譯期間可以確定,則編譯器默認會在匿名內部類(局部內部類)的常量池中添加一個內容相等的字面量或直接將相應的字節碼嵌入到執行字節碼中。這樣一來,匿名內部類使用的變量是另一個局部變量,只不過值和方法中局部變量的值相等,因此和方法中的局部變量完全獨立開。

匿名內部類使用方法的參數

public class Outer{

    public void test(final int a) {
        new Inner() {
            public void innerMethod() {
                System.out.println(a);
        }
    }
    
    interface Inner{
        void innerMethod();
    }
}

從上代碼比較直觀的翻譯是:

public void test(final int a) {
    class Inner {
        public void innerMethod() {
            System.out.println(a);
        }
    }
    Inner inner = new Inner();
    inner.innerMethod();
}

從上面代碼可以認為內部類直接調用了參數a。其實Java編譯后內部類單獨放在自己的字節碼文件中,可以直觀的翻譯為:

public class Outer$Inner {
    public Outer$Inner(final int a) {
        this.Inner$a = a;
    }
    
    public void innerMethod() {
        System.out.println(this.Inner$a);
    }
}

從上面內部類的構造函數中可以看到,這里是將變量test方法中的形參a以參數的形式傳進來對匿名內部類中的拷貝(變量a的拷貝)進行賦值初始化。內部的方法調用的實際是自己的屬性而不是外部類方法的參數。這么做的好處解決了上一節所說的生命周期的問題。

總結

也就說如果局部變量的值在編譯期間就可以確定,則直接在匿名內部里面創建一個拷貝。如果局部變量的值無法在編譯期間確定,則通過構造器傳參的方式來對拷貝進行初始化賦值。

方法參數或者局部變量和匿名內部類使用的變量看似是同一個,其實在匿名內部類中實行了拷貝操作,兩個并不是同一個變量。如果在內部類中修改了這個變量,方法的參數或者局部變量并不會受到影響,這樣就失去了一致性,這是程序猿不愿意看到的。所以使用final來修飾,保證它的不可變,達到變量的一致性。

簡單理解就是,拷貝引用,為了避免引用值發生改變,例如被外部類的方法修改等,而導致內部類得到的值不一致,于是用final來讓該引用不可改變。

參考

java提高篇(十四)-----關鍵字final

Java內部類的使用小結

Java內部類詳解

Java關鍵字static

Java中沒有全局變量的概念,但是可以通過static來實現“全局”的概念。static關鍵字可以用來修飾成員變量,方法以及代碼塊。static關鍵字表示“全局”或者“靜態”的意思。

固定內存分配

靜態變量

Java類加載過程中有兩個階段對類變量初始化。一個是在連接階段的準備部分中對類變量分配內存并設置JVM默認值;另一個是類加載的最后階段,初始化,根據類變量的聲明進行賦值初始化或者在靜態代碼塊中執行相應的賦值語句。

那么分配在哪塊內存中呢?在運行時數據區的方法區內。

方法區主要存儲已被虛擬機加載的類信息、常量、靜態變量、即使編譯器編譯后的代碼等數據。

靜態方法

方法區會存儲即使編譯器編譯后的代碼。

即使編譯器可以監控經常執行哪些方法代碼優化這些代碼以提高速度。更為復雜的優化是消除函數調用(即“內聯”)。即使編譯器知道哪些類已經加載。給予當前加載的類集,如果特定的函數不會被覆蓋,就可以使用內聯。

摘抄自java核心技術,不是很理解。

由于靜態方法不能覆寫,所以它門也被分配在方法區中(final修飾的方法也不可覆寫,也分配在方法區?)。

總結

一旦類加載執行完,JVM就可以方便地在方法區中就找到它們(類變量,靜態方法,靜態代碼塊)。所以static修飾的對象,可以在類實例化之前調用,無需持有相應對象的引用。

特點

被static修飾的成員變量和成員方法是獨立于該類的,它不依賴于某個特定的實例變量,也就是說它被該類的所有實例共享。即便創建無數個對象,也不會有靜態變量的副本。同時靜態方法無法被覆寫。

static變量

static變量,一般稱之為靜態變量,也可以稱為類變量。與之相對應的是實例變量。它們兩者的區別在于:

對于靜態變量在內存中只有一個拷貝(節省內存),JVM只為靜態分配一次內存,在加載類的過程中完成靜態變量的內存分配,可用類名直接訪問(方便),當然也可以通過對象來訪問(不應該這么做,概念混淆)。

對于實例變量,每創建一個實例,就會為實例變量分配一次內存。實例變量可以在內存中有多個拷貝,互不影響(靈活)。

static方法

靜態方法,可以通過類名直接調用,任何實例來調用。所以靜態方法中不能使用this和super關鍵字。

靜態方法不能直接訪問實例變量,調用實例方法。可以通過創建對象后調用實例方法,實例變量(例如主方法中)。

由于靜態方法不依賴任何實例,所以靜態方法必須實現,而不能是抽象的。

靜態代碼塊

靜態代碼塊會在類加載最后階段初始化中執行,利用靜態代碼塊可以做一些初始化,例如類變量的賦值...

靜態方法的局限

  1. 它只能直接訪問靜態變量
  2. 它只能直接調用其他靜態方法
  3. 不能以任何形式引用this或者super
  4. 不能被覆寫

上述1,2兩點針對的是本類中的其他靜態方法和靜態變量。

public class Base {
    public static void method(int i) {
        System.out.println(i);
    }
}

public class Son extends Base {
    @Override
    public static void method(int i) {
        i += 1;
        System.out.println(i);
    }
}

編譯Son.java后,Console輸出:

靜態方法不能被覆寫.png

說明靜態方法不能被覆寫。

public class A {
    public static void method() {
        System.out.println("This method action in father");
    }
}

public class B extends A{
    public static void method() {
        System.out.println("This method action by son");
    }
}

public class Test {
    public static void main(String[] args) {
        //Son.method(20);
        A a = new B();
        a.method();
        B.method();
    }
}

但是這樣的代碼可以編譯通過,執行測試類后,Console輸出:This method action in father This method action by son

分析:覆寫指的是根據運行時對象來決定調用哪個方法,而不是根據編譯時的類型。

聲明為A類型的變量名存儲在棧中,而指向堆內存的卻是B的實例。如果調用變量a的非靜態方法,解釋器會從堆內存中找到指向的B類型實例,然后調用它的方法。而靜態方法屬于類方法,在編譯階段就已經確定了它屬于A類的靜態方法,所以執行的是A類的方法。所以達不到覆寫的效果。

總結,靜態方法的覆寫只是形式上的,實際上達不到覆寫的效果(也就是多態),只能隱藏(也就是通過子類類名調用靜態方法,執行的是子類實現的方法)。而編譯器沒有報錯,是因為編譯器認為這是子類實現的新方法,如果加上注解@Override會去檢查父類是否有相同方法名的方法,由于靜態方法覆寫無效果,無法覆寫,那么就無法編譯通過。

參考子類為什么不能重寫父類的靜態方法
可以重寫靜態方法嗎?

一個實例對象有兩個類型:表明類型(Apparent Type)和實際類型(Actual Type)。表面類型是聲明時的類型,實際類型是對象創建時的類型。語句A a = new B();變量a表面類型是A,實際類型是B。非靜態方法根據實際類型來執行,而對于靜態方法,通過對象來調用,JVM會通過表面類型查找到靜態方法入口來執行。

參考建議33: 不要覆寫靜態方法

參考

java提高篇(七)-----關鍵字static

Java關鍵字final、static使用總結

Static 關鍵字

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容