點選進入我的部落格
在物件導向的程式設計語言中,多型是繼資料抽象(封裝)和繼承之後的第三種基本特徵。
多型通過分離做什麼和怎麼做,從另一角度將介面和實現分離開來。
多型的作用是消除型別之間的耦合關係。
8.1 再論向上轉型
物件既可以作為它自己的本類使用,也可以作為它的基類使用。
8.1.1 忘記物件型別
我們只寫一個簡單的方法,它接受基類作為引數,而不是那些特殊的匯出類。
public class Test {
public static void main(String[] args) {
func(new Unicycle());
func(new Bicycle());
func(new Tricycle());
}
public static void func(Cycle cycle) {
cycle.ride();
}
}
class Cycle {
void ride() {}
}
class Unicycle extends Cycle {
void ride() {
System.out.println("Unicycle");
}
}
class Bicycle extends Cycle {
void ride() {
System.out.println("Bicycle");
}
}
class Tricycle extends Cycle {
void ride() {
System.out.println("Tricycle");
}
}
8.2 轉機
func(Cycle cycle)
接受一個Cycle
引用,那麼編譯器怎麼才能知道這個Cycle
引用指的是哪個具體物件呢?實際上,編譯器並不知道。
8.2.1 方法呼叫繫結
- 繫結:講一個方法呼叫同一個方法主體關聯起來被稱作繫結。
- 前期繫結:程式執行前進行繫結(由編譯器和連線程式實現)叫做前期繫結。
- 後期繫結(動態繫結、執行時繫結):在執行時根據物件的型別進行繫結。
- Java中除了
static
方法和final
方法(private
方法屬於final
方法)之外,其他所有的方法都是後期繫結。 - 在講解
final
關鍵字的時候講到final
關鍵字曾經可以提高執行效率,原因就在於它可以關閉動態繫結,必須前期繫結。
8.2.2 產生正確的行為
在編譯時,編譯器不需要獲得任何特殊資訊就能進行正確的呼叫。
Cycle cycle = new Tricycle();
cycle.ride();
8.2.3 可擴充套件性
一個良好的OOP程式中,大多數或所有方法都會遵循基類的模型,而且只與基類介面通訊。
這樣的程式是可擴充套件的,因為可以從通用的基類繼承出新的資料型別。
多型是一項讓程式設計師“將改變的事物與未變的事物分離開來”的重要技術。
8.2.4 缺陷:“覆蓋私有方法”
- 父類的私有方法子類是無法過載的,即子類的方法是一個全新的方法
- 只有非private的方法才能被覆蓋
- 下述程式呼叫的依然是父類的對應方法
- 約定:子類中的方法不能和父類中的
private
方法同名,能用起個名字解決的問題不要搞得那麼複雜
public class Test {
public static void main(String[] args) {
Test test = new TestDemo();
test.func();
// Output: Test
}
private void func() {
System.out.println("Test");
}
}
class TestDemo extends Test {
public void func() {
System.out.println("TestDemo");
}
}
8.2.5 缺陷:域和靜態方法
- 只有普通方法的呼叫是多型的
- 當子類物件轉型為父類物件時,任何域訪問操作都由編譯器解析,因此不是多型的
- 如果某個方法是靜態的,那麼他就不是多型的
8.3 構造器和多型
構造器不具有多型性,因為它們也是隱式宣告為static
的
8.3.1 構造器的呼叫順序
- 基類的構造器總是在匯出類的構造過程中被呼叫,而且按照繼承層次逐漸想和那個連結,以便每個基類的構造器都能得到呼叫。
- 因為只有基類的構造器才有恰當的方法和許可權來初始化自己的元素,所以必須令所有構造器都得到呼叫,這樣才能正確的構造物件。
- 沒有明確指定基類構造器,就是呼叫預設構造器
物件呼叫構造器順序
- 呼叫基類構造器(從根構造器開始)
- 按宣告順序呼叫成員的初始化方法
- 呼叫匯出類的構造器
8.3.2 繼承與清理
- 通過組合和繼承方法來建立新類時,永遠不必擔心物件的清理問題,子物件通常會留給GC進行處理。
- 如果確實遇到清理的問題,在清理方法中要先寫子類的清理邏輯,然後呼叫父類的清理方法;即清理順序應該和初始化順序相反。
8.3.3 構造器內部的多型方法的行為
如果在構造器的內部呼叫正在構造的物件的某個動態繫結方法,會發生什麼情況?
初始化的實際過程:
- 在其他任何事情發生之前,將分配給物件的儲存空間初始化成二進位制的零。
- 如8.3.1中那樣呼叫基類構造器。因為在基類構造器中呼叫了
func()
,其實是被覆蓋的func()
方法。 - 按照宣告的順序呼叫成員的初始化方法。
- 呼叫匯出類的構造器主體。
public class Test {
public static void main(String[] args) {
new Child(100);
}
}
class Child extends Parent {
private int i;
void func() {
System.out.println("Child func, i = " + i);
}
public Child(int i) {
this.i = i;
System.out.println("Before Child constructor, i = " + i);
func();
System.out.println("After Child constructor, i = " + i);
}
}
class Parent {
void func() {
System.out.println("Parent func");
}
public Parent() {
System.out.println("Before Parent constructor");
func();
System.out.println("After Parent constructor");
}
}
Output:
Before Parent constructor
Child func, i = 0
After Parent constructor
Before Child constructor, i = 100
Child func, i = 100
After Child constructor, i = 100
編寫構造器準則:
- 用盡可能簡單的方法使物件進入正常狀態,如果可以的話,避免呼叫其他方法。
- 在構造器中唯一能夠安全呼叫的是基類中的
final
或private
方法,因為這些方法不會被覆蓋。上述程式碼中把Parent
中的func()
變成private
的會得到不一樣的結果。
8.4 協變返回型別
子類覆蓋(重寫)父類的方法時,可以返回父類返回型別的子類。
這是JSE 5之後增加的功能,如下所示。Child
中的func()
返回的是父類返回型別List
的子類ArrayList
。
class Child extends Parent {
@Override
ArrayList func() {
return null;
}
}
class Parent {
List func() {
return null;
}
}
8.5 用繼承進行設計
準則:用繼承表達行為間的差異,用欄位表達狀態上的變化。
8.5.1 純繼承與擴充套件
純繼承
- 只有在基類已經建立的方法才可以在匯出類中被覆蓋,純粹的
“is-a”
的關係。
擴充套件
- 由
extends
關鍵詞的意思可以看出,彷彿是希望我們在基類的基礎上擴充套件功能,即增加基類中不存在的方法,這可以稱為“”is-like-a“”
的關係。 - 這樣的缺點就是擴充套件部分不能被基類訪問,主要是在向上轉型的時候。
8.5.2 向下轉型與執行時型別識別(RTTI)
- 向上轉型是安全的,因為基類不會具有大於匯出類的介面。
- 向下轉型時會有執行時型別識別(Run-Time Type Identification)機制對型別進行檢查,如果發現轉型失敗,會丟擲一個執行時異常(
ClassCastException
)。 - RTTI的內容不僅包括轉型處理,還可以檢視物件型別。