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

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

多態

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

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

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");

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

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

這個例子也不知足多態,它使用父類的引用指向了父類,這裏就是一個正常的類方法調用,它會調用父類的方法操作系統

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

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

那麼多態有什麼好處呢?引入多態實質上也是爲了不重複的代碼,並且程序更具備擴展性,咱們經過println函數來講明這個問題。code

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修飾的方法不能被重寫
  • 修飾成員變量:表示成員變量是常量,不能被修改
  • 修飾局部變量:表示局部變量是常量,在對應做用域內不可被修改

相關文章
相關標籤/搜索