1. 程式人生 > 實用技巧 >JavaSE第10篇:面向物件之繼承

JavaSE第10篇:面向物件之繼承

目錄

本篇我們將繼續學習面向物件程式設計,在之前我們已經學習過了面向物件之封裝,知道了如何定義一個標準的類及如何建立和使用物件。面向物件有三大特徵,封裝、繼承、多型。那麼接下來,我們將會學習面向物件程式設計的繼承。

第一章:繼承

1.1-繼承概述(瞭解)

什麼是繼承

繼承,指的是事物與事物之間的關係。

在生活中,我們所理解的“繼承”更多的是“子承父業”,就是兒子與父親之間存在“繼承”關係。

在Java程式設計中,我們用類來描述事物,那麼在面向物件程式設計中,“繼承”指的是類與類之間的關係,通常是子類父類之間的關係,子類可以繼承父類中的非私有成員(屬性和方法)。

為什麼要有繼承

繼承有什麼好處呢?

在生活中,很顯然,兒子繼承父親的財產,可以少奮鬥幾十年甚至幾輩子。

在程式設計中,繼承有什麼作用呢?我們先來看一個需求,需求如下:

  • 用面向物件的方式,描述幾種動物,狗、貓、牛
    • 狗,有名字、年齡、性別、毛色、愛吃食物、會看門
    • 貓,有名字、年齡、性別、毛色、愛吃食物、會撒嬌
    • 牛,有名字、年齡、性別、毛色、愛吃食物、會耕地

此時面臨這個需求時,我們通常會定義三種類來描述,描述如下:

我們觀察可以發現,會存在這樣一種現象:貓、狗、牛,存在一些共性的成員,這些共性都分別在不同的類中定義了。

問題:這樣在程式設計中,就出現的程式碼冗餘(重複),若是以後有更多的動物種類出現,還是要重新定義它們的共性成員,在整個工程看來,程式碼將會變得越來越冗餘,越來越臃腫,不易於程式後期的維護。那如何解決呢?

解決:此時就可以使用繼承機制解決程式碼的冗餘問題,抽取共性,讓共性複用。抽取的方式就是,抽象出更高階的類(超類、父類),把共性定義在父類中,讓其他也要擁有這些共性成員的類作為子類繼承父類。

對於上述需求,我們可以抽象出它們的父類,動物類。它們都是動物,動物都有屬性-名字、年齡、毛色、性別,都是吃貨,最終類的定義描述如下:

所以,在面向物件程式設計中,繼承的作用:

  • 提高程式碼的複用性,減少程式碼冗餘
  • 繼承是多型的基礎(後續講解)

總結

  1. 什麼是繼承:
    • 繼承就是,子類與父類之間的關係,子類可以繼承父類中的成員。
  2. 繼承的作用:
    • 提高程式碼複用性,減少程式碼冗餘。

1.2-繼承的格式(記憶)

通過上述講解,我們知道了什麼是繼承以及繼承的作用,接下來我們來學習一下Java中定義繼承的格式。

格式

關鍵字:extends

class 父類 {
	...
}

class 子類 extends 父類 {
	...
}

示例

父類:動物類

package www.penglei666.com.demo03;
/**
 * 動物類,父類
 */
public class Animal {
    String name;
    int age;
    String color;
    String gender;
    public void eat(){
        System.out.println("我是吃貨!");
    }
}

子類:狗類

package www.penglei666.com.demo03;

/**
 * 狗類,子類,繼承了Animal
 */
public class Dog extends Animal {
    public void alert(){
        System.out.println("警告,我是看門專家");
    }
}

測試類:Test

package www.penglei666.com.demo03;

public class Test {
    public static void main(String[] args) {
        Dog wc = new Dog();
        // 可以使用父類中的屬性:name、age、gender、color
        wc.name = "旺財";
        wc.age = 10;
        wc.gender = "公";
        wc.color = "yellow";
        // 可以呼叫父類中的方法:eat,也可以呼叫自己的方法
        wc.eat();   // 我是吃貨!
        wc.alert(); // 警告,我是看門專家

    }
}

在上述程式碼中,Dog類通過extends關鍵字繼承了Animal類,這樣Dog類便是Animal類的子類。

從執行結果不難看出,子類雖然沒有定義name、age、gender等屬性和eat方法,但是卻能操作這幾個成員。這就說明,子類在繼承父類的時候,會自動擁有父類的成員。

1.3-super關鍵字(記憶)

若是父類中的成員和子類中的成員重名,子類物件在呼叫重名的成員時,會使怎樣的現象呢?

子類和父類成員重名時

父類:Animal

package www.penglei666.com.demo03;
/**
 * 動物類,父類
 */
public class Animal {
    int age = 10;
}

子類:Dog

package www.penglei666.com.demo03;

/**
 * 狗類,子類,繼承了Animal
 */
