從JVM看Java語言特性(四) 接口和抽象類
接口和抽象類都是上層抽象, 一個類可以實現(xiàn)多個接口卻只能繼承一個抽象類. 從上一篇文章中我們大致明白了繼承和多態(tài)是如何實現(xiàn)的, 多態(tài)通過JVM在vtable放置不同的方法指針來決定到底是調(diào)用父類的方法還是子類的方法. 按照多態(tài)的這個思路的話, 接口應該就是一個指示器, 方便JVM來判斷vtable里面需要哪些方法. 那么到底是不是這么回事兒我們目前還不知道, 這篇文章讓我們探索一下接口和抽象類的一些細節(jié).
1. 接口與抽象類的實現(xiàn)
如果一個接口里面有一個getA()方法的話, 它的方法字節(jié)碼是這樣的:
public abstract int getA();
descriptor: ()I
flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
另一方面, 如果一個抽象類里面只有一個abstract getA()方法, 它的方法字節(jié)碼是這樣的:
public Dao.FInterface();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
abstract int getA();
descriptor: ()I
flags: (0x0400) ACC_ABSTRACT
可以看到, 抽象類有構(gòu)造器, 如果里面沒有抽象方法的話它和一般的類沒什么區(qū)別. 但是如果抽象類里有抽象方法的話, 那么這些抽象方法和接口里的抽象方法也幾乎沒有區(qū)別, 唯一的區(qū)別是抽象類的抽象方法可以是加上訪問修飾符, 如public或protected, 也可以不加修飾符(給抽象方法加private會報錯, 因為這樣的話這個抽象方法永遠無法被實現(xiàn), 進而就沒辦法生成這個類的對象, 沒有意義), 而接口只能為public(不加修飾符也會默認為public).
這樣來看, 抽象類的結(jié)構(gòu)像是一個普通類加上接口的抽象方法.
接口不能有構(gòu)造器, 因為接口是一種規(guī)范, 類可以實現(xiàn)多個接口,若多個接口都有自己的構(gòu)造器,則不好決定構(gòu)造器鏈的調(diào)用次序.
2. 接口和抽象類的意義
傳統(tǒng)多繼承的復雜度一直是逃避不開的問題, 如果繼承的兩個父類里面包含了相同名稱的方法或參量, 那么 JVM無法判斷到底需要執(zhí)行哪一種, 沖突問題就會非常明顯. 由此, Java采用了單繼承來避免這個問題, 為了提高靈活程度, 又引入接口來實現(xiàn)輕耦合的多繼承. 接口用不會引起沖突的方式解決了多重繼承的問題
接口的目的是實現(xiàn), 它和實現(xiàn)類的關(guān)系是"has a", 也就是說接口可以是實現(xiàn)類的一個組件, 一個部分.
抽象類的目的則是繼承, 它和實現(xiàn)類的關(guān)系是"is a", 其實完全可以不存在抽象類這個東西, 用一個普通類代替抽象類, 將所有抽象方法都改成空的實方法, 然后再讓子類將其重寫, 這也就成了變相的抽象類. 這樣子想下去抽象類的唯一作用就是幫助程序員少犯錯(抽象類無法實現(xiàn), 只能多態(tài), 減少程序員誤操作的可能), 并且?guī)椭绦騿T理順繼承關(guān)系(名字就叫抽象類, 一看就知道可能有多個類繼承于它).
在知乎上看到的一句話說的特別好[傳送門] "抽象類主要是用來抽象類別, 接口主要是用來抽象方法功能. 當你關(guān)注事物的本質(zhì)的時候, 用抽象類; 當你關(guān)注一種操作的時候用接口",
3. 抽象類與接口中的參數(shù)
抽象類由于是一個類, 并且有構(gòu)造函數(shù), 所以它和一個普通類對參數(shù)的處理是一樣的, 具體可見我的這篇文章, 因此這里就不展開了.
但是對于接口來說, 沒有構(gòu)造器的話接口是如何初始化它的數(shù)據(jù)的? 這個問題值得探討. 我們假設(shè)有下面這個接口:
public interface FInterface {
int a = 1;
public static int c = 2;
public static final int d = 1;
int getA();
}
它的方法字節(jié)碼是:
public static final int a;
descriptor: I
flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 1
public static final int b;
descriptor: I
flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 2
public static final int c;
descriptor: I
flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 1
public abstract int getA();
descriptor: ()I
flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
經(jīng)過測試, 發(fā)現(xiàn)無論有沒有public static final, 生成的class文件里都會有這三個關(guān)鍵詞. 也就意味著接口里面所有的參數(shù)都是常量, 在類加載過程中都已經(jīng)被解析并且被放進了常量池里.
4. jdk8 和 jdk9 中接口的變化
-
jdk8 中接口允許有default方法
default方法是指接口默認實現(xiàn)的方法, 主要是為了解決舊版本的兼容性問題, 以下是無default關(guān)鍵字和有default關(guān)鍵字的字節(jié)碼對比:
public abstract int getA(); descriptor: ()I flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
public int getA(); descriptor: ()I flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: iconst_1 1: ireturn LineNumberTable: line 13: 0
可以發(fā)現(xiàn)default方法就是一個普通類里面實際存在的方法. 這樣Java就實現(xiàn)了方法的多繼承, 雖然設(shè)計的初衷并不是讓使用者去濫用default, 只是一種為了不破壞之前代碼的一種妥協(xié).
那如果兩個接口都有相同的方法不就沖突了嗎? 其實可以使用接口名+.super.+方法名來區(qū)分不同的方法, 就像下面這樣:
A.super.getA(); B.super.getA();
這樣就不會引起沖突了. 但是Java為了讓程序員少犯錯誤, 除非實現(xiàn)類重寫了這個方法, 不允許實現(xiàn)的兩個接口里有相同的default方法.
除此之外, jdk9支持接口存在私有方法, 由于私有方法不存在子類直接調(diào)用, 因此也不會有沖突的問題, 這里就不再展開了.
?
總結(jié)
? 為了寫這一章, 本人查閱了大量資料, 包括了很多博客. 但是經(jīng)過實踐之后, 很多博客都有或多或少的錯誤, 比如有的說接口不能有靜態(tài)方法(靜態(tài)方法的解析是類加載的一部分, 當然可以有!), 我自己以后寫博客的時候會盡量找官方或者權(quán)威的說法來論證, 不會輕易相信網(wǎng)上的東西.
? 總的來說, 接口和抽象類是越來越相似了, 兩者的主要區(qū)別在于接口可以實現(xiàn)多個和接口沒有構(gòu)造器.