你很清楚的知道什么時候用抽象類,什么時候用接口么?
p.s. 多文字預警!
1 抽象類和接口簡介
1.1 抽象類
1.1.1 一個小案例
我們先來看這樣一個案例:世界上有許許多多不同種類的動物,每一種動物都要吃東西,移動(走路?飛?)等等。現在讓你用java語言描述一下這個案例。
啊,你會覺得,so easy。我可是學過繼承的人,一個小繼承就能解決問題:
// 父類
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
看起來,你大概完成的不錯。
但是,我想問你一個問題: new Animal().move()
這段代碼描述了一個什么現實情景?
”創建了一個動物,然后讓這個動物移動“,你可能會這么回答我。但是,你難道沒有發現問題么?現實世界里,有叫做【動物】的生物么?你見過這個叫做【動物】的生物移動么?
動物,是對生物的一種統稱,狗是動物,鳥也是動物。但是【動物】本身是一個抽象的概念,你在現實世界中,并沒有見過一種叫做【動物】生物吧?
你應該明白了,我們可以new一個Bird,new一個Dog,因為它們是實實在在的對象,但是我們不應該new出一個Animal來,因為動物是一個抽象的概念,實際上它并不存在。
事實上,Animal中的move()方法,也是有問題的不是么?既然Animal不存在,那它怎么會有真實存在的move()方法呢?
問題來了。。。
1.1.2 抽象類和抽象方法
在面向對象的概念中,所有的對象都是通過類來描繪的,但是反過來,并不是所有的類都是用來描繪對象的,如果一個類中沒有包含足夠的信息來描繪一個具體的對象,這樣的類就是抽象類。
就像我們上面中的例子一樣。Dog和Bird可以用一個普通類來描繪,但是Animal不可以,Animal就應該是一個抽象類。
在java中,被abstract修飾的類,叫做抽象類。抽象類中可以定義抽象方法,也可以定義普通方法。抽象類不可以被實例化,只有被實體類繼承后,抽象類才會有作用。
抽象方法:
- 被abstract修飾的方法叫做抽象方法,抽象方法沒有方法體,也就是說抽象方法沒有具體的實現。
- 抽象方法必須定義在抽象類中。
- 舉個例子:
abstrac void move();
。這就是一個抽象方法。
回到剛才的問題,我們現在利用抽象類來重構一下我們的代碼:
// 父類
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
因為抽象類不可以實例化,所以現在就不用擔心new Animal()
這樣的情況出現了。并且我們將Animal類中的move方法也定義為抽象方法,所以上面的所有問題,都迎刃而解了。
抽象類就是用來被繼承的,脫離的繼承,抽象類就失去了價值。繼承了抽象類的子類,需要重寫抽象類中所有的抽象方法。
在使用抽象類時需要注意幾點:
- 抽象類不能被實例化,實例化的工作應該交由它的子類來完成,它只需要有一個引用即可。
為什么抽象類不能實例化對象:
- 抽象類的設計目的就是為了處理類似于Animal這種無法準確描述為一個對象的情況。所以不可以實例化。
- 抽象類中可以定義抽象方法。抽象方法是沒有方法體的,必須被子類重寫后,該方法才能被正確調用。如果抽象類能實例化,那么抽象方法也就可以被調用,這顯然是不行的。
-
子類必須重寫所有抽象方法。
當然,不都重寫也可以,但是這樣的話,子類也必須是抽象類。
一個類里只要有一個抽象方法,那么這個類必須定義為抽象類。
抽象類中可以包含具體的方法,當然也可以不包含抽象方法。
-
abstract不能與final并列修飾同一個類。
abstract類就是為了讓子類繼承,而final類不能被繼承。
-
abstract 不能與private、static、final或native并列修飾同一個方法。
抽象方法必須被子類重寫才能使用。
1.2 接口
java中的接口是一系列方法的聲明,是一些方法特征的集合,一個接口只有方法的特征沒有方法的實現,因此這些方法可以在不同的地方被不同的類實現,而這些實現可以具有不同的行為。
接口是一種比抽象類更加抽象的【類】。這里給【類】加引號是我找不到更好的詞來表示,但是我們要明確一點就是,接口本身就不是類。為什么說它更抽象呢?因為抽象類中還可以定義普通方法,但是接口中只能寫抽象方法。
接口是用來建立類與類之間的協議,它所提供的只是一種形式,而沒有具體的實現。接口中的所有方法默認都是public abstract的。
接口是抽象類的延伸,java了保證數據安全是不能多重繼承的,也就是說繼承只能存在一個父類,但是接口不同,一個類可以同時實現多個接口,不管這些接口之間有沒有關系,所以接口彌補了抽象類不能多重繼承的缺陷,但是推薦繼承和接口共同使用,因為這樣既可以保證數據安全性又可以實現多重繼承。
在使用接口過程中需要注意如下幾個問題:
接口中的所有方法訪問權限自動被聲明為public。確切的說只能為public,當然你可以顯示的聲明為protected、private,但是編譯會出錯。
接口中可以定義變量,但是它會被強制變為不可變的常量,因為接口中的“成員變量”會自動變為為public static final。可以通過類命名直接訪問:ImplementClass.name。
實現接口的非抽象類必須要實現該接口的所有方法。抽象類可以不用實現。
在實現多接口的時候一定要避免方法名的重復。
因為一個類可能會實現多個接口,如果這兩個接口有名字相同的方法,會產生意想不到的問題。
不能使用new操作符實例化一個接口,但可以聲明一個接口變量,該變量必須引用(refer to)一個實現該接口的類的對象。可以使用 instanceof 檢查一個對象是否實現了某個特定的接口。例如:if(anObject instanceof Comparable){}。
接口中不存在具體的方法。
值得一提的是,在java8中,接口里也可以定義默認方法:
public interface java8{
//在接口里定義默認方法
default void test(){
System.out.println("java 新特性");
}
}
2 抽象類和接口的區別
基礎知識看完了,我們來看抽象類和接口的區別。
2.1 從概念上來看
前面講過了,這里不再贅述。
2.2 語法定義層面看
在語法層面,Java語言對于abstract class和interface給出了不同的定義方式。
//抽象類
public abstract class AbstractTest {
abstract void method1();
void method2(){
//實現
}
}
//接口
interface InterfaceTest {
void method1();
void method2();
}
2.3 設計理念層面看
前面已經提到過,abstarct class在Java語言中體現了一種繼承關系,要想使得繼承關系合理,父類和派生類之間必須存在【is-a】關系,即父類和派生類在概念本質上應該是相同的。
對于interface 來說則不然,并不要求interface的實現者和interface定義在概念本質上是一致的,僅僅是實現了interface定義的協議而已。
我們來看一個例子:假設在我們的問題領域中有一個關于Door的抽象概念,該Door具有執行兩個動作open和close,此時我們可以通過abstract class或者interface來定義一 個表示該抽象概念的類型,定義方式分別如下所示:
//抽象類
abstract class Door{
abstract void open();
abstract void close();
}
//接口
interface Door{
void open();
void close();
}
其他具體的Door類型可以extends使用abstract class方式定義的Door或者implements使 用interface方式定義的Door。看起來好像使用abstract class和interface沒有大的區別。
如果現在要求Door還要具有報警的功能。我們該如何設計針對該例子的類結構呢(在本例中,主要是為了展示abstract class和interface反映在設計理念上的區別,其他方面無關的問題都做了簡化或者忽略)下面將羅列出可能的解決方案,并從設計理念層面對 這些不同的方案進行分析。
解決方案一:
簡單的在Door的定義中增加一個alarm方法,如下:
abstract class Door{
abstract void open();
abstract void close();
abstract void alarm();
}
或者
interface Door{
void open();
void close();
void alarm();
}
這種方法違反了面向對象設計中的一個核心原則 ISP,在Door的定義中把Door概念本身固有的行為方法和另外一個概念"報警器"的行為方法混在了一起。這樣引起的一個問題是那些僅僅依賴于Door這個概念的模塊會因為"報警器"這個概念的改變而改變,反之依然。
比如說,有一個普普通通的門,實現了Door接口,或者繼承了Door抽象類,它只需要開門和關門的行為,但是當你像上面一樣修改了接口或者抽象類以后,那么這個【普通門】也不得不具備了【報警】的功能,這顯然是不合理的。
ISP(Interface Segregation Principle):面向對象的一個核心原則。它表明使用多個專門的接口比使用單一的總接口要好。
一個類對另外一個類的依賴性應當是建立在最小的接口上的。
一個接口代表一個角色,不應當將不同的角色都交給一個接口。沒有關系的接口合并在一起,形成一個臃腫的大接口,這是對角色和接口的污染。
解決方案二
既然open()、close()和alarm()屬于兩個不同的概念,那么我們依據ISP原則將它們分開定義在兩個代表兩個不同概念的抽象類里面,定義的方式有三種:
- 兩個都使用抽象類來定義。
- 兩個都使用接口來定義。
- 一個使用抽象類定義,一個是用接口定義。
由于java不支持多繼承所以第一種是不可行的。后面兩種都是可行的,但是選擇何種就反映了你對問題域本質的理解。
如果選擇第二種都是接口來定義,那么就反映了兩個問題:
- 我們可能沒有理解清楚問題域,AlarmDoor在概念本質上到底是門還報警器。
- 如果我們對問題域的理解沒有問題,比如我們在分析時確定了AlarmDoor在本質上概念是一致的,那么我們在設計時就沒有正確的反映出我們的設計意圖。因為你使用了兩個接口來進行定義,他們概念的定義并不能夠反映上述含義。
第三種,如果我們對問題域的理解是這樣的:
- AlarmDoor本質上Door,但同時它也擁有報警的行為功能,這個時候我們使用第三種方案恰好可以闡述我們的設計意圖。
- AlarmDoor本質是門,所以對于這個概念我們使用抽象類來定義,同時AlarmDoor具備報警功能,說明它能夠完成報警概念中定義的行為功能,所以alarm可以使用接口來進行定義。如下:
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(){}
}
這種實現方式基本上能夠明確的反映出我們對于問題領域的理解,正確的揭示我們的設計意圖。
其實abstract class表示的是【is-a】關系,interface表示的是【like- a】關系,大家在選擇時可以作為一個依據,當然這是建立在對問題領域的理解上的,比如:如果我們認為AlarmDoor在概念本質上是報警器,同時又具有 Door的功能,那么上述的定義方式就要反過來了。
3 抽象類和接口的使用
看了那么多亂糟糟的分析,我們究竟如何選擇呢?到底是使用抽象類,還是使用接口?
首先,我們要明確一點:抽象類是為了把相同的東西提取出來, 是為了重用; 而接口的作用是提供程序里面固化的契約, 是為了降低偶合。抽象類表示的是,這個對象是什么。接口表示的是,這個對象能做什么。
比如說,現在,我要用java描述一下學生和老師。學生和老師都有姓名,年齡,性別等,都會走路,吃飯;但是老師要授課,而學生要聽課,不同的老師授課的科目不同,不同專業的學生聽的課也不同。
我們可以把老師和學生共有的屬性和方法提取出來,用抽象類表示:
public abstract class Person {
String name;
int age;
String sex;
abstract void eat();
abstract void run();
}
老師會授課,不同的老師授課不同,我們可以定義一個接口:
public interface Teach {
void teach(String className);
}
學生要上課,不同專業的學生上的科目不同,我們也可以定義為接口:
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");
}
}
定義學生:
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");
}
}
這樣使用抽象類和接口,我覺得是一種很合理的方式。
現在有很多討論和建議提倡用interface代替abstract類,兩者從理論上可以做一般性的混用,但是在實際應用中,他們還是有一定區別的。抽象類一般作為公共的父類為子類的擴展提供基礎,這里的擴展包括了屬性上和行為上的。而接口一般來說不考慮屬性,只考慮方法,使得子類可以自由的填補或者擴展接口所定義的方法。
就像這個老師和學生的例子,抽象類提取了他們共有的屬性,他們各自有什么屬性可以交給子類去完成。有人可能會說,為什么不把eat 和 run 方法定義為接口呢?這當然也是可以的。但是我覺得,吃和走,是人自身的一種行為,它不像授課和上課這種是因為某種身份而特有的行為,吃和走與人自身的屬性(姓名,年齡)都是【人】本身就有的,所以我覺得一起放到抽象類里更合適一些。當-然,你單獨定義一個【人行為】的接口從語法角度講也沒問題。
4 再談多態
前面講過,繼承(實現)是多態的前提之一。現在學完了抽象類和接口,多態的使用場景就更多了。
比如我們常用的List接口:
List<String> l = new ArrayList<>();
List<Integer> l1 = new LinkedList<>();
這就是多態的體現。
由于篇幅已經過長,我就不細說了~
5 總結
總結一下抽象類和接口:
1、抽象類和接口都不能直接實例化,如果要實例化,抽象類變量必須指向實現所有抽象方法的子類對象,接口變量必須指向實現所有接口方法的類對象。
2、抽象類要被子類繼承,接口要被類實現。
3、接口只能做方法申明,抽象類中可以做方法申明,也可以做方法實現(不討論java8的情況下)
4、接口里定義的變量只能是公共的靜態的常量,抽象類中的變量是普通變量。
5、抽象類里的抽象方法必須全部被子類所實現,如果子類不能全部實現父類抽象方法,那么該子類只能是抽象類。同樣,一個實現接口的時候,如不能全部實現接口方法,那么該類也只能為抽象類。
6、抽象方法只能申明,不能實現。不能寫成abstract void abc(){}。
7、抽象類里可以沒有抽象方法
8、如果一個類里有抽象方法,那么這個類只能是抽象類
9、抽象方法要被實現,所以不能是靜態的,也不能是私有的。
10、接口可繼承接口,并可多繼承接口,但類只能單根繼承。
11、從實踐的角度來看,如果依賴于抽象類來定義行為,往往導致過于復雜的繼承關系,而通過接口定義行為能夠更有效地分離行為與實現,為代碼的維護和修改帶來方便。
12、選擇抽象類和接口的時候記得一句話:抽象類表示的是,這個對象是什么。接口表示的是,這個對象能做什么。
13、使用抽象類,要保證和實現類之間是【is-a】關系。
其實抽象類和接口的使用時有很多爭議的,沒有一個人敢說他的想法就是絕對正確的,而別人的想法就是錯誤的。在設計的時候,如何選擇,不僅僅是根據一些理解,還需要一些經驗。有的時候,抽象類是配合接口一起使用的,接口為幾個【普通類】定義了一系列方法,然后抽象類實現該接口并實現了這幾個【普通類】共同的方法,然后幾個【普通類】再繼承抽象類,分別實現各自不同的方法。
知識是死的,人是活的,怎么使用不是聽別人怎么說你就怎么用,更多的是自己的理解。因為,他說的也不一定對啊?
本篇文章到這里就結束了。滿滿的文字,大概你認真看完也累的半死了。但是學習就是這樣,不辛苦一點怎么能學的全,學得會呢?
如果文章內容有什么問題或者錯誤,請及時與我聯系。
轉載請注明出處:
本文地址:http://blog.csdn.net/qq_31655965/article/details/54972723
原創自csdn:http://blog.csdn.net/qq_31655965
同步更新在:
我的博客:wpblog.improvecfan.cn
簡書:http://www.lxweimin.com/u/8dc5811b228f
博主:cleverfan
看完了,如果對你有幫助,隨手點個贊唄~~~
參考資料:
http://blog.csdn.net/ttgjz/article/details/2960451
http://blog.csdn.net/xw13106209/article/details/6923556
http://blog.csdn.net/wenwen091100304/article/details/48381023