[ Java學習基礎 ] Java的繼承與多態


看到自己寫的東西(4.22的隨筆[ Java學習基礎 ] Java構造函數)第一次達到閱讀100+的成就還是挺欣慰的,感謝大家的支持!希望以后能繼續和大家共同學習,共同努力,一起進步!共勉!

------------------------------------

一、Java繼承

      繼承是java面向對象編程技術的一塊基石,因為它允許創建分等級層次的類。

      繼承就是子類繼承父類的特征和行為,使得子類對象(實例)具有父類的實例域和方法,或子類從父類繼承方法,使得子類具有父類相同的行為。

生活中的繼承如圖:

      兔子和羊屬於食草動物類,獅子和豹屬於食肉動物類;食草動物和食肉動物又是屬於動物類。所以繼承需要符合的關系是:父類更通用,子類更具體。雖然食草動物和食肉動物都是屬於動物,但是兩者的屬性和行為上有差別,所以子類會具有父類的一般特性也會具有自身的特性。

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

 1 //Person.java文件
 2 package com.Kevin;
 3 
 4 import java.util.Date;
 5 
 6 public class Person {
 7 
 8     // 名字
 9     private String name;
10     // 年齡
11     private int age;
12     // 出生日期
13     private Date birthDate;
14 
15     public String getInfo() {
16         return "Person [name=" + name
17                 + ", age=" + age
18                 + ", birthDate=" + birthDate + "]";
19     }
20 
21 }

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

 1 //Student.java文件
 2 package com.Kevin;
 3 
 4 import java.util.Date;
 5 
 6 public class Student {
 7 
 8     // 所在學校
 9     public String school;
10     // 名字
11     private String name;
12     // 年齡
13     private int age;
14     // 出生日期
15     private Date birthDate;
16 
17     public String getInfo() {
18         return "Person [name=" + name
19                 + ", age=" + age
20                 + ", birthDate=" + birthDate + "]";
21     }
22 }

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

1 //Student.java文件
2 package com.Kevin;
3 
4 import java.util.Date;
5 
6 public class Student extends Person {
7     // 所在學校
8     private String school;
9 }

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

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

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

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

 圖1

圖2

繼承的特性:

  • 子類擁有父類非private的屬性,方法。

  • 子類可以擁有自己的屬性和方法,即子類可以對父類進行擴展。

  • 子類可以用自己的方式實現父類的方法。

  • Java的繼承是單繼承,但是可以多重繼承,單繼承就是一個子類只能繼承一個父類,多重繼承就是,例如A類繼承B類,B類繼承C類,所以按照關系就是C類是B類的父類,B類是A類的父類,這是java繼承區別於C++繼承的一個特性。

  • 提高了類之間的耦合性(繼承的缺點,耦合度高就會造成代碼之間的聯系)。

二、調用父類構造方法

      當子類實例化時,不僅需要初始化子類成員變量,也需要初始化父類成員變量,初始化父類成員變量需要調用父類構造方法,子類使用super關鍵字調用父類構造方法。下面看一個示例,現有父類Person和子類Student,它們類圖如下圖所示:

 

父類Person代碼如下:

 1 //Person.java文件
 2 package com.Kevin;
 3 
 4 import java.util.Date;
 5 
 6 public class Person {
 7 
 8     // 名字
 9     private String name;
10     // 年齡
11     private int age;
12     // 出生日期
13     private Date birthDate;
14 
15     // 三個參數構造方法
16     public Person(String name, int age, Date d) {
17         this.name = name;
18         this.age = age;
19         birthDate = d;
20     }
21 
22     public Person(String name, int age) {
23         // 調用三個參數構造方法
24         this(name, age, new Date());
25     }
26     ...
27 }

