JAVA final 、super 關鍵字以及繼承關係中父類與子類例項變數初始化的 理解
1,final 修飾符修飾變數、方法、類 時有什麼作用?
①final 修飾變數:該變數被賦初值後,不能對它重新賦值
②final 修飾方法:此方法不能重寫,即父類中某方法被final修飾,在子類中將不能定義一個與父類final 方法同名且有相同方法識別符號(引數個數也相同,返回值型別相同)的方法
③final 修飾類:此類不能再派生子類
④final 修飾的例項變數在使用前必須顯示地初始化。對於普通例項變數,可以預設初始化:如引用型別的例項變數,預設初始化值為null,但使用final修飾例項變數後,該變數可使用以下三種方式顯示初始化: a)在定義final例項變數時指定初始值 b)在非靜態初始化塊中指定初始值 c)在構造器中初始化
⑤區域性變數被final修飾時,a)區域性變數的值不能再被改變 b)區域性變數在使用前必須顯示初始化(其實JAVA本來就要求區域性變數在使用之前要先初始化)
❻當使用final修飾變數時(包括區域性變數、例項變數、類變數),如果在定義該final變數時就指定了初始值,則該變數的值在編譯階段就已經被確定下來了,此時該變數類似於C中的巨集變數。認識到這一點很重要!!!這樣就能理解為什麼有些變數值的初始化在奇怪,原來它們是在編譯期就已經確定了。這也是為什麼在《JAVA併發程式設計實踐》一書中
為了保證執行緒的安全性,應儘量將變數定義成final的一個原因。另一個原因,我想應該是:帶有final修飾的變數具有“不變性”,而不變性正是執行緒安全的一個特徵。
⑦為什麼要求內部類中訪問的區域性變數(在方法體中定義的變數)必須使用final修飾?
內部類訪問區域性變數,顯然該內部類是一個區域性內部類(在方法中定義的類)。那為什麼區域性變數要用final修飾呢?答案是final修飾的區域性變數不可變。對於區域性變數而言,它的生命週期侷限於方法體內,當方法結束時,區域性變數的作用域也隨之結束。但當在內部類中訪問區域性變數時,內部類會擴大區域性變數作用域的生命週期,這時,保證該區域性變數的不可變性對於程式的安全是很重要的。看下面示例:
public class Test { public static void main(String[] args) { final String str = "Hello"; new Thread(new Runnable() { @Override public void run() { for(int i = 0; i < 100; i++){ System.out.println(str); try{ Thread.sleep(100); }catch(InterruptedException e){} } } }).start(); } }
區域性變數str 在main方法中定義,當main方法執行到 .start() 時,main方法的主執行緒結束了,但是new Thread建立的執行緒還未結束,它繼續在訪問str變數的值。
2,super 關鍵字如何理解?
①藉助super關鍵字,可以在子類中呼叫父類的變數或方法(假設訪問許可權允許)。但需要注意的是:super不是一個變數,不能像this那樣,當做變數來用。如,return this; 是允許的,但是 return super; 則不行。super神奇的地方在於,super並不是一個父類物件的引用(變數),但是可以通過super.method()、super.field 來訪問呼叫父類中的方法以及訪問父類中的變數。
3,子類繼承了父類時,子類中的例項變數與父類中的例項變數有哪些關係?
①在繼承中,當new 一個子類物件時,首先會去執行父類的構造器,初始化父類的例項變數。即相當於new 子類物件時,隱含生成了一個父類物件,但是貌似是無法直接操縱這個父類物件的,能對父類做一些操作都是通過super關鍵字來完成的,如super.method(),super.field。
那麼,當子類中有與父類同名的例項變數時,子類的例項變數會隱藏父類的例項變數,若要訪問父類的例項變數就是前面提到的使用super關鍵字。
②通過引用來呼叫方法和通過引用來訪問變數,JAVA處理是不同的。如,假設有一個引用變數obj,obj.method() 呼叫的是obj實際指向的引用型別的方法,而obj.field 訪問的是宣告obj型別的屬性。看下面示例:
class Base{
int count = 2;
void method(){
System.out.println("Base Method");
}
}
public class Sub extends Base{
int count = 20;
@Override
void method(){
System.out.println("Sub Method");
}
public static void main(String[] args) {
Sub s = new Sub();
Base b = s;
System.out.println(s.count);//20
System.out.println(b.count);//2
s.method();//Sub Method
b.method();//Sub Method
}
}
b.count 為2,因為b宣告成Base型別。b.method()輸出: Sub Method,因為b實際指向的還是子型別Sub。
看一段解釋:當變數的編譯時型別和執行時型別不同時,通過該變數訪問它引用的物件的例項變數時,該例項變數的值由宣告該變數的型別決定。但是通過該變數呼叫它引用的物件的例項方法時,該方法行為將由它實際所引用的物件來決定。
③不要在父類的構造器中呼叫被子類重寫的方法(說得更直白一點:不要在父類的構造器中呼叫子類的方法),因為當 new 子類物件時,會先執行父類構造器中的方法,在父類構造器中呼叫子類的方法,而此時子類的物件都還沒有初始化完成!這也是為什麼在JAVA多執行緒中所不允許的。這會造成很大的執行緒不安全性。比如,子類中過載的某方法會修改子類的例項變數,若在父類的構造器呼叫了該過載方法修改了子類的例項變數,而此時子類變數尚未初始化,將會造成子類例項變數的極大不確定性。