程式設計思想 之「多型、初始化順序、協變返回型別」
溫馨提示:本系列博文(含示例程式碼)已經同步到 GitHub,地址為「java-skills」,歡迎感興趣的童鞋
Star
、Fork
,糾錯。
在面向物件的程式語言中,有三個特性,分別為:封裝、繼承和多型。實現多型的前提是繼承,多型的作用是消除型別之間的耦合關係。對於多型,我們常說的詞有兩個,分別為:向上轉型和向下轉型。
我們把對某個物件的引用視為其基型別的引用的做法,稱之為向上轉型;把對某個物件的引用視為其匯出型別的引用的做法,稱之為向下轉型。之所以如此稱呼,是因為我們習慣性在類的繼承體系中把基類放在最上面,而把匯出類放在下面,因此從匯出類到基類的過程是一個向上看的過程,反之亦然。
在「語言導論」中,我們曾提到前期繫結和後期繫結的概念,我們將一個方法呼叫同一個方法主體關聯起來的動作稱之為「繫結」。如果在程式執行前就進行繫結(由編譯器和連結程式實現),這就是前期繫結,它是面向過程的程式語言的預設繫結方式,例如 C 語言;如果在執行時根據物件的型別進行繫結,這就是後期繫結,也稱之為動態繫結和執行時繫結。
在 Java 中,除了static
方法和final
方法(private
方法屬於final
方法)之外,其他所有方法都是後期繫結。如果想要取消某個方法的後期繫結,將其宣告為final
型別即可。
對於多型或者說繼承,Java 是有一個“缺陷”的,那就是:不能覆蓋private
package com.hit.chapter8;
/**
* author:Charies Gavin
* date:2018/1/3,8:55
* https:github.com/guobinhit
* description:測試私有方法是否能被覆蓋
*/
public class PrivateOverride {
private void test_1() {
System.out.println("私有方法能被覆蓋嗎?答案:不能。");
}
public void test_2() {
System.out.println("公有方法能被覆蓋嗎?答案:能。" );
}
public static void main(String[] args) {
PrivateOverride po = new DerivedOverride();
po.test_1();
po.test_2();
}
}
class DerivedOverride extends PrivateOverride {
public void test_1() {
System.out.println("Oh, my god, 我們成功覆蓋了私有方法!");
}
public void test_2() {
System.out.println("Hi, buddy, 我們成功覆蓋了公有方法!");
}
}
通過上面的測試,我們發現:只有非private
方法才能被覆蓋。因此,在匯出類中,對於基類中的private
方法,最好採用不同的名字,以防止混亂的發生。此外,只有普通的方法呼叫是多型的,如果某個方法是靜態的,它的行為就是不具有多型性。靜態方法是與類,而不是與單個的物件相關聯的。任何域訪問操作都是由編譯器繼解析,因此域也不是多型的。
初始化順序測試
在這一部分,我們來測試程式的初始化順序,包括靜態初始化、非靜態初始化、例項初始化和構造器等,完整的測試程式碼如下:
package com.hit.chapter8;
/**
* author:Charies Gavin
* date:2018/1/3,9:16
* https:github.com/guobinhit
* description:測試初始化順序,球形繼承圓形類,間接繼承圖形基類(不是特別恰當)
*/
public class Global extends Circle {
/**
* 靜態初始化塊
*/
static {
System.out.println("Global: Static Initial.");
}
/**
* 非靜態初始化塊
*/ {
System.out.println("Global: Non-static Initial.");
}
/**
* 預設無參構造方法
*/
Global() {
System.out.println("Global: Structure Method Initial.");
}
/**
* 有參構造方法
*/
Global(String str) {
System.out.println("Global: Structure Method Initial ... " + str);
}
/**
* 靜態成員例項初始化
*/
private static Circle staticircle = new Circle("in Global of Static Instance Initial.");
/**
* 非靜態成員例項初始化
*/
private Circle circle = new Circle("in Global of Non-static Instance Initial.");
public static void main(String[] args) {
/**
* 在主方法中呼叫匯出類構造方法,測試初始化順序
*/
new Global();
System.out.println("Initial Over!");
}
}
/**
* 圖形基類
*/
class Shape {
/**
* 靜態初始化塊
*/
static {
System.out.println("Shape: Static Initial.");
}
/**
* 非靜態初始化塊
*/ {
System.out.println("Shape: Non-static Initial.");
}
/**
* 預設無參構造方法
*/
Shape() {
System.out.println("Shape: Structure Method Initial.");
}
/**
* 有參構造方法
*/
Shape(String str) {
System.out.println("Shape: Structure Method Initial ... " + str);
}
}
/**
* 圓形繼承圖形基類
*/
class Circle extends Shape {
/**
* 靜態初始化塊
*/
static {
System.out.println("Circle: Static Initial.");
}
/**
* 非靜態初始化塊
*/ {
System.out.println("Circle: Non-static Initial.");
}
/**
* 預設無參構造方法
*/
Circle() {
System.out.println("Circle: Structure Method Initial.");
}
/**
* 有參構造方法
*/
Circle(String str) {
System.out.println("Circle: Structure Method Initial ... " + str);
}
/**
* 靜態成員例項初始化
*/
private static Shape staticShape = new Shape("in Circle of Static Instance Initial.");
/**
* 非靜態成員例項初始化
*/
private Shape shape = new Shape("in Circle of Non-static Instance Initial.");
}
觀察上圖,我們能夠發現程式的初始化規律。以基類1 -> 基類2 -> 匯出類
的繼承結構為例,初始化順序大致如下:
- 從
基類1
開始,先進行靜態初始化,然後依次向外擴散至基類2
和匯出類
; - 然後,依次進行
基類1
的非靜態初始化和構造器初始化; - 再依次進行
基類2
的非靜態初始化和構造器初始化; - 最後,才是
匯出類
的非靜態初始化和構造器初始化。
如果當前類含有靜態例項初始化,則它的靜態例項初始化將在基類的非靜態初始化和構造器初始化之前執行,也在匯出類的靜態初始化之前執行;如果還含有非靜態例項初始化,它會在當前類的非靜態初始化之後、構造器初始化之前,進行初始化。此外,無論是靜態初始化還是非靜態初始化,都會在構造器初始化之前進行初始化。實際上,在上述任何初始化動作發生之前,都會先將分配給物件的儲存空間初始化為二進位制的零。
現在,我們已經知道了物件的初始化順序,與之相反的,則是物件的銷燬順序。由於欄位的初始化順序是按照宣告的順序進行的,因此對於欄位,銷燬的順序意味著與宣告的順序相反。對於基類,則是先對其匯出類進行清理,然後才是基類。
協變返回型別
在 Java SE5 中,添加了協變返回型別,它表示在匯出類中的被覆蓋的方法可以返回基類方法的返回型別的某種匯出型別。對於上述協變返回型別的定義,讀起來有些讓人吐血,簡單點,通過下面的程式理解協變返回型別:
package com.hit.chapter8;
/**
* author:Charies Gavin
* date:2018/1/4,22:10
* https:github.com/guobinhit
* description:測試協變返回型別
*/
public class CovariantReturnType {
public static void main(String[] args) {
Flower flower = new Flower();
Plant plant = flower.kind();
System.out.println("未使用協變返回型別:" + plant);
// 使用協變返回型別
flower = new Luoyangred();
plant = flower.kind();
System.out.println("使用協變返回型別後:" + plant);
}
}
/**
* 植物基類
*/
class Plant {
public String toString() {
return "Plant";
}
}
/**
* 牡丹花,繼承自植物基類
*/
class Peony extends Plant {
public String toString() {
return "Peony";
}
}
/**
* 花
*/
class Flower {
Plant kind() {
return new Plant();
}
}
/**
* 洛陽紅,十大貴品牡丹花之一
*/
class Luoyangred extends Flower {
Peony kind() {
return new Peony();
}
}
如上圖所示,展示了使用協變返回型別後的效果。在 Java SE5 之前,強制匯出類中被覆蓋的方法必須返回基類方法的返回型別,但是在增加協變返回型別之後,我們可以讓在匯出類中被覆蓋的方法返回基類方法的返回型別的某種匯出型別,也就是說可以返回更加具體的返回型別。例如上例中的kind()
方法,在 Java SE5 之前,只能返回Plant
,但是在使用協變返回型別之後,我們可以直接返回更加具體的Peony
型別。
———— ☆☆☆ —— 返回 -> 那些年,關於 Java 的那些事兒 <- 目錄 —— ☆☆☆ ————