接上文《JavaSE 基礎學習之二 —— Java 的部分基本語法》
三. Java 的繼承與接口
1. java 中的繼承
繼承是 java 面向對象編程技術的一塊基石,因為它允許創建分等級層次的類。
繼承就是<font color=red>子類繼承父類的特征和行為,使得子類對象(實例)具有父類的實例域和方法</font>,或子類從父類繼承方法,使得子類具有父類相同的行為。
——摘自《Java 繼承 | 菜鳥教程》
繼承使用的關鍵字是 extend,格式為:
class 子類 extends 父類 {
}
用圓形為例,舉例如下:
public class Circle extends Shape {
// ...
}
繼承是使用已存在的類的定義作為基礎建立新類的技術,新類的定義可以增加新的數據或新的功能,也可以用父類的功能,但不能選擇性地繼承父類。通過使用繼承我們能夠非常方便地復用以前的代碼,能夠大大的提高開發的效率。此外,繼承的代碼復用體現了一種 is-a 關系:比如PC 機是計算機,工作站也是計算機。PC 機和工作站是兩種不同類型的計算機,但都繼承了計算機的共同特性。因此在用 Java 語言實現時,應該將 PC 機和工作站定義成兩種類,均繼承計算機類。
Java 中除了構造函數之外,子類可以繼承父類所有函數。
關于子類的構造函數,其實子類是可以通過 super() 方法訪問到父類的構造函數的。子類的無參構造函數,默認調用父類無參數的構造函數。如果要顯式的調用構造函數,需要使用 super 關鍵字,而且要把 super() 放在子類構造函數的第一句,就可以在子類中調用父類的構造函數了。大致如下所示:
public Circle extends Shape {
public Shape() {
// 調用父類構造函數
super();
//...其他初始化方法....
}
}
注:關于 super 關鍵字:
- super 關鍵字也有兩種意義:調用父類的方法,或是調用父類的構造器。但是,super并不表示一個指向對象的引用,它只是一個特殊的關鍵字,用來告訴編譯器,現在要調用的是父類的方法。
- 理論上,子類一定會調用父類相應的構造函數,只是使用了 super 關鍵字是顯式的調用而已,而且通常情況下 super 關鍵字的調用時都被省略了;
2. 動態綁定 (Dynamic Binding)
程序綁定指的是一個方法的調用與方法所在的類關聯起來。對 Java 來說,綁定分為<font color=red>靜態綁定</font>和<font color=red>動態綁定</font>(或者叫做前期綁定和后期綁定)。
靜態綁定是指在程序執行前方法已經被綁定,也就是說在編譯過程中,就已經知道該方法是屬于哪個類中的方法。此時由編譯器或其它連接程序實現。針對 Java,可以簡單理解為程序編譯期的綁定。這里特別說明一點,Java 當中的方法只有 <font color=red>final, static, private 和構造方法</font>是靜態綁定。(具體分析見參考網址)
動態綁定即后期綁定,指在運行時根據具體對象的類型進行綁定。如果一種語言實現了后期綁定(如 Java, C++),同時必須提供一些機制,可在運行期間判斷對象的類型,并分別調用適當的方法。也就是說,在運行時編譯器依然不知道對象的類型,但方法調用機制能自己去調查,找到正確的方法主體。不同的語言對后期綁定的實現方法是有所區別的,但我們至少可以這樣認為:它們都要在對象中安插某些特殊類型的信息。
動態綁定的典型,就是父類的引用可以引用任何子類的實例。比如有如下父類子類關系,介紹說明動態綁定的具體過程:
Parent p = new Children();
- 編譯器檢查對象的聲明類型和方法名;
- 假如我們有 Children 的實例對象 child,此時想要調用 Children 的 fun(args) 方法,那么編譯器就會列舉出所有名稱為 fun 的方法(所有方法簽名相同,參數列表不同的 fun 方法),并列舉 Children 的超類 Parent 中 fun 方法;
- 編譯器檢查方法調用中提供的參數類型;
- 如果所有簽名為 fun 的方法中,有一個參數類型和調用時提供的參數類型最匹配,那么就調用該方法。該過程成為<font color=red>重載解析</font>;
- 當程序運行并使用動態綁定調用方法時,虛擬機必須調用與 child 指向的對象的實際類型相匹配的方法版本。調用方法的時候,如果當前子類已經對父類實現了方法的重寫,則調用子類重寫后的方法;否則只調用父類的方法。即如果子類 Children 中如果實現了對應的 fun(args) 方法,則調用 Children 的方法,否則就在父類 Parent 中尋找;在 Parent 中找不到,則在 Parent 的父類中找,直到最頂層的父類。
JVM 調用一個類方法時(即標注 static 的靜態方法),它會基于對象引用的類型來選擇所調用的方法,通常在編譯時 JVM 就知道了要調用什么方法,這就是靜態綁定。相反,如果 JVM 調用一個實例對象方法時,它會基于對象實際的類型來選擇所調用的方法,具體調用什么方法只能在運行時得知。這就是動態綁定,是多態的一種。動態綁定為解決實際的業務問題提供了很大的靈活性,是一種非常優美的機制。
參考網址:《Java靜態綁定與動態綁定》
3. 類的初始化順序
創建一個實例對象時,考慮到該對象的父子關系,JVM 按照一定的順序進行初始化:
- 先父類靜態,再子類靜態
- 父類的定義初始化 + 構造函數
- 子類定義初始化 + 構造函數
以例程來說明初始化順序:
package oop4;
public class Test2 {
public static void main(String[] args) {
D d = new D();
}
}
class C{
// C 的定義初始化
{System.out.println("aa..");}
// C 的靜態初始化
static{
System.out.println("bb..");
}
// C 的構造函數
C(){System.out.println("cc..");}
}
class D extends C{
// D 的定義初始化
{System.out.println("dd...");}
// D 的靜態初始化
static{
System.out.println("ee..");
}
// D 的構造函數
D(){
System.out.println("ff...");
}
}
分析該段程序,先后順序應該如下:
- 父類 C 的靜態初始化:
bb..
- 子類 D 的靜態初始化:
ee..
- 父類 C 的定義初始化:
aa..
- 父類 C 的構造函數:
cc..
- 子類 D 的定義初始化:
dd...
- 子類 D 的構造函數:
ff...
綜上所述,該段程序輸出的結果:
bb..
ee..
aa..
cc..
dd...
ff...
4. Java 的單繼承
Java 中的繼承只能是<font color=red>單一繼承</font>,即 extends 關鍵字只能有一個類名;但 java 的繼承具有傳遞性。
為什么 Java 只能單繼承,而不像 C++ 一樣能夠多繼承?從技術的角度來說,是為了降低復雜性。例如,A 類中有一個 m 方法,B 類中也有一個 m 方法。如果 C 類單獨繼承 A 類或者 B 類時,C 類中的 m 方法要么繼承于 A 類,要么繼承于 B 類。而如果多重繼承的話,C 類的 m 方法有可能來自 A 類,又有可能來自 B 類,就會造成沖突。這樣的繼承關系,就會增加復雜性,甚至進一步影響多態的靈活性。
此外,java.lang.Object 是一切類的父類。或者可以說,如果一個類沒有父類,那么它的父類就是 java.lang.Object。Object 類型有幾個方法比較實用:
-
equals 方法:用來判斷兩個 obj 對象的地址是否相等。
- 由于 Object 的原始 equals 方法比較時,比較雙方如果地址相同,則返回 true,否則返回 false,所以對于很多 Object 的子類并不適用,故很多 Object 的子類經常會重寫 equals 方法。以后如果有調用 equals 方法的時候,需要了解該 equals 方法的具體意義;
- toString() 方法:打印一個對象,就會打印該對象的 toString 的返回值;
如果要判斷一個實例對象 obj 是否屬于某個類型 T,可以使用關鍵字 instanceof。對于表達式 obj instanceof T
,如果實例 obj 屬于 T 類型,則返回 true;否則返回 false。
5. 抽象類
對于普通的類,其本身就是一個完善的功能類,可以直接產生實例化對象,并且在普通類中可以包含構造方法、普通方法、static 方法、常量和變量等內容。抽象類,就是指在普通類的結構里面增加抽象方法的組成部分。
那么什么叫抽象方法呢?抽象方法,是指沒有方法體的方法,即一個方法只有聲明,沒有實現。同時抽象方法還必須用 abstract 關鍵字來聲明。只要擁有一個抽象方法的類就是抽象類。
抽象類的使用原則如下:
- 抽象方法必須為 public 或者 protected(因為如果為 private,則不能被子類繼承,子類便無法實現該方法),缺省情況下,默認為public;
- 抽象類不能直接實例化,需要依靠子類采用向上轉型的方式處理;
- 抽象類必須有子類,使用 extends 繼承,一個子類只能繼承一個抽象類;
- 對于不是抽象類的子類,必須覆寫抽象類之中的全部抽象方法(如果子類沒有實現父類的抽象方法,則必須將子類也定義為 abstract 類);
對于抽象類,還有一些需要注意的地方:
- 抽象類繼承子類,其中有明確的方法覆寫要求,而普通類可以有選擇性的來決定是否需要覆寫;
- 抽象類實際上就比普通類多了一些抽象方法而已,其他組成部分和普通類完全一樣;
- 普通類對象可以直接實例化,但抽象類的對象必須經過向上轉型之后才可以得到。
可以看出,雖然一個類的子類可以去繼承任意的一個普通類,可是從開發的實際要求來講,普通類盡量不要去繼承另外一個普通類,而是去繼承抽象類。
6. final 關鍵字
在 Java 中,final 關鍵字可以用來修飾類、方法和變量(包括成員變量和局部變量)。
用 final 關鍵字修飾變量:
- final 關鍵字來修飾類的變量,只能被賦一次值。
- final 修飾的成員變量也只能賦值一次;但在對象創建的時候,成員變量必須賦值,即在定義初始化或構造函數中對 final 修飾的成員變量進行賦值;
- java 語言中沒有常量,但可以<font color=red>通過 public static final</font> 來定義常量,且一般大寫;
- 例:
public static final int CELL_WIDTH = 50
;
- 例:
用 final 關鍵字修飾的類不能被繼承;例如,String, Math 類就是 Java 中典型的 final 關鍵字修飾的類;
用 final 關鍵字修飾的方法,不能夠被重寫。
需要注意的是,用 final 修飾的數組,與普通的變量理解起來難度。如下例中:
//========================================
final int a = 10;
a = 20; // 錯誤,a 變量只能賦值一次
//========================================
final int[] b = {1, 2, 3, 4};
b[0] = 10; // 正確
//========================================
int 類型的 a 由于被 final 關鍵字修飾,所以不能被二次賦值,這比較容易理解。但下面的例子中,看起來好像是數組的二次賦值也可以完成。其實實際上對于被 final 關鍵字修飾的數組而言,數組的引用地址是不能改變的。上例程中,b[0] = 10 僅改變了 b 數組 0 位置的元素內容而已,而該位置的地址引用沒有發生任何改變,所以是可以完成的。
7. 接口
接口體現的是一種標準,外部體現為方法的聲明。接口用關鍵字 interface 修飾。提供一個接口,是為了實現某種標準的對接過程,而實現接口,就是意味著符合這個標準。對接口的實現,需要使用 implements 關鍵字。實現一個接口,就要重寫接口中的方法;換個角度來說,如果不實現接口,就變成了一個抽象類。
接口里的方法,默認都是 public abstract 類型的。此外接口里也可以聲明變量,變量的類型也默認為 public static final 類型。例如:
public interface Memory {
public void memo(); // 等價于 public abstract void memo();
int i = 1; // 等價于 public static final int i = 1;
}
Java 中的接口與繼承最大的不同是,繼承是單一繼承,但接口與接口之間可以多繼承。此外一個類可以繼承一個父類,同時實現多個接口。舉一個例子,如何定義一個英雄?我們假定一個人,如果同時滿足可以飛、可以打架、可以游泳,那么他就是一個英雄。同時,人又屬于動物。那么我們就可以定義英雄 Hero 如下:
public class Hero extends Animal implemets CanFly, CanFight, CanSwim {}
上例中,也可以看到接口與繼承的另一個區別:繼承體現了 is-a 關系(單繼承),接口體現了 can-do 關系(多繼承)。
接口與抽象又有一些相似的共同點:如果看到接口類型的引用,那么引用的一定是實現了該接口的類的實例;如果看到抽象類型的引用,那么引用的一定是繼承了該抽象類的類的實例。
8. 內部類
使用內部類的原因,在于內部類提供了更好的封裝,只有外部類可以訪問內部類。此外內部類中的屬性和方法,即使是外部類也不能直接訪問,相反,內部類可以直接訪問包括 private 聲明的外部類的屬性和方法。另外屬于內部類的匿名內部類也十分利于回調函數的編寫。
內部類與外部類是一個相對獨立的實體,它與外部類并不是 is-a 關系。比如我們定義了內部類外部類的 OuterClass.java 如下:
public class OuterClass {
private String outerName;
private int outerAge;
public class InnerClass{
private String innerName;
private int innerAge;
}
}
在該文件的路徑下輸入指令:
javac OuterClass.java
結果如圖:
從編譯的結果就可以看出來,編譯后外部類及其內部類會生成兩個獨立的 .class 文件:OuterClass.class 和 OuterClass$InnerClass.class。說明內部類是一個編譯時的概念。
此外,內部類可以直接訪問外部類的元素,但是外部類不可以直接訪問內部類的元素;而且外部類可以通過內部類引用間接訪問內部類元素。
關于內部類的創建,如果在外部類中創建內部類,那么就和普通的創建對象是一樣的:
InnerClass innerClass = new InnerClass();
如果在外部類之外創建外部類中的內部類(有點拗口),就需要 outerClass.new 來創建:
//================================================
OuterClass outerClass = new OuterClass();
OuterClass.InnerClass innerClass = outerClass.new InnerClass();
//================================================
// 或者一步到位的方法:
OuterClass.InnerClass innerClass = new OuterClass().new InnerClass();
//================================================
Java中內部類主要分為四種:成員內部類、方法內部類、匿名內部類、靜態內部類。
(1) 成員內部類
成員內部類也是最普通的內部類,上面的 InnerClass 與 OuterClass就是屬于成員內部類與其外部類。成員內部類又稱為局部內部類,它是外部類的一個成員,所以他是可以無限制的訪問外圍類的所有成員屬性和方法,盡管是 private 的,但是外部類要訪問內部類的成員屬性和方法,就需要通過內部類實例來訪問。
在成員內部類中要注意兩點:
- 成員內部類中不能存在任何 static 的變量和方法
- 成員內部類是依附于外圍類的,所以只有先創建了外圍類才能夠創建內部類
(2) 靜態內部類
static 關鍵字可以修飾成員變量、方法、代碼塊,其實它還可以修飾內部類,使用 static 修飾的內部類我們稱之為靜態內部類。靜態內部類與非靜態內部類之間存在一個最大的區別,我們知道非靜態內部類在編譯完成之后會隱含地保存著一個引用,該引用是指向創建它的外圍內,但是靜態內部類卻沒有。沒有這個引用就意味著靜態內部類的兩個屬性:
- 靜態內部類的創建不需要依賴于外圍類,可以直接創建;
- 靜態內部類不可以使用任何外圍類的非 static 成員變量和方法,而內部類則都可以;
靜態內部類的示例如下:
public class OuterClass {
private static String outerName;
public int age;
static class InnerClass1{
// 在靜態內部類中可以存在靜態成員
public static String _innerName = "static variable";
public void display(){
/*=========================================
* 靜態內部類只能訪問外部類的靜態成員變量和方法
* 不能訪問外部類的非靜態成員變量和方法
==========================================
*/
System.out.println("OutClass name :" + outerName);
}
}
class InnerClass2{
// 非靜態內部類中不能存在靜態成員
public String _innerName = "no static variable";
// 非靜態內部類中可以調用外部類的任何成員,不管是靜態的還是非靜態的
public void display() {
System.out.println("OuterClass name:" + outerName);
System.out.println("OuterClass age:" + age);
}
}
public void display(){
// 外部類能直接訪問靜態內部類靜態元素
System.out.println(InnerClass1._innerName);
// 靜態內部類可以直接創建實例不需要依賴于外部類
new InnerClass1().display();
// 非靜態內部的創建需要依賴于外部類
OuterClass.InnerClass2 inner2 = new OuterClass().new InnerClass2();
// 非靜態內部類的成員需要使用非靜態內部類的實例訪問
System.out.println(inner2._innerName);
inner2.display();
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
outer.display();
}
}
(3) 方法內部類
方法內部類定義在外部類的方法中,局部內部類和成員內部類基本一致,只是它們的作用域不同,方法內部類只能在該方法中被使用,出了該方法就會失效。 對于這個類的使用主要是應用與解決比較復雜的問題,想創建一個類來輔助我們的解決方案,到那時又不希望這個類是公共可用的,所以就產生了局部內部類。
(4) 匿名內部類
匿名內部類是沒有名字的局部內部類,它沒有 class, interface, implements, extends 等關鍵字的修飾,也沒有構造器,它一般隱式的繼承某一個父類,或者具體實現某一個接口。
-
什么時候用:
- 已知父類,要獲取其子類的實例對象;
- 已知接口,要獲取其實現了該接口的類的實例;
- 怎么用:
對于子類繼承:
new 父類(給父類的構造函數傳遞參數) {
// 子類具體實現部分;
}
// 此處得到的是子類的實例對象
對于接口實現:
new 接口() {
// 實現了該接口的類的實現部分;
}
// 此處得到的是接口的實現類的實例對象
后面將會在很多地方看到匿名內部類的使用,比如在后面講到的 TreeSet,JDBC 的 JdbcTemplate.query 方法中的 RowMapper 繼承類實現等。此處以 TreeSet 為例,需要實現一個比較器 Comparator 的 compareTo 方法,這里就可以實現匿名內部類。代碼如下:
TreeSet<T> ts = new TreeSet<T>(new Comparator<T>() {
public int compare(T o1, T o2) {
// TODO Auto-generated method stub
return o2.getName().compareTo(o1.getName());
}
});
上面的代碼中,new TreeSet< T > 后面傳入的參數,是直接定義得到的一個 new Comparator< T >(){...} 。這里就體現了匿名內部類直接對接口的實現,確定了數據類型為 T 的兩個對象 o1, o2 的名稱按照字母順序進行排列的規定。
后續的 RowMapper 繼承,也會用到匿名內部類。代碼大致如下,到后面會詳細講解:
@Test
public void testResultSet1() {
jdbcTemplate.update("insert into test(name) values('name5')");
String listSql = "select * from test";
List result = jdbcTemplate.query(listSql, new RowMapper<Map>() {
@Override
public Map mapRow(ResultSet rs, int rowNum) throws SQLException {
Map row = new HashMap();
row.put(rs.getInt("id"), rs.getString("name"));
return row;
}});
Assert.assertEquals(1, result.size());
jdbcTemplate.update("delete from test where name='name5'");
}
內部類相關內容參考地址:《
java 內部類(inner class)詳解》