1. 程式人生 > >JAVA final 、super 關鍵字以及繼承關係中父類與子類例項變數初始化的 理解

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多執行緒中所不允許的。這會造成很大的執行緒不安全性。比如,子類中過載的某方法會修改子類的例項變數,若在父類的構造器呼叫了該過載方法修改了子類的例項變數,而此時子類變數尚未初始化,將會造成子類例項變數的極大不確定性。