Java基礎教程(18)--繼承
一.繼承的概念
繼承是面向物件中一個非常重要的概念,使用繼承可以從邏輯和層次上更好地組織程式碼,大大提高程式碼的複用性。在Java中,繼承可以使得子類具有父類的屬性和方法或者重新定義、追加屬性和方法。在Java中,使用extends關鍵字來表示繼承。
從另一個類派生而來的類稱為子類(也稱為派生類),派生子類的類稱為父類(也稱為超類或基類)。當建立一個類時,總是在繼承(Object類除外),如果沒有明確指出要繼承的類,這個類就會隱式地繼承Object類。在Java中,Object類是所有類的祖先,所有類都是Object類的後代。
在上一篇教程中,我們知道介面可以多繼承。但是類不支援多繼承,也就是說每個類只能直接繼承自一個父類。
繼承的想法很簡單但又很強大。當你想建立一個新類並且已經有一個包含你想要的程式碼的類時,你可以從現有的類來派生你的新類。在這樣做時,你可以重用現有類的域和方法,而無需重新編寫它們。
繼承父類的成員
子類繼承了父類所有的public成員和protected成員(域、方法和巢狀類)。如果子類和父類在同一個包中,它還繼承了父類的包私有成員。可以按原樣使用繼承自父類的成員,也可以替換、隱藏或補充它們。構造方法不是成員,因此它們不會被子類繼承,但是可以在子類中呼叫父類的構造方法:
- 繼承的域可以直接使用,就像普通的域一樣。
- 可以在子類中宣告相同名稱的域來隱藏繼承自父類中的域(不推薦)。
- 可以宣告父類中不存在的新域。
- 繼承的方法可以直接使用,就像普通的方法一樣。
- 可以在子類中編寫簽名相同的例項方法來重寫繼承自父類中的方法。
- 可以在子類中編寫簽名相同的靜態方法來隱藏父類中的靜態方法。
- 可以在子類中編寫父類中不存在的方法。
- 可以在子類的構造方法中呼叫父類的構造方法,無論是隱式地或者顯式地通過super關鍵字去呼叫。
子類不繼承父類的private成員。但是,如果父類具有訪問其private欄位的public或protected方法,則子類也可以使用這些方法。
可以在子類中宣告相同名稱的域來隱藏繼承自父類中的域,即使它們型別不同。覆蓋之後只能通過super關鍵字去訪問。一般來說,不建議隱藏域,因為這樣做會大大降低程式碼的可讀性。
巢狀類可以訪問其外部類的所有私有成員。因此,子類繼承的public或protected巢狀類可以間接訪問父類的所有私有成員。
型別轉換
我們知道,可以將對一個物件的引用賦值給它對應的類型別的變數,例如:
MountainBike myBike = new MountainBike();
MountainBike類繼承自Bicycle類,而Bicycle類繼承自Object類。因此,我們可以說一個MountBike一定是一個Bicycle,也可以說一個MountainBike一定是一個Object,因此,下面的兩種寫法都是正確的:
Bicycle bicycle = new MountainBike();
Object obj = new MountainBike();
這種型別轉換是隱式的,也就是說父類型別的變數可以引用子類物件。這種將子類型別的變數轉換為父類型別的操作稱為向上轉型。實際上,在上一篇教程中,我們使用介面型別的變數去引用實現類的例項也是這向上轉型。
但反過來不一定成立。我們不能說一個Bicycle一定是一個MountBike,也不能說一個Object一定是一個MountBike。因此,如果我們像下面這樣寫:
Bicycle bicycle = new MountainBike();
MountBike myBike = bicycle;
則會得到一個編譯時錯誤。因為編譯器不知道bicycle是否引用了一個MountBike的例項。當然,如果我們確定bicycle引用的就是MountBike類的例項,可以對它進行型別轉換:
Bicycle bicycle = new MountainBike();
MountBike myBike = (MountBike) bicycle;
bicycle變數本來是Bicycle型別的,這裡我們將它進行轉換後賦值給了子類型別MountBike。這種型別轉換必須顯示指定,將父類型別的變數轉換為子類型別的操作稱為向下轉型。
這種寫法可以通過編譯,但是有可能產生執行時異常。例如:
String bicycle = "MountBike";
MountBike myBike = (MountBike) bicycle;
上面的寫法將會在執行的時候丟擲一個ClassCastException異常。因為bicycle引用的物件是String型別的,它無法轉換成MountBike型別。可以使用instanceof運算子來在轉換前進行檢測:
if (myBike instanceof MountBike) {
MountBike myBike = (MountBike) bicycle;
}
instanceof運算子前面是需檢測的變數,後面可以是類,也可以是介面。它用來檢測前面的物件是否是後面的類的例項或介面的實現類。
二.重寫或隱藏方法
1.重寫父類的例項方法
在子類中,可以重新編寫簽名相同(回憶一下,簽名相同是指兩個方法的名稱以及引數列表都相同)的方法來覆蓋繼承自父類的方法,這樣一來,子類中的方法將會覆蓋父類中的方法,當通過子類的物件去呼叫這個方法時,將會執行子類中的方法而不是父類中的方法。考慮下面的Animal類:
public class Animal {
public void method() {
System.out.println("Method from Animal.");
}
}
現在我們要編寫一個子類Tiger,並覆蓋父類中的method方法:
public class Tiger extends Animal {
public void method() {
System.out.println("Method from Tiger.");
}
}
當呼叫new Tiger().method()時,將會輸出“Method from Tiger.”。這樣就完成了方法的覆蓋,這種行為也稱為重寫。
但是,在覆蓋父類的過程中,必須要遵守以下幾個原則,否則編譯器將給出錯誤:
- 子類方法的許可權不能低於父類方法的許可權。例如,父類中的方法時public,子類覆蓋這個方法後將許可權修改為private,編譯器將給出錯誤提示;
- 子類方法的返回值必須和父類一樣或者小於父類方法的返回值型別。例如,父類方法的返回值是String,子類方法的返回值是Object,編譯器將給出錯誤提示;
- 子類方法丟擲的異常必須和父類一樣或者小於父類方法丟擲的異常(有關異常的內容會在以後的文章中進行介紹)。例如,父類丟擲IndexOutOfBoundException,子類丟擲Exception,編譯器將會給出錯誤提示。
在覆蓋父類方法時,可以在方法上面加上@Override註解(在Java基礎教程(16)--註解一文中有提到)。這個註解一來可以提高程式碼的可讀性,在閱讀程式碼時可以知道這個方法是在覆蓋父類方法;二來也可以讓編譯器幫我們校驗父類中是否存在這個方法,以避免手誤帶來的問題。
2.隱藏父類的靜態方法
如果在子類中定義了與父類簽名相同的靜態方法,那麼子類中的這個方法會將父類中的方法隱藏。。子類中與父類簽名相同的例項方法會將父類中的方法覆蓋,覆蓋之後父類中的方法在子類中將不存在。例如:
class SuperClass {
public void instanceMethod() {
System.out.println("Instance method in SuperClass.");
}
}
public class SubClass extends SuperClass {
public void instanceMethod() {
System.out.println("Instance method in SubClass.");
}
public static void main(String[] args) {
SubClass sub = new SubClass();
sub.instanceMethod();
SuperClass sup = sub;
sup.instanceMethod();
}
}
上面的例子會輸出:
Instance method in SubClass.
Instance method in SubClass.
子類覆蓋了父類的方法,所以子類中只有一個instanceMethod,無論怎麼變換引用變數的型別,都會呼叫子類重寫後的這個方法。
而子類中與父類簽名相同的靜態方法會將父類中的方法隱藏,也就是說實際上在子類中這兩個方法是同時存在的,如何去呼叫它們完全取決於引用變數的型別,例如:
class SuperClass {
public static void staticMethod() {
System.out.println("Static method in SuperClass.");
}
}
public class SubClass extends SuperClass {
public static void staticMethod() {
System.out.println("Static method in SubClass.");
}
public static void main(String[] args) {
SubClass sub = new SubClass();
sub.staticMethod();
SuperClass sup = sub;
sup.staticMethod();
}
}
上面的例子會輸出:
Static method in SubClass.
Static method in SuperClass.
子類隱藏了父類的靜態方法。如果通過子類型別的引用變數呼叫這個方法,則會選擇子類的靜態方法;如果通過父類型別的引用變數去呼叫這個方法,則會選擇父類的靜態方法。
3.介面中的方法
介面中的預設方法和抽象方法就像例項方法那樣被實現類繼承。然而,當父類和介面提供相同簽名的預設方法時,Java編譯器將會按照以下規則來解決衝突:
- 例項方法優先於介面的靜態方法。
考慮下面的類和介面:
public class Horse {
public String identifyMyself() {
return "I am a horse.";
}
}
public interface Flyer {
default public String identifyMyself() {
return "I am able to fly.";
}
}
public interface Mythical {
default public String identifyMyself() {
return "I am a mythical creature.";
}
}
public class Pegasus extends Horse implements Flyer, Mythical {
public static void main(String[] args) {
Pegasus myApp = new Pegasus();
System.out.println(myApp.identifyMyself());
}
}
上面的例子將會輸出“I am a horse.”。
- 已經被覆蓋的方法將被忽略。當父型別擁有共同的祖先時,可能會出現這種情況。考慮下面的類和介面:
public interface Animal {
default public String identifyMyself() {
return "I am an animal.";
}
}
public interface EggLayer extends Animal {
default public String identifyMyself() {
return "I am able to lay eggs.";
}
}
public interface FireBreather extends Animal { }
public class Dragon implements EggLayer, FireBreather {
public static void main (String... args) {
Dragon myApp = new Dragon();
System.out.println(myApp.identifyMyself());
}
}
上面的例子將輸出“I am able to lay eggs.”。
不只是兩個介面,一個類和一個介面也是如此:
public interface Animal {
default public String identifyMyself() {
return "I am an animal.";
}
}
public interface EggLayer extends Animal {
default public String identifyMyself() {
return "I am able to lay eggs.";
}
}
class FireBreather implements Animal { }
public class Dragon extends FireBreather implements EggLayer {
public static void main (String... args) {
Dragon myApp = new Dragon();
System.out.println(myApp.identifyMyself());
}
}
上面的例子仍然會輸出“I am able to lay eggs.”。
如果兩個或多個獨立定義的預設方法衝突,或者預設方法與抽象方法衝突,則Java編譯器會產生編譯錯誤。此時必須顯式覆蓋超型別方法。假設存在由計算機控制並且可以飛的汽車,現在有兩個介面提供了startEngine方法的預設實現:
public interface OperateCar {
// ...
default public int startEngine(EncryptedKey key) {
// Implementation
}
}
public interface FlyCar {
// ...
default public int startEngine(EncryptedKey key) {
// Implementation
}
}
同時實現了OperateCar和FlyCar介面的類必須覆蓋方法startEngine。不過可以通過super關鍵字來呼叫預設實現:
public class FlyingCar implements OperateCar, FlyCar {
// ...
public int startEngine(EncryptedKey key) {
FlyCar.super.startEngine(key);
OperateCar.super.startEngine(key);
}
}
super前面的型別必須定義或者繼承了被呼叫的預設方法。這種形式的呼叫用來區分多實現時簽名相同的預設方法。在類中或在介面中都可以使用super關鍵字來呼叫預設方法。
繼承的例項方法可以覆蓋抽象方法。考慮下面的例子:
public interface Mammal {
String identifyMyself();
}
public class Horse {
public String identifyMyself() {
return "I am a horse.";
}
}
public class Mustang extends Horse implements Mammal {
public static void main(String... args) {
Mustang myApp = new Mustang();
System.out.println(myApp.identifyMyself());
}
}
上面的例子將會輸出“I am a horse.”。Mustang類繼承了Horse的identifyMyself方法,這個方法剛好覆蓋了Mammal類中的抽象方法identifyMyself。
介面中的靜態方法永遠不會被繼承。
下面的表格總結了當定義與父類中籤名相同的方法時可能出現的情況:
||父類的例項方法|父類的靜態方法|
|--|--|--|
|子類的例項方法|覆蓋|編譯錯誤|
|子類的靜態方法|編譯錯誤|隱藏|
在子類中,可以對從父類中繼承的方法進行過載。這樣的方法既不隱藏也不覆蓋父類的方法,它們是新方法,對於子類來說是唯一的。
三.多型
在生物學中,多型的定義是說一個器官或物種會有不同的形態或階段。這個定義現在也被應用在像Java這樣的面嚮物件語言中。子類可以定義屬於它們自己的獨特的行為,但仍然保留父類中一些原始的功能。
例如,Bicycle類中有一個方法printDescription:
public void printDescription(){
System.out.println("Bike is in gear " + this.gear + " with a cadence of " +
this.cadence + " and travelling at a speed of " + this.speed + ". ");
}
為了演示多型性,我們定義了兩個Bicycle類的子類MountainBike和RoadBike。
首先建立MountainBike類,這裡我們給MountainBike類增加了一個suspension域,用來表示自行車的減震器型別:
public class MountainBike extends Bicycle {
private String suspension;
public MountainBike(
int startCadence,
int startSpeed,
int startGear,
String suspensionType){
super(startCadence,
startSpeed,
startGear);
this.setSuspension(suspensionType);
}
public String getSuspension(){
return this.suspension;
}
public void setSuspension(String suspensionType) {
this.suspension = suspensionType;
}
public void printDescription() {
super.printDescription();
System.out.println("The " + "MountainBike has a" +
getSuspension() + " suspension.");
}
}
注意被覆蓋的方法printDescription。不但輸出了繼承自Bicycle類中的屬性,還將suspension屬性也新增到了輸出中。
接下來建立RoadBike類。因為公路自行車有很多非常細的輪胎,這裡增加了一個tireWidth屬性來表示輪胎的寬度:
public class RoadBike extends Bicycle{
// In millimeters (mm)
private int tireWidth;
public RoadBike(int startCadence,
int startSpeed,
int startGear,
int newTireWidth){
super(startCadence,
startSpeed,
startGear);
this.setTireWidth(newTireWidth);
}
public int getTireWidth(){
return this.tireWidth;
}
public void setTireWidth(int newTireWidth){
this.tireWidth = newTireWidth;
}
public void printDescription(){
super.printDescription();
System.out.println("The RoadBike" + " has " + getTireWidth() +
" MM tires.");
}
}
這裡的printDescription方法和上面一樣,不但輸出了之前的屬性,還輸出了新增加的tireWidth屬性。
下面的測試程式建立了三個Bicycle變數,這三個變數分別引用了Bicycle、MountainBike和RoadBike類的例項:
public class TestBikes {
public static void main(String[] args){
Bicycle bike01, bike02, bike03;
bike01 = new Bicycle(20, 10, 1);
bike02 = new MountainBike(20, 10, 5, "Dual");
bike03 = new RoadBike(40, 20, 8, 23);
bike01.printDescription();
bike02.printDescription();
bike03.printDescription();
}
}
上面的程式輸出如下:
Bike is in gear 1 with a cadence of 20 and travelling at a speed of 10.
Bike is in gear 5 with a cadence of 20 and travelling at a speed of 10.
The MountainBike has a Dual suspension.
Bike is in gear 8 with a cadence of 40 and travelling at a speed of 20.
The RoadBike has 23 MM tires.
JVM為每個物件呼叫合適的方法,而不是完全呼叫變數型別中定義的方法。此行為稱為虛方法呼叫,它是Java語言中表現多型特徵的一種方式。
四.super關鍵字
1.訪問父類成員
如果子類覆蓋了父類的方法,可以通過super關鍵字去呼叫父類的方法。還可以使用super關鍵字去訪問被隱藏的域(儘管不鼓勵隱藏域)。
考慮下面的SuperClass類:
public class Superclass {
public void printMethod() {
System.out.println("Printed in Superclass.");
}
}
下面是它的子類SubClass,它覆蓋了printMethod方法:
public class Subclass extends Superclass {
// overrides printMethod in Superclass
public void printMethod() {
super.printMethod();
System.out.println("Printed in Subclass.");
}
public static void main(String[] args) {
Subclass s = new Subclass();
s.printMethod();
}
}
在上面的例子中,雖然printMethod方法被覆蓋,但仍然可以通過super關鍵字去呼叫它。這個程式的輸出如下:
Printed in Superclass.
Printed in Subclass.
2.父類構造器
可以通過super關鍵字來呼叫父類的構造方法。在上面的MountBike類的構造方法中,它先是呼叫了父類的構造方法,然後又添加了自己的初始化程式碼:
public MountainBike(int startHeight, int startCadence, int startSpeed, int startGear) {
super(startCadence, startSpeed, startGear);
seatHeight = startHeight;
}
呼叫父類構造器的語法如下:
super();
或
super(parameter list);
super()將會呼叫父類的無參構造方法,super(parameter list)將會呼叫匹配引數列表的父類構造方法。呼叫父類構造方法的語句必須放在子類構造方法的第一行。
如果子類的構造方法沒有顯式地呼叫父類的構造方法,編譯器在編譯時將會自動在子類構造方法的第一行插入對父類無參構造方法的呼叫。如果父類沒有無參構造方法,則會產生編譯錯誤。Object類有無參構造方法,如果Object類是唯一的父類,則不會出現這個問題。
由於子類的構造方法會顯式或隱式地呼叫父類的構造方法,而父類構造方法又會顯式或隱式地呼叫它的父類的構造方法,這樣最終一定會回到Object類的構造方法,我們將這種行為稱為構造方法鏈。
五.final關鍵字
可以將在方法前使用final關鍵字,這樣這個方法將不能被子類重寫。Object類中就有不少這樣的方法,例如getClass方法:
public final Class<?> getClass()
這意味著無法在任何一個類中重寫getClass()方法。
在構造方法中呼叫的方法通常應該生命為final。如果建構函式呼叫非final的方法,那麼子類重寫這個方法後可能會造成預想不到的結果。
還可以將類宣告為final。宣告為final的類不能被繼承。例如String類就是一個final類,沒有任何一個類可以繼承它。
六.抽象類和抽象方法
抽象類是在類定義前使用了abstract關鍵字的類,它既可以包含也可以不包含抽象方法。抽象類無法例項化,但是可以被繼承。
抽象方法是指沒有具體實現的方法,例如:
abstract void moveTo(double deltaX, double deltaY);
抽象類中的抽象方法前面必須要使用abstract關鍵字,而介面中的抽象方法則不需要使用abstract關鍵字(可以使用,但沒必要)。抽象方法前不可以使用private修飾符,因為抽象類的抽象方法是一定要被子類繼承或重寫的,如果使用private修飾符,那麼子類將無法繼承父類的抽象方法。正是因為抽象類的抽象方法一定要被子類繼承或重寫,因此protected修飾符也是沒有意義的。此外,如果不適用許可權修飾符,則抽象方法的許可權是包私有的,這種寫法在當前抽象類中雖然不會產生錯誤,但當子類與這個抽象類不在同一個包中時,子類無法繼承父類的抽象方法,也會產生錯誤。綜上所述,抽象類的抽象方法前必須使用public許可權修飾符。
如果某個類中包含抽象方法,那麼這個類必須宣告為abstract。子類會繼承父類的抽象方法。只要子類中有沒覆蓋的抽象方法,那麼子類也必須宣告為abstract。
一般來說,一個類在實現介面時,必須實現該介面的全部抽象方法。但是如果沒有實現所有的抽象方法,則需要將這個類宣告為抽象類。
抽象類與介面的比較
下面從語法方面對抽象類與介面進行比較:
- 抽象方法:抽象類與介面中都既可以包含抽象方法,也可以不包含抽象方法。但不同的是,抽象類中的抽象方法前面必須要使用abstract關鍵字,而介面中的抽象方法則不需要;抽象類的抽象方法的許可權修飾符必須使用public修飾符,而介面中的抽象方法則不需要,因為介面中的抽象方法預設是且必須是public的。
- 預設方法:從Java8開始,介面中可以為方法提供預設實現,這樣的方法稱為預設方法,預設方法前要使用default關鍵字。而抽象類中也可以為方法提供實現,不過我們並不將這樣的方法稱為預設方法,為了與抽象方法區分,我們將抽象類中除了抽象方法之外的方法稱為具體方法。介面中的預設方法預設是且必須是public,而抽象類中的具體方法則可以使用任何許可權修飾符。
- 靜態方法:就像普通的類一樣,抽象類中也可以有靜態方法。從Java8開始,介面中也可以定義靜態方法。抽象類中的靜態方法可以使用任何許可權修飾符,而介面中的靜態方法則預設是且只能是public。
- 域:抽象類和介面中都可以定義域。不同的是,介面中的域預設都是public、static和final的,而抽象類則沒有此限制,可以在抽象類中宣告非public域、非static域和非final域。
- 繼承:和普通類一樣,抽象類只支援單繼承,而介面則支援多繼承。
接下來,從設計層面上對抽象類與介面進行比較:
宣告:以下內容節選自大神海子的《深入理解Java的介面和抽象類》一文,原文連結深入理解Java的介面和抽象類,如有侵權請聯絡本人刪除。轉載本文時請保留此段宣告,否則保留追究其法律責任的權利。
- 抽象類是對一種事物的抽象,即對類抽象,而介面是對行為的抽象。抽象類是對整個類整體進行抽象,包括屬性、行為,但是介面卻是對類區域性(行為)進行抽象。舉個簡單的例子,飛機和鳥是不同類的事物,但是它們都有一個共性,就是都會飛。那麼在設計的時候,可以將飛機設計為一個類Airplane,將鳥設計為一個類Bird,但是不能將 飛行 這個特性也設計為類,因此它只是一個行為特性,並不是對一類事物的抽象描述。此時可以將 飛行 設計為一個介面Fly,包含方法fly( ),然後Airplane和Bird分別根據自己的需要實現Fly這個介面。然後至於有不同種類的飛機,比如戰鬥機、民用飛機等直接繼承Airplane即可,對於鳥也是類似的,不同種類的鳥直接繼承Bird類即可。從這裡可以看出,繼承是一個 "是不是"的關係,而 介面 實現則是 "有沒有"的關係。如果一個類繼承了某個抽象類,則子類必定是抽象類的種類,而介面實現則是有沒有、具備不具備的關係,比如鳥是否能飛(或者是否具備飛行這個特點),能飛行則可以實現這個介面,不能飛行就不實現這個介面。
- 設計層面不同,抽象類作為很多子類的父類,它是一種模板式設計。而介面是一種行為規範,它是一種輻射式設計。什麼是模板式設計?最簡單例子,大家都用過ppt裡面的模板,如果用模板A設計了ppt B和ppt C,ppt B和ppt C公共的部分就是模板A了,如果它們的公共部分需要改動,則只需要改動模板A就可以了,不需要重新對ppt B和ppt C進行改動。而輻射式設計,比如某個電梯都裝了某種報警器,一旦要更新報警器,就必須全部更新。也就是說對於抽象類,如果需要新增新的方法,可以直接在抽象類中新增具體的實現,子類可以不進行變更;而對於介面則不行,如果介面進行了變更,則所有實現這個介面的類都必須進行相應的改動。
下面看一個網上流傳最廣泛的例子:門和警報的例子:門都有open( )和close( )兩個動作,此時我們可以定義通過抽象類和介面來定義這個抽象概念:abstract class Door { public abstract void open(); public abstract void close(); }
或者:
interface Door { public abstract void open(); public abstract void close(); }
但是現在如果我們需要門具有報警alarm( )的功能,那麼該如何實現?下面提供兩種思路:
1) 將這三個功能都放在抽象類裡面,但是這樣一來所有繼承於這個抽象類的子類都具備了報警功能,但是有的門並不一定具備報警功能;
2) 將這三個功能都放在接口裡面,需要用到報警功能的類就需要實現這個介面中的open( )和close( ),也許這個類根本就不具備open( )和close( )這兩個功能,比如火災報警器。
從這裡可以看出, Door的open() 、close()和alarm()根本就屬於兩個不同範疇內的行為,open()和close()屬於門本身固有的行為特性,而alarm()屬於延伸的附加行為。因此最好的解決辦法是單獨將報警設計為一個介面,包含alarm()行為,Door設計為單獨的一個抽象類,包含open和close兩種行為。再設計一個報警門繼承Door類和實現Alarm介面。interface Alram { void alarm(); } abstract class Door { void open(); void close(); } class AlarmDoor extends Door implements Alarm { void oepn() { //.... } void close() { //.... } void alarm() { //.... } }
在瞭解了介面和抽象類的區別和聯絡後,我們在編寫程式碼時就可以根據自己的需求來靈活選擇使用介面還是抽象類。