可能是把 Java 接口講得最通俗的一篇文章

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

對(duì)于面向?qū)ο缶幊虂碚f,抽象是一個(gè)極具魅力的特征。如果一個(gè)程序員的抽象思維很差,那他在編程中就會(huì)遇到很多困難,無法把業(yè)務(wù)變成具體的代碼。在 Java 中,可以通過兩種形式來達(dá)到抽象的目的,一種是抽象類,另外一種就是接口。

如果你現(xiàn)在就想知道抽象類與接口之間的區(qū)別,我可以提前給你說一個(gè):

  • 一個(gè)類只能繼承一個(gè)抽象類,但卻可以實(shí)現(xiàn)多個(gè)接口。

當(dāng)然了,在沒有搞清楚接口到底是什么,它可以做什么之前,這個(gè)區(qū)別理解起來會(huì)有點(diǎn)難度。

01、接口是什么

接口是通過 interface 關(guān)鍵字定義的,它可以包含一些常量和方法,來看下面這個(gè)示例。

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

    // 抽象方法
    int getElectricityUse();

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

    // 默認(rèn)方法
    default void printDescription() {
        System.out.println("電子");
    }
}

1)接口中定義的變量會(huì)在編譯的時(shí)候自動(dòng)加上 public static final 修飾符,也就是說 LED 變量其實(shí)是一個(gè)常量。

Java 官方文檔上有這樣的聲明:

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

換句話說,接口可以用來作為常量類使用,還能省略掉 public static final,看似不錯(cuò)的一種選擇,對(duì)吧?

不過,這種選擇并不可取。因?yàn)榻涌诘谋疽馐菍?duì)方法進(jìn)行抽象,而常量接口會(huì)對(duì)子類中的變量造成命名空間上的“污染”。

2)沒有使用 private、default 或者 static 關(guān)鍵字修飾的方法是隱式抽象的,在編譯的時(shí)候會(huì)自動(dòng)加上 public abstract 修飾符。也就是說 getElectricityUse() 其實(shí)是一個(gè)抽象方法,沒有方法體——這是定義接口的本意。

3)從 Java 8 開始,接口中允許有靜態(tài)方法,比如說 isEnergyEfficient() 方法。

靜態(tài)方法無法由(實(shí)現(xiàn)了該接口的)類的對(duì)象調(diào)用,它只能通過接口的名字來調(diào)用,比如說 Electronic.isEnergyEfficient("LED")。

接口中定義靜態(tài)方法的目的是為了提供一種簡(jiǎn)單的機(jī)制,使我們不必創(chuàng)建對(duì)象就能調(diào)用方法,從而提高接口的競(jìng)爭(zhēng)力。

4)接口中允許定義 default 方法也是從 Java 8 開始的,比如說 printDescription(),它始終由一個(gè)代碼塊組成,為實(shí)現(xiàn)該接口而不覆蓋該方法的類提供默認(rèn)實(shí)現(xiàn),也就是說,無法直接使用一個(gè)“;”號(hào)來結(jié)束默認(rèn)方法——編譯器會(huì)報(bào)錯(cuò)的。

允許在接口中定義默認(rèn)方法的理由是很充分的,因?yàn)橐粋€(gè)接口可能有多個(gè)實(shí)現(xiàn)類,這些類就必須實(shí)現(xiàn)接口中定義的抽象類,否則編譯器就會(huì)報(bào)錯(cuò)。假如我們需要在所有的實(shí)現(xiàn)類中追加某個(gè)具體的方法,在沒有 default 方法的幫助下,我們就必須挨個(gè)對(duì)實(shí)現(xiàn)類進(jìn)行修改。

來看一下 Electronic 接口反編譯后的字節(jié)碼吧,你會(huì)發(fā)現(xiàn),接口中定義的所有變量或者方法,都會(huì)自動(dòng)添加上 public 關(guān)鍵字——假如你想知道編譯器在背后都默默做了哪些輔助,記住反編譯字節(jié)碼就對(duì)了。

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

有些讀者可能會(huì)問,“二哥,為什么我反編譯后的字節(jié)碼和你的不一樣,你用了什么反編譯工具?”其實(shí)沒有什么秘密,微信搜「沉默王二」回復(fù)關(guān)鍵字「JAD」就可以免費(fèi)獲取了,超級(jí)好用。

02、定義接口的注意事項(xiàng)

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

  • 接口中允許定義變量
  • 接口中允許定義抽象方法
  • 接口中允許定義靜態(tài)方法(Java 8 之后)
  • 接口中允許定義默認(rèn)方法(Java 8 之后)

除此之外,我們還應(yīng)該知道:

1)接口不允許直接實(shí)例化。

需要定義一個(gè)類去實(shí)現(xiàn)接口,然后再實(shí)例化。

public class Computer implements Electronic {

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

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

2)接口可以是空的,既不定義變量,也不定義方法。

public interface Serializable {
}

Serializable 是最典型的一個(gè)空的接口,我之前分享過一篇文章《Java Serializable:明明就一個(gè)空的接口嘛》,感興趣的讀者可以去我的個(gè)人博客看一看,你就明白了空接口的意義。

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

3)不要在定義接口的時(shí)候使用 final 關(guān)鍵字,否則會(huì)報(bào)編譯錯(cuò)誤,因?yàn)榻涌诰褪菫榱俗屪宇悓?shí)現(xiàn)的,而 final 阻止了這種行為。

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

5)接口的變量是隱式 public static final,所以其值無法改變。

03、接口可以做什么

