詳解抽象類和接口

前言:

在JAVA中經常會接觸到接口和抽象類這兩個概念,對於一些剛接觸的人來說簡直有時候懵逼得分不清兩者的區別。最近在複習總結一些基礎知識時也進過這兩個東西的一些坑,翻閱一下課本和網上的一些零散資料來總結和詳解一下接口和抽象類。先分別來認識一下各自的特點再來進行兩者的一些比較,看到網上大多都只是寫着兩者的區別而沒有解析其中的一些原因,所以很多時候都只能單憑靠記憶了,這樣的效果不是很理想。

一、抽象類

我們可能常說“好抽象啊,能不能說詳細點啊”,這裏的抽象和JAVA裏的抽象也差不多了。抽象只是一個泛指並沒有具體的實體和實現,描述的內容越少就越抽象、外延越大。放在JAVA的面向對象的概念中理解,如果一個類中沒有包含足夠的信息來描繪一個具體的對象,這樣的類就是抽象類。eg:我說“交通工具“,但你不知道我說的是哪一種交通工具吧,可能是汽車、火車、飛機、輪船這些工具,那麼這“交通工具”就是一個抽象類,因爲它沒有足夠信息讓我們知道它是一種什麼樣的“交通工具”。

抽象類一種專門用來當做父類的類,其功能類似於我們所說的“模板”(模板設計模式亦由此延伸)。那麼好了,既然是模板那麼我們使用的時候就是Ctrl+C->修修改改->Ctrl+V了,其中Ctrl+C對應的是繼承,“修修改改”對應重寫覆蓋,Ctrl+V對應實體實現。那麼第一個問題來了,抽象類能直接實例化嗎?答案是:

  • 抽象類不能被直接實例化,只能通過抽象類派生出的非抽象類來創建對象和實例化。

這就好比如直接去實現一個”模板“,這樣的操作是毫無意義的,這樣就和普通類沒有質的區別了。接下來用代碼來介紹一下抽象類的一些特點吧。

首先是抽象類的定義格式:

abstract class 抽象類名稱{
    屬性;
    [訪問權限] [返回值類型] 方法名稱(參數){       //普通方法
        [return 返回值];
    }
    [訪問權限] [abstracy] [返回值類型] 方法名稱(參數);     //抽象方法
    // 在抽象方法中是沒有方法體的
}

從以上格式中可以發現,抽象類的定義比普通類多了一些抽象方法,其他地方與普類的組成基本上都是一樣的。那麼在這裏就又可以解決一些疑問了,抽象類可以有屬性嗎?可以包含普通方法(非抽象方法)嗎?答案當然是可以有啦,就如交通工具也得有發明時間吧,都可以運動吧。

  • 抽象方法可以定義有屬性,其訪問權限是任意的,也可以包含普通方法(非抽象方法)。

從而又可以推斷出抽象類也可以有mian方法,因爲mian方法也只是一種靜態方法,只是在mian方法中不能實例化一個抽象類。接下來寫一個交通工具的抽象類:

import java.util.Date;

public abstract class Transportation {    //定義交通工具抽象類
    public Date InventiveTime;   //定義交通工具的發明時間
    public void sport(){       //定義交通工具可以運動的普通方法
        System.out.println("我是抽象類的普通方法");
    }
    public abstract void sportSpeed();    //定義抽象方法,交通工具的運動速度
}

從以上代碼可以發現得出,抽象類的定義比普通類多了一些用abstract修飾的抽象方法,其他地方與普通類的組成上基本是一樣的。那除了這個abstract方法外還有什麼是和普通類是不一樣的嗎?那問題又來了,一個抽象類可以用final關鍵字來聲明嗎?抽象類的方法可以用private、final和static來聲明嗎?抽象類的方法默認訪問權限是什麼?我們先來看答案再來解析一下:

  • 抽象類不能使用final關鍵字來聲明,而且抽象方法不能使用private、final和static修飾,當然也不能使用static修飾;關於抽象方法默認訪問權限:JDK 1.8以前,抽象類的方法默認訪問權限爲protected,而JDK 1.8時,抽象類的方法默認訪問權限變爲default。

 對於第一個問題很容易得出原因,從上面可以得知抽象類就是爲了繼承而生,如果被final關鍵字來聲明那麼此類就不能能子類繼承,而抽象類又必須被子類覆寫,這不就是前後矛盾了嗎?故抽象類不能被final聲明。由此又可以得到抽象方法要被子類覆寫,那就說明抽象方法不能使用private聲明,否則子類還是無法覆寫的,同樣也不能使用static和final來修飾,靜態方法和final方法是不可以繼承的。對於抽象方法的默認訪問權限就要分JDK版本來說了,結論就是上面的答案那樣。

