天天看點

Java 8 有多牛逼?打破一切你對接口的認知!

前段時間面試了一個 39 歲的程式員,結果不是很理想,沒看過的點選這裡閱讀。

最近也面試一些 Java 程式員,不乏工作 4、5 年經驗的,當我問他一些 Java 8 的新特性時,大多卻答不上來。

比如下面這道題:

棧長:接口裡面可以寫方法嗎?

小A:當然可以啊,預設就是抽象方法。

棧長:那接口裡面可以寫實作方法嗎?

小A:不可以,所有方法必須是抽象的。

棧長:你确定嗎?

小A:确定……

小A看起來對我的問題有點懷疑人生,心裡肯定估摸着,我不會在給他埋了什麼坑吧。然後他還是仔細再想了一下,最後還是斬釘截鐵的告訴我:接口裡面隻能寫抽象方法,不能寫實作方法。

棧長:接口裡面是可以寫實作方法的,Java 8 開始就可以了,你用過 Java 8 嗎?

小A:好吧,看來是我學藝不精,Java 8 有了解一點,比如那個 Lambda 表達式,但實際項目中也沒怎麼用。

通過和小A的交流,我也看到了許多開發者的問題,雖然開發版本用的是 Java 8,但實際用的還是 Java 8 之前的最基礎的文法,對 Java 8 新增的特性一無所知。

Java 8 至 2014 年釋出至今,已經過了 6 個年頭了,最新的 Java 14 都釋出了,OK,這個不在本篇讨論範圍之内, Java 8+ 系列教程請關注公衆号回複 "java" 進行閱讀,本篇就是想順着問小A的這個問題展開。

什麼是預設方法和靜态方法?

上面也說了,Java 8 開始是可以有方法實作的,可以在接口中添加預設方法和靜态方法。

預設方法用 default 修飾,隻能用在接口中,靜态方法用 static 修飾,這個我們不陌生了。并且接口中的預設方法、靜态方法可以同時有多個。

在接口中寫實作方法一點也不稀奇,像這樣的用法,從 Java 8 到 Java 14 已是遍地開花,到處都可以看到接口預設方法和靜态方法的身影。

比如我們來看下在 JDK API 中 java.util.Map 關于接口預設方法和靜态方法的應用。

/*
* 來源公衆号:Java技術棧 
*/
public interface Map<K,V> {

    ...

    /**
    * 接口預設方法
    */
    default boolean remove(Object key, Object value) {
        Object curValue = get(key);
        if (!Objects.equals(curValue, value) ||
            (curValue == null && !containsKey(key))) {
            return false;
        }
        remove(key);
        return true;
    }

    ...

    /**
    * 接口靜态方法
    */
    public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
        return (Comparator<Map.Entry<K, V>> & Serializable)
            (c1, c2) -> c1.getKey().compareTo(c2.getKey());
    }

    ...

}          

為什麼要有接口預設方法?

舉一個很現實的例子:

我們的接口老早就寫好了,後面因為各種業務問題,避免不了要修改接口。

在 Java 8 之前,比如要在一個接口中添加一個抽象方法,那所有的接口實作類都要去實作這個方法,不然就會編譯錯誤,而某些實作類根本就不需要實作這個方法也被迫要寫一個空實作,改動會非常大。

是以,接口預設方法就是為了解決這個問題,隻要在一個接口添加了一個預設方法,所有的實作類就自動繼承,不需要改動任何實作類,也不會影響業務,爽歪歪。

另外,接口預設方法可以被接口實作類重寫。

為什麼要有接口靜态方法?

接口靜态方法和預設方法類似,隻是接口靜态方法不可以被接口實作類重寫。

接口靜态方法隻可以直接通過靜态方法所在的 接口名.靜态方法名 來調用。

接口預設方法多繼承沖突問題

因為接口預設方法可以被繼承并重寫,如果繼承的多個接口都存在相同的預設方法,那就存在沖突問題。

下面我會列舉 3 個沖突示例場景。

沖突一

來看下面這段程式:

/*
* 來源公衆号:Java技術棧 
*/
interface People {
    default void eat(){
        System.out.println("人吃飯");
    }
}

/*
* 來源公衆号:Java技術棧 
*/
interface Man {
    default void eat(){
        System.out.println("男人吃飯");
    }
}

/*
* 來源公衆号:Java技術棧 
*/
interface Boy extends Man, People {

}      

Boy 同時繼承了 People 和 Man,此時在 IDEA 編輯器中就會報錯:

Java 8 有多牛逼?打破一切你對接口的認知!

這就是接口多繼承帶來的沖突問題,Boy 不知道該繼承誰的,這顯然也是個問題,IDEA 也會提示,需要重寫這個方法才能解決問題:

/*
* 來源公衆号:Java技術棧 
*/
interface Boy extends Man, People {

    @Override
    default void eat() {
        System.out.println("男孩吃飯");
    }
}      

在方法裡面還能直接調用指定父接口的預設方法,比如:

/*
* 來源公衆号:Java技術棧 
*/
interface Boy extends Man, People {

    @Override
    default void eat() {
        People.super.eat();
        Man.super.eat();
        System.out.println("男孩吃飯");
    }
}      

再加個實作類測試一下:

/*
* 來源公衆号:Java技術棧 
*/
static class Student implements Boy {

    public static void main(String[] args) {
        Student student = new Student();
        student.eat();
    }

}      

輸出:

人吃飯
男人吃飯
男孩吃飯      

嗯,很強大!

沖突二

我們再換一種寫法,把 Man 繼承 People,然後 Man 重寫 People 中的預設方法。

Java 8 有多牛逼?打破一切你對接口的認知!

此時,編輯器不報錯了,而 People 的預設方法置灰了,提示沒有被用到。

再運作一下上面的示例,輸出:

男人吃飯

因為 Man 繼承 People,Man 又重定了預設方法。很顯然,這個時候,Boy 知道該繼承誰的預設方法了。

沖突三

在 Man 接口中新增一個方法:say,然後在 Boy 接口中新增一個預設方法:say。

Java 8 有多牛逼?打破一切你對接口的認知!

這時候,Man 中的抽象方法居然被忽略了,IDEA 都提示說沒用到,這顯然是預設方法優先于抽象方法。

總結

本文介紹了 Java 8 的預設方法和靜态方法,以及預設方法的沖突問題解決方案。是以,大家出去面試時,再也不要說接口不能寫實作方法了,那就太 OUT 了。。

文中隻舉了 3 個預設方法的沖突場景,不确定還沒有更多沖突問題。預設方法雖然解決了接口變動帶來的問題,但如果設計不當,或者過度設計,其帶來的方法沖突問題也是需要引起注意的。