子類Student代碼如下:

 1 //Student.java文件
 2 package com.Kevin;
 3 
 4 import java.util.Date;
 5 
 6 public class Student extends Person {
 7 
 8     // 所在學校
 9     private String school;
10 
11     public Student(String name, int age, Date d, String school) {
12         super(name, age, d);                                        
13         this.school = school;
14     }
15 
16     public Student(String name, int age, String school) {
17         // this.school = school;//編譯錯誤
18         super(name, age);                                           
19         this.school = school;
20     }
21 
22     public Student(String name, String school) { // 編譯錯誤        
23         // super(name, 30);
24         this.school = school;
25     }
26 }

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

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

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

  1. 在父類Person中添加默認構造方法,子類Student會隱式調用父類的默認構造方法。

  2. 在子類Studen構造方法添加super語句,顯式調用父類構造方法,super語句必須是第一條語句。

  3. 在子類Studen構造方法添加this語句,顯式調用當前對象其他構造方法,this語句必須是第一條語句。

三、成員變量隱藏和方法覆蓋

3.1 成員變量隱藏      

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

 1 //ParentClass.java文件
 2 package com.Kevin;
 3 
 4 class ParentClass {
 5     // x成員變量
 6     int x = 10;                                
 7 }
 8 
 9 class SubClass extends ParentClass {
10     // 屏蔽父類x成員變量
11     int x = 20;                                
12 
13     public void print() {
14         // 訪問子類對象x成員變量
15         System.out.println("x = " + x);                
16         // 訪問父類x成員變量
17         System.out.println("super.x = " + super.x);    
18     }
19 }

調用代碼如下:

 1 //HelloWorld.java文件
 2 package com.Kevin;
 3 
 4 public class HelloWorld {
 5 
 6     public static void main(String[] args) {
 7         //實例化子類SubClass
 8         SubClass pObj = new SubClass();
 9         //調用子類print方法
10         pObj.print();
11     }
12 }

運行結果如下:

x = 20
super.x = 10

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

3.2 方法的覆蓋

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

 1 //ParentClass.java文件
 2 package com.Kevin;
 3 
 4 class ParentClass {
 5     // x成員變量
 6     int x;
 7 
 8     protected void setValue() {                     
 9         x = 10;
10     }
11 }
12 
13 class SubClass extends ParentClass {
14     // 屏蔽父類x成員變量
15     int x;
16 
17     @Override
18     public void setValue() { // 覆蓋父類方法        
19         // 訪問子類對象x成員變量
20         x = 20;
21         // 調用父類setValue()方法
22         super.setValue();
23     }
24 
25     public void print() {
26         // 訪問子類對象x成員變量
27         System.out.println("x = " + x);
28         // 訪問父類x成員變量
29         System.out.println("super.x = " + super.x);
30     }
31 }

調用代碼如下:

//HelloWorld.java文件
package com.Kevin;

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

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

  1. 提高程序的可讀性。

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

注意:方法覆蓋時應遵循的原則:

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

  2. 覆蓋后的方法不能比原方法產生更多的異常。

四、多態

4.1 

      多態是同一個行為具有多個不同表現形式或形態的能力,也就是同一個接口,使用不同的實例而執行不同操作,如圖所示:

多態性是對象多種表現形式的體現。

現實中,比如我們按下 F1 鍵這個動作:

  • 如果當前在 Flash 界面下彈出的就是 AS 3 的幫助文檔;
  • 如果當前在 Word 下彈出的就是 Word 幫助;
  • 在 Windows 下彈出的就是 Windows 幫助和支持。

同一個事件發生在不同的對象上會產生不同的結果。

多態的優點:

  • 1. 消除類型之間的耦合關系
  • 2. 可替換性
  • 3. 可擴充性
  • 4. 接口性
  • 5. 靈活性
  • 6. 簡化性

4.2 發生多態的三個前提條件:

  1. 繼承。多態發生一定要子類和父類之間。

  2. 覆蓋。子類覆蓋了父類的方法。

  3. 聲明的變量類型是父類類型,但實例則指向子類實例。

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

 