那搞清楚與普通類的一些區別後我們來繼承一個抽象類,用代碼來實現一個交通工具的汽車類——Car類:

public class Car extends Transportation{     //Car類是通過繼承Transportation抽象類得到的
    private String CarInventiveTime;
    private static int wheel;    //定義車輪
    public static void main(String[] args){
        wheel=4;
        System.out.println("我是汽車實體類,"+"我有"+wheel+"個輪子");
        Car car=new Car();
        car.sportSpeed();
    }
    public void sportSpeed(){    //重寫父類Transportation的抽象方法
        super.sport();     //可調用super引用父類方法
        System.out.println("我是Car重寫的抽象方法");
    }
}

代碼執行結果如下:

我是汽車實體類,我有4個輪子
我是抽象類的普通方法
我是Car重寫的抽象方法

上面這個例子是繼承抽象類得到的普通子類,那麼抽象類可以派生出另一個抽象類嗎?答案是當然是可以啦:

  • 抽象類派生的子類(非抽象類)必須覆寫抽象類中的全部抽象方法,如果沒有覆寫全部抽象方法或者又加上新的抽象方法,那麼這個子類又必須定義成抽象類用abstract來聲明。
public abstract class LandTranspotation extends Transportation {   //繼承交通工具抽象類得到陸地交通類
    public abstract String  sportTrack();    //定義新的抽象方法,分別是否是軌道交通工具
}

 上面例子代碼就是抽象類派生的另一個抽象類,其中包含新的抽象方法,但不包含父類的抽象方法。然後還有一個問題就是,抽象類裏可以定義構造方法嗎?答案是可以的:

  • 抽象類中是允許存在構造方法的

這個問題可能會有點難解析,實際上在一個抽象類中是允許存在構造方法的,因爲抽象類依然使用的是類的繼承關係,而且抽象類中也存在各個屬性,所以子類在實例化前必須先要對父類進行實例化。下面改一下上面Transportation類的代碼來驗證一下:

import java.util.Date;

public abstract class Transportation {    //定義交通工具抽象類
    public Date InventiveTime;   //定義交通工具的發明時間
    public Transportation(){      //定義Transportation的構造方法
        System.out.println("我是Transportation抽象類的構造方法");
    }
    public void sport(){       //定義交通工具可以運動的普通方法
        System.out.println("我是抽象類的普通方法");
    }
    public abstract void sportSpeed();    //定義抽象方法,交通工具的運動速度
}

 代碼中只是增加了一個無參構造方法,運行Car類中的main方法結果如下:

我是汽車實體類,我有4個輪子
我是Transportation抽象類的構造方法
我是抽象類的普通方法
我是Car重寫的抽象方法

可以看出抽象類的構造方法是會被其派生子類實例化時調用,其實子類中就是省略了super()這個調用父類無參構造的方法,即使我們在定義是沒寫上構造方法但子類還是會默認調用父類的無參構造方法的。同理得到在抽象類中定義有參構造方法也是可以的。

既然抽象類有構造方法,那麼抽象類是否能繼承實體類?

  • 抽象類是可以繼承實體類,但前提是實體類必須有明確的構造函數。

對於明確的構造函數,我理解爲可以提供給子類訪問的構造器。因爲所有的class都必須有一個構造方法,如果你沒有在代碼裏聲明構造方法,系統會自動給你生成一個公有無參的構造方法。而且所有的子類構造器都要求在第一行代碼中調用父類構造器,如果不寫,系統默認去調用父類的無參構造器。

二、接口

或許在開發過程中產品那邊會提到“寫一個接口來實現這個功能”,雖然表達上是要實現某個功能,但是這裏說的接口API和JAVA裏的接口還是有點差別的。在JAVA中接口也是一種特殊的類,接口是抽象類的變體但比抽象類更加抽象,接口中所有的方法都是抽象的。其中接口更關注的是對行爲的抽象,就如汽車能幹什麼,它能載人、能加速還能漂移,那麼我就能把這些寫成一個接口,然後讓這些類去實現這些功能。那麼好了,第一個問題來了,既然汽車能有那麼多用處那我能不能都用接口來實現呀?接口能直接實例化嗎?這就是接口的多繼承了,即一個類能實現多個接口,但接口也不能直接實例化。

  • 一個類只能繼承一個類,但是可以實現多個接口
  • 接口不能直接實例化,要其派生類(非接口類)來實現接口從而實例化類。

這就好像汽車它只能是陸地交通工具而不能同時又是航空交通工具(飛行汽車是耍流氓,我不管),但汽車能載人、能加速還能漂移。同樣的你的這個接口只定義了載人的功能,但沒說到是什麼載人啊,和抽象類一樣存在抽象方法,沒有具體指明是哪個對象,當然不能直接實例化了。下面來看一下接口的定義格式:

