《Java從小白到大牛》紙質版已經上架了!!!
html
類的繼承性是面嚮對象語言的基本特性,多態性前提是繼承性。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()等方法。ide
提示 通常狀況下,一個子類只能繼承一個父類,這稱爲「單繼承」,但有的狀況下一個子類能夠有多個不一樣的父類,這稱爲「多重繼承」。在Java中,類的繼承只能是單繼承,而多重繼承能夠經過實現多個接口實現。也就是說,在Java中,一個類只能繼承一個父類,可是能夠實現多個接口。學習
提示 面向對象分析與設計(OOAD)時,會用到UML圖[^11],其中類圖很是重要,用來描述系統靜態結構。Student繼承Person的類圖如圖12-1所示。類圖中的各個元素說明如圖12-2所示,類用矩形表示,通常分爲上、中、下三個部分,上部分是類名,中部分是成員變量,下部分是成員方法。實線+空心箭頭表示繼承關係,箭頭指向父類,箭頭末端是子類。UML類圖中還有不少關係,如圖12-3所示,如圖虛線+空心箭頭表示實線關係,箭頭指向接口, 箭頭末端是實線類。
this
[^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並無默認構造方法,所以會發生編譯錯誤。解決這個編譯錯誤有三種辦法:
子類繼承父類後,有子類中有可能聲明瞭與父類同樣的成員變量或方法,那麼會出現什麼狀況呢?
子類成員變量與父類同樣,會屏蔽父類中的成員變量,稱爲「成員變量隱藏」。示例代碼以下:
//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)父類方法。
示例代碼以下:
//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註解的方法在父類中是否存在,若是不存在則報錯。
注意 方法重寫時應遵循的原則:
- 覆蓋後的方法不能比原方法有更嚴格的訪問控制(能夠相同)。例如將代碼第②行訪問控制public修改private,那麼會發生編譯錯誤,由於父類原方法是protected。
- 覆蓋後的方法不能比原方法產生更多的異常。
多態
在面向對象程序設計中多態是一個很是重要的特性,理解多態有利於進行面向對象的分析與設計。
發生多態要有三個前提條件:
下面經過一個示例理解什麼多態。如圖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虛擬機運行時根據引用變量指向的實例調用它的方法,而不是根據引用變量的類型調用。
有時候須要在運行時判斷一個對象是否屬於某個引用類型,這時可使用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
在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個實例p一、p二、p三、p4和p5,它們的類型都是Person繼承層次樹中的引用類型,p1和p4是Student實例,p2和p5是Worker實例,p3是Person實例。首先,對象類型轉換必定發生在繼承的前提下,p1和p2都聲明爲Person類型,而實例是由Person子類型實例化的。
表12-1概括了p一、p二、p三、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關鍵字,在Java中final關鍵字的做用還有不少,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關鍵字修飾父類。
示例代碼以下:
//SuperClass.java文件 package com.a51work6; final class SuperClass { } class SubClass extends SuperClass { //編譯錯誤 }
在聲明SubClass類時會發生編譯錯誤。
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