接口和抽象類的區別

很多常見的面試題都會出諸如抽象類和接口有什麼區別,什麼情況下會使用抽象類和什麼情況你會使用接口這樣的問題。本文我們將仔細討論這些話題。

接口(interface)可以說成是抽象類的一種特例,接口中的所有方法都必須是抽象的。接口中的方法定義默認爲public abstract類型,接口中的成員變量類型默認爲public static final。另外,接口和抽象類在方法上有區別:    
1.抽象類可以有構造方法,接口中不能有構造方法。  
2.抽象類中可以包含非抽象的普通方法,接口中的所有方法必須都是抽象的,不能有非抽象的普通方法。
3.抽象類中可以有普通成員變量,接口中沒有普通成員變量 
4. 抽象類中的抽象方法的訪問類型可以是public,protected和默認類型
5. 抽象類中可以包含靜態方法,接口中不能包含靜態方法
6. 抽象類和接口中都可以包含靜態成員變量,抽象類中的靜態成員變量的訪問類型可以任意,但接口中定義的變量只能是public static final類型,並且默認即爲public static final類型
7. 一個類可以實現多個接口,但只能繼承一個抽象類。
二者在應用方面也有一定的區別:
接口更多的是在系統架構設計方法發揮作用,主要用於定義模塊之間的通信契約
而抽象類在代碼實現方面發揮作用,可以實現代碼的重用,例如,模板方法設計模式是抽象類的一個典型應用,假設某個項目的所有Servlet類都要用相同的方式進行權限判斷、記錄訪問日誌和處理異常,那麼就可以定義一個抽象的基類,讓所有的Servlet都繼承這個抽象基類,在抽象基類的service方法中完成權限判斷、記錄訪問日誌和處理異常的代碼,在各個子類中只是完成各自的業務邏輯代碼。


1.抽象類可以有構造方法,接口中不能有構造方法。 

在抽象類中可以有構造方法,只是不能直接創建抽象類的實例對象,但實例化子類的時候,就會初始化父類,不管父類是不是抽象類都會調用父類的構造方法初始化一個類,先初始化父類。

在接口裏寫入構造方法時,編譯器提示:Interfaces cannot have constructors

A. 構造方法用於初始化成員變量,但是接口成員變量是常量,無需修改。接口是一種規範,被調用時,主要關注的是裏邊的方法,而方法是不需要初始化的,

B. 類可以實現多個接口,若多個接口都有自己的構造器,則不好決定構造器鏈的調用次序

C. 構造器是屬於類自己的,不能繼承。因爲是純虛的,接口不需要構造器。


3.抽象類中可以有普通成員變量,接口中沒有普通成員變量

也就是爲什麼接口中的成員變量非得是public static final的呢?

首先明白一個原理,就是接口的存在意義。接口就是爲了實現多繼承的抽象類,是一種高度抽象的模板、標準或者說協議。規定了什麼東西該是這樣,如果你繼承了我這接口,就必須這樣。比如USB接口,就是小方口,兩根電源線和兩根數據線,不能多不能少。

1public

 既然是公共的模板或者協議,那麼如果定義成private就沒有意義了,因爲所有繼承了你這接口的類都不能用,並且接口中的方法是不能夠被具體實現的,因此,接口內部中也沒有任何方法可使用。因此,爲了讓所有實現了該接口的類能夠使用,就必須是public的。接口中定義的所有東西就應該是對所有用戶開放的東西。

 

2static

所謂static,顧名思義,它代表着靜態,靜止不動的,都知道JVM內存中除了程序計數器,堆內存,棧內存,還有一塊不大的內存區,叫做靜態存儲區。這塊區域就是用來存儲所有靜態的方法和變量的。我想,java一開始設計static的原因也許是不想和實例捆綁在一起。你想想,java的主類中一定會有個public static main()方法,那是程序的執行入口。當初我在想,爲什麼一定要有個static呢?如果沒有static,那麼就必須先實例化main方法所在類對象,可是在哪實例化呢?JVM這樣就找不到入口,因爲沒有static就必須先實例化分配內存後,纔有main方法。如此死循環。

有了static就不一樣了,JVM加載類的時候,首先開始加載的就是靜態的成員變量和方法。而main()方法就在裏面,JVM不需要加載任何實例對象,就能開始執行了,因此main方法是必須static的。

好,羅嗦了。回到題目,如果接口中的成員變量是非靜態的,那麼每一個實現了該接口的類都會有這麼一個變量。那麼,因爲接口是多繼承的,那麼如果另一個接口也是有同樣這樣一個變量呢,那你用哪一個?所以,因爲是標準,所以我規定從一開始,這個東西只能有一份,只能放在靜態存儲區,如果第二個接口也想同命名這麼一個變量,那麼存儲時候就會報錯,因爲我靜態存儲區已經有一份了。你改名吧。

 

 

3final

 想想,如果不是final的,那麼意味着每一個實現了該接口的子類都可以去修改這個變量。我們開頭說了,接口就是標準規範,也改也只能是制定該接口的架構師來改,如果某類隨便改的話,那麼其他也繼承了該接口的類就會受到影響。牽一髮而動全身!!因此,既然是標準,那麼就不能改,方便管理。

 

最後歸納:

  • public是因爲接口是標準,必須對外完全開放,自己藏着掖着沒意義;
  • static是因爲要確保該變量只有一份,避免重名;
  • final是因爲接口的東西是大家共用的,不能隨便修改,因此乾脆不然你有修改的權限! 