public class Dog extends Animal {
    int age = 11;
    public void printAge(){
        System.out.println("年齡:" + this.age);
    }
}

測試類:Test

package www.penglei666.com.demo03;

public class Test {
    public static void main(String[] args) {
        Dog wc = new Dog();
        wc.printAge(); // 輸出結果:年齡:11
    }
}

通過輸出結果可以發現,輸出的並不是父類中的結果值,而是子類自己的。

那麼,如何在重名的情況下,如何訪問到父類中的成員呢?此時可以使用關鍵字:super

我們之前,學習過this關鍵字,表示代表呼叫者本身的引用,而super關鍵字,表示父類的引用。

super 關鍵字的使用

使用格式:super.父類成員變數名

修改子類程式碼:Dog類

package www.penglei666.com.demo03;

/**
 * 狗類,子類,繼承了Animal
 */
public class Dog extends Animal {
    int age = 11;
    public void printAge(){
        System.out.println("父age:" + super.age);
        System.out.println("子age:" + this.age);
    }
}

測試類:Test

package www.penglei666.com.demo03;

public class Test {
    public static void main(String[] args) {
        Dog wc = new Dog();
        wc.printAge(); 
        /* 輸出結果:
        	父age:10
			子age:11
        */
    }
}

1.4-super和this關鍵字(理解)

父類空間優先於子類物件的產生

在每次建立子類物件時,先初始化父類空間,再建立其子類物件本身。

目的在於子類物件中包含了其對應的父類空間,便可以包含其父類的成員,如果父類成員非private修飾,則子類可以隨意使用父類成員。

程式碼體現在子類的構造方法呼叫時,一定先呼叫父類的構造方法。理解圖解如下:

super和this的含義:

  • super :代表父類的儲存空間標識(可以理解為父親的引用)。
  • this :代表當前物件的引用(誰呼叫就代表誰)。

super和this呼叫成員屬性和方法

this.成員變數    	--    本類的
super.成員變數    	--    父類的

this.成員方法名()  	--    本類的    
super.成員方法名()   --    父類的

super和this呼叫構造方法

this(...)    	--    本類的構造方法
super(...)   	--    父類的構造方法

子類的每個構造方法中均有預設的super(),呼叫父類的空參構造。

手動呼叫父類構造會覆蓋預設的super()。

super() 和 this() 都必須是在構造方法的第一行,所以不能同時出現。

1.5-子類可重寫父類方法(理解)

方法重寫Override

如果子類父類中出現不重名的成員方法,這時的呼叫是沒有影響的

物件呼叫方法時,會先在子類中查詢有沒有對應的方法,若子類中存在就會執行子類中的方法,若子類中不存在就會執行父類中相應的方法。程式碼如下:

父類:Fu

package www.penglei666.com.demo04;

public class Fu {
    public void fn1(){
        System.out.println("父類方法:fn");
    }
}

子類:Zi

package www.penglei666.com.demo04;

public class Zi extends Fu {
    @Override
    public void fn2() {
        System.out.println("Zi類中的方法:fn");
    }
}

測試類:Test

package www.penglei666.com.demo04;

public class Test {
    public static void main(String[] args) {
        Zi zi = new Zi();
        zi.fn1(); // 輸出結果:父類方法:fn
        zi.fn2(); // 輸出結果:Zi類中的方法:fn
    }
}

如果子類父類中出現重名的成員方法,這時的訪問是一種特殊情況,叫做方法重寫 (Override)。

方法重寫 :子類中出現與父類一模一樣的方法時(返回值型別,方法名和引數列表都相同),會出現覆蓋效果,也稱為重寫或者複寫。宣告不變,重新實現

程式碼如下:

父類:Fu

package www.penglei666.com.demo04;

public class Fu {
    public void fn(){
        System.out.println("父類方法:fn");
    }
}

子類:Zi

package www.penglei666.com.demo04;

public class Zi extends Fu {
    @Override
    public void fn() {
        System.out.println("Zi類中的方法:fn");
    }
}

測試類:

package www.penglei666.com.demo04;

public class Test {
    public static void main(String[] args) {
        Zi zi = new Zi();
        zi.fn(); // 輸出結果:Zi類中的方法:fn
    }
}

方法重寫的應用

子類可以根據需要,定義特定於自己的行為。

既沿襲了父類的功能名稱,又根據子類的需要重新實現父類方法,從而進行擴充套件增強。

比如新的手機增加來電顯示頭像的功能,程式碼如下:

父類:Phone

package www.penglei666.com.demo05;

/**
 * 父類:Phone
 */
public class Phone {
    public void sendMessage(){
        System.out.println("傳送簡訊");
    }
    public void call(){
        System.out.println("撥打電話");
    }
    public void showNum(){
        System.out.println("來電顯示對方號碼");
    }
}

子類:

package www.penglei666.com.demo05;

/**
 * 子類:NewPhone
 */
public class NewPhone extends Phone {
    //重寫父類的來電顯示號碼功能,並增加自己的顯示姓名和圖片功能
    public void showNum(){
        //呼叫父類已經存在的功能使用super
        super.showNum();
        //增加自己特有顯示姓名和圖片功能
        System.out.println("來電顯示對方姓名");
        System.out.println("來電顯示對方頭像");
    }
}

測試類:Test

package www.penglei666.com.demo05;

public class Test {
    public static void main(String[] args) {
        NewPhone np = new NewPhone();
        np.showNum();
        /**
         * 輸出結果:
         * 來電顯示號碼
         * 來電顯示對方姓名
         * 來電顯示對方頭像
         */
    }
}

這裡重寫時,用到super.父類成員方法,表示呼叫父類的成員方法。

1.6-子類的初始化過程(理解)

構造方法的作用是初始化成員變數的。所以子類的初始化過程中,必須先執行父類的初始化動作。子類的構造方法中預設有一個super() ,表示呼叫父類的構造方法,父類成員變數初始化後,才可以給子類使用。程式碼如下:

父類:

package www.penglei666.com.demo06;

public class Fu {
    public Fu(){
        System.out.println("Fu類構造方法初始化");
    }
}

子類:

package www.penglei666.com.demo06;

public class Zi extends Fu {
    public Zi(){
        // super(); 預設呼叫super
        System.out.println("Zi類構造方法初始化");
    }
}

測試類:

package www.penglei666.com.demo06;

public class Test {
    public static void main(String[] args) {
        Zi zi = new Zi();
        /* 執行結果:
        	Fu類構造方法初始化
			Zi類構造方法初始化
        */
    }
    
}

1.7-繼承的特點(理解)

特點1:Java只支援單繼承,不支援多繼承。

//一個類只能有一個父類,不可以有多個父類。
class C extends A{} 	//ok
class C extends A,B...	//error

特點2:Java支援多層繼承(繼承體系)。

class A{}
class B extends A{}
class C extends B{}

第二章:抽象類

2.1-抽象類概述(瞭解)

抽象類的由來

當編寫一個類時,我們往往會為該類定義一些方法,這些方法是用來描述該類的功能具體實現方式,那麼這些方法都有具體的方法體。

分析事物時,發現了共性內容,就出現向上抽取(父類中定義)。

但是,可能會有這樣一種特殊情況,就是方法功能宣告相同,但方法功能主體不同。

那麼這時也可以抽取,但只抽取方法宣告,不抽取方法主體。那麼此方法就是一個抽象方法。

如:

  • 描述狗的行為:吃
  • 描述貓的行為:吃
  • 描述牛的行為:吃

狗、貓、牛之間有共性,可以進行向上抽取父類定義共性。

抽取它們的所屬共性型別:動物。

由於狗、貓、牛都具有吃的功能,但是它們吃的食物不一樣(比如:開篇我們的需求狗、貓、牛都是吃貨,都有吃的行為,但是吃的食物第不同的,狗啃骨頭、貓吃魚、牛吃草)。

這時在描述動物類時,發現了有些功能不能夠具體描述,那麼,這些不具體的功能,需要在類中標識出來,通過java中的關鍵字abstract(抽象)修飾。當定義了抽象方法的類也必須被abstract關鍵字修飾,被abstract關鍵字修飾的類是抽象類

總結

  • 抽象方法 : 沒有方法體的方法。
  • 抽象類:包含抽象方法的類。

2.2-抽象類、抽象方法的定義和使用(記憶)

抽象方法:

使用abstract 關鍵字修飾方法,該方法就成了抽象方法,抽象方法只包含一個方法名,而沒有方法體。

定義格式:

修飾符 abstract 返回值型別 方法名 (引數列表);

示例程式碼:

public abstract void eat();

抽象類:

如果一個類包含抽象方法,那麼該類必須是抽象類。

定義格式:

public abstract class 類名字 { 
  
}

示例程式碼:

public abstract class Animal { 
  public abstract void eat();
}

抽象類的使用:

繼承抽象類的子類必須重寫父類所有的抽象方法。否則,該子類也必須宣告為抽象類。最終,必須有子類實現該父類的抽象方法,否則,從最初的父類到最終的子類都不能建立物件,失去意義。

父類:Animal

package www.penglei666.com.demo07;
public abstract class Animal {
    public abstract void eat();
}

子類:Dog

package www.penglei666.com.demo07;
public class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("我愛啃骨頭");
    }
}

此時的方法重寫,是子類對父類抽象方法的完成實現,我們將這種方法重寫的操作,也叫做實現方法

