Java類的多型機制

孫群發表於2015-06-06

Java中將一個方法呼叫同一個方法主體關聯起來被稱作繫結。繫結分為前期繫結和後期繫結。前期繫結是在編譯器決定的,而後期繫結是在程式執行時決定的。Java中除了static方法和final方法(private方法也是final方法,只不過是隱式的為final)之外,其他所有的方法都是後期繫結。Java類的多型指的是當將子類向上轉型為父型別並呼叫父型別中相應的方法時,多型機制會根據動態繫結自動判斷出呼叫相應的子類的方法。也就是說多型機制的存在的基礎是子類實現了對父類中相應方法的函式覆蓋。比如有一個Shape類,該類有一個draw方法,並有Circle、Triangle、Square這三個類均繼承自Shape類,並且都重寫了父類的draw方法,程式碼如下所示:

class Shape{
	void draw(){
		System.out.println("Draw Shape");
	}
}

class Circle extends Shape{
	void draw(){
		System.out.println("Draw Circle");
	}
}

class Triangle extends Shape{
	void draw(){
		System.out.println("Draw Triangle");
	}
}

class Square extends Shape{
	void draw(){
		System.out.println("Draw Square");
	}
}

public class Test {
	
	 public static void main(String[] args) {
		 Shape[] shapes = {new Circle(), new Triangle(), new Square()};
		 for(Shape s : shapes){
			 s.draw();
		 }
	 }
}

輸出結果為:

Draw Circle
Draw Triangle
Draw Square

我們建立了一個Shapes陣列,裡面分別是Circle、Triangle和Square的例項,我們在遍歷該Shapes陣列時其實已經對其進行了向上轉型,即已經模糊了其實際型別,在遍歷Shapes陣列時,只知道每個元素都是一個Shape型別,然後依次呼叫元素的draw方法。結果沒有呼叫基類Shape的draw方法,而是呼叫的物件實際子型別中的darw方法,這種現象就稱之為多型。那此處的多型究竟是怎麼發生的呢?前面說過只要類中的方法不是static和final的,那麼該方法是後期繫結也就是在執行時才決定呼叫主體。在執行s.draw()這句程式碼時,Java知道了要呼叫darw方法了,s雖然表面看起來是Shape型別,但是它能判斷出s實際上是一個Circle/Triangle/Square型別,這樣就將方法的呼叫主體設定為更為具體的子類,這樣就執行了具體子類的draw方法而非父類的draw方法。


我們在看如下一段Java程式碼:

class Shape{
	void draw(){
		System.out.println("Draw Shape");
	}
	
	void show(){
		draw();
	}
}

class Circle extends Shape{
	void draw(){
		System.out.println("Draw Circle");
	}
}

public class Test {	
	 public static void main(String[] args) {
		 Shape s = new Circle();
		 s.show();
	 }
}
執行結果為:Draw Circle
基類Shape中新增了一個show方法,在show方法中會呼叫draw方法,當執行程式碼Shape s = new Circle()時,我們建立了一個Circle型別的例項,並將其向上轉型為Shape型別,然後呼叫基類的show方法,結果基類Shape的show方法呼叫了子類Circle的draw方法而非基類Shape的draw方法。出現這種情況的原因還是多型機制。當執行基類Shape中的show方法時,show方法內部要執行darw方法,darw方法是要執行的方法名,由於draw方法既不是static的,又不是final的,所以draw方法是後期繫結,也就是在執行時判斷呼叫主體。Java知道s實際上是Circle型別的例項,所以會在基類Shape的show方法中會將子類Circle作為呼叫主體去呼叫子類Circle中的draw方法而非基類Shape的draw方法。


那麼我們再對上面的例子進行一處修改,我們將基類Shape的draw方法設定為private私有的,程式碼如下:

class Shape{
	private void draw(){
		System.out.println("Draw Shape");
	}
	
	void show(){
		draw();
	}
}

class Circle extends Shape{
	void draw(){
		System.out.println("Draw Circle");
	}
}

public class Test {	
	 public static void main(String[] args) {
		 Shape s = new Circle();
		 s.show();
	 }
}
執行結果為:Draw Shape

我們再來分析一下原因。此處還是將子類Circle型別的例項向上轉型為基類Shape,在執行基類Shape的show方法時,show方法內要執行draw方法,由於基類Shape中的draw方法被定義為private私有的,而private修飾的方法都實際是final方法(只不過是隱式地修飾為final),所以基類的draw方法是final的,由於final方法的呼叫都是前期繫結,也就是final方法的呼叫是在編譯器決定的。所以此處不會發生後期繫結,從而自然執行了基類的draw方法而非子類Circle的draw方法。也可以這樣認為,在我們編寫完這個Java檔案用IDE對其編譯生成class檔案時,由於private方法的前期繫結特性,編譯器會將Shape中的draw方法的程式碼都copy到show方法內部。如下所示:

class Shape{
	private void draw(){
		System.out.println("Draw Shape");
	}
	
	void show(){
		//編譯時將基類draw方法內的程式碼都copy到show方法中
		System.out.println("Draw Shape");
	}
}

可以這樣理解,在生成的class檔案中在show方法中就抹去了draw,只留下copy過來的基類中的draw程式碼。

此處程式碼沒有執行多型還有一個原因是,多型機制的基礎是子類對父類進行了函式覆蓋。但是在上面的例子中父類Shape中的draw方法被修飾為private的,子類雖然也有一個draw方法,但是這不屬於函式覆蓋。因為父類中的draw方法為private的,對子類是完全遮蔽的,只有子類覆寫了能夠訪問的父類中的方法時,才存在函式覆蓋一說。所以壓根就不存在子類覆蓋父類中的private方法一說。既然Circle和Shape之間不存在函式覆蓋,那麼在在基類Shape的show方法執行draw時就不存在多型呼叫了。


還有一點需要說明的是只有普通的方法呼叫可以是多型的,欄位不是多型的。欄位的訪問操作是前期繫結,由編譯器解析,所以不是多型的。此處就不舉例了。

相關文章