可能是把 Java 介面講得最通俗的一篇文章

沉默王二發表於2020-05-15

讀者春夏秋冬在抽象類的那篇文章中留言,“二哥,面試官最喜歡問的一個問題就是,‘兄弟,說說抽象類和介面之間的區別?’,啥時候講講介面唄!”

對於物件導向程式設計來說,抽象是一個極具魅力的特徵。如果一個程式設計師的抽象思維很差,那他在程式設計中就會遇到很多困難,無法把業務變成具體的程式碼。在 Java 中,可以通過兩種形式來達到抽象的目的,一種是抽象類,另外一種就是介面。

如果你現在就想知道抽象類與介面之間的區別,我可以提前給你說一個:

  • 一個類只能繼承一個抽象類,但卻可以實現多個介面。

當然了,在沒有搞清楚介面到底是什麼,它可以做什麼之前,這個區別理解起來會有點難度。

01、介面是什麼

介面是通過 interface 關鍵字定義的,它可以包含一些常量和方法,來看下面這個示例。

public interface Electronic {
    // 常量
    String LED = "LED";

    // 抽象方法
    int getElectricityUse();

    // 靜態方法
    static boolean isEnergyEfficient(String electtronicType) {
        return electtronicType.equals(LED);
    }

    // 預設方法
    default void printDescription() {
        System.out.println("電子");
    }
}

1)介面中定義的變數會在編譯的時候自動加上 public static final 修飾符,也就是說 LED 變數其實是一個常量。

Java 官方文件上有這樣的宣告:

Every field declaration in the body of an interface is implicitly public, static, and final.

換句話說,介面可以用來作為常量類使用,還能省略掉 public static final,看似不錯的一種選擇,對吧?

不過,這種選擇並不可取。因為介面的本意是對方法進行抽象,而常量介面會對子類中的變數造成名稱空間上的“汙染”。

2)沒有使用 privatedefault 或者 static 關鍵字修飾的方法是隱式抽象的,在編譯的時候會自動加上 public abstract 修飾符。也就是說 getElectricityUse() 其實是一個抽象方法,沒有方法體——這是定義介面的本意。

3)從 Java 8 開始,介面中允許有靜態方法,比如說 isEnergyEfficient() 方法。

靜態方法無法由(實現了該介面的)類的物件呼叫,它只能通過介面的名字來呼叫,比如說 Electronic.isEnergyEfficient("LED")

介面中定義靜態方法的目的是為了提供一種簡單的機制,使我們不必建立物件就能呼叫方法,從而提高介面的競爭力。

4)介面中允許定義 default 方法也是從 Java 8 開始的,比如說 printDescription(),它始終由一個程式碼塊組成,為實現該介面而不覆蓋該方法的類提供預設實現,也就是說,無法直接使用一個“;”號來結束預設方法——編譯器會報錯的。

允許在介面中定義預設方法的理由是很充分的,因為一個介面可能有多個實現類,這些類就必須實現介面中定義的抽象類,否則編譯器就會報錯。假如我們需要在所有的實現類中追加某個具體的方法,在沒有 default 方法的幫助下,我們就必須挨個對實現類進行修改。

來看一下 Electronic 介面反編譯後的位元組碼吧,你會發現,介面中定義的所有變數或者方法,都會自動新增上 public 關鍵字——假如你想知道編譯器在背後都默默做了哪些輔助,記住反編譯位元組碼就對了。

public interface Electronic
{

    public abstract int getElectricityUse();

    public static boolean isEnergyEfficient(String electtronicType)
    
{
        return electtronicType.equals("LED");
    }

    public void printDescription()
    
{
        System.out.println("\u7535\u5B50");
    }

    public static final String LED = "LED";
}

有些讀者可能會問,“二哥,為什麼我反編譯後的位元組碼和你的不一樣,你用了什麼反編譯工具?”其實沒有什麼祕密,微信搜「沉默王二」回覆關鍵字「JAD」就可以免費獲取了,超級好用。

02、定義介面的注意事項

由之前的例子我們就可以得出下面這些結論:

  • 介面中允許定義變數
  • 介面中允許定義抽象方法
  • 介面中允許定義靜態方法(Java 8 之後)
  • 介面中允許定義預設方法(Java 8 之後)

除此之外,我們還應該知道:

1)介面不允許直接例項化。

需要定義一個類去實現介面,然後再例項化。

public class Computer implements Electronic {

    public static void main(String[] args) {
        new Computer();
    }

    @Override
    public int getElectricityUse() {
        return 0;
    }
}

2)介面可以是空的,既不定義變數,也不定義方法。

public interface Serializable {
}

Serializable 是最典型的一個空的介面,我之前分享過一篇文章《Java Serializable:明明就一個空的介面嘛》,感興趣的讀者可以去我的個人部落格看一看,你就明白了空介面的意義。

http://www.itwanger.com/java/2019/11/14/java-serializable.html

3)不要在定義介面的時候使用 final 關鍵字,否則會報編譯錯誤,因為介面就是為了讓子類實現的,而 final 阻止了這種行為。

4)介面的抽象方法不能是 private、protected 或者 final。

5)介面的變數是隱式 public static final,所以其值無法改變。

03、介面可以做什麼

1)使某些實現類具有我們想要的功能,比如說,實現了 Cloneable 介面的類具有拷貝的功能,實現了 Comparable 或者 Comparator 的類具有比較功能。

Cloneable 和 Serializable 一樣,都屬於標記型介面,它們內部都是空的。實現了 Cloneable 介面的類可以使用 Object.clone() 方法,否則會丟擲 CloneNotSupportedException。