1)使某些實(shí)現(xiàn)類具有我們想要的功能,比如說,實(shí)現(xiàn)了 Cloneable 接口的類具有拷貝的功能,實(shí)現(xiàn)了 Comparable 或者 Comparator 的類具有比較功能。

Cloneable 和 Serializable 一樣,都屬于標(biāo)記型接口,它們內(nèi)部都是空的。實(shí)現(xiàn)了 Cloneable 接口的類可以使用 Object.clone() 方法,否則會(huì)拋出 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();
    }
}

運(yùn)行后沒有報(bào)錯(cuò)?,F(xiàn)在把 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();

    }
}

運(yùn)行后拋出 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 原則上只支持單一繼承,但通過接口可以實(shí)現(xiàn)多重繼承的目的。

可能有些讀者會(huì)問,“二哥,為什么 Java 只支持單一繼承?”簡(jiǎn)單來解釋一下。

如果有兩個(gè)類共同繼承(extends)一個(gè)有特定方法的父類,那么該方法會(huì)被兩個(gè)子類重寫。然后,如果你決定同時(shí)繼承這兩個(gè)子類,那么在你調(diào)用該重寫方法時(shí),編譯器不能識(shí)別你要調(diào)用哪個(gè)子類的方法。這也正是著名的菱形問題,見下圖。

ClassC 同時(shí)繼承了 ClassA 和 ClassB,ClassC 的對(duì)象在調(diào)用 ClassA 和 ClassB 中重載的方法時(shí),就不知道該調(diào)用 ClassA 的方法,還是 ClassB 的方法。

接口沒有這方面的困擾。來定義兩個(gè)接口,F(xiàn)ly 會(huì)飛,Run 會(huì)跑。

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

然后讓一個(gè)類同時(shí)實(shí)現(xiàn)這兩個(gè)接口。

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

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

這就在某種形式上達(dá)到了多重繼承的目的:現(xiàn)實(shí)世界里,豬的確只會(huì)跑,但在雷軍的眼里,站在風(fēng)口的豬就會(huì)飛,這就需要賦予這只豬更多的能力,通過抽象類是無法實(shí)現(xiàn)的,只能通過接口。

3)實(shí)現(xiàn)多態(tài)。

什么是多態(tài)呢?通俗的理解,就是同一個(gè)事件發(fā)生在不同的對(duì)象上會(huì)產(chǎn)生不同的結(jié)果,鼠標(biāo)左鍵點(diǎn)擊窗口上的 X 號(hào)可以關(guān)閉窗口,點(diǎn)擊超鏈接卻可以打開新的網(wǎng)頁。

多態(tài)可以通過繼承(extends)的關(guān)系實(shí)現(xiàn),也可以通過接口的形式實(shí)現(xiàn)。來看這樣一個(gè)例子。

Shape 是表示一個(gè)形狀。

public interface Shape {
    String name();
}

圓是一個(gè)形狀。

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

正方形也是一個(gè)形狀。

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

然后來看測(cè)試類。

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());
}

多態(tài)的存在 3 個(gè)前提:

1、要有繼承關(guān)系,Circle 和 Square 都實(shí)現(xiàn)了 Shape 接口
2、子類要重寫父類的方法,Circle 和 Square 都重寫了 name() 方法
3、父類引用指向子類對(duì)象,circleShape 和 squareShape 的類型都為 Shape,但前者指向的是 Circle 對(duì)象,后者指向的是 Square 對(duì)象。

然后,我們來看一下測(cè)試結(jié)果:

圓
正方形

也就意味著,盡管在 for 循環(huán)中,shape 的類型都為 Shape,但在調(diào)用 name() 方法的時(shí)候,它知道 Circle 對(duì)象應(yīng)該調(diào)用 Circle 類的 name() 方法,Square 對(duì)象應(yīng)該調(diào)用 Square 類的 name() 方法。

04、接口與抽象類的區(qū)別

好了,關(guān)于接口的一切,你應(yīng)該都搞清楚了?,F(xiàn)在回到讀者春夏秋冬的那條留言,“兄弟,說說抽象類和接口之間的區(qū)別?”

1)語法層面上

  • 接口中不能有 public 和 protected 修飾的方法,抽象類中可以有。
  • 接口中的變量只能是隱式的常量,抽象類中可以有任意類型的變量。
  • 一個(gè)類只能繼承一個(gè)抽象類,但卻可以實(shí)現(xiàn)多個(gè)接口。

2)設(shè)計(jì)層面上

抽象類是對(duì)類的一種抽象,繼承抽象類的類和抽象類本身是一種 is-a 的關(guān)系。

接口是對(duì)類的某種行為的一種抽象,接口和類之間并沒有很強(qiáng)的關(guān)聯(lián)關(guān)系,所有的類都可以實(shí)現(xiàn) Serializable 接口,從而具有序列化的功能。

就這么多吧,能說道這份上,我相信面試官就不會(huì)為難你了。

如果覺得文章對(duì)你有點(diǎn)幫助,請(qǐng)微信搜索「 沉默王二 」第一時(shí)間閱讀,回復(fù)「并發(fā)」更有一份阿里大牛重寫的 Java 并發(fā)編程實(shí)戰(zhàn),從此再也不用擔(dān)心面試官在這方面的刁難了。

本文已收錄 GitHub,傳送門~ ,里面更有大廠面試完整考點(diǎn),歡迎 Star。

我是沉默王二,一枚有顏值卻靠才華茍且的程序員。關(guān)注即可提升學(xué)習(xí)效率,別忘了三連啊,點(diǎn)贊、收藏、留言,我不挑,嘻嘻。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容