1、 抽象類和接口簡介java
1.1 抽象類安全
1.1.1 一個小案例ide
咱們先來看這樣一個事例:世界上有許多的動物,每一種動物都要吃,移動(走?飛?)等等。如今讓你用java語言描述一下這個案例。spa
你會說:很簡單啊,我但是學過繼承的人,一個小繼承就能解決問題。設計
// 父類 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
看起來,你完成的不錯。code
可是,我想問你一個問題: new Animal().move()這段代碼描述了一個什麼現實情景?對象
」建立了一個動物,而後讓這個動物移動「,你可能會這麼回答我。可是,你難道沒有發現問題麼?現實世界裏,有叫作【動物】的生物麼?你見過這個叫作【動物】的生物移動麼?blog
動物,是對生物的一種統稱,狗是動物,鳥也是動物。可是【動物】自己是一個抽象的概念,你在現實世界中,並無見過一種叫作【動物】生物吧?繼承
你應該明白了,咱們能夠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 語法定義層面
在語法層面,Java語言對於abstract class和interface給出了不一樣的定義方式。
//抽象類 public abstract class AbstractTest { abstract void method1(); void method2(){ //實現 } } //接口 interface InterfaceTest { void method1(); void method2(); }
2.2 設計理念層面
前面已經提到過,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、 總結
一、抽象類和接口都不能直接實例化,若是要實例化,抽象類變量必須指向實現全部抽象方法的子類對象,接口變量必須指向實現全部接口方法的類對象。
二、抽象類要被子類繼承,接口要被類實現。
三、接口只能作方法申明,抽象類中能夠作方法申明,也能夠作方法實現(不討論java8的狀況下)
四、接口裏定義的變量只能是公共的靜態的常量,抽象類中的變量是普通變量。
五、抽象類裏的抽象方法必須所有被子類所實現,若是子類不能所有實現父類抽象方法,那麼該子類只能是抽象類。一樣,一個實現接口的時候,如不能所有實現接口方法,那麼該類也只能爲抽象類。
六、抽象方法只能申明,不能實現。不能寫成abstract void abc(){}。
七、抽象類裏能夠沒有抽象方法
八、若是一個類裏有抽象方法,那麼這個類只能是抽象類
九、從實踐的角度來看,若是依賴於抽象類來定義行爲,每每致使過於複雜的繼承關係,而經過接口定義行爲可以更有效地分離行爲與實現,爲代碼的維護和修改帶來方便。
十、選擇抽象類和接口的時候記得一句話:抽象類表示的是,這個對象是什麼。接口表示的是,這個對象能作什麼。