Java關鍵字final
在設計程序時肮韧,出于效率或者設計的原因嘉蕾,有時候希望某些數(shù)據是不可改變的挤茄。這時候可以使用final關鍵字曙咽,修飾這部分是無法修改的,達到了終態(tài)。final可以修飾非抽象類,非抽象類成員變量和方法。
final常量
在Java中败晴,利用關鍵字final指示常量。而常量有兩種:
- final修飾實例域栽渴,final+類型
- final修飾的類常量尖坤,static final+類型
類型可以是基本數(shù)據類型,也可以是引用數(shù)據類型闲擦。
如果根據初始化時機分:
- 編譯期常量慢味,static final修飾的基本數(shù)據類型或者String類型。需要注意的是這種情況必須是在聲明時就顯示的賦值(基本類型賦予直接量墅冷,String類型直接用字符串字面量聲明)纯路。因為它們在類加載的加載階段被放入方法區(qū)中的運行時常量池中,能夠存放的只有聲明為final的常量值寞忿,字符串字面量驰唬。一旦類加載完畢,就不能在更改。編譯期可以將它代入到任何用到它的計算式中叫编,也就是說可以在編譯期執(zhí)行計算式辖佣。
- 運行期常量,final修飾的基本數(shù)據類型或者引用數(shù)據類型搓逾。它們是在實例化過程中依據不同對象的要求進行不同的初始化卷谈。同時由于final的特性一旦被初始化就不會改變。這是由于final空白特性恃逻。在聲明final常量時,可以不賦初值藕施,但是編譯器必須確保使用該空白final常量時寇损,已經被賦值(初始化)。所以必須在執(zhí)行完構造函數(shù)之后必須已經被初始化裳食。
- 介于編譯期和運行期矛市,static final修飾的其他引用數(shù)據類型(包括不使用字符串字面量聲明的String對象)或者在聲明中未賦值的基本類型,必須在類加載的初始化階段被初始化(在static代碼塊中)诲祸。
注意一旦給final變量初值后浊吏,值就不能再改變了。但是有一個誤區(qū)救氯,當修飾引用數(shù)據類型時找田,而且類型是可變類,那么不可變的是引用地址着憨,而對象的內容是可變的墩衙。
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);
}
}
執(zhí)行Test.java后,Console輸出:Inititalization! i is 18 18
甲抖。說明只有在聲明時賦值的static final修飾的常量才屬于編譯期常量漆改。而static final int i = new Random(47).nextInt(20)
是在類加載的初始化階段初始化的。
final方法
如果一個方法被final修飾准谚。那么其子類不能覆寫該方法挫剑。這樣做的原因出于兩個方面的考慮:
- 把方法鎖定,防止子類修改它的意義和實現(xiàn)
- 高效柱衔。編譯器在遇到調用final方法時候會轉入內嵌機制樊破,大大提高執(zhí)行效率。
在java的早期實現(xiàn)中唆铐,如果將一個方法指明為final捶码,就是同意編譯器將針對該方法的所有調用都轉為內嵌調用。當編譯器發(fā)現(xiàn)一個final方法調用命令時或链,它會根據自己的謹慎判斷惫恼,跳過插入程序代碼這種正常的調用方式而執(zhí)行方法調用機制(將參數(shù)壓入棧,跳至方法代碼處執(zhí)行澳盐,然后跳回并清理棧中的參數(shù)祈纯,處理返回值)令宿,并且以方法體中的實際代碼的副本來代替方法調用。這將消除方法調用的開銷腕窥。當然粒没,如果一個方法很大,你的程序代碼會膨脹簇爆,因而可能看不到內嵌所帶來的性能上的提高癞松,因為所帶來的性能會花費于方法內的時間量而被縮減(不是很理解)。
final類
在設計類的時候入蛆,出于某些因素的考慮响蓉,這個類的實現(xiàn)細節(jié)不允許隨意修改,而且不需要子類哨毁,確定它不會要被擴展枫甲。那么設計時使用final修飾。final類是不允許被繼承的扼褪,表明該類事最終類想幻。由于final類是無法繼承的,所以類方法會默認加上final修飾话浇。而它的成員變量并沒有強制規(guī)定被final修飾脏毯。
final參數(shù)
final可以修飾方法參數(shù)列表中的參數(shù),一旦調用方法傳遞參數(shù)后幔崖,方法內不可以修改參數(shù)(基本數(shù)據類型不能修改值抄沮,引用類型的可變類不能修改地址,不可變類完全不可變)岖瑰。最常見的就是方法中將參數(shù)傳遞給匿名內部類使用叛买,此時該參數(shù)必須為final。
那么為什么匿名內部類在使用方法中的局部變量或者方法的參數(shù)時蹋订,需要使用final修飾率挣?首先來了解一個基本概念:
內部類被編譯時,字節(jié)碼會單獨放在一個.class文件中露戒,與外部類的字節(jié)碼文件分開椒功。
匿名內部類使用方法局部變量
public class OuterClass{
public void test() {
final int a = 10;
new Thread() {
public void run() {
System.out.println(a);
}
}.start();
}
}
如果執(zhí)行test()
完成后,那么在站內存中的變量a就會被回收智什,而此時如果匿名內部類(Thread)生命周期沒有結束动漾,那么在run()
方法中訪問變量a就無法實現(xiàn)。所以Java通過復制的手段來避免這個問題荠锭。
這個過程是在編譯期間由編譯器默認進行旱眯,如果這個變量的值在編譯期間可以確定,則編譯器默認會在匿名內部類(局部內部類)的常量池中添加一個內容相等的字面量或直接將相應的字節(jié)碼嵌入到執(zhí)行字節(jié)碼中。這樣一來删豺,匿名內部類使用的變量是另一個局部變量共虑,只不過值和方法中局部變量的值相等,因此和方法中的局部變量完全獨立開呀页。
匿名內部類使用方法的參數(shù)
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();
}
從上面代碼可以認為內部類直接調用了參數(shù)a妈拌。其實Java編譯后內部類單獨放在自己的字節(jié)碼文件中,可以直觀的翻譯為:
public class Outer$Inner {
public Outer$Inner(final int a) {
this.Inner$a = a;
}
public void innerMethod() {
System.out.println(this.Inner$a);
}
}
從上面內部類的構造函數(shù)中可以看到蓬蝶,這里是將變量test方法中的形參a以參數(shù)的形式傳進來對匿名內部類中的拷貝(變量a的拷貝)進行賦值初始化尘分。內部的方法調用的實際是自己的屬性而不是外部類方法的參數(shù)。這么做的好處解決了上一節(jié)所說的生命周期的問題丸氛。
總結
也就說如果局部變量的值在編譯期間就可以確定培愁,則直接在匿名內部里面創(chuàng)建一個拷貝。如果局部變量的值無法在編譯期間確定雪位,則通過構造器傳參的方式來對拷貝進行初始化賦值竭钝。
方法參數(shù)或者局部變量和匿名內部類使用的變量看似是同一個梨撞,其實在匿名內部類中實行了拷貝操作雹洗,兩個并不是同一個變量。如果在內部類中修改了這個變量卧波,方法的參數(shù)或者局部變量并不會受到影響时肿,這樣就失去了一致性,這是程序猿不愿意看到的港粱。所以使用final來修飾螃成,保證它的不可變,達到變量的一致性查坪。
簡單理解就是寸宏,拷貝引用,為了避免引用值發(fā)生改變偿曙,例如被外部類的方法修改等氮凝,而導致內部類得到的值不一致,于是用final來讓該引用不可改變望忆。
參考
Java關鍵字static
Java中沒有全局變量的概念罩阵,但是可以通過static來實現(xiàn)“全局”的概念。static關鍵字可以用來修飾成員變量启摄,方法以及代碼塊稿壁。static關鍵字表示“全局”或者“靜態(tài)”的意思。
固定內存分配
靜態(tài)變量
Java類加載過程中有兩個階段對類變量初始化歉备。一個是在連接階段的準備部分中對類變量分配內存并設置JVM默認值傅是;另一個是類加載的最后階段,初始化,根據類變量的聲明進行賦值初始化或者在靜態(tài)代碼塊中執(zhí)行相應的賦值語句落午。
那么分配在哪塊內存中呢谎懦?在運行時數(shù)據區(qū)的方法區(qū)內。
方法區(qū)主要存儲已被虛擬機加載的類信息溃斋、常量界拦、靜態(tài)變量、即使編譯器編譯后的代碼等數(shù)據梗劫。
靜態(tài)方法
方法區(qū)會存儲即使編譯器編譯后的代碼享甸。
即使編譯器可以監(jiān)控經常執(zhí)行哪些方法代碼優(yōu)化這些代碼以提高速度。更為復雜的優(yōu)化是消除函數(shù)調用(即“內聯(lián)”)梳侨。即使編譯器知道哪些類已經加載蛉威。給予當前加載的類集,如果特定的函數(shù)不會被覆蓋走哺,就可以使用內聯(lián)蚯嫌。
摘抄自java核心技術,不是很理解丙躏。
由于靜態(tài)方法不能覆寫择示,所以它門也被分配在方法區(qū)中(final修飾的方法也不可覆寫,也分配在方法區(qū)晒旅?)栅盲。
總結
一旦類加載執(zhí)行完,JVM就可以方便地在方法區(qū)中就找到它們(類變量废恋,靜態(tài)方法谈秫,靜態(tài)代碼塊)。所以static修飾的對象鱼鼓,可以在類實例化之前調用拟烫,無需持有相應對象的引用。
特點
被static修飾的成員變量和成員方法是獨立于該類的迄本,它不依賴于某個特定的實例變量硕淑,也就是說它被該類的所有實例共享。即便創(chuàng)建無數(shù)個對象岸梨,也不會有靜態(tài)變量的副本喜颁。同時靜態(tài)方法無法被覆寫。
static變量
static變量曹阔,一般稱之為靜態(tài)變量半开,也可以稱為類變量。與之相對應的是實例變量赃份。它們兩者的區(qū)別在于:
對于靜態(tài)變量在內存中只有一個拷貝(節(jié)省內存)寂拆,JVM只為靜態(tài)分配一次內存奢米,在加載類的過程中完成靜態(tài)變量的內存分配,可用類名直接訪問(方便)纠永,當然也可以通過對象來訪問(不應該這么做鬓长,概念混淆)。
對于實例變量尝江,每創(chuàng)建一個實例涉波,就會為實例變量分配一次內存。實例變量可以在內存中有多個拷貝炭序,互不影響(靈活)啤覆。
static方法
靜態(tài)方法,可以通過類名直接調用惭聂,任何實例來調用窗声。所以靜態(tài)方法中不能使用this和super關鍵字。
靜態(tài)方法不能直接訪問實例變量辜纲,調用實例方法笨觅。可以通過創(chuàng)建對象后調用實例方法耕腾,實例變量(例如主方法中)见剩。
由于靜態(tài)方法不依賴任何實例,所以靜態(tài)方法必須實現(xiàn)幽邓,而不能是抽象的炮温。
靜態(tài)代碼塊
靜態(tài)代碼塊會在類加載最后階段初始化中執(zhí)行火脉,利用靜態(tài)代碼塊可以做一些初始化牵舵,例如類變量的賦值...
靜態(tài)方法的局限
- 它只能直接訪問靜態(tài)變量
- 它只能直接調用其他靜態(tài)方法
- 不能以任何形式引用this或者super
- 不能被覆寫
上述1,2兩點針對的是本類中的其他靜態(tài)方法和靜態(tài)變量倦挂。
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輸出:
說明靜態(tài)方法不能被覆寫。
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();
}
}
但是這樣的代碼可以編譯通過方援,執(zhí)行測試類后没炒,Console輸出:This method action in father This method action by son
分析:覆寫指的是根據運行時對象來決定調用哪個方法,而不是根據編譯時的類型犯戏。
聲明為A類型的變量名存儲在棧中送火,而指向堆內存的卻是B的實例。如果調用變量a的非靜態(tài)方法先匪,解釋器會從堆內存中找到指向的B類型實例种吸,然后調用它的方法。而靜態(tài)方法屬于類方法呀非,在編譯階段就已經確定了它屬于A類的靜態(tài)方法坚俗,所以執(zhí)行的是A類的方法镜盯。所以達不到覆寫的效果。
總結猖败,靜態(tài)方法的覆寫只是形式上的速缆,實際上達不到覆寫的效果(也就是多態(tài)),只能隱藏(也就是通過子類類名調用靜態(tài)方法恩闻,執(zhí)行的是子類實現(xiàn)的方法)艺糜。而編譯器沒有報錯,是因為編譯器認為這是子類實現(xiàn)的新方法幢尚,如果加上注解@Override會去檢查父類是否有相同方法名的方法倦踢,由于靜態(tài)方法覆寫無效果,無法覆寫侠草,那么就無法編譯通過辱挥。
一個實例對象有兩個類型:表明類型(Apparent Type)和實際類型(Actual Type)晤碘。表面類型是聲明時的類型,實際類型是對象創(chuàng)建時的類型功蜓。語句A a = new B();
變量a表面類型是A园爷,實際類型是B。非靜態(tài)方法根據實際類型來執(zhí)行式撼,而對于靜態(tài)方法童社,通過對象來調用,JVM會通過表面類型查找到靜態(tài)方法入口來執(zhí)行著隆。