深入解析Java編程中final關(guān)鍵字的作用
final class
當(dāng)一個(gè)類被定義成final class,表示該類的不能被其他類繼承,即不能用在extends之后。否則在編譯期間就會(huì)得到錯(cuò)誤。
package com.iderzheng.finalkeyword; public final class FinalClass { } // Error: cannot inherit from final class PackageClass extends FinalClass { }
Java支持把class定義成final,似乎違背了面向?qū)ο缶幊痰幕驹瓌t,但在另一方面,封閉的類也保證了該類的所有方法都是固定不變的,不會(huì)有子類的覆蓋方法需要去動(dòng)態(tài)加載。這給編譯器做優(yōu)化時(shí)提供了更多的可能,最好的例子是String,它就是final類,Java編譯器就可以把字符串常量(那些包含在雙引號(hào)中的內(nèi)容)直接變成String對(duì)象,同時(shí)對(duì)運(yùn)算符+的操作直接優(yōu)化成新的常量,因?yàn)閒inal修飾保證了不會(huì)有子類對(duì)拼接操作返回不同的值。
對(duì)于所有不同的類定義—頂層類(全局或包可見(jiàn))、嵌套類(內(nèi)部類或靜態(tài)嵌套類)都可以用final來(lái)修飾。但是一般來(lái)說(shuō)final多用來(lái)修飾在被定義成全局(public)的類上,因?yàn)閷?duì)于非全局類,訪問(wèn)修飾符已經(jīng)將他們限制了它們的也可見(jiàn)性,想要繼承這些類已經(jīng)很困難,就不用再加一層final限制。
另外要提到的是匿名類(Anonymous Class)雖然說(shuō)同樣不能被繼承,但它們并沒(méi)有被編譯器限制成final。
import java.lang.reflect.Modifier; public class Main { public static void main(String[] args) { Runnable anonymous = new Runnable() { @Override public void run() { } }; System.out.println(Modifier.isFinal(anonymous.getClass().getModifiers())); } }
輸出:
false
final Method
跟繼承觀念密切相關(guān)是多態(tài)(Polymorphism),其中牽扯到了覆蓋(Overriding)和隱藏(Hiding)的概念區(qū)別(為方便起見(jiàn),以下對(duì)這兩個(gè)概念統(tǒng)一稱為“重寫”)。但不同于C++中方法定義是否有加virtual關(guān)鍵字會(huì)影響子類相同方法簽名的方法是覆蓋還是隱藏,在Java里子類用相同方法簽名重寫父類方法,對(duì)于類方法(靜態(tài)方法)會(huì)形成隱藏,而對(duì)象方法(非靜態(tài)方法)只發(fā)生覆蓋。由于Java允許通過(guò)對(duì)象直接訪問(wèn)類方法,也使得Java不允許在同一個(gè)類中類方法和對(duì)象方法有相同的簽名。
final類限定了整個(gè)類不能被繼承,進(jìn)而也表示該類里的所有方法都不能被子類所覆蓋和隱藏。當(dāng)類不被final修飾時(shí),依然可以對(duì)部分方法使用final進(jìn)行修飾來(lái)防止這些方法被子類重寫。
同樣的,這樣的設(shè)計(jì)破壞了面向?qū)ο蟮亩鄳B(tài)性,但是final方法可以保證其執(zhí)行的確定性,從而確保了方法調(diào)用的穩(wěn)定性。在一些框架設(shè)計(jì)中就會(huì)經(jīng)常見(jiàn)到抽象類的一些已實(shí)現(xiàn)方法的方法被限制成final,因?yàn)樵诳蚣苤幸恍?qū)動(dòng)代碼會(huì)依賴這些方法的實(shí)現(xiàn)了完成既定的目標(biāo),所以不希望有子類對(duì)它進(jìn)行覆蓋。
下邊的例子展示了final修飾在不同類型的方法中起到的作用:
package com.iderzheng.other; public class FinalMethods { public static void publicStaticMethod() { } public final void publicFinalMethod() { } public static final void publicStaticFinalMethod() { } protected final void protectedFinalMethod() { } protected static final void protectedStaticFinalMethod() { } final void finalMethod() { } static final void staticFinalMethod() { } private static final void privateStaticFinalMethod() { } private final void privateFinalMethod() { } } package com.iderzheng.finalkeyword; import com.iderzheng.other.FinalMethods; public class Methods extends FinalMethods { public static void publicStaticMethod() { } // Error: cannot override public final void publicFinalMethod() { } // Error: cannot override public static final void publicStaticFinalMethod() { } // Error: cannot override protected final void protectedFinalMethod() { } // Error: cannot override protected static final void protectedStaticFinalMethod() { } final void finalMethod() { } static final void staticFinalMethod() { } private static final void privateStaticFinalMethod() { } private final void privateFinalMethod() { } }
首先注意上邊的例子里,F(xiàn)inalMethods和Methods是定義在不同的包(package)下。對(duì)于第一個(gè)publicStaticMethod,子類成功重寫了父類的靜態(tài)方法,但因?yàn)槭庆o態(tài)方法所以發(fā)生的其實(shí)是“隱藏”。具體表現(xiàn)為調(diào)用Methods.publicStaticMethod()會(huì)執(zhí)行Methods類中的實(shí)現(xiàn),調(diào)用FinalMethods.publicStaticMethod()時(shí)執(zhí)行并不會(huì)發(fā)生多態(tài)加載子類的實(shí)現(xiàn),而是直接使用FinalMethods的實(shí)現(xiàn)。所以在用子類去訪問(wèn)方法時(shí),會(huì)隱藏了父類相同方法簽名的方法的可見(jiàn)性。
對(duì)于全局方法publicFinalMethod就像final修飾方法描述的那樣禁止子類定義相同的方法去覆蓋它,在編譯時(shí)就會(huì)拋出異常。不過(guò)在子類定義方法名字一樣但是帶有個(gè)參數(shù),比如:publicFinalMethod(String x)是可以的,因?yàn)檫@是同步的方法簽名。
在Intellij里,IDE對(duì)publicStaticFinalMethod顯示了一個(gè)警告:'static' method declared 'final'。在它看來(lái)這是多余的,但從實(shí)例中可以看出final同樣禁止了子類定義相同的靜態(tài)方法去隱藏它。在實(shí)際開發(fā)中,子類和父類定義相同的靜態(tài)方法的行為是極為不推薦的,因?yàn)殡[藏方法需要開發(fā)者注意使用不同類名限定會(huì)有不同的效果,就很容易帶來(lái)錯(cuò)誤。而且在類的內(nèi)部是可以不使用類名限定直接調(diào)用靜態(tài)方法,開發(fā)者再度做繼承時(shí)可能沒(méi)有注意到隱藏的存在默認(rèn)在使用父類的方法時(shí)就會(huì)發(fā)現(xiàn)不是預(yù)期的結(jié)果。所以對(duì)靜態(tài)方法應(yīng)該默認(rèn)已經(jīng)是final而不該去隱藏他們,也因此IDE覺(jué)得是多余的修飾。
父類中protected修飾和public修飾的方法對(duì)于子類都是可見(jiàn)的,所以final修飾protected方法的情況和public方法是一樣的。想提到的是在實(shí)際開發(fā)中一般很少定義protected靜態(tài)方法,因?yàn)檫@樣的方法實(shí)用性太低。
對(duì)于父類package方法,處在不同的package下的子類是不可見(jiàn)的,private方法已經(jīng)定制了只有父類自己可訪問(wèn)。所以編譯器允許子類去定義相同的方法。但這不形成覆蓋或隱藏,因?yàn)楦割愐呀?jīng)通過(guò)修飾符來(lái)隱藏了這些方法,而非子類的重寫造成的。當(dāng)然如果子類和父類在同一package下,那么情況也和之前的public、protected一樣了。
final方法為何會(huì)高效:
final方法會(huì)在編譯的過(guò)程中利用內(nèi)嵌機(jī)制進(jìn)行inline優(yōu)化。inline優(yōu)化是指:在編譯的時(shí)候直接調(diào)用函數(shù)代碼替換,而不是在運(yùn)行時(shí)調(diào)用函數(shù)。inline需要在編譯的時(shí)候就知道最后要用哪個(gè)函數(shù), 顯然,非final是不行的。非final方法可能在子類中被重寫,由于可能出現(xiàn)多態(tài)的情況,編譯器在編譯階段并不能確定將來(lái)調(diào)用方法的對(duì)象的真正類型,也就無(wú)法確定到底調(diào)用哪個(gè)方法。
final Variable
簡(jiǎn)單說(shuō),Java里的final變量只能且必須被初始化一次,之后該變量就與該值綁定。但該次賦值不一定要在變量被定義時(shí)被立刻初始化,Java也支持通過(guò)條件語(yǔ)句給final變量不同的結(jié)果,只是無(wú)論如何該變量都只能變賦值一次。
不過(guò)Java的final變量并非絕對(duì)的常量,因?yàn)镴ava的對(duì)象變量只是引用值,所以final只是表示該引用不能改變,而對(duì)象的內(nèi)容依然可以修改。對(duì)比C/C++的指針,它更像是type * const variable而非type const * variable。
Java的變量可以分為兩類:局部變量(Local Variable)和類成員變量(Class Field)。下邊還是用代碼來(lái)分別介紹它們的初始化情況。
Local Variable
局部變量主要指定義在方法中的變量,出了方法它們就會(huì)消失不可訪問(wèn)。其中有可分出一種特殊情況:函數(shù)參數(shù)。對(duì)于這種情況,其初始化與函數(shù)被調(diào)用時(shí)傳入的參數(shù)綁定。
對(duì)于其他的局部變量,它們被定義在方法中,其值就可以被有條件的初始化:
public String method(final boolean finalParam) { // Error: final parameter finalParam may not be assigned // finalParam = true; final Object finalLocal = finalParam ? new Object() : null; final int finalVar; if (finalLocal != null) { finalVar = 21; } else { finalVar = 7; } // Error: variable finalVar might already have been assigned // finalVar = 80; final String finalRet; switch (finalVar) { case 21: finalRet = "me"; break; case 7: finalRet = "she"; break; default: finalRet = null; } return finalRet; }
從上述例子中可以看出被final修飾的函數(shù)參數(shù)無(wú)法被賦予新的值,但是其他final的局部變量則可以在條件語(yǔ)句中被賦值。這樣也給final提供了一定的靈活性。
當(dāng)然條件語(yǔ)句中的所有條件里都應(yīng)該包含對(duì)final局部變量的賦值,否則就會(huì)得到變量可能未被初始化的錯(cuò)誤
public String method(final Object finalParam) { final int finalVar; if (finalParam != null) { finalVar = 21; } final String finalRet; // Error: variable finalVar might not have been initialized switch (finalVar) { case 21: finalRet = "me"; break; case 7: finalRet = "she"; break; } // Error: variable finalRet might not have been initialized return finalRet; }
理論上局部變量沒(méi)有被定義成final的必要,合理設(shè)計(jì)的方法應(yīng)該可以很好的維護(hù)局部變量。只是在Java方法中使用匿名函數(shù)做閉包時(shí),Java要求被引用的局部變量必須被定義為final:
public Runnable method(String string) { int integer = 12; return new Runnable() { @Override public void run() { // ERROR: needs to be declared final System.out.println(string); // ERROR: needs to be declared final System.out.println(integer); } }; }
Class Field
類成員變量其實(shí)也能分成兩種:靜態(tài)和非靜態(tài)。對(duì)于靜態(tài)類成員變量,因?yàn)樗鼈兣c類相關(guān),所以除了在定義時(shí)直接初始化,還可以放在static block中,而使用后者可以執(zhí)行更多復(fù)雜的語(yǔ)句:
package com.iderzheng.finalkeyword; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; public class StaticFinalFields { static final int STATIC_FINAL_INIT_INLINE = 7; static final Set<Integer> STATIC_FINAL_INIT_STATIC_BLOCK; /** Static Block **/ static { if (System.currentTimeMillis() % 2 == 0) { STATIC_FINAL_INIT_STATIC_BLOCK = new HashSet<>(); } else { STATIC_FINAL_INIT_STATIC_BLOCK = new LinkedHashSet<>(); } STATIC_FINAL_INIT_STATIC_BLOCK.add(7); STATIC_FINAL_INIT_STATIC_BLOCK.add(21); } }
Java中也有非靜態(tài)的block可以對(duì)非靜態(tài)的成員變量進(jìn)行初始化,但是對(duì)于這些變量,更多的時(shí)候還是放在構(gòu)造函數(shù)(constructor)里進(jìn)行初始化。當(dāng)然必須保證每個(gè)final變量在構(gòu)造函數(shù)里都有被初始化一次,如果通過(guò)this()調(diào)用了其他的構(gòu)造函數(shù),則這些final變量不能再在該構(gòu)造函數(shù)里被賦值了。
package com.iderzheng.finalkeyword; public class FinalFields { final long FINAL_INIT_INLINE = System.currentTimeMillis(); final long FINAL_INIT_BLOCK; final long FINAL_INIT_CONSTRUCTOR; /** Initial Block **/ { FINAL_INIT_BLOCK = System.nanoTime(); } FinalFields() { this(217); } FinalFields(boolean bool) { FINAL_INIT_CONSTRUCTOR = 721; } FinalFields(long init) { FINAL_INIT_CONSTRUCTOR = init; } }
當(dāng)final用來(lái)修飾類(Class) 和方法(Method)時(shí),它主要影響面向?qū)ο蟮睦^承性,沒(méi)有了繼承性就沒(méi)有了子類對(duì)父類的代碼依賴,所以在維護(hù)時(shí)修改代碼就不用考慮會(huì)不會(huì)破壞子類的實(shí)現(xiàn),就顯得更加方便。而當(dāng)它用在變量(Variable)上時(shí),Java保證了變量值不會(huì)修改,更進(jìn)一步設(shè)計(jì)保證類的成員也不能修改的話,那么整個(gè)變量就可以變成常量使用,對(duì)于多線程編程是非常有利的。所以final對(duì)于代碼維護(hù)有非常好的作用。
相關(guān)文章
Java手機(jī)號(hào)碼工具類示例詳解(判斷運(yùn)營(yíng)商、獲取歸屬地)
這篇文章主要介紹了Java手機(jī)號(hào)碼工具類示例詳解,通過(guò)手機(jī)號(hào)碼來(lái)判斷運(yùn)營(yíng)商獲取歸屬地,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-02-02Java如何處理json字符串value多余雙引號(hào)
這篇文章主要介紹了Java如何處理json字符串value多余雙引號(hào),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03Java之獲取客戶端真實(shí)IP地址的實(shí)現(xiàn)
在開發(fā)工作中,我們常常需要獲取客戶端的IP,本文主要介紹了Jav之獲取客戶端真實(shí)IP地址的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2023-12-12springboot驗(yàn)證碼生成以及驗(yàn)證功能舉例詳解
登錄注冊(cè)是大部分系統(tǒng)需要實(shí)現(xiàn)的基本功能,同時(shí)也會(huì)對(duì)登錄驗(yàn)證增加需求,下面這篇文章主要給大家介紹了關(guān)于springboot驗(yàn)證碼生成以及驗(yàn)證功能的相關(guān)資料,需要的朋友可以參考下2023-04-04