1. 程式人生 > >《Java從小白到大牛》之第12章 繼承與多態

《Java從小白到大牛》之第12章 繼承與多態

數值類型 大牛 聲明 清華 school 重寫 協作圖 類型變量 caf

《Java從小白到大牛》紙質版已經上架了!!!
技術分享圖片

類的繼承性是面向對象語言的基本特性,多態性前提是繼承性。Java支持繼承性和多態性。這一章討論Java繼承性和多態性。

Java中的繼承 {#java}

為了了解繼承性,先看這樣一個場景:一位面向對象的程序員小趙,在編程過程中需要描述和處理個人信息,於是定義了類Person,如下所示:

//Person.java文件

package com.a51work6;

import java.util.Date;

public class Person {

// 名字

private String name;

// 年齡

private int age;

// 出生日期

private Date birthDate;

public String getInfo() {

return "Person [name=" + name

+ ", age=" + age

+ ", birthDate=" + birthDate + "]";

}

}

一周以後,小趙又遇到了新的需求,需要描述和處理學生信息,於是他又定義了一個新的類Student,如下所示:

//Student.java文件

package com.a51work6;

import java.util.Date;

public class Student {

// 所在學校

public String school;

// 名字

private String name;

// 年齡

private int age;

// 出生日期

private Date birthDate;

public String getInfo() {

return "Person [name=" + name

+ ", age=" + age

+ ", birthDate=" + birthDate + "]";

}

}

很多人會認為小趙的做法能夠理解並相信這是可行的,但問題在於Student和Person兩個類的結構太接近了,後者只比前者多了一個屬性school,卻要重復定義其他所有的內容,實在讓人“不甘心”。Java提供了解決類似問題的機制,那就是類的繼承,代碼如下所示:

//Student.java文件

package com.a51work6;

import java.util.Date;

public class Student extends Person {

// 所在學校

private String school;

}

Student類繼承了Person類中的所有成員變量和方法,從上述代碼可以見繼承使用的關鍵字是extends,extends後面的Person是父類。

如果在類的聲明中沒有使用extends關鍵字指明其父類,則默認父類為Object類,java.lang.Object類是Java的根類,所有Java類包括數組都直接或間接繼承了Object類,在Object類中定義了一些有關面向對象機制的基本方法,如equals()、toString()和finalize()等方法。

提示 一般情況下,一個子類只能繼承一個父類,這稱為“單繼承”,但有的情況下一個子類可以有多個不同的父類,這稱為“多重繼承”。在Java中,類的繼承只能是單繼承,而多重繼承可以通過實現多個接口實現。也就是說,在Java中,一個類只能繼承一個父類,但是可以實現多個接口。

提示 面向對象分析與設計(OOAD)時,會用到UML圖[^11],其中類圖非常重要,用來描述系統靜態結構。Student繼承Person的類圖如圖12-1所示。類圖中的各個元素說明如圖12-2所示,類用矩形表示,一般分為上、中、下三個部分,上部分是類名,中部分是成員變量,下部分是成員方法。實線+空心箭頭表示繼承關系,箭頭指向父類,箭頭末端是子類。UML類圖中還有很多關系,如圖12-3所示,如圖虛線+空心箭頭表示實線關系,箭頭指向接口, 箭頭末端是實線類。
技術分享圖片

技術分享圖片

技術分享圖片

[^11]: UML是Unified Modeling Language的縮寫,既統一標準建模語言。它集成了各種優秀的建模方法學發展而來的。UML圖常用的有例圖、協作圖、活動圖、序列圖、部署圖、構件圖、類圖、狀態圖。

調用父類構造方法

當子類實例化時,不僅需要初始化子類成員變量,也需要初始化父類成員變量,初始化父類成員變量需要調用父類構造方法,子類使用super關鍵字調用父類構造方法。

下面看一個示例,現有父類Person和子類Student,它們類圖如圖12-4所示。

技術分享圖片

父類Person代碼如下:

//Person.java文件

package com.a51work6;

import java.util.Date;

public class Person {

// 名字

private String name;

// 年齡

private int age;

// 出生日期

private Date birthDate;

// 三個參數構造方法

public Person(String name, int age, Date d) {

this.name = name;

this.age = age;

birthDate = d;

}

public Person(String name, int age) {

// 調用三個參數構造方法

this(name, age, new Date());

}

...

}

子類Student代碼如下:

//Student.java文件

package com.a51work6;

import java.util.Date;

public class Student extends Person {

// 所在學校

private String school;

public Student(String name, int age, Date d, String school) {

super(name, age, d); ①

this.school = school;

}

public Student(String name, int age, String school) {

// this.school = school;//編譯錯誤

super(name, age); ②

this.school = school;

}

public Student(String name, String school) { // 編譯錯誤 ③

// super(name, 30);

this.school = school;

}

}

在Student子類代碼第①行和第②行是調用父類構造方法,代碼第①行super(name, age, d)語句是調用父類的Person(String name, int age, Date d)構造方法,代碼第②行super(name, age)語句是調用父類的Person(String name, int age)構造方法。

提示 super語句必須位於子類構造方法的第一行。

代碼第③行構造方法由於沒有super語句,編譯器會試圖調用父類默認構造方法(無參數構造方法),但是父類Person並沒有默認構造方法,因此會發生編譯錯誤。解決這個編譯錯誤有三種辦法:

  1. 在父類Person中添加默認構造方法,子類Student會隱式調用父類的默認構造方法。
  2. 在子類Studen構造方法添加super語句,顯式調用父類構造方法,super語句必須是第一條語句。
  3. 在子類Studen構造方法添加this語句,顯式調用當前對象其他構造方法,this語句必須是第一條語句。

    成員變量隱藏和方法覆蓋

子類繼承父類後,有子類中有可能聲明了與父類一樣的成員變量或方法,那麽會出現什麽情況呢?

成員變量隱藏 {#-0}

子類成員變量與父類一樣,會屏蔽父類中的成員變量,稱為“成員變量隱藏”。示例代碼如下:

//ParentClass.java文件

package com.a51work6;

class ParentClass {

// x成員變量

int x = 10; ①

}

class SubClass extends ParentClass {

// 屏蔽父類x成員變量

int x = 20; ②

public void print() {

// 訪問子類對象x成員變量

System.out.println("x = " + x); ③

// 訪問父類x成員變量

System.out.println("super.x = " + super.x); ④

}

}

調用代碼如下:

//HelloWorld.java文件

package com.a51work6;

public class HelloWorld {

public static void main(String[] args) {

//實例化子類SubClass

SubClass pObj = new SubClass();

//調用子類print方法

pObj.print();

}

}

運行結果如下:

x = 20

super.x = 10

上述代碼第①行是在ParentClass類聲明x成員變量,那麽在它的子類SubClass代碼第②行也聲明了x成員變量,它會屏蔽父類中的x成員變量。那麽代碼第③行的x是子類中的x成員變量。如果要調用父類中的x成員變量,則需要super關鍵字,見代碼第④行的super.x。

方法的覆蓋(Override) {#override}

如果子類方法完全與父類方法相同,即:相同的方法名、相同的參數列表和相同的返回值,只是方法體不同,這稱為子類覆蓋(Override)父類方法。

示例代碼如下:

//ParentClass.java文件

package com.a51work6;

class ParentClass {

// x成員變量

int x;

protected void setValue() { ①

x = 10;

}

}

class SubClass extends ParentClass {

// 屏蔽父類x成員變量

int x;

@Override

public void setValue() { // 覆蓋父類方法 ②

// 訪問子類對象x成員變量

x = 20;

// 調用父類setValue()方法

super.setValue();

}

public void print() {

// 訪問子類對象x成員變量

System.out.println("x = " + x);

// 訪問父類x成員變量

System.out.println("super.x = " + super.x);

}

}

調用代碼如下:

//HelloWorld.java文件

package com.a51work6;

public class HelloWorld {

public static void main(String[] args) {

//實例化子類SubClass

SubClass pObj = new SubClass();

//調用setValue方法

pObj.setValue();

//調用子類print方法

pObj.print();

}

}

運行結果如下:

x = 20

super.x = 10

上述代碼第①行是在ParentClass類聲明setValue方法,那麽在它的子類SubClass代碼第②行覆蓋父類中的setValue方法,在聲明方法時添加@Override註解,@Override註解不是方法覆蓋必須的,它只是錦上添花,但添加@Override註解有兩個好處:

1. 提高程序的可讀性。

2. 編譯器檢查@Override註解的方法在父類中是否存在,如果不存在則報錯。

註意 方法重寫時應遵循的原則:

  1. 覆蓋後的方法不能比原方法有更嚴格的訪問控制(可以相同)。例如將代碼第②行訪問控制public修改private,那麽會發生編譯錯誤,因為父類原方法是protected。
  2. 覆蓋後的方法不能比原方法產生更多的異常。

    多態

在面向對象程序設計中多態是一個非常重要的特性,理解多態有利於進行面向對象的分析與設計。

多態概念 {#-0}

發生多態要有三個前提條件:

  1. 繼承。多態發生一定要子類和父類之間。
  2. 覆蓋。子類覆蓋了父類的方法。
  3. 聲明的變量類型是父類類型,但實例則指向子類實例。

下面通過一個示例理解什麽多態。如圖12-5所示,父類Figure(幾何圖形)類有一個onDraw(繪圖)方法,Figure(幾何圖形)它有兩個子類Ellipse(橢圓形)和Triangle(三角形),Ellipse和Triangle覆蓋onDraw方法。Ellipse和Triangle都有onDraw方法,但具體實現的方式不同。

技術分享圖片

具體代碼如下:

//Figure.java文件

package com.a51work6;

public class Figure {

//繪制幾何圖形方法

public void onDraw() {

System.out.println("繪制Figure...");

}

}

//Ellipse.java文件

package com.a51work6;

//幾何圖形橢圓形

public class Ellipse extends Figure {

//繪制幾何圖形方法

@Override

public void onDraw() {

System.out.println("繪制橢圓形...");

}

}

//Triangle.java文件

package com.a51work6;

//幾何圖形三角形

public class Triangle extends Figure {

// 繪制幾何圖形方法

@Override

public void onDraw() {

System.out.println("繪制三角形...");

}

}

調用代碼如下:

//HelloWorld.java文件

package com.a51work6;

public class HelloWorld {

public static void main(String[] args) {

// f1變量是父類類型,指向父類實例

Figure f1 = new Figure(); ①

f1.onDraw();

//f2變量是父類類型,指向子類實例,發生多態

Figure f2 = new Triangle(); ②

f2.onDraw();

//f3變量是父類類型,指向子類實例,發生多態

Figure f3 = new Ellipse(); ③

f3.onDraw();

//f4變量是子類類型,指向子類實例

Triangle f4 = new Triangle(); ④

f4.onDraw();

}

}

上述帶代碼第②行和第③行是符合多態的三個前提,因此會發生多態。而代碼第①行和第④行都不符合,沒有發生多態。

運行結果如下:

繪制Figure...

繪制三角形...

繪制橢圓形...

繪制三角形...

從運行結果可知,多態發生時,Java虛擬機運行時根據引用變量指向的實例調用它的方法,而不是根據引用變量的類型調用。

引用類型檢查 {#-1}

有時候需要在運行時判斷一個對象是否屬於某個引用類型,這時可以使用instanceof運算符,instanceof運算符語法格式如下:

obj instanceof type

其中obj是一個對象,type是引用類型,如果obj對象是type引用類型實例則返回true,否則false。

為了介紹引用類型檢查,先看一個示例,如同12-6所示的類圖,展示了繼承層次樹,Person類是根類,Student是Person的直接子類,Worker是Person的直接子類。
技術分享圖片

繼承層次樹中具體實現代碼如下:

//Person.java文件

package com.a51work6;

public class Person {

String name;

int age;

public Person(String name, int age) {

this.name = name;

this.age = age;

}

@Override

public String toString() {

return "Person [name=" + name

+ ", age=" + age + "]";

}

}

//Worker.java文件

package com.a51work6;

public class Worker extends Person {

String factory;

public Worker(String name, int age, String factory) {

super(name, age);

this.factory = factory;

}

@Override

public String toString() {

return "Worker [factory=" + factory

+ ", name=" + name

+ ", age=" + age + "]";

}

}

//Student.java文件

package com.a51work6;

public class Student extends Person {

String school;

public Student(String name, int age, String school) {

super(name, age);

this.school = school;

}

@Override

public String toString() {

return "Student [school=" + school

+ ", name=" + name

+ ", age=" + age + "]";

}

}

調用代碼如下:

//HelloWorld.java文件

package com.a51work6;

public class HelloWorld {

public static void main(String[] args) {

Student student1 = new Student("Tom", 18, "清華大學"); ①

Student student2 = new Student("Ben", 28, "北京大學");

Student student3 = new Student("Tony", 38, "香港大學"); ②

Worker worker1 = new Worker("Tom", 18, "鋼廠"); ③

Worker worker2 = new Worker("Ben", 20, "電廠"); ④

Person[] people = { student1, student2, student3, worker1, worker2 }; ⑤

int studentCount = 0;

int workerCount = 0;

for (Person item : people) { ⑥

if (item instanceof Worker) { ⑦

workerCount++;

} else if (item instanceof Student) { ⑧

studentCount++;

}

}

System.out.printf("工人人數:%d,學生人數:%d", workerCount, studentCount);

}

}

上述代碼第①行和第②行創建了3個Student實例,代碼第③行和第④行創建了兩個Worker實例,然後程序把這5個實例放入people數組中。

代碼第⑥行使用for-each遍歷people數組集合,當從people數組中取出元素時,元素類型是People類型,但是實例不知道是哪個子類(Student和Worker)實例。代碼第⑦行item instanceof Worker表達式是判斷數組中的元素是否是Worker實例;類似地,第⑧行item instanceof Student表達式是判斷數組中的元素是否是Student實例。

輸出結果如下:

工人人數:2,學生人數:3

引用類型轉換 {#-2}

在5.7節介紹過數值類型相互轉換,引用類型可以進行轉換,但並不是所有的引用類型都能互相轉換,只有屬於同一顆繼承層次樹中的引用類型才可以轉換。

在上一節示例上修改HelloWorld.java代碼如下:

//HelloWorld.java文件

package com.a51work6;

public class HelloWorld {

public static void main(String[] args) {

Person p1 = new Student("Tom", 18, "清華大學");

Person p2 = new Worker("Tom", 18, "鋼廠");

Person p3 = new Person("Tom", 28);

Student p4 = new Student("Ben", 40, "清華大學");

Worker p5 = new Worker("Tony", 28, "鋼廠");

…

}

}

上述代碼創建了3個實例p1、p2、p3、p4和p5,它們的類型都是Person繼承層次樹中的引用類型,p1和p4是Student實例,p2和p5是Worker實例,p3是Person實例。首先,對象類型轉換一定發生在繼承的前提下,p1和p2都聲明為Person類型,而實例是由Person子類型實例化的。

表12-1歸納了p1、p2、p3、p4和p5這5個實例與Worker、Student和Person這3種類型之間的轉換關系。

表 12-1 類型轉換

對 象 Person類型 Worker類型 Student類型 說  明
p1 支持 不支持 支持(向下轉型) 類型:Person實例:Student
p2 支持 支持(向下轉型) 不支持 類型:Person實例:Worker
p3 支持 不支持 不支持 類型:Person實例:Person
p4 支持(向上轉型) 不支持 支持 類型:Student實例:Student
p5 支持(向上轉型) 支持 不支持 類型:Worker實例:Worker

作為這段程序的編寫者是知道p1本質上是Student實例,但是表面上看是Person類型,編譯器也無法推斷p1的實例是Person、Student還是Worker。此時可以使用instanceof操作符來判斷它是哪一類的實例。

引用類型轉換也是通過小括號運算符實現,類型轉換有兩個方向:將父類引用類型變量轉換為子類類型,這種轉換稱為向下轉型(downcast);將子類引用類型變量轉換為父類類型,這種轉換稱為向上轉型(upcast)。向下轉型需要強制轉換,而向上轉型是自動的。

下面通過示例詳細說明一下向下轉型和向上轉型,在HelloWorld.java的main方法中添加如下代碼:

// 向上轉型

Person p = (Person) p4; ①

// 向下轉型

Student p11 = (Student) p1; ②

Worker p12 = (Worker) p2; ③

// Student p111 = (Student) p2; //運行時異常 ④

if (p2 instanceof Student) {

Student p111 = (Student) p2;

}

// Worker p121 = (Worker) p1; //運行時異常 ⑤

if (p1 instanceof Worker) {

Worker p121 = (Worker) p1;

}

// Student p131 = (Student) p3; //運行時異常 ⑥

if (p3 instanceof Student) {

Student p131 = (Student) p3;

}

上述代碼第①行將p4對象轉換為Person類型,p4本質上是Student實例,這是向上轉型,這種轉換是自動的,其實不需要小括號(Person)進行強制類型轉換。

代碼第②行和第③行是向下類型轉換,它們的轉型都能成功。而代碼第④、⑤、⑥行都會發生運行時異常ClassCastException,如果不能確定實例是哪一種類型,可以在轉型之前使用instanceof運算符判斷一下。

再談final關鍵字 {#final}

在前面的學習過程中,為了聲明常量使用過final關鍵字,在Java中final關鍵字的作用還有很多,final關鍵字能修飾變量、方法和類。下面詳細說明。

final修飾變量 {#final-0}

final修飾的變量即成為常量,只能賦值一次,但是final所修飾局部變量和成員變量有所不同。

  1. final修飾的局部變量必須使用之前被賦值一次才能使用。
  2. final修飾的成員變量在聲明時沒有賦值的叫“空白final變量”。空白final變量必須在構造方法或靜態代碼塊中初始化。

final修飾變量示例代碼如下:

//FinalDemo.java文件

package com.a51work6;

class FinalDemo {

void doSomething() {

// 沒有在聲明的同時賦值

final int e; ①

// 只能賦值一次

e = 100; ②

System.out.print(e);

// 聲明的同時賦值

final int f = 200; ③

}

//實例常量

final int a = 5; // 直接賦值 ④

final int b; // 空白final變量 ⑤

//靜態常量

final static int c = 12;// 直接賦值 ⑥

final static int d; // 空白final變量 ⑦

// 靜態代碼塊

static {

// 初始化靜態變量

d = 32; ⑧

}

// 構造方法

FinalDemo() {

// 初始化實例變量

b = 3; ⑨

// 第二次賦值,會發生編譯錯誤

// b = 4; ⑩

}

}

上述代碼第①行和第③行是聲明局部常量,其中第①行只是聲明沒有賦值,但必須在使用之前賦值(見代碼第②行),其實局部常量最好在聲明的同時初始化。

代碼第④、⑤、⑥和⑦行都聲明成員常量。代碼第④和⑤行是實例常量,如果是空白final變量(見代碼第⑤行),則需要在構造方法中初始化(見代碼第⑨行)。代碼第⑥和⑦行是靜態常量,如果是空白final變量(見代碼第⑦行),則需要在靜態代碼塊中初始化(見代碼第⑧行)。

另外,無論是那種常量只能賦值一次,見代碼第⑩行為b常量賦值,因為之前b已經賦值過一次,因此這裏會發生編譯錯誤。

final修飾類 {#final-1}

final修飾的類不能被繼承。有時出於設計安全的目的,不想讓自己編寫的類被別人繼承,這是可以使用final關鍵字修飾父類。

示例代碼如下:

//SuperClass.java文件

package com.a51work6;

final class SuperClass {

}

class SubClass extends SuperClass { //編譯錯誤

}

在聲明SubClass類時會發生編譯錯誤。

final修飾方法 {#final-2}

final修飾的方法不能被子類覆蓋。有時也是出於設計安全的目的,父類中的方法不想被別人覆蓋,這是可以使用final關鍵字修飾父類中方法。

示例代碼如下:

//SuperClass.java文件

package com.a51work6;

class SuperClass {

final void doSomething() {

System.out.println("in SuperClass.doSomething()");

}

}

class SubClass extends SuperClass {

@Override

void doSomething() { //編譯錯誤

System.out.println("in SubClass.doSomething()");

}

}

子類中的void doSomething()方法試圖覆蓋父類中void doSomething()方法,父類中的void doSomething()方法是final的,因此會發生編譯錯誤。

本章小結

通過對本章的學習,首先介紹了Java中的繼承概念,在繼承時會發生方法的覆蓋、變量的隱藏。然後介紹了Java中的多態概念,廣大讀者需要熟悉多態發生的條件,掌握引用類型檢查和類型轉換。最後還介紹了final關鍵字。

配套視頻

http://edu.51cto.com/topic/1246.html

配套源代碼

http://www.zhijieketang.com/group/5

《Java從小白到大牛》之第12章 繼承與多態