接口默認方法是什麼鬼

接口之因此成爲接口,就在於它沒有實現,只是聲明。但後來一切都變了,Java 裏出現了默認方法,C# 也出現了默認方法。接口已經不像傳統意義上的接口,其概念開始向抽象類靠近,一個純抽象的東西,忽然出現了實體,因而開始傻傻分不清了。java

世界已經變了,可他是怎麼開始改變的呢?程序員

1. 緣起

雖然本文有提到 Java,可是筆者近年主要仍是在寫 C# 程序,因此未明確語言的命名規範會更傾向 C# 的規範一些,敬請諒解。編輯器

曾經,咱們定義了 IStringList 接口,它聲明瞭一個列表:ide

這只是個例子,爲了不引入更多的技術概念,這裏沒有使用泛型舉例。函數

interface IStringList {
    void Add(string o); // 添加元素
    void Remove(int i); // 刪除元素
    string Get(int i);  // 獲取元素
    int Length { get; } // 獲取列表長度
}

無論怎麼說,這個列表已經擁有了基本的增刪除改查功能,好比遍歷,能夠這樣寫設計

IStringList list = createList();
for (var i = 0; i < list.Length; i++) {
    string o = list.Get(i);
    // Do something with o
}

這個 IStringList 做爲一個基礎接口在類庫中發佈以後,大量的程序員使用了這個接口,實現了一堆各類各類各樣的列表,像 StringArrayListLinkedStringListStringQueueStringStackSortedStringList……有抽象類,有擴展接口,也有各類實現類。總之,通過較長一段時間的積累,IStringList 的子孫遍及全球。代碼規範

而後 IStringList 的發明者,決定爲列表定義更多的方法,以適合在技術飛速發展下開發者們對 IStringList 使用便捷性的要求,因而code

interface IStringList {
    int IndexOf(string o);          // 查找元素的索引,未找到返回 -1
    void Insert(string o, int i);   // 在指定位置插入元素

    // ------------------------------
    void Add(string o); // 添加元素
    void Remove(int i); // 刪除元素
    string Get(int i);  // 獲取元素
    int Length { get; } // 獲取列表長度
}

固然,接口變化以外全部實現類都必須實現它,否則編譯器會報錯,基礎庫的抽象類 AbstractStringList 中實現了上述新增長的接口。整個基礎庫完美編譯,發佈了 2.0 版本。對象

然而,現實很是殘酷!blog

基礎庫的用戶們(開發者)發出了極大的報怨聲,由於他們太多代碼編譯不過了!

是的,並非全部用戶都會直接繼承 AbstractStringList,不少用戶直接實現了 IStringList。還有很多用戶甚至擴展了 IStringList,但他們沒有定義 int IndexOf(string o) 而是定義的 int Find(string o)。因爲基礎庫接口 IStringList 的變化,用戶們須要花大量地時間去代碼來實現 IStringList 中定義的新方法。

這個例子是提到了 IStringList,只添加了兩個方法。這對用戶形成的麻煩雖然已經不小,但工做量還算能夠接受。可是想一想 JDK 和 .NET Framework/Core 龐大的基礎庫,恐怕用戶只能用「崩潰」來形容!

2. 辦法

確定不能讓用戶崩潰,得想辦法解決這個問題。因而,Java 和 C# 的兩個方案出現了

  • Java 提出了默認方法,即在接口中添加默認實現
  • C# 提出了擴展方法,即經過改變靜態方法的調用形式來僞裝是對象調用

不得不說 C# 的擴展方法很聰明,但它畢竟不是真正對接口進行擴展,因此在 C# 8 中也加入了默認方法來解決接口擴展形成的問題。

接口擴展方法提出來以後,雖然解決了默認實現的問題,卻又帶出了新的問題。

  • 接口實現了默認方法,實現接口的類還須要實現嗎?若是不實現會怎麼樣?
  • 不管 Java 仍是 C# 都不容許類多繼承,可是接口能夠。而接口中的默認實現帶來了相似於類多繼承所產生的問題,怎麼辦?
  • 在複雜的實現和繼承關係中,最終執行的到底會是哪個方法?

3. 問題一,默認方法和類實現方法的關係

忽略上面 IStringList 接口中補充的 Insert(Object, int) 方法,咱們把關注點放在 IndexOf(Object) 上。Java 和 C# 的語法殊途同歸:

3.1. 先來看看默認方法的語法

  • Java 版
interface StringList {
    void add(Object s);
    void remove(int i);
    Object get(int i);
    int getLength();

    default int indexOf(Object s) {
        for (int i = 0; i < getLength(); i++) {
            if (get(i) == s) { return i; }
        }
        return -1;
    }
}
  • C# 版
interface IStringList
{
    public void Add(string s);
    void Remove(int i);
    string Get(int i);
    int Length { get; }
    int IndexOf(string s)
    {
        for (var i = 0; i < Length; i++)
        {
            if (Get(i) == s) { return i; }
        }
        return -1;
    }
}

這裏把 C# 和 Java 的接口都寫出來,主要是由於兩者講法和命名規範略有不一樣。接下來進行的研究 C# 和 Java 行爲類似的地方,就主要以 C# 爲例了。

怎麼區分是 C# 示例仍是 Java 示例?看代碼規範,最明顯的是 C# 方法用 Pascal 命名規則,Java 方法用 camel 命名規則。固然,還有 Lambda 的箭頭也不同。

接下來的實現,僅以 C# 爲例:

class MyList : IStringList
{
    List<string> list = new List<string>();  // 偷懶用現成的

    public int Length => list.Count;
    public void Add(string o) => list.Add(o);
    public string Get(int i) => list[i];
    public void Remove(int i) => list.RemoveAt(i);
}

MyList 沒有實現 IndexOf,可是使用起來不會有任何問題

class Program
{
    static void Main(string[] args)
    {
        IStringList myList = new MyList();
        myList.Add("First");
        myList.Add("Second");
        myList.Add("Third");

        Console.WriteLine(myList.IndexOf("Third"));  // 輸出 2
        Console.WriteLine(myList.IndexOf("first"));  // 輸出 -1,注意 first 大小寫
    }
}

3.2. 在 MyList 中實現 IndexOf

如今,在 MyList 中添加 IndexOf,實現對字符串忽略大小寫的查找:

// 這裏用 partial class 表示是部分實現,
// 對不住 javaer,Java 沒有部分類語法
partial class MyList
{
    public int IndexOf(string s)
    {
        return list.FindIndex(el =>
        {
            return el == s
                || (el != null && el.Equals(s, StringComparison.OrdinalIgnoreCase));
        });
    }
}

而後 Main 函數中輸出的內容變了

Console.WriteLine(myList.IndexOf("Third")); // 仍是返回 2
Console.WriteLine(myList.IndexOf("first")); // 返回 0,不是 -1

顯然這裏調用了 MyList.IndexOf()

3.3. 結論,以及 Java 和 C# 的不一樣之處

上面主要是以 C# 做爲示例,其實 Java 也是同樣的。上面的示例中是經過接口類型來調用的 IndexOf 方法。第一次調用的是 IStringList.IndexOf 默認實現,由於這時候 MyList 並無實現 IndexOf;第二次調用的是 MyList.IndexOf 實現。筆者使用 Java 寫了相似的代碼,行爲徹底一致。

所以,對於默認方法,會優先調用類中的實現,若是類中沒有實現具備默認方法的接口,纔會去調用接口中的默認方法。

可是!!!前面的示例是使用的接口類型引用實現,若是換成實例類類型來引用實例呢?

若是 MyList 中實現了 IndexOf,那結果沒什麼區別。可是若是 MyList 中沒有實現 IndexOf 的時候,Java 和 C# 在處理上有就區別了。

先看看 C# 的 Main 函數,編譯不過(Compiler Error CS1929),由於 MyList 中沒有定義 IndexOf

接口默認方法是什麼鬼

而 Java 呢?經過了,一如既往的運行出告終果!

接口默認方法是什麼鬼

從 C# 的角度來看,MyList 既然知道有 IndexOf 接口,那就應該實現它,而不能僞裝不知道。可是若是經過 IStringList 來調用 IndexOf,那麼就能夠認爲 MyList 並不知道有 IndexOf 接口,所以容許調用默認接口。接口仍是接口,不知道有新接口方法,沒實現,不怪你;可是你明知道還不實現,那就是你的不對了。

但從 Java 的角度來看,MyList 的消費者並不必定是 MyList 的生產者。從消費者的角度來看,MyList 實現了 StringList 接口,而接口定義有 indexOf 方法,因此消費者調用 myList.indexOf 是合理的。

Java 的行爲相對寬鬆,只要有實現你就用,不要管是什麼實現。

而 C# 的行爲更爲嚴格,消費者在使用的時候能夠經過編譯器很容易瞭解到本身使用的是類實現,仍是接口中的默認實現(雖然知道了也沒多少用)。實際上,若是沒在在類裏面實現,接口文檔中就不會寫出來相關的接口,編輯器的智能提示也不會彈出來。實在要寫,能夠顯示轉換爲接口來調用:

Console.WriteLine(((IStringList)myList).IndexOf("Third"));

並且根據上面的試驗結果,未來 MyList 實現了 IndexOf 以後,這樣的調用會直接切換到調用 MyList 中的實現,不會產生語義上的問題。

4. 問題二,關於多重繼承的問題

不管 Java 仍是 C# 都不容許類多繼承,可是接口能夠。而接口中的默認實現帶來了相似於類多繼承所產生的問題,怎麼辦?

舉個例,人能夠走,鳥也能夠走,那麼「雲中君」該怎麼走?

4.1. 先來看 C# 的

類中不實現默認接口的狀況:

interface IPerson
{
    void Walk() => Console.WriteLine("IPerson.Walk()");
}

interface IBird
{
    void Walk() => Console.WriteLine("IBird.Walk()");
}

class BirdPerson : IPerson, IBird { }

調用結果:

BirdPerson birdPerson = new BirdPerson();
// birdPerson.Walk();           // CS1061,沒有實現 Walk
((IPerson)birdPerson).Walk();   // 輸出 IPerson.Walk()
((IBird)birdPerson).Walk();     // 輸出 IBird.Walk()

不能直接使用 birdPerson.Walk(),道理前面已經講過。不過經過不一樣的接口類型來調用,行爲是不一致的,徹底由接口的默認方法來決定。這也能夠理解,既然類沒有本身的實現,那麼用什麼接口來引用,說明開發者但願使用那個接口所規定的默認行爲。

說得直白一點,你把雲中君看做人,他就用人的走法;你把雲中君看做鳥,它就用鳥的走法。

然而,若是類中有實現,狀況就不同了:

class BirdPerson : IPerson, IBird
{
    // 注意這裏的 public 可不能少
    public void Walk() => Console.WriteLine("BirdPerson.Walk()");
}
BirdPerson birdPerson = new BirdPerson();
birdPerson.Test();              // 輸出 BirdPerson.Walk()
((IPerson)birdPerson).Walk();   // 輸出 BirdPerson.Walk()
((IBird)birdPerson).Walk();     // 輸出 BirdPerson.Walk()

輸出徹底一致,接口中定義的默認行爲,在類中有實現的時候,就當不存在!

雲中君有個性:無論你怎麼看,我就這麼走。

這裏惟一須要注意的是 BirdPerson 中實現的 Walk() 必須聲明爲 public,不然 C# 會把它看成類的內部行爲,而不是實現的接口行爲。這一點和 C# 對實現接口方法的要求是一致的:實現接口成員必須聲明爲 public

4.2. 接着看 Java 的不一樣

轉到 Java 這邊,狀況就不一樣了,編譯根本不讓過

interface Person {
    default void walk() {
        out.println("IPerson.walk()");
    }
}

interface Bird {
    default void walk() {
        out.println("Bird.walk()");
    }
}

// Duplicate default methods named walk with the parameters () and ()
// are inherited from the types Bird and Person
class BirdPerson implements Person, Bird { }

這個意思就是,PersonBird 都爲簽名相同的 walk 方法定義了默認現,因此編譯器不知道 BirdPerson 到底該怎麼辦了。那麼若是隻有一個 walk 有默認實現呢?

interface Person {
    default void walk() {
        out.println("IPerson.walk()");
    }
}

interface Bird {
    void walk();
}

// The default method walk() inherited from Person conflicts
// with another method inherited from Bird
class BirdPerson implements Person, Bird { }

這意思是,兩個接口行爲不一致,編譯器仍是不知道該怎麼處理 BirdPerson

總之,無論怎麼樣,就是要 BirdPerson 必須實現本身的 walk()。既然 BirdPerson 本身實現了 walk(),那調用行爲也就沒有什麼懸念了:

BirdPerson birdPerson = new BirdPerson();
birdPerson.walk();              // 輸出 BirdPerson.walk()
((Person) birdPerson).walk();   // 輸出 BirdPerson.walk()
((Bird) birdPerson).walk();     // 輸出 BirdPerson.walk()

4.3. 結論,多繼承沒有問題

若是一個類實現的多個接口中定義了相同簽名的方法,沒有默認實現的狀況下,固然不會有問題。

若是類中實現了這個簽名的方法,那不管如何,調用的都是這個方法,也不會有問題。

但在接口有默認實現,而類中沒有實現的狀況下,C# 將實際行爲交給引用類型去處理;Java 則直接報錯,交給開發者去處理。筆者比較贊同 C# 的作法,畢竟默認方法的初衷就是爲了避免強制開發者去處理增長接口方法帶來的麻煩。

5. 問題三,更復雜的狀況怎麼去分析

對於更復雜的狀況,多數時候仍是能夠猜到會怎麼去調用的,畢竟有個基本原則在那裏。

5.1. 在類中的實現優先

好比,WalkBase 定義了 Walk() 方法,但沒實現任何接口,BirdPersonWalkBase 繼承,實現了 IPerson 接口,但沒實現 Walk() 方法,那麼該執行哪一個 Walk 呢?

會執行 WalkBase.Walk()——無論什麼狀況下,類方法優先

class WalkBase
{
    public void Walk() => Console.WriteLine("WalkBase.Walk()");
}

class BirdPerson : WalkBase, IPerson { }

static void Main(string[] args)
{
    BirdPerson birdPerson = new BirdPerson();
    birdPerson.Walk();              // 輸出 WalkBase.Walk()
    ((IPerson)birdPerson).Walk();   // 輸出 WalkBase.Walk()
}

若是父類子類都有實現,但子類不是「重載」,而是「覆蓋」實現,那要根據引用類型來找最近的類,好比

class WalkBase : IBird  // <== 注意這裏實現了 IBird
{
    public void Walk() => Console.WriteLine("WalkBase.Walk()");
}

class BirdPerson : WalkBase, IPerson  // <== 這裏_沒有_實現 IBird
{
    // 注意:這裏是 new,而不是 override
    public new void Walk() => Console.WriteLine("BirdPerson.Walk()");
}

static void Main(string[] args)
{
    BirdPerson birdPerson = new BirdPerson();
    birdPerson.Walk();              // 輸出 BirdPerson.Walk()
    ((WalkBase)birdPerson).Walk();  // 輸出 WalkBase.Walk()
    ((IPerson)birdPerson).Walk();   // 輸出 BirdPerson.Walk()
    ((IBird)birdPerson).Walk();     // 輸出 WalkBase.Walk()
}

若是 WalkBase 中以 virtual 定義 Walk(),而 BirdPerson 中以 override 定義 Walk(),那毫無懸念輸出全都是 BirdPerson.Walk()

class WalkBase : IBird
{
    public virtual void Walk() => Console.WriteLine("WalkBase.Walk()");
}

class BirdPerson : WalkBase, IPerson
{
    public override void Walk() => Console.WriteLine("BirdPerson.Walk()");
}

static void Main(string[] args)
{
    BirdPerson birdPerson = new BirdPerson();
    birdPerson.Walk();              // 輸出 BirdPerson.Walk()
    ((WalkBase)birdPerson).Walk();  // 輸出 BirdPerson.Walk()
    ((IPerson)birdPerson).Walk();   // 輸出 BirdPerson.Walk()
    ((IBird)birdPerson).Walk();     // 輸出 BirdPerson.Walk()
}

上面示例中的候最後一句輸出,是經過 IBird.Walk() 找到 WalkBase.Walk(),而 WalkBase.Walk() 又經過虛方法鏈找到 BirdPerson.Walk(),因此輸出仍然是 BirdPerson.Walk()。學過 C++ 的同窗這時候可能就會頗有感受了!

至於 Java,全部方法都是虛方法。雖然能夠經過 final 讓它非虛,可是在子類中不能定義相同簽名的方法,因此 Java 的狀況會更簡單一些。

5.2. 類中無實現,根據引用類型找最近的默認實現

仍是拿 WalkBaseBirdPerson 分別實現了 IBirdIPerson 的例子,

class WalkBase : IBird { }
class BirdPerson : WalkBase, IPerson { }

((IPerson)birdPerson).Walk();   // 輸出 IPerson.Walk()
((IBird)birdPerson).Walk();     // 輸出 IBird.Walk()

哦,固然 Java 中不存在,由於編譯器會要求必須實現 BirdPerson.Walk()

5.3. 如何還有更復雜的狀況

講真,若是真的還有更復雜的狀況,我建議仍是作作實驗吧!

6. 慎用默認方法

默認方法的出現有其歷史緣由,因此在設計一個新庫的時候,最好不要過早考慮默認方法這個問題。若是真的有須要實現的默認行爲,可能仍是抽象的基類更適合一些。

可是,若是設計出來的類和接口關係確實很是複雜,甚至須要相似多重繼承的關係,那麼適當的考慮一下默認方法也何嘗不可。

相關文章
相關標籤/搜索