可能是把 Java 接口講得最通俗的一篇文章
摘要:來看一下 Electronic 接口反編譯後的字節碼吧,你會發現,接口中定義的所有變量或者方法,都會自動添加上 public 關鍵字——假如你想知道編譯器在背後都默默做了哪些輔助,記住反編譯字節碼就對了。1)接口中定義的變量會在編譯的時候自動加上 public static final 修飾符,也就是說 LED 變量其實是一個常量。
讀者春夏秋冬在抽象類的那篇文章中留言,“二哥,面試官最喜歡問的一個問題就是,‘兄弟,說說抽象類和接口之間的區別?’,啥時候講講接口唄!”
對於面向對象編程來說,抽象是一個極具魅力的特徵。如果一個程序員的抽象思維很差,那他在編程中就會遇到很多困難,無法把業務變成具體的代碼。在 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)沒有使用 private
、 default
或者 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
接口,從而具有序列化的功能。
就這麼多吧,能說道這份上,我相信面試官就不會爲難你了。
------------------
公衆號:沉默王二(ID:cmower)
CSDN:沉默王二
這是一個有顏值卻靠才華喫飯的程序員,你知道,他的文章風趣幽默,讀起來就好像花錢一樣爽快。
長按下圖二維碼關注,你將感受到一個有趣的靈魂, 且每篇文章都有乾貨。
-------------- --- -
原創不易,莫要白票,覺得有點用的話,就請你爲本文點個 在看 ,或者無情地 轉發 吧 ,因爲這將是我寫作更多優質文章的最強動力。