Java學習之介面

-哦吼發表於2020-11-07

簡介

介面和內部類為我們提供了一種將介面與實現分離的更加結構化的方式

抽象類和抽象方法

在學習到抽象類之前,我們實現多型的方式就是通過實現一個基類,然後在不同的匯出類中重寫基類的方法以及進行一些個性的擴充來實現多型。在這種關係中,基類的目的是為它的所有匯出類建立一個通用介面。但是,純粹作為介面而言,普通的類顯得有些多餘。因為它會要求基類同樣實現介面方法。但實際上對於這些方法的實現有時候並不是必須的。因此我們引入了抽象類和抽象方法的概念

這種方法是不完整的,僅有宣告而沒有方法體。下面是抽象方法宣告採用的語法

abstract void f();

包含抽象方法的類被稱為抽象類。如果一個類包含一個或多個抽象方法,該類必須被限定未抽象的,否則編譯錯誤。同時由於抽象類是不完整的(存在沒有方法實現的抽象方法),所以我們無法通過抽象類來建立例項。

如果從一個抽象類繼承,並想建立該新類的物件,那麼就必須為抽象類中所有抽象方法提供方法定義。如果不這麼做,那該類依舊是抽象類,需要用abstract關鍵字來限定

abstract class A{
    abstract void f1();

    abstract  void f2();
}

abstract class A1 extends A{
    @Override
    public void f1(){
        System.out.println("f1");
    }
}

class A2 extends A1{

    @Override
    void f2() {
        System.out.println("f2");
    }
}

介面

inteface關鍵字使得抽象的概念更近非同步,abstract關鍵字允許我們在一個類中建立一個或多個沒有實現的方法宣告,然後由繼承者實現。而interface關鍵字則產生了一個完全抽象的類,它只提供了形式而不具有任何具體實現。當然,在java8後,介面中允許加入預設的方法定義。
由於我們知道java是單繼承體系,而C++則是允許多繼承,相較於單繼承,多繼承可以為新類整合多種不同的介面,但是由於如果在java中加入多繼承則會帶來一些定義不清淅的問題,假設我們繼承了A和B兩類,但是兩個類中存在相同的域,那麼我們到底繼承誰的。所以Java採用了繼承+介面來實現多重繼承。

介面也可以包含域,但這些域隱式地是static和final的。同時,介面中的方法自動就是public(也只能是public),當然我們可以顯式地將方法宣告為public

完全解耦

只要有一個方法操作地是類而不是介面,那麼你就只能使用這個類及其子類。如果你想將該方法應用於不在此繼承結構中的某個類,那你就要觸黴頭了。介面在很大程度上放寬了這種限制。因此介面使得我們編寫出複用性更好的程式碼

解釋一下:假設我們為了多型以及程式碼的複用,希望新類都可以實現某個相同的方法。我們在學習介面之前通常會使用類繼承來解決,但是繼承給我們解決問題的同時也帶來了很多的麻煩。比如:我現在希望匯出類實現一個f()方法,但是基類中又不止一個f()方法,還有f1(),f2()等等,這些方法很可能是與我新類本身定位無關的方法,我也必須要繼承下來。這是不合理的,也是違反單一職責原則的。同時,由於java實現的是單繼承結構,如果我們還希望繼承一些其他的方法,那我必須要求基類增加這些方法,而基類一旦增加了這些方法,意味著該基類的其他匯出類都會繼承這些它們並不一定用的到的方法。這就是個惡性迴圈。

上面的解釋也是有點抽象,因此我們舉個例子,我們希望建立一隻鳥和一隻貓。因此我們會建立一個Bird類和Cat類,但是處於程式碼複用和多型的考慮我們會去抽取它們的共性來作為基類,所以它們的共性是它們都是動物,都會喝水,都會吃東西。好,按照這個思路我們開始使用我們剛學到的抽象類來寫程式碼

abstract class Animal{
	public void eat();
	public void drink();
}

class Bird extends Animal{
	public void eat(){
		System.out.println("鳥吃蟲子");
	}	
	
	public void drink(){
		System.out.println("鳥喝水");
	}
}

class Cat extends Animal{
	public void eat(){
		System.out.println("貓吃魚");
	}	
	
	public void drink(){
		System.out.println("貓喝水");
	}
}

這樣乍一看很完美了,好像沒什麼問題。但是仔細一想我們就會發現,貓除了喝水吃飯,還會跳躍,會抓老鼠。而鳥還會飛行。因此為了鳥能夠非我們需要加上飛行這個行為。
按照以往的想法我們可以這麼幹

class Bird extends Animal{
	public void eat(){
		System.out.println("鳥吃蟲子");
	}	
	
	public void drink(){
		System.out.println("鳥喝水");
	}
	public void fly(){
		System.out.println("飛行");
	}
}

