- 本文是自己對抽象類和接口的理解,如果不對請指正,謝謝
抽象類的簡介
- 抽象?抽象是什麼意思?之前在我的 封裝繼承多態 一文中提到了一個杯子的概念,簡單概括一下就是嘴說出來的是一個抽象的概念,因爲並不知道這個杯子的具體參數,比如顏色之類的特點,所以抽象也就是將一個事物的大體結構提取出來,比如我的杯子有蓋子,是保溫的等,然而蓋子是彈射開的還是擰開的以及保溫材料的使用一概不知,所以對應到Java中的抽象類,那麼這個 抽象類也就是對一個事物的概括,(只是嘴說出來的)
- 之前提到的
is-a
和has-a
在這看來,抽象類更符合is-a
的關係,抽象類可以提供方法實現,也可以不提供,但是其被稱爲抽象類的話,那麼必定在類描述上有abstract
關鍵字,而其中的方法完全可以沒有抽象方法的定義 - 方法提供實現與否即是否是抽象方法,就像是你看中一款杯子,但是杯子的提供商拿不準每個人的手型,所以在你購買這個杯子的時候,需要自己選擇杯子柄的形狀,這是強制的,對應到Java抽象類中就是抽象方法,即必須由子類提供實現
- 而杯子的其他特點已經是大衆認可了,比如杯子口是圓的,所以提供商就在你不指定的情況下默認這個形狀了,對應到Java抽象類中就是非抽象方法,當然你也可以定製杯口形狀,對應到Java抽象類中就是子類重寫父類方法了
- 上面提到了杯柄的強制指定,所以在你不指定杯柄的情況下,杯子提供商是不知道你的意思的,因此就無法爲你生成一個合適你自己的杯子,那麼對應到抽象類中就是強制子類去實現這個抽象方法,所以在這就可以看到抽象類是不提供創建抽象對象的操作的,因爲這是風險的,如果你不指定實現,那麼它就不知道怎麼做,做什麼,換句話說就是抽象類就是爲了被繼承而存在的
- 總結:抽象類是對一個事物的概括,屬於is-a,並且由abstract關鍵字進行修飾,其中的內在方法可以有方法實現也可以沒有,沒有方法實現的,子類必須重寫,有方法實現的,子類可以沿用父類的實現,或者再進行重寫定製
抽象類的語法
- 上面提到了重寫,那麼就必然涉及到繼承,所以在抽象類中, 方法不可以是
private abstract
,因爲這些限定符就使得子類獲取不到父類的方法了,違背抽象類的使用原則,所以方法的修飾符就只能是public
與protected
,默認爲public
- abstract修飾類,表示只能被繼承,修飾方法,表明必須由子類實現重寫.而final修飾的類不能被繼承,final修飾的方法不能被重寫,所以final和abstract不能同時修飾類和方法
- static與abstract不能同時修飾某個方法,即沒有所謂的類抽象方法.但是可以同時修飾內部類,
- 如果有一個子類繼承了抽象類,抽象類其中有抽象方法,如果子類也不實現父類中的抽象方法,那麼這個子類也必須定義爲抽象類,原因很簡單:因爲子類也拿不準主意,所以還需要其他類提供實現,因此一個子類如果繼承了抽象類,必須實現抽象類中定義的所有抽象方法
- 抽象類因爲是類,也是class修飾,所以它的子類需要繼承抽象類的時候,也是採用
extends
- 抽象方法不能有方法體,必須由abstract修飾
- 抽象類可以包含成員變量,方法,構造器,初始化塊,內部類.抽象類的構造器不能用於創建實例,主要是用於被其子類調用
- 總結:上面都需要記住
抽象類的使用
- 拿得準的實現,通用的實現寫到抽象類中,否則你就定義抽象方法,由於類是單繼承了,所以只能實現一個抽象類,就不存在抽象方法衝突了
-
下面是一個簡單實現.
public abstract class Animals { abstract void say(); } class Cat extends Animals{ @Override void say() { System.out.println("mm"); } } class Dog extends Animals{ @Override void say() { System.out.println("ww"); } } class Tests { public static void main(String[] args) { Animals cat = new Cat(); Animals dog = new Dog(); cat.say(); dog.say(); } }
-
上面僅僅是實現了父類中的方法,那麼跟下面這種有啥區別呢?
public abstract class Animals { } class Cat extends Animals{ void say() { System.out.println("mm"); } } class Dog extends Animals{ void say() { System.out.println("ww"); } }
- 上面代碼沒有錯誤,但是當運行多態去編寫代碼的時候就會出錯了,因爲父類
Animals
中並沒有say
方法,雖然程序運行邏輯看子類但是父類總的先定義一下,所以抽象類的存在就使我們可以更加方便的運行多態,多態其好處是一旦需求有改動的時候,修改起來靈活,變化起來容易,不用修改過多的代碼 - 抽象類就是爲繼承而存在的,繼承是複用代碼的一個重要的機制,所以抽象類可以將一些事物的默認實現儘量的在類中進行實現,以減少子類代碼的書寫
- 總結:存在繼承關係在在抽象類,抽象類使多態運用的更加靈活,不足的就是單繼承的限制
接口的簡介
- 接口是啥?我可能直接想到的就是網線接口,USB接口,所以這就給了我們很好的啓發了,當需要用USB的時候,我們就插上,不需要就拔下來相當靈活,所以對應到Java中接口主要也是類似的作用,比抽象類更加的靈活,因爲接口可以多實現,需要一個功能我們就可以實現一個接口
- 使用USB你會發現插在那個主機上都可以使用,所以這裏面存在一個USB協議,大家都遵守這個規定,所以USB可以到處插拔,接口的第二個作用就是在這了,即定義協議,一切按協議走,方便你我他
- 在Java中的接口的定義是使用
interface
修飾符的
接口的演進和語法
- 在JDK8之前,接口只能是有抽象方法,就是跟抽象類中的抽象方法一樣的作用,必須由子類實現,而且接口沒有實現,所以在之前接口比抽象類還抽象
-
但是在JDK8的更新中,加入了Stream,Lmabda等一系列功能以及函數式編程的支持,所以新增了一個概念叫做:函數式接口,該接口依舊是
interface
定義,不同的是其中只允許有一個抽象方法的定義,並且標有@FunctionalInterface
註解,這些功能有一部分是對當前的集合類進行操作,但是之前的集合類接口上都是抽象方法,怎麼才能直接對接口進行操作呢,他們沒實現啊,所以爲了這個要求JDK8加入了default
修飾符在接口中,比如public interface Eat { default void say(){ System.out.println("say"); } } class Animals implements Eat{} class Tests{ public static void main(String[] args) { Eat e = new Animals(); e.say(); } }
-
如上代碼是正確的,所以你要知道JDK8的接口中可以有默認實現了,一些集合類中把他的子類的相同操作邏輯提取到了默認方法,所以纔可以直接對接口中的方法進行操作,比如List接口中的替換方法
default void replaceAll(UnaryOperator<E> operator) { Objects.requireNonNull(operator); final ListIterator<E> li = this.listIterator(); while (li.hasNext()) { li.set(operator.apply(li.next())); } }
-
還加入了static修飾的方法
public interface Eat { static void st(){ System.out.println("st"); } } class Animals implements Eat{} class Tests{ public static void main(String[] args) { Eat.st(); //error Static method may be invoked on containing interface class only Animals.st(); } }
- 上面說明在接口定義的靜態方法,只可以
interfaceName.method
調用 -
在JDK9中還加入了private修飾方法,JDK8中不支持,但是後來發現如果沒有這個private修飾的方法,會造成接口中的實現會有重複代碼,所以引入了
private
,如下public interface SSSS { private static void st(){ System.out.println("st"); } private void sts(){ System.out.println("sts"); } static void impl(){ st(); sts(); //error 靜態不能引用實例 } } class Testss{ public static void main(String[] args) { SSSS.impl(); } }
- 如上如果將那個error去掉,代碼就是正確的,但是接口中依舊是不可以對普通方法提供實現的,因爲這是抽象方法
-
下面是函數式接口的一個例子
@FunctionalInterface public interface SSSS { void say(); static void sta(){ } }
- 上面是正確的,有且僅有一個抽象方法的接口可以被標註爲
@FunctionalInterface
,如果你還不是很瞭解函數式接口,可以去看一下我的 Java8學習專輯,這是非常有用的 -
一些細枝末節:
- 由於接口定義的是一種規範,因此接口裏不能包含構造器和初始化塊定義,可以包含常量(靜態常量),方法(只能是抽象實例方法,類方法,默認和私有方法),內部類定義
- 接口中的靜態常量是接口相關的.因此默認增加static和final修飾符.所以在接口中的成員變量總是使用public static final修飾,並且只能在定義時指定初始值
- 接口中如果不是定義默認方法類方法或私有方法,系統自動爲普通方法增加abstract修飾符.接口中的普通方法總是使用public abstract修飾
- 接口中定義的內部類,內部接口,內部枚舉默認都採用public static兩個修飾符,不管定義時是否指定,系統會爲其自動修飾
- 類方法總是public修飾,可以直接使用接口名來調用
- 默認方法都是採用default修飾,不能使用static修飾,並且總是public修飾.需要使用接口的實現類實例來調用這些方法
-
對了,如果一個類實現的兩個接口中有相同的默認方法,那麼必須在子類中進行重新實現
interface SSSS { default void say(){ System.out.println("ss"); } } interface AAAA { default void say(){ System.out.println("aa"); } } class Demo implements AAAA,SSSS{ @Override public void say() { } }
抽象類與接口語法對比
- 接口裏只能包含抽象方法,靜態方法,默認方法和私有方法.不能爲普通方法提供實現,抽象類完全可以
- 接口裏只能定義靜態常量,不能定義普通成員變量,抽象類都可以
- 接口裏不包含構造器;抽象類裏可以包含構造器,抽象類裏的構造器並不是用於創建對象.而是讓其子類調用這些構造器來完成屬於抽象類的初始化操作
- 接口裏不能包含初始化塊,但抽象類則完全可以包含
- 一個類最多只能有一個直接父類,包括抽象類.但一個類可以實現多個接口,通過實現多個接口可以彌補java單繼承的不足
抽象類與接口設計對比
- 並沒有代碼,現在有一個門的對象Door,熟知Door有一個開門以及關門的功能,這是一個門的最基本的功能,那麼我們如果在寫完代碼後再次修改門對象的定義,需要添加一個報警功能,那麼我們該怎麼辦,如果在抽象類中直接添加報警功能,如果是抽象方法,就必須重寫,如果是父類已經實現的方法,子類如果在細化實現的話,那麼也要重寫,並且你父類中的實現可能會影響到子類的其他方法,這是一種方法,但是如果一直有改動或者方法很多的話,那麼這個抽象類將變得相當麻煩,第二種方法就是:在不更改抽象類的情況下,可以編寫一個報警的接口,用子類來實現他,那麼子類就必須去實現此方法,這樣就可以達到不做抽象類的更改並添加了報警功能.
- 抽象類的編寫就是基於子類的共同特性的,它是描述一個類的大致情況的,外貌輪廓,接口則是行爲形式,描述是具體幹什麼的,如果一個工廠有什麼部門,那麼如果按照第一種方法,再去每個部門添加部門具體是做什麼的,那麼不僅僅影響到了繼承他的子類,而且使代碼變的不太容易維護,雜亂,採用第二種,可以避免這種情況,子類需要什麼功能就實現什麼接口,更加的靈活
總結
抽象類的實現就是基於子類的共同特性的,它是描述一個類的大致情況的,外貌輪廓,接口則是行爲形式,描述是具體幹什麼的,在使用的時候,我們可以將相同子類的共同特性抽檢出一個抽象類來作爲其子類的大致輪廓,具體實現細節,可以編寫接口並實現即可、一個類繼承一個抽象類.抽象類規定了子類的大概輪廓,其具體實現的方法,可以使用抽象類中的,也可以通過實現接口,重寫接口中的方法來實現子類的細化、可以利用抽象類和藉口配合使類更具體,即抽象類和接口的配合可以生成一個獨一無二的定製類