JavaSE 基礎學習之三 ——Java 的繼承與接口

接上文《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 關鍵字:

  1. super 關鍵字也有兩種意義:調用父類的方法,或是調用父類的構造器。但是,super并不表示一個指向對象的引用,它只是一個特殊的關鍵字,用來告訴編譯器,現在要調用的是父類的方法。
  2. 理論上,子類一定會調用父類相應的構造函數,只是使用了 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();
  1. 編譯器檢查對象的聲明類型和方法名;
    • 假如我們有 Children 的實例對象 child,此時想要調用 Children 的 fun(args) 方法,那么編譯器就會列舉出所有名稱為 fun 的方法(所有方法簽名相同,參數列表不同的 fun 方法),并列舉 Children 的超類 Parent 中 fun 方法;
  2. 編譯器檢查方法調用中提供的參數類型;
    • 如果所有簽名為 fun 的方法中,有一個參數類型和調用時提供的參數類型最匹配,那么就調用該方法。該過程成為<font color=red>重載解析</font>
  3. 當程序運行并使用動態綁定調用方法時,虛擬機必須調用與 child 指向的對象的實際類型相匹配的方法版本。調用方法的時候,如果當前子類已經對父類實現了方法的重寫,則調用子類重寫后的方法;否則只調用父類的方法。即如果子類 Children 中如果實現了對應的 fun(args) 方法,則調用 Children 的方法,否則就在父類 Parent 中尋找;在 Parent 中找不到,則在 Parent 的父類中找,直到最頂層的父類。

JVM 調用一個類方法時(即標注 static 的靜態方法),它會基于對象引用的類型來選擇所調用的方法,通常在編譯時 JVM 就知道了要調用什么方法,這就是靜態綁定。相反,如果 JVM 調用一個實例對象方法時,它會基于對象實際的類型來選擇所調用的方法,具體調用什么方法只能在運行時得知。這就是動態綁定,是多態的一種。動態綁定為解決實際的業務問題提供了很大的靈活性,是一種非常優美的機制。

參考網址:《Java靜態綁定與動態綁定》

3. 類的初始化順序

創建一個實例對象時,考慮到該對象的父子關系,JVM 按照一定的順序進行初始化:

  1. 先父類靜態,再子類靜態
  2. 父類的定義初始化 + 構造函數
  3. 子類定義初始化 + 構造函數

以例程來說明初始化順序:

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...");
    }
}

分析該段程序,先后順序應該如下:

  1. 父類 C 的靜態初始化:bb..
  2. 子類 D 的靜態初始化:ee..
  3. 父類 C 的定義初始化:aa..
  4. 父類 C 的構造函數:cc..
  5. 子類 D 的定義初始化:dd...
  6. 子類 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

結果如圖:

3-01.png

從編譯的結果就可以看出來,編譯后外部類及其內部類會生成兩個獨立的 .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)詳解》


接下篇《JavaSE 基礎學習之四 —— 異常的處理》

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容

  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,710評論 18 399
  • 一:java概述: 1,JDK:Java Development Kit,java的開發和運行環境,java的開發...
    慕容小偉閱讀 1,812評論 0 10
  • 一、Java 簡介 Java是由Sun Microsystems公司于1995年5月推出的Java面向對象程序設計...
    子非魚_t_閱讀 4,224評論 1 44
  • 覺察D1:最近事情比較多,工作也比較忙,昨天還在糾結請假的事,這幾天還有買房的事也湊在這幾天,不管怎么樣還是參加課...
    幸福實修金芳閱讀 110評論 0 1
  • 成都。 這個雖不是在我最愛的城市名單之列,但卻又是說來就來的目的地,原因完全在于好吃噠(太沒出息了...
    莫斐如是閱讀 265評論 0 0