但是注意,我們實現多型的原因就是為了程式碼的可複用性以及抽象性。像在匯出類直接加一個方法這也的行為不是不可以,但是會失去多型和複用性這些特性,以後,如果我要編寫另一個會飛的生物例如蝙蝠、鋼鐵俠。那豈不是要在每一個新類中都重新增加一個飛行方法,同時,由於失去了多型性,我以後希望呼叫飛行方法時,都需要指定具體的匯出類作為引數。所以我們重新用介面來做

interface Fly{
	public void fly();
}

class Bird extends Animal implements Fly{
	public void eat(){
		System.out.println("鳥吃蟲子");
	}	
	
	public void drink(){
		System.out.println("鳥喝水");
	}
	public void fly(){
		System.out.println("鳥振翅飛行");
	}
}

class IronMan implements Fly{
	public void fly(){
		System.out.println("鋼鐵俠噴射飛行");
	}
}

public class Test{
	public static void main(){
		Test.fly(new Bird());
		Test.fly(new IronMan());
		/**Output
		*鳥振翅飛行
		*鋼鐵俠噴射飛行
		**/
	}

	public static void doFly(Fly fly){
		fly.fly();
	}
}

這樣以來,這個鳥類又重新具有了多型性,當我需要在方法內呼叫飛行方法時,完全可以指定引數型別為Fly,這樣我不管你傳入的是鳥還是鋼鐵俠,我只要呼叫Fly介面的fly方法就能夠呼叫對應實際傳入型別的物件的飛行方法

Java中的多重繼承

介面不僅是一種更加純粹的抽象類,它的目標更高。因為介面是沒有任何具體實現的。也就是說,沒有任何與介面相關的儲存。因此也就無法阻止多個介面的組合。這一點是有價值的,因為我們有時需要標識“一個x是一個a和一個b以及一個e。在C+中組合多個類的介面的行為被稱之為多重繼承。它會使得你的新類揹負的太多,因為每個類都會都具體實現,而在Java中,你可以執行相同的行為,但只有一個類可以有具體實現。

在匯出類中,不強制要求你必須有一個具體或抽象的基類,但是如果從一個非介面的類繼承,那麼只能從一個類中去繼承,其餘元素必須是介面,然後使用implements關鍵字實現,不同的介面使用逗號隔開

interface A{}

interface B{}

class C implements A,B{}

組合介面時發生名字衝突

實現多介面繼承時會發生一個小陷阱,那就是介面中存在相同方法,同時基類中也有相同的方法。同時,同名方法我們可以認為歸一當作一個用就可以了,但是如果它們的返回型別又不同呢,那我們該如果抉擇。其實我們不用決戰,因為這種會產生混淆的問題編譯器根本不能通過。當然,如果基類和介面中的方法同名同參,那麼我們就會認為介面中的方法已被實現不需要再次在匯出類中實現。

介面中的域

介面中所有的域都預設是static final的。同時由於介面中不存在方法的具體實現,也沒有構造方法,所以無法去對域進行任何操作,因此,編譯器不允許介面中出現空final的存在。我們前面提到,介面中不存在任何儲存,所以,這些域由於是靜態的,它們不屬於介面的一部分,它們被儲存在該介面的靜態儲存區內

補充

介面和類中允許巢狀介面。但是這種方式比較少見,所以我們也只是提一下

介面和工廠模式

介面經常穿插於各種設計模式之間,因此我們現在介紹其中一種最常見之一的工廠模式,它與介面之間的關係非常之緊密

interface Service{
	void f1();
}

interface ServiceFactory{
	Service getService():
}

class ServiceImplement1 implements Service{
	public void f1(){
		Systrm.out.println("ServiceImplement1.f1");
	}
}

class ServiceFactory1 implements ServiceFactory{
	public Service getService(){
		return new ServiceImplement1()
	}
}


class ServiceImplement2 implements Service{
	public void f1(){
		Systrm.out.println("ServiceImplement1.f1");
	}
}

class ServiceFactory2 implements ServiceFactory{
	public Service getService(){
		return new ServiceImplement2()
	}
}

上面就是工廠模式最簡單的體現,這裡不做贅述,有需要的小夥伴可以去我的部落格看一下,有一章專門對工廠模式進行了分析解釋

總結

我們可以看到很多介面的妙用,甚至一度認為介面相對於基類它更靈活更好用,所以要優先使用介面而非類。這種認知是存在誤區的,因為我們是這麼認為的,基類決定了你是什麼,而介面決定了你擁有什麼樣的行為,屬於對基類的補充而非替代。過度使用介面只會為程度帶來很大程度的複雜性以及降低程式碼的複用性。同時,由於介面不可以儲存普通的域,所以在資料方面,介面存在很大的短板。我們設計時可以這麼去做,比如我們要設計一個人,那麼首先我們去定義其作為人類的基類,該基類記錄了一些人本身的特性,比如身份證,性別,年齡等。這是伴隨人類本身存在的,不會因為不同的人類的不同實現(種族、膚色)產生歧義或者變化,變化的只是資料本身。但是人和人是不同的,比如由於工種的不同,不同的人具有不同的技能,這些技能就需要通過介面來進行補充了。所以不要因為介面靈活就去濫用了。

相關文章