摘要:關鍵字的字面意思是最終的不可修改的這似乎是一個看見名字就大概能知道怎么用的語法但你是否有深究過在各個場景中的具體使用方法注意事項以及背后涉及的設計思想呢一修飾變量基礎修飾基本數據類型變量和引用數據類型變量相信大家都具備基本的常識被修飾的變量
final關鍵字的字面意思是最終的, 不可修改的. 這似乎是一個看見名字就大概能知道怎么用的語法, 但你是否有深究過final在各個場景中的具體使用方法, 注意事項, 以及背后涉及的Java設計思想呢);
一. 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), 避免數字默認就存在常量池中.
上面的代碼運作過程是這樣的:
首先根據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修飾的常量會在編譯期進入常量池, 如果有涉及到該常量的操作, 很有可能在編譯期就已經完成.
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."":()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方法.
參考資料
www.cnblogs.com/ChenLLang/p…
www.cnblogs.com/xrq730/p/48…
gitbook.cn/books/5c6e1…
www.cnblogs.com/xrq730/p/48…
www.cnblogs.com/dolphin0520…
www.cnblogs.com/dolphin0520…
最后歡迎關注我的免費知識星球, 我會在星球中持續更新系統的Java后端面試題分析, 將會囊括Java基礎知識到主流框架原理. 還會分享關于編程的趣味漫畫.
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/7169.html
摘要:關鍵字的字面意思是最終的不可修改的這似乎是一個看見名字就大概能知道怎么用的語法但你是否有深究過在各個場景中的具體使用方法注意事項以及背后涉及的設計思想呢一修飾變量基礎修飾基本數據類型變量和引用數據類型變量相信大家都具備基本的常識被修飾的變量 final關鍵字的字面意思是最終的, 不可修改的. 這似乎是一個看見名字就大概能知道怎么用的語法, 但你是否有深究過final在各個場景中的具體使用方法...
摘要:關鍵字的字面意思是最終的不可修改的這似乎是一個看見名字就大概能知道怎么用的語法但你是否有深究過在各個場景中的具體使用方法注意事項以及背后涉及的設計思想呢一修飾變量基礎修飾基本數據類型變量和引用數據類型變量相信大家都具備基本的常識被修飾的變量 final關鍵字的字面意思是最終的, 不可修改的. 這似乎是一個看見名字就大概能知道怎么用的語法, 但你是否有深究過final在各個場景中的具體使用方法...
摘要:類型是位二進制標示,其中高位用來表示線程池狀態,后面位用來記錄線程池線程個數。創建一個最小線程個數為,最大為,阻塞隊列為的線程池。 一、 前言 線程池主要解決兩個問題:一方面當執行大量異步任務時候線程池能夠提供較好的性能,這是因為使用線程池可以使每個任務的調用開銷減少(因為線程池線程是可以復用的)。另一方面線程池提供了一種資源限制和管理的手段,比如當執行一系列任務時候對線程的管理,每個...
摘要:類型是位二進制標示,其中高位用來表示線程池狀態,后面位用來記錄線程池線程個數。創建一個最小線程個數為,最大為,阻塞隊列為的線程池。 一、 前言 線程池主要解決兩個問題:一方面當執行大量異步任務時候線程池能夠提供較好的性能,這是因為使用線程池可以使每個任務的調用開銷減少(因為線程池線程是可以復用的)。另一方面線程池提供了一種資源限制和管理的手段,比如當執行一系列任務時候對線程的管理,每個...
閱讀 713·2023-04-25 19:43
閱讀 3910·2021-11-30 14:52
閱讀 3784·2021-11-30 14:52
閱讀 3852·2021-11-29 11:00
閱讀 3783·2021-11-29 11:00
閱讀 3869·2021-11-29 11:00
閱讀 3558·2021-11-29 11:00
閱讀 6105·2021-11-29 11:00