Java 學習筆記(7)——接口與多態

上一篇說了Java面向對象中的繼承關系,在繼承中說到:調用對象中的成員變量時,根據引用類型來決定調用誰,而調用成員方法時由于多態的存在,具體調用誰的方法需要根據new出來的對象決定,這篇主要描述的是Java中的多態以及利用多態形成的接口

多態

當時在學習C++時,要使用多態需要定義函數為virtual,也就是虛函數。類中存在虛函數時,對象會有一個虛函數表的頭指針,虛函數表會存儲虛函數的地址,在使用父類的指針或者引用來調用方法時會根據虛函數表中的函數地址來調用函數,會形成多態。

當時學習C++時對多態有一個非常精煉的定義:基類的指針指向不同的派生類,其行為不同。這里行為不同指的是調用同一個虛函數時,會調用不同的派生類函數。這里我們說形成多態的幾個基本條件:1)指針或者引用類型是基類;2)需要指向派生類;3)調用的函數必須是基類重寫的函數。

public class Parent{
    public void sayHelllo(){
        System.out.println("Hello Parent");
    }

    public void sayHello(String name){
        System.out.println("Hello" + name);
    }
}

public class Child extends Parent{
    public void sayHello(){
        System.out.println("Hello Child");
    }
}

根據上述的繼承關系,我們來看下面幾個實例代碼,分析一下哪些是多態

Parent obj = new  Child();
obj.sayHello();

該實例構成了多態,它滿足了多態的三個條件:Parent 類型的 obj 引用指向了 new出來的Child子類、并且調用了二者共有的方法。

Parent obj = new  Child();
obj.sayHello("Tom");

這個例子沒有構成多態,雖然它滿足基類的引用指向派生類,但是它調用了父類特有的方法。

Parent obj = new  Parent();
obj.sayHello();

這個例子也不滿足多態,它使用父類的引用指向了父類,這里就是一個正常的類方法調用,它會調用父類的方法

Child obj = new Child();
obj.sayHello();

這個例子也不滿足多態,它使用子類的引用指向了子類,這里就是一個正常的類方法調用,它會調用子類的方法

那么多態有什么好處呢?引入多態實質上也是為了避免重復的代碼,而且程序更具有擴展性,我們通過println函數來說明這個問題。

public void println(Object x) {
    String s = String.valueOf(x);
    synchronized (this) {
        print(s);
        newLine();
    }
}

//Class String
public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

函數println實現了一個傳入Object的重載,該函數調用了String類的靜態方法 valueOf, 進一步跟到String類中發現,該方法只是調用了類的 toString 方法,傳入的obj可以是任意繼承Object的類(在Java中只要是對象就一定是繼承自Object),只要類重寫了 toString 方法就可以直接打印。這樣一個函數就實現了重用,相比于需要后來的人額外重載println函數來說,要方便很多。

類類型轉化

上面的println 函數,它需要傳入的是Object類的引用,但是在調用該方法時,從來都沒有進行過類型轉化,都是直接傳的,這里是需要進行類型轉化的,在由子類轉到父類的時候,Java進行了隱式類型轉化。大轉小一定是安全的(這里的大轉小是對象的內存包含關系),子類一定可以包含父類的成員,所以即使轉化為父類也不存在問題。而父類引用指向的內存不一定就是包含了子類成員,所以小轉大不安全。

為什么要進行小轉大呢?雖然多態給了我們很大的方便,但是多態最大的問題就是父類引用無法看到子類的成員,也就是無法使用子類中的成員。這個時候如果要使用子類的成員就必須進行小轉大的操作。之前說過小轉大不安全,由于父類可能有多個實現類,我們無法確定傳進來的參數就是我們需要的子類的對象,所以java引入了一個關鍵字 instanceof 來判斷是否可以進行安全的轉化,只要傳進來的對象引用是目標類的對象或者父類對象它就會返回true,比如下面的例子