2.3-注意事項(瞭解)

  1. 抽象類不能建立物件,如果建立,編譯無法通過而報錯。只能建立其非抽象子類的物件。
    • 理解方式:假設建立了抽象類的物件,呼叫抽象的方法,而抽象方法沒有具體的方法體,沒有意義。
  2. 抽象類中,可以有構造方法,是供子類建立物件時,初始化父類成員使用的。
    • 理解方式:子類的構造方法中,有預設的super(),需要訪問父類構造方法。
  3. 抽象類中,可以有成員變數。
    • 理解方式:子類的共性的成員變數 , 可以定義在抽象父類中。
  4. 抽象類中,不一定包含抽象方法,但是有抽象方法的類必定是抽象類。
    • 理解方式:未包含抽象方法的抽象類,目的就是不想讓呼叫者建立該類物件,通常用於某些特殊的類結構設計。
  5. 抽象類的子類,必須重寫抽象父類中所有的抽象方法,否則,編譯無法通過而報錯。除非該子類也是抽象類。
    • 理解方式:假設不重寫所有抽象方法,則類中可能包含抽象方法。那麼建立物件後,呼叫抽象的方法,沒有意義。

第三章:綜合案例

案例需求:

某IT公司有多名員工,按照員工負責的工作不同,進行了部門的劃分(研發部員工、維護部員工)。研發部根據所需研發的內容不同,又分為JavaEE工程師、Android工程師;維護部根據所需維護的內容不同,又分為網路維護工程師、硬體維護工程師。

公司的每名員工都有他們自己的員工編號、姓名,並要做它們所負責的工作。

工作內容:

  • JavaEE工程師: 員工號為xxx的 xxx員工,正在研發淘寶網站

  • Android工程師:員工號為xxx的 xxx員工,正在研發淘寶手機客戶端軟體

  • 網路維護工程師:員工號為xxx的 xxx員工,正在檢查網路是否暢通

  • 硬體維護工程師:員工號為xxx的 xxx員工,正在修復印表機

請根據描述,完成員工體系中所有類的定義,並指定類之間的繼承關係。進行XX工程師類的物件建立,完成工作方法的呼叫。

案例分析:

根據上述部門的描述,得出如下的員工體系圖

根據員工資訊的描述,確定每個員工都有員工編號、姓名、要進行工作。則把這些共同的屬性與功能抽取到父類中(員工類),關於工作的內容由具體的工程師來進行指定。

工作內容:

  • JavaEE工程師:員工號為xxx的 xxx員工,正在研發淘寶網站
  • Android工程師:員工號為xxx的 xxx員工,正在研發淘寶手機客戶端軟體
  • 網路維護工程師:員工號為xxx的 xxx員工,正在檢查網路是否暢通
  • 硬體維護工程師:員工號為xxx的 xxx員工,正在修復印表機

建立JavaEE工程師物件,完成工作方法的呼叫。

實現程式碼:

員工類:Employee

public abstract class Employee {
	private String id;// 員工編號
	private String name; // 員工姓名

	public String getId() {
		returnid;
	}
	publicvoid setId(String id) {
		this.id = id;
	}
	public String getName() {
		returnname;
	}
	publicvoid setName(String name) {
		this.name = name;
	}
	
	//工作方法(抽象方法)
	public abstract void work(); 
}

定義研發部員工類Developer 繼承 員工類Employee

public abstract class Developer extends Employee {
}

定義維護部員工類Maintainer 繼承 員工類Employee

public abstract class Maintainer extends Employee {
}

定義JavaEE工程師 繼承 研發部員工類,重寫工作方法

public class JavaEE extends Developer {
	@Override
	public void work() {
		System.out.println("員工號為 " + getId() + " 的 " + getName() + " 員工,正在研發淘寶網站");
	}
}

定義Android工程師 繼承 研發部員工類,重寫工作方法

public class Android extends Developer {
	@Override
	public void work() {
		System.out.println("員工號為 " + getId() + " 的 " + getName() + " 員工,正在研發淘寶手機客戶端軟體");
	}
}

定義Network網路維護工程師 繼承 維護部員工類,重寫工作方法

public class Network extends Maintainer {
	@Override
	public void work() {
		System.out.println("員工號為 " + getId() + " 的 " + getName() + " 員工,正在檢查網路是否暢通");
	}
}

定義Hardware硬體維護工程師 繼承 維護部員工類,重寫工作方法

public class Hardware extends Maintainer {
	@Override
	public void work() {
		System.out.println("員工號為 " + getId() + " 的 " + getName() + " 員工,正在修復印表機");
	}
}

在測試類中,建立JavaEE工程師物件,完成工作方法的呼叫

public class Test {
	public static void main(String[] args) {
		//建立JavaEE工程師員工物件
		JavaEE ee = new JavaEE();
		//設定該員工的編號
		ee.setId("000015");
		//設定該員工的姓名
		ee.setName("小明");
		//呼叫該員工的工作方法
		ee.work();
	}
}