你很清楚的知道什么時(shí)候用抽象類顽爹,什么時(shí)候用接口么?
p.s. 多文字預(yù)警兼耀!
1 抽象類和接口簡(jiǎn)介
1.1 抽象類
1.1.1 一個(gè)小案例
我們先來(lái)看這樣一個(gè)案例:世界上有許許多多不同種類的動(dòng)物猾蒂,每一種動(dòng)物都要吃東西,移動(dòng)(走路寸谜?飛?)等等∈翳耄現(xiàn)在讓你用java語(yǔ)言描述一下這個(gè)案例熊痴。
啊他爸,你會(huì)覺得,so easy果善。我可是學(xué)過繼承的人诊笤,一個(gè)小繼承就能解決問題:
// 父類
public class Animal {
public void move(){
System.out.println("i an move");
}
}
// 鳥 類
public class Bird extends Animal {
@Override
public void move() {
System.out.println("i can fly");
}
}
// 狗 類
public class Dog extends Animal {
@Override
public void move() {
System.out.println("i can run");
}
}
//......more
看起來(lái),你大概完成的不錯(cuò)巾陕。
但是讨跟,我想問你一個(gè)問題: new Animal().move()
這段代碼描述了一個(gè)什么現(xiàn)實(shí)情景?
”創(chuàng)建了一個(gè)動(dòng)物鄙煤,然后讓這個(gè)動(dòng)物移動(dòng)“晾匠,你可能會(huì)這么回答我。但是梯刚,你難道沒有發(fā)現(xiàn)問題么凉馆?現(xiàn)實(shí)世界里,有叫做【動(dòng)物】的生物么亡资?你見過這個(gè)叫做【動(dòng)物】的生物移動(dòng)么澜共?
動(dòng)物,是對(duì)生物的一種統(tǒng)稱锥腻,狗是動(dòng)物咳胃,鳥也是動(dòng)物。但是【動(dòng)物】本身是一個(gè)抽象的概念旷太,你在現(xiàn)實(shí)世界中,并沒有見過一種叫做【動(dòng)物】生物吧销睁?
你應(yīng)該明白了供璧,我們可以new一個(gè)Bird,new一個(gè)Dog冻记,因?yàn)樗鼈兪菍?shí)實(shí)在在的對(duì)象睡毒,但是我們不應(yīng)該new出一個(gè)Animal來(lái),因?yàn)閯?dòng)物是一個(gè)抽象的概念冗栗,實(shí)際上它并不存在演顾。
事實(shí)上,Animal中的move()方法隅居,也是有問題的不是么钠至?既然Animal不存在,那它怎么會(huì)有真實(shí)存在的move()方法呢胎源?
問題來(lái)了棉钧。。涕蚤。
1.1.2 抽象類和抽象方法
在面向?qū)ο蟮母拍钪邢芮洌械膶?duì)象都是通過類來(lái)描繪的的诵,但是反過來(lái),并不是所有的類都是用來(lái)描繪對(duì)象的佑钾,如果一個(gè)類中沒有包含足夠的信息來(lái)描繪一個(gè)具體的對(duì)象西疤,這樣的類就是抽象類。
就像我們上面中的例子一樣休溶。Dog和Bird可以用一個(gè)普通類來(lái)描繪代赁,但是Animal不可以,Animal就應(yīng)該是一個(gè)抽象類邮偎。
在java中管跺,被abstract修飾的類,叫做抽象類禾进。抽象類中可以定義抽象方法豁跑,也可以定義普通方法。抽象類不可以被實(shí)例化泻云,只有被實(shí)體類繼承后艇拍,抽象類才會(huì)有作用。
抽象方法:
- 被abstract修飾的方法叫做抽象方法宠纯,抽象方法沒有方法體卸夕,也就是說抽象方法沒有具體的實(shí)現(xiàn)。
- 抽象方法必須定義在抽象類中婆瓜。
- 舉個(gè)例子:
abstrac void move();
快集。這就是一個(gè)抽象方法。
回到剛才的問題廉白,我們現(xiàn)在利用抽象類來(lái)重構(gòu)一下我們的代碼:
// 父類
public abstract class Animal {
public abstract void move();//抽象方法
}
// 鳥 類
public class Bird extends Animal {
@Override
public void move() {
System.out.println("i can fly");
}
}
// 狗 類
public class Dog extends Animal {
@Override
public void move() {
System.out.println("i can run");
}
}
//......more
因?yàn)槌橄箢惒豢梢詫?shí)例化个初,所以現(xiàn)在就不用擔(dān)心new Animal()
這樣的情況出現(xiàn)了。并且我們將Animal類中的move方法也定義為抽象方法猴蹂,所以上面的所有問題院溺,都迎刃而解了。
抽象類就是用來(lái)被繼承的磅轻,脫離的繼承珍逸,抽象類就失去了價(jià)值。繼承了抽象類的子類聋溜,需要重寫抽象類中所有的抽象方法谆膳。
在使用抽象類時(shí)需要注意幾點(diǎn):
- 抽象類不能被實(shí)例化,實(shí)例化的工作應(yīng)該交由它的子類來(lái)完成撮躁,它只需要有一個(gè)引用即可摹量。
為什么抽象類不能實(shí)例化對(duì)象:
- 抽象類的設(shè)計(jì)目的就是為了處理類似于Animal這種無(wú)法準(zhǔn)確描述為一個(gè)對(duì)象的情況。所以不可以實(shí)例化。
- 抽象類中可以定義抽象方法缨称。抽象方法是沒有方法體的凝果,必須被子類重寫后,該方法才能被正確調(diào)用睦尽。如果抽象類能實(shí)例化器净,那么抽象方法也就可以被調(diào)用,這顯然是不行的当凡。
-
子類必須重寫所有抽象方法山害。
當(dāng)然,不都重寫也可以沿量,但是這樣的話浪慌,子類也必須是抽象類躏将。
一個(gè)類里只要有一個(gè)抽象方法刷钢,那么這個(gè)類必須定義為抽象類。
抽象類中可以包含具體的方法逼龟,當(dāng)然也可以不包含抽象方法乌妒。
-
abstract不能與final并列修飾同一個(gè)類汹想。
abstract類就是為了讓子類繼承,而final類不能被繼承撤蚊。
-
abstract 不能與private古掏、static、final或native并列修飾同一個(gè)方法侦啸。
抽象方法必須被子類重寫才能使用槽唾。
1.2 接口
java中的接口是一系列方法的聲明,是一些方法特征的集合光涂,一個(gè)接口只有方法的特征沒有方法的實(shí)現(xiàn)夏漱,因此這些方法可以在不同的地方被不同的類實(shí)現(xiàn),而這些實(shí)現(xiàn)可以具有不同的行為顶捷。
接口是一種比抽象類更加抽象的【類】。這里給【類】加引號(hào)是我找不到更好的詞來(lái)表示屎篱,但是我們要明確一點(diǎn)就是服赎,接口本身就不是類。為什么說它更抽象呢交播?因?yàn)槌橄箢愔羞€可以定義普通方法重虑,但是接口中只能寫抽象方法。
接口是用來(lái)建立類與類之間的協(xié)議秦士,它所提供的只是一種形式缺厉,而沒有具體的實(shí)現(xiàn)。接口中的所有方法默認(rèn)都是public abstract的。
接口是抽象類的延伸提针,java了保證數(shù)據(jù)安全是不能多重繼承的命爬,也就是說繼承只能存在一個(gè)父類,但是接口不同辐脖,一個(gè)類可以同時(shí)實(shí)現(xiàn)多個(gè)接口饲宛,不管這些接口之間有沒有關(guān)系,所以接口彌補(bǔ)了抽象類不能多重繼承的缺陷嗜价,但是推薦繼承和接口共同使用艇抠,因?yàn)檫@樣既可以保證數(shù)據(jù)安全性又可以實(shí)現(xiàn)多重繼承。
在使用接口過程中需要注意如下幾個(gè)問題:
接口中的所有方法訪問權(quán)限自動(dòng)被聲明為public久锥。確切的說只能為public家淤,當(dāng)然你可以顯示的聲明為protected、private瑟由,但是編譯會(huì)出錯(cuò)絮重。
接口中可以定義變量,但是它會(huì)被強(qiáng)制變?yōu)椴豢勺兊某A看硌驗(yàn)榻涌谥械摹俺蓡T變量”會(huì)自動(dòng)變?yōu)闉閜ublic static final绿鸣。可以通過類命名直接訪問:ImplementClass.name暂氯。
實(shí)現(xiàn)接口的非抽象類必須要實(shí)現(xiàn)該接口的所有方法潮模。抽象類可以不用實(shí)現(xiàn)。
在實(shí)現(xiàn)多接口的時(shí)候一定要避免方法名的重復(fù)痴施。
因?yàn)橐粋€(gè)類可能會(huì)實(shí)現(xiàn)多個(gè)接口擎厢,如果這兩個(gè)接口有名字相同的方法,會(huì)產(chǎn)生意想不到的問題辣吃。
不能使用new操作符實(shí)例化一個(gè)接口动遭,但可以聲明一個(gè)接口變量,該變量必須引用(refer to)一個(gè)實(shí)現(xiàn)該接口的類的對(duì)象神得±宓耄可以使用 instanceof 檢查一個(gè)對(duì)象是否實(shí)現(xiàn)了某個(gè)特定的接口。例如:if(anObject instanceof Comparable){}哩簿。
接口中不存在具體的方法宵蕉。
值得一提的是,在java8中节榜,接口里也可以定義默認(rèn)方法:
public interface java8{
//在接口里定義默認(rèn)方法
default void test(){
System.out.println("java 新特性");
}
}
2 抽象類和接口的區(qū)別
基礎(chǔ)知識(shí)看完了羡玛,我們來(lái)看抽象類和接口的區(qū)別。
2.1 從概念上來(lái)看
前面講過了宗苍,這里不再贅述稼稿。
2.2 語(yǔ)法定義層面看
在語(yǔ)法層面薄榛,Java語(yǔ)言對(duì)于abstract class和interface給出了不同的定義方式。
//抽象類
public abstract class AbstractTest {
abstract void method1();
void method2(){
//實(shí)現(xiàn)
}
}
//接口
interface InterfaceTest {
void method1();
void method2();
}
2.3 設(shè)計(jì)理念層面看
前面已經(jīng)提到過让歼,abstarct class在Java語(yǔ)言中體現(xiàn)了一種繼承關(guān)系敞恋,要想使得繼承關(guān)系合理,父類和派生類之間必須存在【is-a】關(guān)系是越,即父類和派生類在概念本質(zhì)上應(yīng)該是相同的耳舅。
對(duì)于interface 來(lái)說則不然,并不要求interface的實(shí)現(xiàn)者和interface定義在概念本質(zhì)上是一致的倚评,僅僅是實(shí)現(xiàn)了interface定義的協(xié)議而已浦徊。
我們來(lái)看一個(gè)例子:假設(shè)在我們的問題領(lǐng)域中有一個(gè)關(guān)于Door的抽象概念,該Door具有執(zhí)行兩個(gè)動(dòng)作open和close天梧,此時(shí)我們可以通過abstract class或者interface來(lái)定義一 個(gè)表示該抽象概念的類型盔性,定義方式分別如下所示:
//抽象類
abstract class Door{
abstract void open();
abstract void close();
}
//接口
interface Door{
void open();
void close();
}
其他具體的Door類型可以extends使用abstract class方式定義的Door或者implements使 用interface方式定義的Door呢岗∶嵯悖看起來(lái)好像使用abstract class和interface沒有大的區(qū)別。
如果現(xiàn)在要求Door還要具有報(bào)警的功能后豫。我們?cè)撊绾卧O(shè)計(jì)針對(duì)該例子的類結(jié)構(gòu)呢(在本例中悉尾,主要是為了展示abstract class和interface反映在設(shè)計(jì)理念上的區(qū)別,其他方面無(wú)關(guān)的問題都做了簡(jiǎn)化或者忽略)下面將羅列出可能的解決方案挫酿,并從設(shè)計(jì)理念層面對(duì) 這些不同的方案進(jìn)行分析构眯。
解決方案一:
簡(jiǎn)單的在Door的定義中增加一個(gè)alarm方法,如下:
abstract class Door{
abstract void open();
abstract void close();
abstract void alarm();
}
或者
interface Door{
void open();
void close();
void alarm();
}
這種方法違反了面向?qū)ο笤O(shè)計(jì)中的一個(gè)核心原則 ISP早龟,在Door的定義中把Door概念本身固有的行為方法和另外一個(gè)概念"報(bào)警器"的行為方法混在了一起惫霸。這樣引起的一個(gè)問題是那些僅僅依賴于Door這個(gè)概念的模塊會(huì)因?yàn)?報(bào)警器"這個(gè)概念的改變而改變,反之依然葱弟。
比如說壹店,有一個(gè)普普通通的門,實(shí)現(xiàn)了Door接口芝加,或者繼承了Door抽象類硅卢,它只需要開門和關(guān)門的行為,但是當(dāng)你像上面一樣修改了接口或者抽象類以后藏杖,那么這個(gè)【普通門】也不得不具備了【報(bào)警】的功能将塑,這顯然是不合理的。
ISP(Interface Segregation Principle):面向?qū)ο蟮囊粋€(gè)核心原則制市。它表明使用多個(gè)專門的接口比使用單一的總接口要好。
一個(gè)類對(duì)另外一個(gè)類的依賴性應(yīng)當(dāng)是建立在最小的接口上的弊予。
一個(gè)接口代表一個(gè)角色祥楣,不應(yīng)當(dāng)將不同的角色都交給一個(gè)接口。沒有關(guān)系的接口合并在一起,形成一個(gè)臃腫的大接口误褪,這是對(duì)角色和接口的污染责鳍。
解決方案二
既然open()、close()和alarm()屬于兩個(gè)不同的概念兽间,那么我們依據(jù)ISP原則將它們分開定義在兩個(gè)代表兩個(gè)不同概念的抽象類里面历葛,定義的方式有三種:
- 兩個(gè)都使用抽象類來(lái)定義。
- 兩個(gè)都使用接口來(lái)定義嘀略。
- 一個(gè)使用抽象類定義恤溶,一個(gè)是用接口定義。
由于java不支持多繼承所以第一種是不可行的帜羊。后面兩種都是可行的咒程,但是選擇何種就反映了你對(duì)問題域本質(zhì)的理解。
如果選擇第二種都是接口來(lái)定義讼育,那么就反映了兩個(gè)問題:
- 我們可能沒有理解清楚問題域帐姻,AlarmDoor在概念本質(zhì)上到底是門還報(bào)警器。
- 如果我們對(duì)問題域的理解沒有問題奶段,比如我們?cè)诜治鰰r(shí)確定了AlarmDoor在本質(zhì)上概念是一致的饥瓷,那么我們?cè)谠O(shè)計(jì)時(shí)就沒有正確的反映出我們的設(shè)計(jì)意圖。因?yàn)槟闶褂昧藘蓚€(gè)接口來(lái)進(jìn)行定義痹籍,他們概念的定義并不能夠反映上述含義呢铆。
第三種,如果我們對(duì)問題域的理解是這樣的:
- AlarmDoor本質(zhì)上Door词裤,但同時(shí)它也擁有報(bào)警的行為功能刺洒,這個(gè)時(shí)候我們使用第三種方案恰好可以闡述我們的設(shè)計(jì)意圖。
- AlarmDoor本質(zhì)是門吼砂,所以對(duì)于這個(gè)概念我們使用抽象類來(lái)定義逆航,同時(shí)AlarmDoor具備報(bào)警功能,說明它能夠完成報(bào)警概念中定義的行為功能渔肩,所以alarm可以使用接口來(lái)進(jìn)行定義因俐。如下:
abstract class Door{
abstract void open();
abstract void close();
}
interface Alarm{
void alarm();
}
class AlarmDoor extends Door implements Alarm{
void open(){}
void close(){}
void alarm(){}
}
這種實(shí)現(xiàn)方式基本上能夠明確的反映出我們對(duì)于問題領(lǐng)域的理解,正確的揭示我們的設(shè)計(jì)意圖周偎。
其實(shí)abstract class表示的是【is-a】關(guān)系抹剩,interface表示的是【like- a】關(guān)系,大家在選擇時(shí)可以作為一個(gè)依據(jù)蓉坎,當(dāng)然這是建立在對(duì)問題領(lǐng)域的理解上的澳眷,比如:如果我們認(rèn)為AlarmDoor在概念本質(zhì)上是報(bào)警器,同時(shí)又具有 Door的功能蛉艾,那么上述的定義方式就要反過來(lái)了钳踊。
3 抽象類和接口的使用
看了那么多亂糟糟的分析衷敌,我們究竟如何選擇呢?到底是使用抽象類拓瞪,還是使用接口缴罗?
首先,我們要明確一點(diǎn):抽象類是為了把相同的東西提取出來(lái), 是為了重用; 而接口的作用是提供程序里面固化的契約, 是為了降低偶合祭埂。抽象類表示的是面氓,這個(gè)對(duì)象是什么。接口表示的是蛆橡,這個(gè)對(duì)象能做什么舌界。
比如說,現(xiàn)在航罗,我要用java描述一下學(xué)生和老師禀横。學(xué)生和老師都有姓名,年齡粥血,性別等柏锄,都會(huì)走路,吃飯复亏;但是老師要授課趾娃,而學(xué)生要聽課,不同的老師授課的科目不同缔御,不同專業(yè)的學(xué)生聽的課也不同抬闷。
我們可以把老師和學(xué)生共有的屬性和方法提取出來(lái),用抽象類表示:
public abstract class Person {
String name;
int age;
String sex;
abstract void eat();
abstract void run();
}
老師會(huì)授課耕突,不同的老師授課不同笤成,我們可以定義一個(gè)接口:
public interface Teach {
void teach(String className);
}
學(xué)生要上課,不同專業(yè)的學(xué)生上的科目不同眷茁,我們也可以定義為接口:
public interface TakeClass {
void takeClass(String className);
}
定義老師:
public class Teacher extends Person implements Teach {
@Override
public void teach(String className) {
System.out.println("teach " + className);
}
@Override
void eat() {
System.out.println("teacher eat");
}
@Override
void run() {
System.out.println("teacher run");
}
}
定義學(xué)生:
public class Student extends Person implements TakeClass {
@Override
public void takeClass(String className) {
System.out.println("take class: " + className);
}
@Override
void eat() {
System.out.println("student eat");
}
@Override
void run() {
System.out.println("student run");
}
}
這樣使用抽象類和接口炕泳,我覺得是一種很合理的方式。
現(xiàn)在有很多討論和建議提倡用interface代替abstract類上祈,兩者從理論上可以做一般性的混用培遵,但是在實(shí)際應(yīng)用中,他們還是有一定區(qū)別的登刺。抽象類一般作為公共的父類為子類的擴(kuò)展提供基礎(chǔ)籽腕,這里的擴(kuò)展包括了屬性上和行為上的。而接口一般來(lái)說不考慮屬性纸俭,只考慮方法皇耗,使得子類可以自由的填補(bǔ)或者擴(kuò)展接口所定義的方法。
就像這個(gè)老師和學(xué)生的例子揍很,抽象類提取了他們共有的屬性郎楼,他們各自有什么屬性可以交給子類去完成矾瘾。有人可能會(huì)說,為什么不把eat 和 run 方法定義為接口呢箭启?這當(dāng)然也是可以的。但是我覺得蛉迹,吃和走傅寡,是人自身的一種行為,它不像授課和上課這種是因?yàn)槟撤N身份而特有的行為北救,吃和走與人自身的屬性(姓名荐操,年齡)都是【人】本身就有的,所以我覺得一起放到抽象類里更合適一些珍策。當(dāng)-然托启,你單獨(dú)定義一個(gè)【人行為】的接口從語(yǔ)法角度講也沒問題。
4 再談多態(tài)
前面講過攘宙,繼承(實(shí)現(xiàn))是多態(tài)的前提之一⊥退剩現(xiàn)在學(xué)完了抽象類和接口,多態(tài)的使用場(chǎng)景就更多了蹭劈。
比如我們常用的List接口:
List<String> l = new ArrayList<>();
List<Integer> l1 = new LinkedList<>();
這就是多態(tài)的體現(xiàn)疗绣。
由于篇幅已經(jīng)過長(zhǎng),我就不細(xì)說了~
5 總結(jié)
總結(jié)一下抽象類和接口:
1铺韧、抽象類和接口都不能直接實(shí)例化多矮,如果要實(shí)例化,抽象類變量必須指向?qū)崿F(xiàn)所有抽象方法的子類對(duì)象哈打,接口變量必須指向?qū)崿F(xiàn)所有接口方法的類對(duì)象塔逃。
2、抽象類要被子類繼承料仗,接口要被類實(shí)現(xiàn)湾盗。
3、接口只能做方法申明罢维,抽象類中可以做方法申明淹仑,也可以做方法實(shí)現(xiàn)(不討論java8的情況下)
4、接口里定義的變量只能是公共的靜態(tài)的常量肺孵,抽象類中的變量是普通變量匀借。
5、抽象類里的抽象方法必須全部被子類所實(shí)現(xiàn)平窘,如果子類不能全部實(shí)現(xiàn)父類抽象方法吓肋,那么該子類只能是抽象類。同樣瑰艘,一個(gè)實(shí)現(xiàn)接口的時(shí)候是鬼,如不能全部實(shí)現(xiàn)接口方法肤舞,那么該類也只能為抽象類。
6均蜜、抽象方法只能申明李剖,不能實(shí)現(xiàn)。不能寫成abstract void abc(){}囤耳。
7篙顺、抽象類里可以沒有抽象方法
8、如果一個(gè)類里有抽象方法充择,那么這個(gè)類只能是抽象類
9德玫、抽象方法要被實(shí)現(xiàn),所以不能是靜態(tài)的椎麦,也不能是私有的宰僧。
10、接口可繼承接口观挎,并可多繼承接口琴儿,但類只能單根繼承。
11嘁捷、從實(shí)踐的角度來(lái)看凤类,如果依賴于抽象類來(lái)定義行為,往往導(dǎo)致過于復(fù)雜的繼承關(guān)系普气,而通過接口定義行為能夠更有效地分離行為與實(shí)現(xiàn)谜疤,為代碼的維護(hù)和修改帶來(lái)方便。
12现诀、選擇抽象類和接口的時(shí)候記得一句話:抽象類表示的是夷磕,這個(gè)對(duì)象是什么。接口表示的是仔沿,這個(gè)對(duì)象能做什么坐桩。
13、使用抽象類封锉,要保證和實(shí)現(xiàn)類之間是【is-a】關(guān)系绵跷。
其實(shí)抽象類和接口的使用時(shí)有很多爭(zhēng)議的,沒有一個(gè)人敢說他的想法就是絕對(duì)正確的成福,而別人的想法就是錯(cuò)誤的碾局。在設(shè)計(jì)的時(shí)候,如何選擇奴艾,不僅僅是根據(jù)一些理解净当,還需要一些經(jīng)驗(yàn)。有的時(shí)候,抽象類是配合接口一起使用的像啼,接口為幾個(gè)【普通類】定義了一系列方法俘闯,然后抽象類實(shí)現(xiàn)該接口并實(shí)現(xiàn)了這幾個(gè)【普通類】共同的方法,然后幾個(gè)【普通類】再繼承抽象類忽冻,分別實(shí)現(xiàn)各自不同的方法真朗。
知識(shí)是死的,人是活的僧诚,怎么使用不是聽別人怎么說你就怎么用蜜猾,更多的是自己的理解。因?yàn)檎裎埽f的也不一定對(duì)啊衍菱?
本篇文章到這里就結(jié)束了赶么。滿滿的文字,大概你認(rèn)真看完也累的半死了脊串。但是學(xué)習(xí)就是這樣辫呻,不辛苦一點(diǎn)怎么能學(xué)的全,學(xué)得會(huì)呢琼锋?
如果文章內(nèi)容有什么問題或者錯(cuò)誤放闺,請(qǐng)及時(shí)與我聯(lián)系。
轉(zhuǎn)載請(qǐng)注明出處:
本文地址:http://blog.csdn.net/qq_31655965/article/details/54972723
原創(chuàng)自csdn:http://blog.csdn.net/qq_31655965
同步更新在:
我的博客:wpblog.improvecfan.cn
簡(jiǎn)書:http://www.reibang.com/u/8dc5811b228f
博主:cleverfan
看完了缕坎,如果對(duì)你有幫助怖侦,隨手點(diǎn)個(gè)贊唄~~~
參考資料:
http://blog.csdn.net/ttgjz/article/details/2960451
http://blog.csdn.net/xw13106209/article/details/6923556
http://blog.csdn.net/wenwen091100304/article/details/48381023