public class CloneableTest implements Cloneable {
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        CloneableTest c1 = new CloneableTest();
        CloneableTest c2 = (CloneableTest) c1.clone();
    }
}

執行後沒有報錯。現在把 implements Cloneable 去掉。

public class CloneableTest {
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        CloneableTest c1 = new CloneableTest();
        CloneableTest c2 = (CloneableTest) c1.clone();

    }
}

執行後丟擲 CloneNotSupportedException:

Exception in thread "main" java.lang.CloneNotSupportedException: com.cmower.baeldung.interface1.CloneableTest
    at java.base/java.lang.Object.clone(Native Method)
    at com.cmower.baeldung.interface1.CloneableTest.clone(CloneableTest.java:6)
    at com.cmower.baeldung.interface1.CloneableTest.main(CloneableTest.java:11)

至於 Comparable 和 Comparator 的用法,感興趣的讀者可以參照我之前寫的另外一篇文章《來吧,一文徹底搞懂Java中的Comparable和Comparator》。

http://www.itwanger.com/java/2020/01/04/java-comparable-comparator.html

2)Java 原則上只支援單一繼承,但通過介面可以實現多重繼承的目的。

可能有些讀者會問,“二哥,為什麼 Java 只支援單一繼承?”簡單來解釋一下。

如果有兩個類共同繼承(extends)一個有特定方法的父類,那麼該方法會被兩個子類重寫。然後,如果你決定同時繼承這兩個子類,那麼在你呼叫該重寫方法時,編譯器不能識別你要呼叫哪個子類的方法。這也正是著名的菱形問題,見下圖。

ClassC 同時繼承了 ClassA 和 ClassB,ClassC 的物件在呼叫 ClassA 和 ClassB 中過載的方法時,就不知道該呼叫 ClassA 的方法,還是 ClassB 的方法。

介面沒有這方面的困擾。來定義兩個介面,Fly 會飛,Run 會跑。

public interface Fly {
    void fly();
}
public interface Run {
    void run();
}

然後讓一個類同時實現這兩個介面。

public class Pig implements Fly,Run{
    @Override
    public void fly() {
        System.out.println("會飛的豬");
    }

    @Override
    public void run() {
        System.out.println("會跑的豬");
    }
}

這就在某種形式上達到了多重繼承的目的:現實世界裡,豬的確只會跑,但在雷軍的眼裡,站在風口的豬就會飛,這就需要賦予這隻豬更多的能力,通過抽象類是無法實現的,只能通過介面。

3)實現多型。

什麼是多型呢?通俗的理解,就是同一個事件發生在不同的物件上會產生不同的結果,滑鼠左鍵點選視窗上的 X 號可以關閉視窗,點選超連結卻可以開啟新的網頁。

多型可以通過繼承(extends)的關係實現,也可以通過介面的形式實現。來看這樣一個例子。

Shape 是表示一個形狀。

public interface Shape {
    String name();
}

圓是一個形狀。

public class Circle implements Shape {
    @Override
    public String name() {
        return "圓";
    }
}

正方形也是一個形狀。

public class Square implements Shape {
    @Override
    public String name() {
        return "正方形";
    }
}

然後來看測試類。

List<Shape> shapes = new ArrayList<>();
Shape circleShape = new Circle();
Shape squareShape = new Square();

shapes.add(circleShape);
shapes.add(squareShape);

for (Shape shape : shapes) {
    System.out.println(shape.name());
}

多型的存在 3 個前提:

1、要有繼承關係,Circle 和 Square 都實現了 Shape 介面
2、子類要重寫父類的方法,Circle 和 Square 都重寫了 name() 方法
3、父類引用指向子類物件,circleShape 和 squareShape 的型別都為 Shape,但前者指向的是 Circle 物件,後者指向的是 Square 物件。

然後,我們來看一下測試結果:


正方形

也就意味著,儘管在 for 迴圈中,shape 的型別都為 Shape,但在呼叫 name() 方法的時候,它知道 Circle 物件應該呼叫 Circle 類的 name() 方法,Square 物件應該呼叫 Square 類的 name() 方法。

04、介面與抽象類的區別

好了,關於介面的一切,你應該都搞清楚了。現在回到讀者春夏秋冬的那條留言,“兄弟,說說抽象類和介面之間的區別?”

1)語法層面上

  • 介面中不能有 public 和 protected 修飾的方法,抽象類中可以有。
  • 介面中的變數只能是隱式的常量,抽象類中可以有任意型別的變數。
  • 一個類只能繼承一個抽象類,但卻可以實現多個介面。

2)設計層面上

抽象類是對類的一種抽象,繼承抽象類的類和抽象類本身是一種 is-a 的關係。

介面是對類的某種行為的一種抽象,介面和類之間並沒有很強的關聯關係,所有的類都可以實現 Serializable 介面,從而具有序列化的功能。

就這麼多吧,能說道這份上,我相信面試官就不會為難你了。

如果覺得文章對你有點幫助,請微信搜尋「 沉默王二 」第一時間閱讀,回覆「併發」更有一份阿里大牛重寫的 Java 併發程式設計實戰,從此再也不用擔心面試官在這方面的刁難了。

本文已收錄 GitHub,傳送門~ ,裡面更有大廠面試完整考點,歡迎 Star。

我是沉默王二,一枚有顏值卻靠才華苟且的程式設計師。關注即可提升學習效率,別忘了三連啊,點贊、收藏、留言,我不挑,嘻嘻

相關文章