看到自己寫的東西(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並沒有默認構造方法,因此會發生編譯錯誤。解決這個編譯錯誤有三種辦法:
-
在父類Person中添加默認構造方法,子類Student會隱式調用父類的默認構造方法。
-
在子類Studen構造方法添加super語句,顯式調用父類構造方法,super語句必須是第一條語句。
-
在子類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注解有兩個好處:
-
提高程序的可讀性。
-
編譯器檢查@Override注解的方法在父類中是否存在,如果不存在則報錯。
注意:方法覆蓋時應遵循的原則:
覆蓋后的方法不能比原方法有更嚴格的訪問控制(可以相同)。例如將代碼第18行訪問控制public修改private,那么會發生編譯錯誤,因為父類原方法是protected。
覆蓋后的方法不能比原方法產生更多的異常。
四、多態
4.1
多態是同一個行為具有多個不同表現形式或形態的能力,也就是同一個接口,使用不同的實例而執行不同操作,如圖所示:

多態性是對象多種表現形式的體現。
現實中,比如我們按下 F1 鍵這個動作:
- 如果當前在 Flash 界面下彈出的就是 AS 3 的幫助文檔;
- 如果當前在 Word 下彈出的就是 Word 幫助;
- 在 Windows 下彈出的就是 Windows 幫助和支持。
同一個事件發生在不同的對象上會產生不同的結果。
多態的優點:
- 1. 消除類型之間的耦合關系
- 2. 可替換性
- 3. 可擴充性
- 4. 接口性
- 5. 靈活性
- 6. 簡化性
4.2 發生多態的三個前提條件:
-
繼承。多態發生一定要子類和父類之間。
-
覆蓋。子類覆蓋了父類的方法。
-
聲明的變量類型是父類類型,但實例則指向子類實例。
下面通過一個示例讓我們更好地理解多態。如下圖所示,父類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所修飾局部變量和成員變量有所不同。
-
final修飾的局部變量必須使用之前被賦值一次才能使用。
-
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的,因此會發生編譯錯誤。
