探究final在java中的做用

final關鍵字的字面意思是最終的, 不可修改的. 這彷佛是一個看見名字就大概能知道怎麼用的語法, 但你是否有深究過final在各個場景中的具體使用方法, 注意事項, 以及背後涉及的Java設計思想呢?html

 

一. final修飾變量

1. 基礎: final修飾基本數據類型變量和引用數據類型變量.

  • 相信你們都具有基本的常識: 被final修飾的變量是不可以被改變的. 可是這裏的"不可以被改變"對於不一樣的數據類型是有不一樣的含義的.
  • 當final修飾的是一個基本數據類型數據時, 這個數據的值在初始化後將不能被改變; 當final修飾的是一個引用類型數據時, 也就是修飾一個對象時, 引用在初始化後將永遠指向一個內存地址, 不可修改. 可是該內存地址中保存的對象信息, 是能夠進行修改的.
  • 上一段話可能比較抽象, 但願下面的圖能有助於你理解, 你會發現雖然說有不一樣的含義, 但本質仍是同樣的.
  • 首先是final修飾基本數據類型時的內存示意圖

在這裏插入圖片描述

  • 如上圖, 變量a在初始化後將永遠指向003這塊內存, 而這塊內存在初始化後將永遠保存數值100.
  • 下面是final修飾引用數據類型的示意圖

在這裏插入圖片描述

  • 在上圖中, 變量p指向了0003這塊內存, 0003內存中保存的是對象p的句柄(存放對象p數據的內存地址), 這個句柄值是不能被修改的, 也就是變量p永遠指向p對象. 可是p對象的數據是能夠修改的.
// 代碼示例
public static void main(String[] args) {
    final Person p = new Person(20, "炭燒生蠔");
    p.setAge(18);   //能夠修改p對象的數據
    System.out.println(p.getAge()); //輸出18

    Person pp = new Person(30, "蠔生燒炭");
    p = pp; //這行代碼會報錯, 不能經過編譯, 由於p經final修飾永遠指向上面定義的p對象, 不能指向pp對象. 
}
複製代碼
  • 不難看出final修飾變量的本質: final修飾的變量會指向一塊固定的內存, 這塊內存中的值不能改變.
  • 引用類型變量所指向的對象之因此能夠修改, 是由於引用變量不是直接指向對象的數據, 而是指向對象的引用的. 因此被final修飾的引用類型變量將永遠指向一個固定的對象, 不能被修改; 對象的數據值能夠被修改.

 

2. 進階: 被final修飾的常量在編譯階段會被放入常量池中

  • final是用於定義常量的, 定義常量的好處是: 不須要重複地建立相同的變量. 而常量池是Java的一項重要技術, 由final修飾的變量會在編譯階段放入到調用類的常量池中.
  • 請看下面這段演示代碼. 這個示例是專門爲了演示而設計的, 但願能方便你們理解這個知識點.
public static void main(String[] args) {
    int n1 = 2019;          //普通變量
    final int n2 = 2019;    //final修飾的變量

    String s = "20190522";  
    String s1 = n1 + "0522";	//拼接字符串"20190512"
    String s2 = n2 + "0522";	

    System.out.println(s == s1);	//false
    System.out.println(s == s2);	//true
}
複製代碼

首先要介紹一點: 整數-127-128是默認加載到常量池裏的, 也就是說若是涉及到-127-128的整數操做, 默認在編譯期就能肯定整數的值. 因此這裏我故意選用數字2019(大於128), 避免數字默認就存在常量池中.java

  • 上面的代碼運做過程是這樣的:
  • 首先根據final修飾的常量會在編譯期放到常量池的原則, n2會在編譯期間放到常量池中.
  • 而後s變量所對應的"20190522"字符串會放入到字符串常量池中, 並對外提供一個引用返回給s變量.
  • 這時候拼接字符串s1, 因爲n1對應的數據沒有放入常量池中, 因此s1暫時沒法拼接, 須要等程序加載運行時才能肯定s1對應的值.
  • 但在拼接s2的時候, 因爲n2已經存在於常量池, 因此能夠直接與"0522"拼接, 拼接出的結果是"20190522". 這時系統會查看字符串常量池, 發現已經存在字符串20190522, 因此直接返回20190522的引用. 因此s2和s指向的是同一個引用, 這個引用指向的是字符串常量池中的20190522.

 

  • 當程序執行時, n1變量纔有具體的指向.
  • 當拼接s1的時候, 會建立一個新的String類型對象, 也就是說字符串常量池中的20190522會對外提供一個新的引用.
  • 因此當s1與s用"=="判斷時, 因爲對應的引用不一樣, 會返回false. 而s2和s指向同一個引用, 返回true.

總結: 這個例子想說明的是: 因爲被final修飾的常量會在編譯期進入常量池, 若是有涉及到該常量的操做, 頗有可能在編譯期就已經完成.git

 

