JAVA中的抽象類與接口
在Java中什么時(shí)候應(yīng)該選擇抽象類而不是接口?接受挑戰(zhàn)吧!了解這些Java語(yǔ)言元素之間的區(qū)別以及如何在你的程序中使用它們。
在Java代碼中,甚至在Java開(kāi)發(fā)工具包(JDK)本身中,都有大量的抽象類和接口。每個(gè)代碼元素都有一個(gè)基本的目的:
- 接口是一種代碼契約,必須由一個(gè)具體的類來(lái)實(shí)現(xiàn)。
- 抽象類與普通類相似,不同的是它們可以包括抽象方法,也就是沒(méi)有主體的方法。抽象類不能被實(shí)例化。
許多開(kāi)發(fā)者認(rèn)為接口和抽象類是相似的,但它們實(shí)際上是完全不同的。讓我們來(lái)探討一下它們之間的主要區(qū)別。
接口的本質(zhì)
從本質(zhì)上講,接口是一個(gè)契約,所以它依賴于一個(gè)實(shí)現(xiàn)來(lái)達(dá)到其目的。一個(gè)接口永遠(yuǎn)不可能有狀態(tài),所以它不能使用可變的實(shí)例變量。一個(gè)接口只能使用最終變量。
何時(shí)使用接口
接口對(duì)于解耦代碼和實(shí)現(xiàn)多態(tài)性非常有用。我們可以在JDK中看到一個(gè)例子,就是List 接口:
public interface List<E> extends Collection<E> { int size(); boolean isEmpty(); boolean add(E e); E remove(int index); void clear();}復(fù)制代碼
正如你可能注意到的,這段代碼很短,而且描述性很強(qiáng)。我們可以很容易地看到方法的簽名,我們將用一個(gè)具體的類來(lái)實(shí)現(xiàn)接口中的方法。
List 接口包含一個(gè)契約,可以由ArrayList,Vector,LinkedList, 和其他類來(lái)實(shí)現(xiàn)。
為了使用多態(tài)性,我們可以簡(jiǎn)單地用List 來(lái)聲明我們的變量類型,然后選擇任何一個(gè)可用的實(shí)例化。這里有一個(gè)例子:
List list = new ArrayList();System.out.println(list.getClass()); List list = new LinkedList(); System.out.println(list.getClass());復(fù)制代碼
下面是這段代碼的輸出:
class java.util.ArrayListclass java.util.LinkedList復(fù)制代碼
在這種情況下,ArrayList,LinkedList, 和Vector 的實(shí)現(xiàn)方法都是不同的,這就是使用接口的一個(gè)很好的場(chǎng)景。如果你注意到許多類都屬于一個(gè)父類,其方法動(dòng)作相同,但行為不同,那么使用接口是個(gè)好主意。
接下來(lái),讓我們來(lái)看看我們可以用接口做的幾件事。
重寫(xiě)一個(gè)接口方法
記住,接口是一種必須由具體類來(lái)實(shí)現(xiàn)的契約。接口方法是隱含的抽象的,也需要一個(gè)具體類的實(shí)現(xiàn)。
這里有一個(gè)例子:
public class OverridingDemo { public static void main(String[] args) { Challenger challenger = new JavaChallenger(); challenger.doChallenge(); }}interface Challenger { void doChallenge();}class JavaChallenger implements Challenger { @Override public void doChallenge() { System.out.println("Challenge done!"); }}復(fù)制代碼
下面是這段代碼的輸出:
Challenge done!復(fù)制代碼
注意這個(gè)細(xì)節(jié),接口方法是隱式抽象的。這意味著我們不需要明確地將它們聲明為抽象的。
常量變量
另一條要記住的規(guī)則是,一個(gè)接口只能包含常量變量。因此,下面的代碼是可以的:
public class Challenger { int number = 7; String name = "Java Challenger";}復(fù)制代碼
注意,這兩個(gè)變量都是隱含的final 和static 。這意味著它們是常量,不依賴于一個(gè)實(shí)例,而且不能被改變。
如果我們?cè)噲D改變Challenger 接口中的變量,例如,像這樣:
Challenger.number = 8;Challenger.name = "Another Challenger";復(fù)制代碼
我們會(huì)觸發(fā)一個(gè)編譯錯(cuò)誤,像這樣:
Cannot assign a value to final variable 'number'Cannot assign a value to final variable 'name'復(fù)制代碼
缺省方法
當(dāng)默認(rèn)方法在Java 8中被引入時(shí),一些開(kāi)發(fā)者認(rèn)為它們會(huì)和抽象類一樣。然而這并不正確,因?yàn)榻涌诓荒苡袪顟B(tài)。
默認(rèn)方法可以有一個(gè)實(shí)現(xiàn),而抽象方法則不能。默認(rèn)方法是lambdas和流的偉大創(chuàng)新的結(jié)果,但我們應(yīng)該謹(jǐn)慎使用它們。
JDK中使用默認(rèn)方法的一個(gè)方法是forEach() ,它是Iterable 接口的一部分。我們可以簡(jiǎn)單地重用forEach 方法,而不是將代碼復(fù)制到每個(gè)Iterable 的實(shí)現(xiàn)中:
default void forEach(Consumer<? super T> action) { // Code implementation here…復(fù)制代碼
任何Iterable 實(shí)現(xiàn)都可以使用forEach() 方法,而不需要新的方法實(shí)現(xiàn)。然后,我們可以用一個(gè)默認(rèn)方法來(lái)重用代碼。
讓我們來(lái)創(chuàng)建我們自己的默認(rèn)方法:
public class DefaultMethodExample { public static void main(String[] args) { Challenger challenger = new JavaChallenger(); challenger.doChallenge(); }}class JavaChallenger implements Challenger { }interface Challenger { default void doChallenge() { System.out.println("Challenger doing a challenge!"); }}復(fù)制代碼
下面是輸出結(jié)果:
Challenger doing a challenge!復(fù)制代碼
關(guān)于默認(rèn)方法,需要注意的是,每個(gè)默認(rèn)方法都需要一個(gè)實(shí)現(xiàn)。默認(rèn)方法不能是靜態(tài)的。
現(xiàn)在,讓我們繼續(xù)討論抽象類。
抽象類的本質(zhì)
抽象類可以有實(shí)例變量的狀態(tài)。這意味著一個(gè)實(shí)例變量可以被使用和變異。這里有一個(gè)例子:
public abstract class AbstractClassMutation { private String name = "challenger"; public static void main(String[] args) { AbstractClassMutation abstractClassMutation = new AbstractClassImpl(); abstractClassMutation.name = "mutated challenger"; System.out.println(abstractClassMutation.name); }}class AbstractClassImpl extends AbstractClassMutation { }復(fù)制代碼
下面是輸出結(jié)果:
mutated challenger復(fù)制代碼
抽象類中的抽象方法
就像接口一樣,抽象類可以有抽象方法。抽象方法是一個(gè)沒(méi)有主體的方法。與接口不同,抽象類中的抽象方法必須明確地聲明為抽象的。這里有一個(gè)例子:
public abstract class AbstractMethods { abstract void doSomething();}復(fù)制代碼
試圖聲明一個(gè)沒(méi)有實(shí)現(xiàn)的方法,而且沒(méi)有abstract 關(guān)鍵字,像這樣:
public abstract class AbstractMethods { void doSomethingElse();}復(fù)制代碼
導(dǎo)致了一個(gè)編譯錯(cuò)誤,像這樣:
Missing method body, or declare abstract復(fù)制代碼
什么時(shí)候使用抽象類
當(dāng)你需要實(shí)現(xiàn)可改變狀態(tài)時(shí),使用抽象類是一個(gè)好主意。作為一個(gè)例子,Java集合框架包括AbstractList類,它使用變量的狀態(tài)。
在你不需要維護(hù)類的狀態(tài)的情況下,通常使用一個(gè)接口更好。
實(shí)踐中的抽象類
設(shè)計(jì)模式中的模板方法是使用抽象類的好例子。模板方法模式在具體方法中操作實(shí)例變量。
抽象類和接口的區(qū)別
從面向?qū)ο缶幊痰慕嵌葋?lái)看,接口和抽象類的主要區(qū)別是,接口不能有狀態(tài),而抽象類可以用實(shí)例變量來(lái)有狀態(tài)。
另一個(gè)關(guān)鍵區(qū)別是,類可以實(shí)現(xiàn)一個(gè)以上的接口,但它們只能擴(kuò)展一個(gè)抽象類。這是一個(gè)基于多重繼承(擴(kuò)展一個(gè)以上的類)會(huì)導(dǎo)致代碼死鎖的設(shè)計(jì)決定。Java的工程師們決定要避免這種情況。
另一個(gè)區(qū)別是,接口可以被類實(shí)現(xiàn),也可以被接口擴(kuò)展,但類只能被擴(kuò)展。
還需要注意的是,lambda表達(dá)式只能用于功能接口(指只有一個(gè)方法的接口),而只有一個(gè)抽象方法的抽象類不能使用lambdas。
接受Java代碼挑戰(zhàn)吧!
讓我們通過(guò)一個(gè)Java代碼挑戰(zhàn)來(lái)探索接口和抽象類的主要區(qū)別。我們?cè)谙旅嫣峁┝舜a挑戰(zhàn),你也可以用視頻的形式觀看抽象類與接口的挑戰(zhàn)。
在下面的代碼中,同時(shí)聲明了一個(gè)接口和一個(gè)抽象類,而且代碼中還使用了lambdas:
public class AbstractResidentEvilInterfaceChallenge { static int nemesisRaids = 0; public static void main(String[] args) { Zombie zombie = () -> System.out.println("Graw!!! " + nemesisRaids++); System.out.println("Nemesis raids: " + nemesisRaids); Nemesis nemesis = new Nemesis() { public void shoot() { shoots = 23; }}; Zombie.zombie.shoot(); zombie.shoot(); nemesis.shoot(); System.out.println("Nemesis shoots: " + nemesis.shoots + " and raids: " + nemesisRaids); }}interface Zombie { Zombie zombie = () -> System.out.println("Stars!!!"); void shoot();}abstract class Nemesis implements Zombie { public int shoots = 5;}復(fù)制代碼
你認(rèn)為當(dāng)我們運(yùn)行這段代碼時(shí),會(huì)發(fā)生什么?請(qǐng)從下列選項(xiàng)中選擇一個(gè)。
選項(xiàng)A
Compilation error at line 4復(fù)制代碼
選項(xiàng)B
Graw!!! 0 Nemesis raids: 23 Stars!!! Nemesis shoots: 23 and raids:1復(fù)制代碼
選項(xiàng)C
Nemesis raids: 0 Stars!!! Graw!!! 0 Nemesis shoots: 23 and raids: 1復(fù)制代碼
選項(xiàng)D
Nemesis raids: 0 Stars!!! Graw!!! 1 Nemesis shoots: 23 and raids:1復(fù)制代碼
選項(xiàng)E
Compilation error at line 6復(fù)制代碼
Java代碼挑戰(zhàn)視頻
你為這個(gè)挑戰(zhàn)選擇了正確的輸出嗎?請(qǐng)觀看視頻或繼續(xù)閱讀以了解答案。
了解接口和抽象類及方法
這個(gè)Java代碼挑戰(zhàn)展示了許多關(guān)于接口、抽象方法等的重要概念。逐行瀏覽代碼會(huì)讓我們了解到輸出中發(fā)生的很多事情。
代碼挑戰(zhàn)的第一行包括Zombie 接口的lambda表達(dá)式。請(qǐng)注意,在這個(gè)lambda中,我們正在增加一個(gè)靜態(tài)字段。實(shí)例字段在這里也可以使用,但在lambda之外聲明的局部變量就不行了。因此,到目前為止,這段代碼可以正常編譯。還要注意的是,lambda表達(dá)式還沒(méi)有執(zhí)行,所以nemesisRaids 字段還不會(huì)被遞增。
在這一點(diǎn)上,我們將打印nemesisRaids 字段,它沒(méi)有被增加,因?yàn)?lambda;表達(dá)式還沒(méi)有被調(diào)用,只是被聲明。因此,這一行的輸出將是:
Nemesis raids: 0復(fù)制代碼
這個(gè)Java代碼挑戰(zhàn)中另一個(gè)有趣的概念是,我們正在使用一個(gè)匿名的內(nèi)層類。這基本上意味著任何將實(shí)現(xiàn)Nemesis 抽象類的方法的類。我們并沒(méi)有真正實(shí)例化Nemesis 抽象類,因?yàn)樗鼘?shí)際上是一個(gè)匿名的類。還要注意的是,第一個(gè)具體的類在擴(kuò)展它們的時(shí)候總是有義務(wù)實(shí)現(xiàn)抽象的方法。
在Zombie 接口里面,我們用一個(gè)lambda表達(dá)式聲明了zombie static Zombie 接口。因此,當(dāng)我們調(diào)用zombie shoot 方法時(shí),我們會(huì)打印以下內(nèi)容:
Stars!!!復(fù)制代碼
下一行代碼調(diào)用了我們?cè)陂_(kāi)始時(shí)創(chuàng)建的lambda表達(dá)式。因此,nemesisRaids 這個(gè)變量將被遞增。然而,由于我們使用的是后增量運(yùn)算符,它將只在這條代碼語(yǔ)句之后被增量。接下來(lái)的輸出將是:
Graw!!! 0 復(fù)制代碼
現(xiàn)在,我們將從nemesis 中調(diào)用shoot 方法,這將改變其shoots 實(shí)例變量為23 。 注意,這部分代碼展示了接口和抽象類之間的最大區(qū)別。
最后,我們打印nemesis.shoots 和nemesisRaids 的值。因此,輸出結(jié)果將是:
Nemesis shoots: 23 and raids: 1復(fù)制代碼
綜上所述,正確的輸出是選項(xiàng)C:
Nemesis raids: 0 Stars!!! Graw!!! 0 Nemesis shoots: 23 and raids: 1