Object obj = "hello"
System.out.println(obj instanceof String); //true
System.out.println(obj instanceof Object); //true
System.out.println(obj instanceof StringBuffer); //false
System.out.println(obj instanceof CharSequence); //true

抽象方法和抽象類

我們說有了多態可以使代碼重用性更高。但是某些時候我們針對幾個有共性的類,抽象出了更高層面的基類,但是發現基類雖然有一些共性的內容,但是有些共有的方法不知道如何實現,比如說教科書上經常舉例的動物類,由于不知道具體的動物是什么,所以也無法判斷該動物是食草還是食肉。所以一般將動物的 eat 定義為抽象方法,擁有抽象方法的類一定必須是抽象基類。

抽象方法是不需要寫實現的方法,它只需提供一個函數的原型。而抽象類不能創建實例,必須有派生類重寫抽象方法。為什么抽象類不能創建對象呢?對象調用方法本質上是根據函數表找到函數對應代碼所在的內存地址,而抽象方法是未實現的方法,自然就無法給出方法的地址了,如果創建了對象,而我的對象又想調用這個抽象方法那不就沖突了嗎。所以規定無法實例化抽象類。

抽象方法的定義使用關鍵字 abstract,例如

public abstract class Life{
    public abstract void happy();
}

public class Cat{
    public void happy(){
        System.out.println("貓吃魚");
    }
}

public class Cat{
    public void happy(){
        System.out.println("狗吃肉");
    }
}

public class Altman{
    public void happy(){
        System.out.println("奧特曼打小怪獸");
    }
}

上面定義了一個抽象類Life 代表世間的生物,你要問生物的幸福是什么,可能沒有人給你答案,不同的生物有不同的回答,但是具體到同一種生物,可能就有答案了,這里簡單的給出了答案:幸福就是貓吃魚狗吃肉奧特曼愛打小怪獸。

使用抽象類需要注意下面幾點:

  • 不能直接創建抽象類的對象,必須使用實現類來創建對象
  • 實現類必須實現抽象類的所有抽象方法,否則該實現類也必須是抽象類
  • 抽象類可以有自己的構造方法,該方法僅供子類構造時使用
  • 抽象類可以沒有抽象方法,但是有抽象方法的一定要是抽象類

接口

接口就是一套公共的規范標準,只要符合標準就能通用,比如說USB接口,只要一個設備使用了USB接口,那么我的電腦不管你的設備是什么,插上就應該能用。在代碼中接口就是多個類的公共規范。

Java中接口也是一個引用類型。接口與抽象類非常相似,同樣不能創建對象,必須創建實現類的方法。但是接口與抽象類還是有一些不同的。 抽象類也是一個類,它是從底層類中抽象出來的更高層級的類,但是接口一般用來聯系多個類,是多個類需要實現的一個共同的標準。是從頂層一層層擴展出來的。

接口的一個常見的使用場景就是回調,比如說常見的窗口消息處理函數。這個場景C++中一般使用函數指針,而Java中主要使用接口。
接口使用關鍵字 interface 來定義, 比如

public interface USB{
    public final String deviceType = "USB"; 
    public abstract void open();
    public abstract void close();
}

接口中常見的一個成員是抽象方法,抽象方法也是由實現類來實現,注意事項也與之前的抽象類相同。除了有抽象方法,接口中也可以有常量。

接口中的抽象方法是沒有方法體的,它需要實現類來實現,所以實現類與接口中發生重寫現象時會調用實現類,那么常量呢?

public class Mouse implements USB{
    public final String deviceType = "鼠標";
    public void open(){

    }

    public void close(){

    }
}

public class Demo{
    public static void main(String[] args){
        USB usb = new Mouse();
        System.out.println(usb.deviceType);
    }
}

常量的調用遵循之前說的重載中的屬性成員調用的方式。使用的是什么類型的引用,調用哪個類型中的成員。

與抽象類中另一個重要的不同是,接口運行多繼承,那么在接口的多繼承中是否會出現沖突的問題呢

public interface Storage{
    public final String deviceType = "存儲設備";
    public abstract void write();
    public abstract void read();
}