具體代碼如下:

 1 //Figure.java文件
 2 package com.Kevin;
 3 
 4 public class Figure {
 5 
 6     //繪制幾何圖形方法
 7     public void onDraw() {
 8         System.out.println("繪制Figure...");
 9     }
10 }
11 
12 //Ellipse.java文件
13 package com.Kevin;
14 
15 //幾何圖形橢圓形
16 public class Ellipse extends Figure {
17 
18     //繪制幾何圖形方法
19     @Override
20     public void onDraw() {
21         System.out.println("繪制橢圓形...");
22     }
23 
24 }
25 
26 //Triangle.java文件
27 package com.Kevin;
28 
29 //幾何圖形三角形
30 public class Triangle extends Figure {
31 
32     // 繪制幾何圖形方法
33     @Override
34     public void onDraw() {
35         System.out.println("繪制三角形...");
36     }
37 }

調用代碼如下:

 1 //HelloWorld.java文件
 2 package com.Kevin;
 3 public class HelloWorld {
 4     public static void main(String[] args) {
 5 
 6         // f1變量是父類類型,指向父類實例
 7         Figure f1 = new Figure();                        
 8         f1.onDraw();
 9 
10         //f2變量是父類類型,指向子類實例,發生多態
11         Figure f2 = new Triangle();                      
12         f2.onDraw();
13 
14         //f3變量是父類類型,指向子類實例,發生多態
15         Figure f3 = new Ellipse();                       
16         f3.onDraw();
17 
18         //f4變量是子類類型,指向子類實例
19         Triangle f4 = new Triangle();                    
20         f4.onDraw();
21 
22     }
23 }

上述帶代碼第11行和第15行是符合多態的三個前提,因此會發生多態。而代碼第7行和第19行都不符合,沒有發生多態。

運行結果如下:

繪制Figure...
繪制三角形...
繪制橢圓形...
繪制三角形...

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

4.3 引用類型檢查

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

obj instanceof type

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

      為了介紹引用類型檢查,先看一個示例,如下圖所示的類圖,展示了繼承層次樹,Person類是根類,Student是Person的直接子類,Worker是Person的直接子類。

 

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

 1 //Person.java文件
 2 package com.Kevin;
 3 public class Person {
 4 
 5     String name;
 6     int age;
 7 
 8     public Person(String name, int age) {
 9         this.name = name;
10         this.age = age;
11     }
12 
13     @Override
14     public String toString() {
15         return "Person [name=" + name
16                 + ", age=" + age + "]";
17     }
18 }
19 
20 //Worker.java文件
21 package com.Kevin;
22 public class Worker extends Person {
23 
24     String factory;
25 
26     public Worker(String name, int age, String factory) {
27         super(name, age);
28         this.factory = factory;
29     }
30 
31     @Override
32     public String toString() {
33         return "Worker [factory=" + factory
34                 + ", name=" + name
35                 + ", age=" + age + "]";
36     }
37 }
38 
39 //Student.java文件
40 package com.Kevin;
41 public class Student extends Person {
42 
43     String school;
44 
45     public Student(String name, int age, String school) {
46         super(name, age);
47         this.school = school;
48     }
49 
50     @Override
51     public String toString() {
52         return "Student [school=" + school
53                 + ", name=" + name
54                 + ", age=" + age + "]";
55     }
56 
57 }

調用代碼如下:

 1 //HelloWorld.java文件
 2 package com.Kevin;
 3 
 4 public class HelloWorld {
 5 
 6     public static void main(String[] args) {
 7 
 8         Student student1 = new Student("Tom", 18, "清華大學");          
 9         Student student2 = new Student("Ben", 28, "北京大學");
10         Student student3 = new Student("Tony", 38, "香港大學");        
11 
12         Worker worker1 = new Worker("Tom", 18, "鋼廠");                 
13         Worker worker2 = new Worker("Ben", 20, "電廠");                 
14 
15         Person[] people = { student1, student2, student3, worker1, worker2 };    
16 
17         int studentCount = 0;
18         int workerCount = 0;
19 
20         for (Person item : people) {                 
21             if (item instanceof Worker) {            
22                 workerCount++;
23             } else if (item instanceof Student) {    
24                 studentCount++;
25             }
26         }
27         System.out.printf("工人人數:%d,學生人數:%d", workerCount, studentCount);
28     }
29 }

