**什么是繼承?**
繼承也是面向對象的重要特性之一。顧名思義,繼承就是指從已有的類中派生出新類的動作。新的類能吸收已有類的數據屬性和行為,并能擴展新的能力。
而通俗一點的來說,就是指Java中可以通過繼承的方式,從現有的類派生出新的類。該現有類被稱為超類(父類),而派生出的新類就被稱為子類(派生類)。
首先,子類能夠訪問繼承超類當中的所有非私有的方法和成員變量;其次,還可以在父類原有的成員的基礎上添加一些新的方法和域,或者對父類的方法進行覆寫(override)。
所有通常也這樣講:父類是子類的一般化表現形式;而子類是父類的特有化表現形式。
Java中使用關鍵字“extends”用于聲明一個類繼承自另一個類。
**繼承的體現**
首先,假定我們自定義了一個雇員類“Employee”:
~~~
package com.tsr.j2seoverstudy.extends_demo;
public class Employee {
private String name; // 姓名
private int salary; // 收入
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
}
~~~
一個部門中,雇員通常分為經理和普通雇員。二者之間大部分的行為相似,但可能在收入上略有不同。
假設普通雇員的收入來源為工資,而經理的收入結構則是由工資 + 績效獎金構的。于是這個時候,原有的雇員類就不足以描述經理了。
于是,使用繼承從原有的雇員類中派生出一個新的經理類“Manager”:
~~~
package com.tsr.j2seoverstudy.extends_demo;
public class Manager extends Employee {
// 子類新的特有實例域:績效獎金
private int bonus;
public int getBonus() {
return bonus;
}
public void setBonus(int bonus) {
this.bonus = bonus;
}
}
~~~
**
**
**繼承的特性**
**1、子類會繼承超類當中的方法以和實例域**
~~~
package com.tsr.j2seoverstudy.extends_demo;
public class JavaExtendsDemo {
public static void main(String[] args) {
Manager m = new Manager();
m.setName("張經理");
System.out.println(m.getName());
m.setBonus(20000);
System.out.println(m.getBonus());
}
}
~~~
在這里可以看到的是,我們在派生類“Manager”當中并沒有定義成員變量“name”,也沒有定義其相關的set/get方法。
但是我們仍然可以通過“Manager”類對這些成員進行訪問,正是因為繼承的機制帶來的。
“Manager”繼承自“Employee”類,所以“Emoloyee”類當中的所有非私有化的成員都被隱式的繼承到了子類“Manager”當中。
**2、方法覆寫(override)**
前面我們已經說過了,經理的收入結構為:工資 + 獎金。所以“Employee”類當中獲取雇員收入的方法“getSalary”就不適用于描述經理的收入了。
對于“Manager”類中,“getSalary”方法返回的值應當是:salary + bonus。這時候就涉及到繼承中一個重要的知識點:方法覆寫(override)
覆寫是指:除方法體以外,方法的所有聲明都當與父類中相同,而方法的訪問修飾符只能比父類更加寬松。
一定要牢記覆寫的規則,才不會再使用時出錯。下面通過一道網上看見的華為的面試題,來更形象的理解一下覆寫的概念:
~~~
package com.tsr.j2seoverstudy.extends_demo;
/*
* QUESTION NO: 3
*1. class A {
*2. protected int method1(int a, int b) { return 0; }
*3. }
*
* Which two are valid in a class that extends class A? (Choose two)
* A. public int method1(int a, int b) { return 0; }
* B. private int method1(int a, int b) { return 0; }
* C. private int method1(int a, long b) { return 0; }
* D. public short method1(int a, int b) { return 0; }
* E. static protected int method1(int a, int b) { return 0; }
*/
public class Test {
}
class A {
protected int method1(int a, int b) {
return 0;
}
}
class B extends A {
// 合法,通過提高訪問權限(protected → public)的方式覆寫父類方法。
public int method1(int a, int b) {
return 0;
}
// 不合法,注意覆寫的規則:方法的訪問修飾符只能比父類更加寬松。
private int method1(int a, int b) {
return 0;
}
// 合法,但并不是方法覆寫。而僅僅是在類B中,針對繼承的方法method1進行了重載;
private int method1(int a, long b) {
return 0;
}
// 不合法,首先方法返回類型不能作為方法重載的標示。那么就只能是覆寫的情況,但覆寫要求:除方法體外,方法所有聲明都與父類中相同。
public short method1(int a, int b) {
return 0;
}
// 不合法,同樣還是因為方法聲明與父類不同。
static protected int method1(int a, int b) {
return 0;
}
}
~~~
**
**
**3、引用父類關鍵字“super”**
我們在第2點特性中說到,對于“Manager”類中,“getSalary”方法返回的值應當是:salary + bonus。也就是說“Manager”類中,“getSalary”的實現應當為:
~~~
public int getSalary(){
return bonus + salary;
}
~~~
但實際上,這樣做肯定是行不通的,因為salary在父類中被聲明為私有的。所以在子類中是無法直接進行訪問的。
所以,我們只能通過父類中,針對于該成員變量提供的訪問方法“getSalary”獲取它的值。所以可能正確的實現應該是:
~~~
@Override
public int getSalary() {
return getSalary() + bonus;
}
~~~
但因為我們本身已經根據新的需求對該方法進行了覆寫,所有這樣做也是行不通的。
這是因為通過方法的覆寫,“Employee”類當中已經有了自己特有的“getSalary”方法,所以上面的做法實際上是在讓“getSalary”不斷的調用自身,直到程序崩潰。
想要調用到父類當中的成員,就可以使用Java的關鍵字之一:super。
super是Java提供的一個用于只是編譯器調用超類成員的特殊關鍵字。所以修改后的“getSalary”方法應當為:
~~~
@Override
public int getSalary() {
return super.getSalary() + bonus;
}
~~~
**4、多態以及動態綁定**
**多態**是指:同一事物根據上下文環境不同使用不同定義的能力。例如我們熟悉的重載、覆寫都是多態的一種體現。多態是面向對象的又一重要特性。
而多態在Java里繼承中的體現形式,主要分別為:
- 對象的多態:例如我們定義了一個超類動物類"Animal",其中老虎類"Tiger"和魚類"Fish"都繼承自"Animal"。這就是一種類的對象的多態體現。因為一個動物(Animal)構造出的對象既可以是一頭老虎"Tiger",也可以是一條魚“Fish”。
- 方法的多態:假設動物類“Animal”提供了一個動物呼吸的方法“Breath”。但因為老虎和魚的呼吸方式是不同的,老虎用肺呼吸,而魚通過腮呼吸。所以我們需要分別在對應的子類中覆寫“Breath”方法。這也方法的多態的一種體現形式。
**動態綁定**是指:程序在運行期(而不是編譯時期)根據對象的具體類型進行綁定,所以又被稱為運行時綁定。動態綁定的執行過程大概為:
1、首先,編譯器首先查看并獲取到對象的聲明類型和方法名;
然后在其聲明類型對應的相應類及其超類的方法表進行查找;
最終搜索出所有方法聲明為“pulbic”的對應方法名的方法,得到所有可能被執行的候選方法。
注:方法表也就是指,當一個類第一次運行,被類裝載器(classloader)進行裝載工作時,其自身及其超類的所有方法信息都會被加載到內存中的方法區內。
2、編譯器將查看調用方法時傳入的參數的類型。
如果在執行第一步工作中所獲得的所有候選方法中,存在一個與提供的參數類型完全符合,則會決定綁定調用該方法。
這個過程被稱為重載解析。也就可以明白:用重載表現的多態,其動態綁定的解析工作就是這樣完成的。
另外,由于Java允許類型轉換,所以這一步過程可能會很復雜。
如果沒有找到與參數類型匹配的方法,或者經過類型轉換過后有多個匹配的方法,則會報告編譯錯誤。
注:方法名+參數列表 = 方法簽名。所以前面我們談到的方法的覆寫規則也可以理解為:方法簽名必須與父類相同,訪問修飾符只能更寬松。
但值得注意的是,方法返回類型不是方法簽名的一部分。在JDK1.5之前,要求覆寫時,返回類型必須相同。
而在這之后的版本中,覆寫允許將父類返回類型的子類作為返回類型,這被稱為**協變返回類型**。
????
3、與之對應的:如果是被聲明為private、static、final的方法或構造器,編譯器將可以直接的準確知道應當調用的方法。這種方式又被稱為靜態綁定。
4、如果采用的是動態綁定的方式。當程序運行時,一定會選擇:對象引用所指向的實際對象所屬類型中,最合適的方法。
這也是為什么在繼承中,子類覆寫超類中的方法后。如果使用超類的類型進行聲明,而實際引用子類的對象的對象引用調用方法,會準確的調用到子類中覆寫后的方法的原因。
~~~
package com.tsr.j2seoverstudy.extends_demo;
public class Test {
public static void main(String[] args) {
Animal [] animals = new Animal[3];
animals[0] = new Animal();
animals[1] = new Tiger();
animals[2] = new Fish();
for (Animal animal : animals) {
animal.breath();
}
}
}
class Animal{
void breath(){
System.out.println("動物呼吸");
}
}
class Tiger extends Animal{
@Override
void breath() {
System.<span style="font-size:12px;">out</span>.println("老虎用肺呼吸");
}
}
class Fish extends Animal{
@Override
void breath() {
System.out.println("魚用腮呼吸");
}
}
/*
運行結果為:
動物呼吸
老虎用肺呼吸
魚用腮呼吸
*/
~~~
**5、繼承結構:單繼承和多重繼承**
Java中不支持多繼承。其繼承結構分為:單繼承和多重繼承。
顧名思義,單繼承也就是指:一個類只能繼承自一個超類。
而多重繼承則是指:C類繼承自B類,而B類繼承自A類這樣的繼承層次結構。
多重繼承的應用是很常見的。還是以動物為例,老虎繼承自動物。而老虎可能又分為東北虎之類的很多品種,那么新定義的東北虎類就應該繼承老虎類。
這就是多重繼承的一種常見體現形式。
6、繼承體系中子類的構造過程
~~~
package com.tsr.j2seoverstudy.extends_demo;
public class JavaExtendsDemo {
public static void main(String[] args) {
Son s = new Son();
}
}
class Far{
Far(){
System.out.println("父類構造初始化..");
}
}
class Son extends Far{
Son() {
//這里有一個隱式的構造語句:super(),調用父類的構造初始化工作..
System.out.println("子類構造初始化..");
}
}
/*
運行結果為:
父類構造初始化..
子類構造初始化..
*/
~~~
也就是說,子類的構造過程是依賴于父類的,當開始子類的構造工作之前,會先完成其所屬超類的對象構造工作。
這是理所應當的,因為我們已經知道子類的很多屬性與方法是依賴于父類的,如果在此之前不先完成父類的構造工作,對于子類的使用就很容易引起錯誤。
**
**
**什么時候使用繼承?**
要了解什么使用繼承,首先我們應當知道使用繼承可以帶來的好處是什么:
- 通過繼承構成體系,能讓程序的條理性更加清晰。
- 若出現特殊需求,子類可以較方便的改動父類的實現。
- 最大的好處在于方便實現代碼的復用,減少了編碼工作。
所以,最長在什么時候使用繼承呢?
1、聲名遠揚的“IS-A”關系
例如上面說到的“經理是一個雇員”,“老虎是一種動物”都是歸屬于這種關系。
2、想要通過繼承實現多態
以一段代碼更好理解,假如我們的程序提供了一個接口用于獲取動物的奔跑速度:
~~~
int getSpeed(Animal animal){
return animal.getRunSpeed();
}
~~~
通過繼承,通過這樣簡單的代碼。只要傳入的參數對象的類型位于該Animal的繼承體系當中,就可以通過動態綁定的機制正確獲取到其奔跑速度。
這實際上也是”策略設計模式“的一種體現方式。而如果假設不使用繼承的話,就需要提供對應數量的方法來分別獲取不同動物的奔跑速度。
但與此同時,繼承也有一定的弊端:
- 類繼承是在編譯時刻靜態定義的,所以無法在運行時改變繼承類的實現。
- 通過繼承的復用方式被稱為“白箱復用",因為父類的實現細節對子類可見。
- 父類通常都至少定義了子類的部分行為,所以父類的改變都有可能影響到子類的使用。
所以,正如這個世界上任何事物都有兩面性一樣,對于繼承的使用還是應該結合實際情況作出最適合的決定。
- 前言
- 第一個專欄《重走J2SE之路》,你是否和我有一樣的困擾?
- 磨刀不誤砍材工 - 環境搭建(為什么要配置環境變量)
- 磨刀不誤砍材工 - Java的基礎語言要素(定義良好的標示符)
- 磨刀不誤砍材工 - Java的基礎語言要素(關鍵字)
- 磨刀不誤砍材工 - Java的基礎語言要素(注釋-生成你自己的API說明文檔)
- 磨刀不誤砍材工 - Java的基礎語言要素(從變量/常量切入,看8種基本數據類型)
- 磨刀不誤砍材工 - Java的基礎語言要素(運算符和表達式的應用)
- 磨刀不誤砍材工 - Java的基礎語言要素(語句-深入理解)
- 磨刀不誤砍材工 - Java的基礎語言要素(數組)
- 換一個視角看事務 - 用&quot;Java語言&quot;寫&quot;作文&quot;
- 牛刀小試 - 淺析Java的繼承與動態綁定
- 牛刀小試 - 詳解Java中的接口與內部類的使用
- 牛刀小試 - 趣談Java中的異常處理
- 牛刀小試 - 詳解Java多線程
- 牛刀小試 - 淺析Java集合框架的使用
- 牛刀小試 - Java泛型程序設計
- 牛刀小試 - 詳細總結Java-IO流的使用