什麼?接口中方法可以不是抽象的「JDK8接口新語法的深度思考」

先贊後看,養成習慣

文本已收錄至GitHub開源倉庫 Lu_JavaNodes 碼雲倉庫地址Lu_JavaNodes,包含教程涉及所有思維導圖,案例代碼和後續講解視頻,歡迎Star增磚添瓦。

前言

在傳統的接口語法中,接口中只可以有抽象方法。在是在實際的使用中,我們往往會需要用到很多和接口相關的功能(方法),這些功能會單獨的拿出開放在工具類中。

工具類:類中所有的方法都是靜態的

例如:Collection 和 Collocations,Collection 是一個集合接口,而我們需要很多集合相關的操作,像集合的排序,搜索等等, 這時候人們會把這些靜態方法放在 Collections 工具類中。

在傳統Java中我們經常會看到這樣的情況,有一個接口叫 A,這時候就會有一個類叫 As,As中全是和A接口有關的靜態方法。
例如:Executor 和 Executors

這樣的一種方式總歸來說是有點不方便。於是在JDK8中Java對於接口做了一些改動,允許將靜態方法直接寫入接口中。(接口中可以定義靜態方法,靜態方法肯定不是抽象的,是有實現的)。

接口的靜態方法

代碼案例

根據上述內容,我們來定義一個接口,在接口中寫入一個靜態方法。


public class TestStaticInterface {

    public static void main(String[] args) {
//        靜態方法可以通過類名直接調用  接口可以說是特殊的類 所以通過接口名可以調用接口中的靜態方法
        HelloInterface.printHello();
    }

}

interface HelloInterface{
    int hhh();

//    定義靜態方法
    static void printHello(){
        System.out.println("Hello");
    }
}

運行代碼可以看到如下結果

靜態方法有什麼用呢?

靜態方法實際上是很實用的,最基本的用法:我們可以把產生接口對象的方法放在接口中。

什麼意思???好,接下來我們通過代碼演示一下。

假設現在我們有一個 Animal 接口,那麼這時候如果要獲得一個Animal類型的對象,我們要怎麼做呢?

傳統方法,創建一個Animals工具類,在其中有一個 static Animal createDog()可以獲取一個Animal類型的對象,代碼如下


public class TestStaticInterface {

    public static void main(String[] args) {
//        通過工具類獲取對象
        Animal animal = Animals.createDog();
    }
}

class Animals{
    //    靜態方法獲取對象
    static Animal createDog(){
//        局部內部類
        class Dog implements Animal{

        }
//        返回對象
        return new Dog();
    }
}

但是當你擁抱JDK8的時候,一切都不一樣了,因爲有接口靜態方法,可以直接將接口對象的獲取放在接口的靜態方法中。代碼如下


public class TestStaticInterface {

    public static void main(String[] args) {
//        通過接口的靜態方法獲取一個Animal類型的對象
        Animal animal = Animal.createDog();
    }
}

interface Animal{
//    靜態方法獲取對象
    static Animal createDog(){
//        局部內部類
        class Dog implements Animal{

        }
//        返回對象
        return new Dog();
    }
}


在JDK 的 API 中是怎麼使用靜態方法的

接下來我們通過Java中的API來驗證一下這種使用方法。通過API文檔,可以找到 Comparator 接口(比較器),在這個接口中現在就有很多的靜態方法(JDK8)。如圖

通過這些靜態方法,就可以通過接口直接獲取比較器對象。


public class TestStaticInterface {

    public static void main(String[] args) {
//        通過Comparator接口獲取一個自然排序的比較器(自然排序就是String中默認實現的排序邏輯)
        Comparator<String> comparator = Comparator.naturalOrder();
//        創建集合
        List<String> list = Arrays.asList("b","a","c");
//        通過比較器對集合進行排序
        list.sort(comparator);

        for (String s : list) {
            System.out.println(s);
        }
    }
}

傳統接口的另一個問題:向後兼容性不好

現在接口已經有了靜態方法,但是傳統的接口還有另一個問題。我們舉例說明:

假設你正在公司中做項目,在你的代碼中,有一個UserService的接口,接口中有一個方法String getUsernameById()

interface UserService{
    String getUsernameById();
}

該接口因爲在項目中存在老長時間了,所以實現類衆多,有100個實現類。

one day,領導希望你給這個接口中添加一個新的接口方法String getIdByUsername()。這樣的需求意味着要修改100個實現類,不要說寫代碼了,刪庫跑路的心都有了。

這是一個極端的案例,但是說明了一個事兒,傳統的接口向後兼容性不好,不易於維護和改造

而這個問題,在JDK8中得到了解決,解決方法就是:接口的默認方法

接口的默認方法

Java 8 中允許接口中包含具有具體實現的方法,該方法稱爲 “默認方法”,默認方法使用 default 關鍵字修飾

在接口中使用 default 表示這個方法有實現,接口中所有的方法都是 public

示例代碼


interface UserService{
    String getUsernameById();

//    默認方法
    default void m1(){
        System.out.println("這是一個默認方法");
    }
}

class UserServiceImpl implements UserService{

    @Override
    public String getUsernameById() {
        return null;
    }
}

示例代碼的問題

看了這樣的一段代碼,你一定會有一些疑問,我們一起來解決一下。

接口中的默認方法,實現類能不能繼承到

答:這個當然是可以的,並且在實現類中依然可以進行方法的覆蓋。

如果 UserServiceImpl 再有一個父類,父類中也有m1方法,那麼UserServiceImpl 繼承到的是父類還是接口中的m1方法


interface UserService{
    String getUsernameById();

//    默認方法
    default void m1(){
        System.out.println("這是一個默認方法");
    }
}

//父類
class UserSer{
    public void m1(){
        System.out.println("這是一個默認方法");
    }
}

class UserServiceImpl extends UserSer implements UserService{

    @Override
    public String getUsernameById() {
        return null;
    }
}

答:在實現類中繼承到的是父類中的。因爲接口默認方法有”類優先”的原則

接口默認方法的”類優先”原則
若一個接口中定義了一個默認方法,而另外一個父類或接口中 又定義了一個同名的方法時

  • 選擇父類中的方法。如果一個父類提供了具體的實現,那麼
    接口中具有相同名稱和參數的默認方法會被忽略。
  • 接口衝突。如果一個父接口提供一個默認方法,而另一個接 口也提供了一個具有相同名稱和參數列表的方法(不管方法是否是默認方法),那麼必須覆蓋該方法來解決衝突

對於 JDK8 接口新語法的思考

關於接口新語法的講解實際上已經結束了,但是想要和大家一起延伸一下思考,看下面一個案例。


interface IA{
    default void m2(){
        System.out.println("IA");
    }
}

interface IB{
    default void m2(){
        System.out.println("IB");
    }
}

class ImplC implements IA,IB{
//    接口衝突 通過覆蓋解決
    public void m2(){
        System.out.println("Impl");
    }
}

以上代碼實際上就是 “類優先”原則第二條接口衝突的演示代碼,而我要思考的問題不是這個,而是:1.在實現類中,如何使用super,2.如果IA 和 IB 接口中的m2方法返回值不同怎麼辦?

1.在實現類中,如何使用super?

第一個問題,比較好解決,因爲有m2來自兩個接口,所以我們如果要調用super的話,需要說明要調用那個接口的super,語法:接口名.super.m2()

實現類繼承的方法來自兩個接口,必須覆蓋,否則引用不明確。要調用super,也必須指明要調用那個接口。

其實這個問題來自多繼承,過去接口比較簡單,調用 super肯定不會調用接口,接口中方法都是抽象的,現在不一樣了,父類和接口中都有方法實現,這時候再要調用就要指明要調用誰了。

雖然Java一直都是單繼承,但是這個語法實際上已經是向多繼承靠近了。只不過並沒有把多繼承正式的引入Java,所以會有一定的不足,這就是我們的第二個思考題。

2.如果IA 和 IB 接口中的m2方法返回值不同怎麼辦?

這其實也是一個標準的多繼承的問題,在現版本沒有解決。

在C++中其實就簡單了,可以指定要覆蓋誰

總結

學過了接口的靜態方法和默認方法,彷彿發現了一個事兒,接口和抽象類越來越像了,那麼這時候再問你那個問題:接口和抽象類有什麼區別?

這個問題留給大家,好像以前背答案開始不好使了。

最後我們簡單總結一下JDK8接口語法的新變化:在JDK8以後的接口中,允許有靜態方法和默認方法(default)修飾

求關注,求點贊,求轉發

歡迎關注本人公衆號:鹿老師的Java筆記,將在長期更新Java技術圖文教程和視頻教程,Java學習經驗,Java面試經驗以及Java實戰開發經驗。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章