上述代碼第8行、9行和第10行創建了3個Student實例,代碼第12行和13行創建了兩個Worker實例,然后程序把這5個實例放入people數組中。

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

輸出結果如下:

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

4.4 引用類型轉換:

      引用類型可以進行轉換,但並不是所有的引用類型都能互相轉換,只有屬於同一棵繼承層次樹中的引用類型才可以轉換。示例代碼如下:

 1 //HelloWorld.java文件
 2 package com.Kevin;
 3 
 4 public class HelloWorld {
 5 
 6     public static void main(String[] args) {
 7 
 8         Person p1 = new Student("Tom", 18, "清華大學");
 9         Person p2 = new Worker("Tom", 18, "鋼廠");
10 
11         Person p3 = new Person("Tom", 28);
12         Student p4 = new Student("Ben", 40, "清華大學");
13         Worker p5 = new Worker("Tony", 28, "鋼廠");
14 15     }
16 }

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

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

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

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

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

 1 // 向上轉型
 2 Person p = (Person) p4;            
 3 
 4 // 向下轉型
 5 Student p11 = (Student) p1;        
 6 Worker p12 = (Worker) p2;          
 7 
 8 // Student p111 = (Student) p2;    //運行時異常    
 9 if (p2 instanceof Student) {
10     Student p111 = (Student) p2;
11 }
12 // Worker p121 = (Worker) p1;    //運行時異常      
13 if (p1 instanceof Worker) {
14     Worker p121 = (Worker) p1;
15 }
16 // Student p131 = (Student) p3;    //運行時異常    
17 if (p3 instanceof Student) {
18     Student p131 = (Student) p3;
19 }

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

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

五、final關鍵字

5.1 final修飾變量

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

  1. final修飾的局部變量必須使用之前被賦值一次才能使用。

  2. final修飾的成員變量在聲明時沒有賦值的叫“空白final變量”。空白final變量必須在構造方法或靜態代碼塊中初始化。

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

 1 //FinalDemo.java文件
 2 package com.Kevin;
 3 
 4 class FinalDemo {
 5 
 6     void doSomething() {
 7         // 沒有在聲明的同時賦值
 8         final int e;                    
 9         // 只能賦值一次
10         e = 100;                        
11         System.out.print(e);
12         // 聲明的同時賦值
13         final int f = 200;              
14     }
15 
16     //實例常量
17     final int a = 5; // 直接賦值        
18     final int b; // 空白final變量       
19 
20     //靜態常量
21     final static int c = 12;// 直接賦值     
22     final static int d; // 空白final變量    
23 
24     // 靜態代碼塊
25     static {
26         // 初始化靜態變量
27         d = 32;                          
28     }
29 
30     // 構造方法
31     FinalDemo() {
32         // 初始化實例變量
33         b = 3;                           
34         // 第二次賦值,會發生編譯錯誤
35         // b = 4;                        
36     }
37 }

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

      代碼第17、18、21和22行都聲明成員常量。代碼第17和18行是實例常量,如果是空白final變量(見代碼第18行),則需要在構造方法中初始化(見代碼第33行)。代碼第21和22行是靜態常量,如果是空白final變量(見代碼第22行),則需要在靜態代碼塊中初始化(見代碼第27行)。

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

5.2 final修飾類

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

示例代碼如下:

//SuperClass.java文件
package com.Kevin;

final class SuperClass {
}

class SubClass extends SuperClass { //編譯錯誤
}

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

5.3 final修飾方法

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

示例代碼如下:

 1 //SuperClass.java文件
 2 package com.Kevin;
 3 
 4 class SuperClass {
 5     final void doSomething() {
 6         System.out.println("in SuperClass.doSomething()");
 7     }
 8 }
 9 
10 class SubClass extends SuperClass {
11     @Override
12     void doSomething() { //編譯錯誤
13         System.out.println("in SubClass.doSomething()");
14     }
15 }

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM