1. 程式人生 > >Java介面詳解

Java介面詳解

抽象類是從多個類中抽象出來的模板,如果將這種抽象進行得更徹底,則可以提煉出一種更加特殊的”抽象類“——介面(interface),接口裡包普通方法,接口裡的所有方法都是抽象方法。Java 8對介面進行了改進,允許在介面中定義預設方法,預設方法可以提供方法實現。

一、介面的概念
我們經常在生活中聽到介面這個詞,比如PCI介面、AGP介面,因此很多人認為介面相當於主機板上一個插槽,這其實是一種錯誤的認識。當說PCI介面時,指的是主機板上那個插槽遵守了PCI規範,而具體的PCI插槽只是PCI介面的例項。

對於不同型號的主機板而言,各自的PCI插槽都需要遵守一個規範。對於同一個型號的不同主機板而言,它們的PCI插槽需要有相同的資料交換方式、相同的實現細節,它們都是同一個類的不同例項。

同一個類的內部狀態資料、各種方法的實現細節完全相同,類是一種具體實現體。而介面定義了一種規範,介面定義了某一批類所需要遵守的規範,介面不關心這些類的內部狀態資料,也不關心這些類裡方法的實現細節,它只規定這批類裡必須提供某些方法,提供這些方法的類就可以滿足實際需要

讓規範和實現分離正是介面的好處,讓軟體系統的各元件之間面向介面耦合,是一種鬆耦合的設計。例如主機板上提供的PCI插槽,只要一塊顯示卡遵守PCI介面規範,就可以插入PCI插槽內,與該主機板正常通訊。

類似的,軟體系統的各模組之間也應該採用這種面向介面的耦合,從而儘量降低各模組之間的耦合,為系統提供更好的可擴充套件性和可維護性。

因此,介面定義的是多個類共同的行為規範,這些行為是與外部交流的通道,這就意味著接口裡通常是定義一組公用方法。

二、Java 8中介面的定義
和類定義不同,定義介面不再使用class關鍵字,而是使用interface關鍵字。介面定義的基本格式如下:

[修飾符] interface 介面名 extends 父介面1,父介面2...
{
    零到多個常量定義...
    零到到多個抽象方法定義...
    零到多個內部類、介面、列舉定義...
    零到到多個預設方法或類方法定義...
}

對上面語法的詳細說明如下。
修飾符可以是public或者省略,如果省略了public訪問控制符,則預設採用包許可權訪問控制符,即只有在相同包結構下才可以訪問該介面

介面名應該與類名採用相同的命名規則

一個介面可以有多個直接父介面,但介面只能繼承介面,不能繼承類。

提示:在上面語法定義中,只有在Java 8 以上的版本中才允許在介面中定義預設方法、類方法

由於介面定義的是一種規範,因此接口裡不能包含構造器和初始化塊定義。接口裡可以包含成員變數(只能是靜態常量)、方法(只能是抽象例項方法、類方法或預設方法)、內部類(包括內部介面、列舉)定義。

對比介面和類的定義方式,不難發現介面的成員比類裡的成員少了兩種,而且接口裡成員變數只能是靜態常量,接口裡的方法只能是抽象方法、類方法或預設方法。

前面已經說過了,接口裡定義的是多個類共同的公共行為規範,因此接口裡的所有成員包括常量、方法、內部類和內部列舉都是public訪問許可權。定義介面成員時,可以省略訪問控制修飾符,如果指定訪問控制修飾符,則只能使用public訪問控制修飾符。

對於接口裡定義的靜態常量而言,它們是介面相關的,因此係統會自動為這些成員變數增加static和final兩個修飾符。也就是說,在介面中定義成員變數時,不管是否使用public static final修飾符,接口裡的成員變數總是使用這三個修飾符來修飾。而且接口裡沒有構造器和初始化塊,因此接口裡定義的成員變數只能在定義時指定預設值。

接口裡定義成員變數取樣如下兩行程式碼的結果完全一樣。

//系統自動為接口裡定義的成員變數增加public static final修飾符
int MAX_SIZE = 50;
public static final int MAX_SIZE = 50;

接口裡定義的方法只能是抽象方法、類方法或預設方法,因此如果不是定義預設方法,系統將自動為普通方法增加abstract修飾符;定義接口裡的普通方法時不管是否使用public abstract修飾符,接口裡的普通方法總是使用public abstract來修飾。接口裡的普通方法不能有方法實現(方法體);但類方法、預設方法都必須由方法實現(方法體)。

注意:
接口裡定義的內部類、內部介面、內部列舉類都預設採用public static兩個修飾符,不管定義時是否指定這兩個修飾符,系統都會自動使用public static對它們進行修飾。

下面定義一個介面。

package lee;
public interface Output
{
    //接口裡定義的成員變數只能是常量
}
public interface Output
{
    //接口裡定義的成員變數只能是常量
    int MAX_CACHE_LINE = 50;
    //接口裡定義的普通方法只能是public的抽象方法
    void out();
    void getData(String msg);
    //在接口裡定義預設方法,需要使用default修飾
    default void print(String... msgs)
    {
        for(String msg : msgs)
        {
            System.out.println(msg);
        }
    }
    //在介面中定義預設方法,需要使用default修飾
    default void test()
    {
        System.out.println("預設的test()方法");
    }
    //在介面中定義類方法,需要使用static修飾
    static String staticTest()
    {
        return "接口裡的類方法";
    }
}

上面定義了一個Output介面,這個接口裡包含了一個成員變數:MAX_CACHE_LINE。除此之外,這個介面還定義了兩個普通方法:表示取得資料的getData()方法和表示輸出的out()方法。這就定義了Output介面的規範:只要某個類能取得資料,並且可以將資料輸出,那它就是一個輸出裝置,至於這個裝置的實現細節,這裡暫不關心。

Java 8允許在接口裡定義預設方法,預設方法必須使用default修飾,該方法不能使用static修飾,無論程式是否指定,預設方法總是使用public修飾——如果開發者沒有指定public,系統會自動為預設方法新增public修飾符,由於預設方法並不沒有static修飾,因此不能直接使用介面來呼叫預設方法,需要使用介面的實現類的例項來呼叫這些預設方法。

Java 8允許在介面中定義類方法,類方法必須使用static修飾,該方法不能使用default修飾,無論程式是否指定,類方法總是使用public修飾——如果沒有指定public,系統會自動為類方法新增public修飾符。類方法可以直接使用介面來呼叫。

接口裡的成員預設是使用public static final修飾的,因此即使另一個類處於不同包下,也可以通過介面來訪問接口裡的成員變數

注意:
從某個角度來看,介面可被當成一個特殊的類,因此一個Java原始檔裡最多隻能有一個public介面,如果一個Java原始檔裡定義了一個public介面,則該原始檔的主檔名必須與該介面名相同。

三、介面的繼承

介面的繼承和類繼承不一樣,介面完全支援多繼承,即一個介面可以有多個直接父介面。和類繼承相似,子介面擴充套件某個父介面,將會獲得父接口裡定義的所有方法、常量。

一個介面繼承多個父介面時,多個父介面排在extends關鍵字之後,多個父介面之間以(,)隔開。下面程式定義了三個介面,第三個介面繼承了前面兩個介面。

interface interfaceA
{
    int PROP_A = 5;
    void testA();
}
interface interfaceB
{
    int PROP_B = 6;
    void testB();
}
interface interfaceC extends interfaceA , interfaceB
{
    int PROP_C = 7;
    void testC();
}

public class InterfaceExtendsTest
{
    public static void main(String[] args)
    {
        System.out.println(interface.PROP_A);
        System.out.println(interface.PROP_B);
        System.out.println(interface.PROP_C);
    }
}

上面程式中的interfaceC介面繼承了interfaceA和interfaceB,所以interfaceC中獲得了它們的常量,因此main()方法可以通過interface C來獲取PROP_A、PROP_B常量。

四、使用介面

介面不能用於建立例項,但介面可以用於宣告引用型別變數。當使用介面來宣告引用型別變數時,這個引用型別變數必須引用到其實現類的物件。除此之外,介面的主要用途就是被實現類實現

歸納起來,介面主要有如下用途。
定義變數,也可用於進行強制型別轉換

呼叫介面中定義的變數

被其他類實現

一個類可以實現一個或多個介面,繼承使用extends關鍵字,實現則使用implements關鍵字。因為一個類可以實現多個介面,這也是Java為單繼承靈活性不足所做的補充。類實現介面語法格式如下:

[修飾符]  class 類名 extends 父類 implements 介面1,介面2...
{
    類體部分
}

實現介面與繼承父類相似,一樣可以獲得所實現接口裡定義的常量(成員變數)、方法(包括抽象方法和預設方法)。

讓類實現介面需要類定義後增加implements部分,當需要實現多個介面時,多個介面之間以英文逗號(,)隔開。一個類可以繼承一個父類,並同時實現多個介面,implements部分必須放在extends部分之後

一個類實現了一個或多個介面之後,這個類必須完全實現這些接口裡所定義的全部抽象方法(也就是重寫這些抽象方法);否則,該類將保留從父介面那裡繼承到的抽象方法,該類也必須定義成抽象類

一個類實現某個介面時,該類將會獲得從介面中定義的常量(成員變數)、方法等,因此可以把實現介面理解為一種特殊的繼承,相當於實現類繼承了一個徹底抽象的類(相當於除了預設方法之外,所有方法都是抽象方法的類)。

下面看一個實現介面的類。

public interface Output {
    //接口裡定義的成員變數只能是常量
    int MAX_CACHE_LINE = 50;
    //接口裡定義的普通方法只能是public的抽象方法
    void out();
    void getData(String msg);
    //在介面中定義預設方法,需要使用default修飾
    default void print(String... msgs)
    {
        for(String msg : msgs)
        {
            System.out.println(msg);
        }
    }
    //在介面中定義預設方法,需要使用default修飾
    default void test()
    {
        System.out.println("預設使用test()方法");
    }
    //在介面中定義類方法,需要static修飾
    static String staticTest()
    {
        return "接口裡的類方法";
    }
}
//定義一個Product介面
interface Product
{
    int getProduceTime();
}

//讓Printer類實現Output和Product介面
public class Printer implements Output,Product
{
    private String[] printData = new String[MAX_CACHE_LINE];
    //用以記錄當前需列印的作業數
    private int dataNum = 0;

    @Override
    public int getProduceTime() {
        // TODO Auto-generated method stub
        return 45;
    }

    @Override
    public void out() {
        // TODO Auto-generated method stub
        //只要還有作業,就繼續列印
        while(dataNum > 0)
        {
            System.out.println("印表機列印:" + printData[0]);
            //把作業佇列整體遷移以為,並將剩下的作業數減1
            System.arraycopy(printData, 1, printData, 0, --dataNum);
        }
    }

    @Override
    public void getData(String msg) {
        // TODO Auto-generated method stub
        if(dataNum >= MAX_CACHE_LINE)
        {
            System.out.println("輸出佇列已滿,新增失敗");
        }
        else
        {
            printData[dataNum++] = msg;
        }
    }

    public static void main(String[] args)
    {
        //建立一個Printer物件,當成Output使用
        Output o = new Printer();
        o.getData("Monday");
        o.getData("TuesDay");
        o.out();
        o.getData("Wednesday");
        o.getData("Thursday");
        o.out();
        //呼叫Output介面中定義的預設方法
        o.print("星期一","星期二","星期三");
        o.test();
        //建立一個Printer物件,當成Product使用
        Product p = new Printer();
        System.out.println(p.getProduceTime());
        //所有介面型別的引用變數都可直接賦給Object型別的變數
        Object obj = p;
    }
}

從上面程式可以看出,Printer實現了Output介面和Product介面,因此Printer物件既可以賦給Output,也可直接賦給Product變數,彷彿Printer即是Output類的子類,也是Product類的子類,這就是Java提供的模擬多繼承

上面程式中Printer實現了Output介面,即可獲得Output介面中定義的print()和test()兩個預設方法,因此Printer例項可以直接呼叫者兩個預設方法。

注意:
實現介面方法時,必須使用public訪問控制符,因為接口裡方法都是public的,而子類重寫父類方法時訪問控制符只能更大或者相等,所以實現類實現接口裡方法時只能使用public訪問許可權

介面不能顯式繼承任何類。但介面型別的引用變數可以直接賦給Object型別的引用變數,所以上面程式中可以把Product型別的變數直接賦給Object型別的變數,這是利用向上轉型來實現的。因為編譯器知道任何Java物件都必須是Object或其子類的例項,Product型別的物件也不例外(它必須是Product介面實現類的物件,該類肯定是Object的顯式或隱式子類)。

五、介面和抽象類

介面和抽象類很像,它們都具有如下特徵
介面和抽象類都不能被例項化,它們都位於繼承樹的頂端,用於被其他類實現和繼承

介面和抽象類都可以包含抽象方法,實現介面或繼承抽象類的普通類都必須實現這些抽象方法

但介面和抽象類之間的差別非常大,這種差別主要體現在二者設計目的上。下面具體分析二者差別。

介面作為系統與外界互動的視窗,介面體現的是一種規範。對於介面的實現者而言,介面規定了實現者必須向外提供哪些服務(以方法的形式來提供);對於介面的呼叫者而言,介面規定了呼叫者可以呼叫哪些服務,以及如何呼叫這些服務(就是如何來呼叫方法)。當在一個程式中使用介面時,介面是多個模組間的耦合標準,當在多個應用程式之間使用介面時,介面是多個程式之間的通訊標準

從某種程式來看,介面類似於整個系統的“總綱”,它指定了系統各模組之間應遵循的標準,因此一個系統中的介面不應該經常改變。一旦介面被改變,對整個系統甚至其他系統影響將是輻射式的。

抽象類則不一樣,抽象類作為系統中多個子類的共同父類,它所體現的是一種模板式的設計。抽象類作為多個子類的抽象父類,可以被當成系統實現過程中的中間產品,這個中間產品已經實現的系統的部分功能(那些已經提供實現的方法),但這個產品依然不能當成最終產品,必須有更進一步的完善,這種完善可能有幾種不同方式。

除此之外,介面和抽象類在用法上也存在如下差別
接口裡只能包含抽象方法,不能為普通方法提供方法實現(預設方法可以提供方法實現);抽象類則可以包含普通方法

接口裡只能定義常量,不能定義普通成員變數;抽象類裡則即可以定義普通成員變數,也可以定義靜態常量

接口裡不包含構造器;抽象類可以包含構造器,抽象類裡的構造器並不是用於建立物件,而是讓其子類呼叫這些構造器來完成屬於抽象類的初始化操作

接口裡不能包含初始化塊;但抽象類則可以完全包含初始化塊

抽象類和普通類一樣,只能繼承一個父類,但不能繼承介面;介面可繼承多個父介面,但不能繼承類。

一個類只能有一個直接父類,包括抽象類;但一個類可以實現多個介面,通過實現多個介面可以彌補Java單繼承的不足