至此,解釋完畢。這次分析後,對接口的存在意義更深刻了。


5. 抽象類中可以包含靜態方法,接口中不能包含靜態方法

抽象方法是不能使用static進行修飾,有static的方法是不能override的,所以這樣定義接口才有意義。

Java 8 對接口做了進一步的增強。

public interface JDK8Interface {  
  
    // static修飾符定義靜態方法  
    static void staticMethod() {  
        System.out.println("接口中的靜態方法");  
    }  
  
    // default修飾符定義默認方法  
    default void defaultMethod() {  
        System.out.println("接口中的默認方法");  
    }  
}

接口的實現

public class JDK8InterfaceImpl implements JDK8Interface {  
    //實現接口後,因爲默認方法不是抽象方法,所以可以不重寫,但是如果開發需要,也可以重寫  
}  

靜態方法,只能通過接口名調用,不可以通過實現類的類名或者實現類的對象調用。default方法,只能通過接口實現類的對象來調用。

public class Main {  
    public static void main(String[] args) {  
        // static方法必須通過接口類調用  
        JDK8Interface.staticMethod();  
  
        //default方法必須通過實現類的對象調用  
        new JDK8InterfaceImpl().defaultMethod();  
    }  
}  

a 在接口中可以添加使用 default 關鍵字修飾的非抽象方法。即:默認方法(或擴展方法)

b. 接口裏可以聲明靜態方法,並且可以實現。

我們都知道在Java語言的接口中只能定義方法名,而不能包含方法的具體實現代碼。接口中定義的方法必須在接口的非抽象子類中實現。下面就是關於接口的一個例子:

public interface SimpleInterface {
  public void doSomeWork();
}
 
class SimpleInterfaceImpl implements SimpleInterface{
  @Override
  public void doSomeWork() {
    System.out.println("Do Some Work implementation in the class");
  }
 
  public static void main(String[] args) {
    SimpleInterfaceImpl simpObj = new SimpleInterfaceImpl();
    simpObj.doSomeWork();
  }
}

那麼,如果我們在SimpleInterface裏面添加一個新方法,會怎樣呢?

public interface SimpleInterface {
  public void doSomeWork();
  public void doSomeOtherWork();
}

如果我們嘗試編譯上面的這段代碼,會得到如下結果:

$javac .\SimpleInterface.java
.\SimpleInterface.java:18: error: SimpleInterfaceImpl is not abstract and does not 
override abstract method doSomeOtherWork() in SimpleInterface
class SimpleInterfaceImpl implements SimpleInterface{
^
1 error

因爲接口有這個語法限制,所以要直接改變/擴展接口內的方法變得非常困難。我們在嘗試強化Java 8 Collections API,讓其支持lambda表達式的時候,就面臨了這樣的挑戰。爲了克服這個困難,Java 8中引入了一個新的概念,叫做default方法,也可以稱爲Defender方法,或者虛擬擴展方法(Virtual extension methods)。

Default方法是指,在接口內部包含了一些默認的方法實現(也就是接口中可以包含方法體,這打破了Java之前版本對接口的語法限制),從而使得接口在進行擴展的時候,不會破壞與接口相關的實現類代碼。接下來,讓我們看一個例子:

public interface SimpleInterface {
  public void doSomeWork();
 
  //A default method in the interface created using "default" keyword
  //使用default關鍵字創在interface中直接創建一個default方法,該方法包含了具體的實現代碼
  default public void doSomeOtherWork(){
    System.out.println("DoSomeOtherWork implementation in the interface");
  }
}
 
class SimpleInterfaceImpl implements SimpleInterface{
  @Override
  public void doSomeWork() {
    System.out.println("Do Some Work implementation in the class");
  }
  /*
   * Not required to override to provide an implementation 
   * for doSomeOtherWork.
   * 在SimpleInterfaceImpl裏,不需要再去實現接口中定義的doSomeOtherWork方法
   */
 
  public static void main(String[] args) {
    SimpleInterfaceImpl simpObj = new SimpleInterfaceImpl();
    simpObj.doSomeWork();
    simpObj.doSomeOtherWork();
  }
}

該程序的輸出是:

Do Some Work implementation in the class
DoSomeOtherWork implementation in the interface

現在大家問得比較多的一個問題是:如果一個類實現了兩個接口(可以看做是“多繼承”),這兩個接口又同時都包含了一個名字相同的default方法,那麼會發生什麼情況? 在這樣的情況下,編譯器會報錯。讓我用例子來解釋一下:

public interface InterfaceWithDefaultMethod {
  public void someMethod();
  default public void someOtherMethod(){
    System.out.println("Default method implementation in the interface");
  }
}
public interface InterfaceWithAnotherDefMethod {
  default public void someOtherMethod(){
    System.out.println("Default method implementation in the interface");
  }
}
然後我們定義一個類,同時實現以上兩個接口:
public class DefaultMethodSample implements
  InterfaceWithDefaultMethod, InterfaceWithAnotherDefMethod{
 
  @Override
  public void someMethod(){
    System.out.println("Some method implementation in the class");
  }
  public static void main(String[] args) {
    DefaultMethodSample def1 = new DefaultMethodSample();
    def1.someMethod();
    def1.someOtherMethod();
  }  
}
如果編譯以上的代碼,會得到一個編譯器錯誤,如下所示。因爲編譯器不知道應該在兩個同名的default方法中選擇哪一個,因此產生了二義性。



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