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修飾。那么其子類不能覆寫該方法。這樣做的原因出于兩個方面的考慮:
- 把方法鎖定,防止子類修改它的意義和實現
- 高效。編譯器在遇到調用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關鍵字static
Java中沒有全局變量的概念,但是可以通過static來實現“全局”的概念。static關鍵字可以用來修飾成員變量,方法以及代碼塊。static關鍵字表示“全局”或者“靜態”的意思。
固定內存分配
靜態變量
Java類加載過程中有兩個階段對類變量初始化。一個是在連接階段的準備部分中對類變量分配內存并設置JVM默認值;另一個是類加載的最后階段,初始化,根據類變量的聲明進行賦值初始化或者在靜態代碼塊中執行相應的賦值語句。
那么分配在哪塊內存中呢?在運行時數據區的方法區內。
方法區主要存儲已被虛擬機加載的類信息、常量、靜態變量、即使編譯器編譯后的代碼等數據。
靜態方法
方法區會存儲即使編譯器編譯后的代碼。
即使編譯器可以監控經常執行哪些方法代碼優化這些代碼以提高速度。更為復雜的優化是消除函數調用(即“內聯”)。即使編譯器知道哪些類已經加載。給予當前加載的類集,如果特定的函數不會被覆蓋,就可以使用內聯。
摘抄自java核心技術,不是很理解。
由于靜態方法不能覆寫,所以它門也被分配在方法區中(final修飾的方法也不可覆寫,也分配在方法區?)。
總結
一旦類加載執行完,JVM就可以方便地在方法區中就找到它們(類變量,靜態方法,靜態代碼塊)。所以static修飾的對象,可以在類實例化之前調用,無需持有相應對象的引用。
特點
被static修飾的成員變量和成員方法是獨立于該類的,它不依賴于某個特定的實例變量,也就是說它被該類的所有實例共享。即便創建無數個對象,也不會有靜態變量的副本。同時靜態方法無法被覆寫。
static變量
static變量,一般稱之為靜態變量,也可以稱為類變量。與之相對應的是實例變量。它們兩者的區別在于:
對于靜態變量在內存中只有一個拷貝(節省內存),JVM只為靜態分配一次內存,在加載類的過程中完成靜態變量的內存分配,可用類名直接訪問(方便),當然也可以通過對象來訪問(不應該這么做,概念混淆)。
對于實例變量,每創建一個實例,就會為實例變量分配一次內存。實例變量可以在內存中有多個拷貝,互不影響(靈活)。
static方法
靜態方法,可以通過類名直接調用,任何實例來調用。所以靜態方法中不能使用this和super關鍵字。
靜態方法不能直接訪問實例變量,調用實例方法。可以通過創建對象后調用實例方法,實例變量(例如主方法中)。
由于靜態方法不依賴任何實例,所以靜態方法必須實現,而不能是抽象的。
靜態代碼塊
靜態代碼塊會在類加載最后階段初始化中執行,利用靜態代碼塊可以做一些初始化,例如類變量的賦值...
靜態方法的局限
- 它只能直接訪問靜態變量
- 它只能直接調用其他靜態方法
- 不能以任何形式引用this或者super
- 不能被覆寫
上述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輸出:
說明靜態方法不能被覆寫。
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會通過表面類型查找到靜態方法入口來執行。