3. 探索: 爲何局部/匿名內部類在使用外部局部變量時, 只能使用被final修飾的變量?

提示: 在JDK1.8之後, 經過內部類訪問外部局部變量時, 無需顯式把外部局部變量聲明爲final. 不是說不須要聲明爲final了, 而是這件事情在編譯期間系統幫咱們作了. 可是咱們仍是有必要了解爲何要用final修飾外部局部變量.編程

public class Outter {
    public static void main(String[] args) {
        final int a = 10;
        new Thread(){
            @Override
            public void run() {
                System.out.println(a);
            }
        }.start();
    }
}
複製代碼
  • 在上面這段代碼, 若是沒有給外部局部變量a加上final關鍵字, 是沒法經過編譯的. 能夠試着想一想: 當main方法已經執行完後, main方法的棧幀將會彈出, 若是此時Thread對象的生命週期尚未結束, 尚未執行打印語句的話, 將沒法訪問到外部的a變量.
  • 那麼爲何加上final關鍵字就能正常編譯呢? 咱們經過查看反編譯代碼看看內部類是怎樣調用外部成員變量的.
  • 咱們能夠先經過javac編譯獲得.class文件(用IDE編譯也能夠), 而後在命令行輸入javap -c .class文件的絕對路徑, 就能查看.class文件的反編譯代碼. 以上的Outter類通過編譯產生兩個.class文件, 分別是Outter.class和Outter$1.class, 也就是說內部類會單獨編譯成一個.class文件. 下面給出Outter$1.class的反編譯代碼.
Compiled from "Outter.java"
final class forTest.Outter$1 extends java.lang.Thread {
  forTest.Outter$1();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Thread."<init>":()V
       4: return

  public void run();
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: bipush        10
       5: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
       8: return
}
複製代碼
  • 定位到run()方法反編譯代碼中的第3行:
  • 3: bipush 10
  • 咱們看到a的值在內部類的run()方法執行過程當中是以壓棧的形式存儲到本地變量表中的, 也就是說在內部類打印變量a的值時, 這個變量a不是外部的局部變量a, 由於若是是外部局部變量的話, 應該會使用load指令加載變量的值. 也就是說系統以拷貝的形式把外部局部變量a複製了一個副本到內部類中, 內部類有一個變量指向外部變量a所指向的值.

 

  • 但研究到這裏好像和final的關係還不是很大, 不加final彷佛也能夠拷貝一份變量副本, 只不過不能在編譯期知道變量的值罷了. 這時該思考一個新問題了: 如今咱們知道內部類的變量a和外部局部變量a是兩個徹底不一樣的變量, 那麼若是在執行run()方法的過程當中, 內部類中修改了a變量所指向的值, 就會產生數據不一致問題.
  • 正由於咱們的原意是內部類和外部類訪問的是同一個a變量, 因此當在內部類中使用外部局部變量的時候應該用final修飾局部變量, 這樣局部變量a的值就永遠不會改變, 也避免了數據不一致問題的發生.

 

二. final修飾方法

  • 使用final修飾方法有兩個做用, 首要做用是鎖定方法, 不讓任何繼承類對其進行修改.
  • 另一個做用是在編譯器對方法進行內聯, 提高效率. 可是如今已經不多這麼使用了, 近代的Java版本已經把這部分的優化處理得很好了. 可是爲了知足求知慾仍是瞭解一下什麼是方法內斂.
  • 方法內斂: 當調用一個方法時, 系統須要進行保存現場信息, 創建棧幀, 恢復線程等操做, 這些操做都是相對比較耗時的. 若是使用final修飾一個了一個方法a, 在其餘調用方法a的類進行編譯時, 方法a的代碼會直接嵌入到調用a的代碼塊中.
//原代碼
public static void test(){
    String s1 = "包夾方法a";
    a();
    String s2 = "包夾方法a";
}

public static final void a(){
    System.out.println("我是方法a中的代碼");
    System.out.println("我是方法a中的代碼");
}

//通過編譯後
public static void test(){
    String s1 = "包夾方法a";
    System.out.println("我是方法a中的代碼");
    System.out.println("我是方法a中的代碼");
    String s2 = "包夾方法a";
}
複製代碼
  • 在方法很是龐大的時候, 這樣的內嵌手段是幾乎看不到任何性能上的提高的, 在最近的Java版本中,不須要使用final方法進行這些優化了. --《Java編程思想》

 

三. final修飾類

  • 使用final修飾類的目的簡單明確: 代表這個類不能被繼承.
  • 當程序中有永遠不會被繼承的類時, 可使用final關鍵字修飾
  • 被final修飾的類全部成員方法都將被隱式修飾爲final方法.

   

相關文章
相關標籤/搜索