[訪問權限] interface 接口名稱 [extends父接口名]{
        [訪問權限] [static] [final] 數據類型變量名;    //靜態常量
        [訪問權限] [abstract] [native] [返回值類型] 方法名(參數列表);   //抽象方法
}

在大多數的教程或文章中接口的定義基本格式就是這樣,在JDK1.8之前接口是由全局變量和抽象方法組成的,這樣的定義格式是完全沒問題的。因爲在Java7和更早版本中接口中只能只能定義如下兩種:

  • 常量
  • 抽象方法

但在Java8中就增加了默認方法靜態方法。即定義可以是:

  • 常量
  • 抽象方法
  • 默認方法
  • 靜態方法

到了Java9 就更過分了,它又增加了私有方法。即定義可以是:

  • 常量
  • 抽象方法
  • 默認方法
  • 靜態方法
  • 私有方法
  • 私有靜態方法

沒有什麼是一成不變的,那些教材中重點標出的規矩在今天看來有些是不合適的了,當然你還沒用上Java8及以上時是沒有任何感覺的,但人嘛總得往高處爬,不斷更新自身知識很重要。廢話少說上一段代碼來感受一下,我的環境是Java8的,所以Java9的私有方法就不列出了。

public interface ICar {
    String CarName = "五菱宏光";   //定義靜態變量
    public static final int LimitLoad = 20;

    default void accelerate(int speed){    //默認方法
        System.out.println("我是加速方法,我要加速"+speed+"碼");
    }

    static void drift(){     //靜態方法
        System.out.println("我還會漂移方法");
    }

    // 其他抽象方法
    public abstract void method1();
    void method2(String arg);
}

 相信你也在我的代碼中看出一些貓膩了,我在定義靜態成員變量時一個加了public static final,另一個沒加,但是它會默認加上public static final。同樣地在接口中定義的抽象方法也是默認加上public abstract的,只時平時我們一般會省略掉而已,抽象方法和抽象類中的抽象方法一樣是沒有方法實現體的。

  • 接口僅能夠有靜態、不能修改的成員數據,但在日常使用中很少在接口中定義這些成員數據。
  • Java8中接口能接口中的方法可以是public的,也可以是default的;在Java9中接口中的方法還可以是private的。

接下來就來實現以下這個接口吧。

public class Car implements ICar{
    @Override
    public void method1() {
        System.out.println("我是"+ICar.CarName);
        System.out.println("我限載人數爲"+ICar.LimitLoad);
        ICar.drift();
    }

    @Override
    public void method2(String arg) {

    }

    public static void main(String[] args){
        Car car=new Car();
        car.method1();
        car.accelerate(200);
    }
}

程序運行輸出:

我是五菱宏光
我限載人數爲20
我還會漂移方法
我是加速方法,我要加速200碼

由以上代碼可以看出要實現接口則要實現其全部抽象方法,這和抽象類又有幾分相似。那麼接口中能像抽象類那樣定義構造方法嗎?

  • 接口中不能定義構造器和初始化塊。

構造方法是用來在對象初始化前對對象進行一些預處理的,提供了實例化一個具體東西的入口。接口只是聲明而已,不一定要進行什麼初始化,就算要進行初始化,也可以到實現接口的那一些類裏面去初始化。接口只是用來表述動作表述規範來的,可以實例化一臺汽車,但我們無法實例化載客量、加速度、漂移。因此,接口要構造方法何用?接口是一種規範,被調用時,主要關注的是裏邊的方法,而方法是不需要初始化的,類可以實現多個接口,若多個接口都有自己的構造器,則不好決定構造器的調用次序,構造器是屬於類自己的,不能繼承,因爲是純虛的,接口不需要構造方法。

三、聯繫與總結

  • 抽象類可以實現接口,但接口不能繼承抽象類,接口允許繼承多個接口。

 有的時候將接口和抽象類配合起來使用可以爲開發帶來相當的便利。使用抽象類來實現接口這麼做並非是沒有意義的,當你自己寫的類想用接口中個別方法的時候(注意不是所有的方法),那麼你就可以用一個抽象類先實現這個接口(方法體中爲空),然後再用你的類繼承這個抽象類,這樣就可以達到你的目的了,如果你直接用類實現接口,那是所有方法都必須實現的。

至於具體什麼時候用抽象類和接口就得看你的場景了。在既需要統一的接口,又需要實例變量或缺省的方法的情況下,就可以使用抽象類。而接口更多是需要實現特定的多項功能,而這些功能之間可能完全沒有任何聯繫,即是隻關注其實現的部分功能。

最近看了挺多資料才整理出這文章,其中或許有筆誤,如有錯誤之處請指出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章