1、接口和抽象類
正如面向對象四大特徵:抽象、繼承、封裝、多態所述,定義類的過程就是抽象和封裝的過程。而接口和抽象類則是對實體類進行更高層次的抽象,僅定義公共行爲和特徵。兩者的共同點是都不能被實例化,但可以通過定義引用變量指向實例對象。
下表示接口和抽象類的語法區別:
語法維度 | 抽象類 | 接口 |
---|---|---|
定義關鍵字 | abstract | interface |
子類繼承或實現關鍵字 | extends | implements |
方法實現 | 可以有 | 不能有,JDK8以後,允許有default實現 |
方法訪問控制符 | 無限制 | 有限制,默認是public abstract類型 |
屬性訪問控制符 | 無限制 | 有限制,默認是public static final類型 |
靜態方法 | 可以有 | 不能有,JDK8以後,允許有 |
static{}靜態代碼塊 | 可以有 | 不能有 |
本類型之間擴展 | 單繼承 | 多繼承 |
本類型之間擴展關鍵字 | extends | extends |
抽象類在被繼承時體現的是is-a關係,而接口再被實現時體現的是can-do關係。
與接口相比,抽象類通常是對同類事物相對具體的抽象,通常包含抽象方法、實體方法、屬性變量。如果一個抽象類只有一個抽象方法,那麼它等同於一個接口。
is-a關係需要符合裏式代換原則(即父類適用的地方,用子類來替代同樣適用。舉個例子:警匪片中,警察經常說,放下武器!而對面的匪徒有的使用匕首,有的使用手槍,這些都是武器的子類。父類出現的地方,即“放下武器”,那麼,“放下匕首”、“放下手槍”都是對的)。
can-do關係要符合接口隔離原則。實現類要有能力去實現並執行接口中定義的行爲,例如Plan can fly.Brid can fly.中應該把fly這個動作/功能定義成一個接口,而不是把fly()放在某個抽象類中,再由Plane和Brid利用is-a關係去繼承此抽象類。因爲嚴格意義上來說,除了fly這個行爲外,在Plane和Brid兩者中很難再找到相同或者相似的特徵。
抽象類是模板設計,而接口是契約式設計。
抽象類包含一組相對具體的特徵,性格偏內向,比如某品牌特定型號的汽車,底盤結構、控制電路、剎車系統等是抽象出來的共同特徵,但根據動感型、舒適性、豪華型的區分,內飾、車頭燈、顯示屏等可以存在不同版本的實現。
接口是開放的,性格偏外箱,它就像是一份合同,定義了方法名、參數、返回值,甚至是拋出的異常類型。誰都可以來實現它,但如果向實現它的類就必須遵守這份接口約定合同,比如,任何類型的車輛都必須實現如下的接口:
public interface VehicleSafe{
/**
* @param initSpeed 剎車時的初始速度
* @param brakeTime 從initSpeed開始剎車到停止行駛的時間,單位是毫秒
* @return 從initSpeed開始剎車到停止行駛的距離
*/
double brake(int initSpeed,int brakeTime);
剎車是一個開放式的強制行爲規範,任何車輛都必須具有剎車的能力,要明確在特定初速度的情況下,剎車時間多長,剎車距離多長。此規範對任何車輛都是強約束的,這就是契約。
接口是頂級的“類”,對然關鍵字是interface,但是編譯後的字節碼擴展名還是.class。抽象類是二當家,接口位於頂層,而抽象類對各個接口進行了組合,然後實現部分接口行爲,其中AbstaractCollection是最典型的抽象類:
public abstract class AbstractCollection<E> implements Collection<E>{
//Collection定義的抽象方法,但本類沒有實現
//Collection接口定義的方法,size()這個方法對於鏈表和順序表有不同的實現方式
public abstract int size();
//實現Collection接口的這個方法,因爲對AbstractCollection的子類它們的判空方式是一致的,這就是
//模板式設計,對於所有它的子類,實現共同的方法體,通過多態調用到子類的具體size()實現
public boolean isEmpty(){
//實現Collection的方法
return size() == 0;
}
//其他屬性和部分方法實現.....
}
Java語言中類的繼承採用單繼承形式,避免繼承氾濫、菱形繼承、循環繼承,甚至“四不像”實現類的出現。在JVM中,一個類如果有多個直接父類,那麼綁定機制會變得非常複雜。
接口繼承接口,關鍵字是extends,而不是implements,允許多重繼承是因爲接口有契約式的行爲約定,沒有任何具體實現和屬性,某個實體類在實現多重繼承後的藉口時,只是說明“can do many things”。
當糾結定義接口還是抽象類時,優先推薦定義爲接口,遵循接口隔離原則,按某個維度劃分爲多個接口,然後再用抽象類去implements某些接口,這樣做可方便後續的擴展和重構。
2、內部類
在一個.java的源文件中,只能定義一個類名與文件名完全一致的公開類,並使用public class關鍵字來修飾。但是在面嚮對象語言中,任何一個類都可以在內部定義另外一個類,前者爲外部類,後者爲內部類。內部類本身就是類的一個屬性,與其他屬性定義方式一致。
比如:屬性字段private static String str,由訪問控制符、是否靜態類型、屬性類型、變量名組成。而內部類private static class Inner{},也是按照這樣的順序來定義的。類型可以爲class、enum,甚至是interface,當然在內部類中定義接口是不推薦的。
內部類可以是靜態和非靜態的,它可以出現在屬性定義、方法體和表達式中,甚至可以匿名出現(匿名內部類),具體分爲如下四種:
- 靜態內部類,如:static class StaticInnerClass{};
- 成員內部類,如:private class InstanceInnerClass{};
- 局部內部類,定義在方法或者表達式內部;
- 匿名內部類,如:(new Thread(){}).start()。
如下是最精簡的4中內部類定義方式:
public class OuterClass{
//成員內部類
private class InstanceInnerClass{}
//靜態內部類
static class StaicInnerClass{}
public static void main(String[] args){
//兩個匿名內部類
(new Thread() {}).start();
(new Thread() {}).start();
//兩個方法內部類
class MethodClass1{}
class MethodClass2{}
}
}
無論是什麼類型的內部類,都會編譯成一個獨立的.class文件,上面這個類編譯以後會產生如下的.class文件:
- OuterClass.class
- OuterClass$1.class
- OuterClass$1MethodClass1.class
- OuterClass$1MethodClass2.class
- OuterClass$2.class
- OuterClass$InstanceInnerClass.class
- OuterClass$StaticInnerClass.class
外部類與內部類之間使用$符號分隔,匿名內部類使用數字進行編號,而方法內部類,在類名前還有一個編號來標識是哪個方法。
匿名內部類和靜態內部類是比較常用的方式