public class MobileHardDisk implements USB, Storage{
     public void open(){

    }

    public void close(){

    }

    public void write(){

    }

    public void read(){

    }
}

public class Demo{
    public static void main(String[] args){
        MobileHardDisk mhd = new MobileHardDisk();
        System.out.println(mhd.deviceType);
    }
}

編譯上述代碼時會發現報錯了,提示 USB 中的變量 deviceType 和 Storage 中的變量 deviceType 都匹配 ,也就是說Java中仍然沒有完全避免沖突問題。

接口中的默認方法

有的時候可能會出現這樣的情景,當項目完成后,可能客戶需求有變,導致接口中可能會添加一個方法,如果使用抽象方法,那么接口所有的實現類都得重復實現某個方法,比如說上述的代碼中,USB接口需要添加一個方法通知PC設備我這是什么類型的USB設備,以便操作系統匹配對應的驅動。那么可能USB的實現類都需要添加一個,這樣可能會引入大量重復代碼,針對這個問題,從Java 8開始引入了默認方法。

默認方法為了解決接口升級的問題,接口中新增默認方法時,不用修改之前的實現類。

默認方法的使用如下:

public interface USB{
    public final String deviceType = "USB"; 
    public abstract void open();
    public abstract void close();
    public default String getType(){
        return this.deviceType;
    }
}

默認方法同樣可以被所有的實現類覆蓋重寫。

接口中的靜態方法

從Java 8中開始,允許在接口中定義靜態方法,靜態方法可以使用實現類的對象進行調用,也可以使用接口名直接調用

接口中的私有方法

從Java 9開始運行在接口中定義私有方法,私有方法可以解決在默認方法中存在大量重復代碼的情況。

雖然Java為接口中新增了這么多屬性和擴展,但是我認為不到萬不得已,不要隨便亂用這些東西,畢竟接口中應該定義一系列需要實現的標準,而不是自己去實現這些標準。

最后總結一下使用接口的一些注意事項:

  • 接口沒有靜態代碼塊或者構造方法
  • 一個類的父類只能是一個,但是類可以實現多個接口
  • 如果類實現的多個接口中有重名的默認方法,那么實現類必須重寫這個實現方法,不然會出現沖突。
  • 如果接口的實現類中沒有實現所有的抽象方法,那么這個類必須是抽象類
  • 父類與接口中有重名的方法時,優先使用父類的方法,在Java中繼承關系優于接口實現關系
  • 接口與接口之間是多繼承的,如果多個父接口中存在同名的默認方法,子接口中需要重寫默認方法,不然會出現沖突

final關鍵字

之前提到過final關鍵字,用來表示常量,也就是無法在程序中改變的量。除了這種用法外,它還有其他的用法

  • 修飾類,表示類不能有子類。可以將繼承關系理解為改變了這個類,既然final表示常量,不能修改,那么類自然也不能修改
  • 修飾方法:被final修飾的方法不能被重寫
  • 修飾成員變量:表示成員變量是常量,不能被修改
  • 修飾局部變量:表示局部變量是常量,在對應作用域內不可被修改

<hr />

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,604評論 2 380

推薦閱讀更多精彩內容

  • 今日任務: 1,能夠獨立使用抽象類 2,能夠獨立使用多態 3,能夠獨立使用接口 4,能夠理解適配器設計模式 1. ...
    Villain丶Cc閱讀 1,384評論 0 17
  • 面向對象主要針對面向過程。 面向過程的基本單元是函數。 什么是對象:EVERYTHING IS OBJECT(萬物...
    sinpi閱讀 1,082評論 0 4
  • 整理來自互聯網 1,JDK:Java Development Kit,java的開發和運行環境,java的開發工具...
    Ncompass閱讀 1,550評論 0 6
  • Swift1> Swift和OC的區別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,131評論 1 32
  • 一:java概述: 1,JDK:Java Development Kit,java的開發和運行環境,java的開發...
    慕容小偉閱讀 1,822評論 0 10