介面語法細節

June 9, 2022

在 Java 中,使用 interface 來定義抽象的行為外觀,方法可宣告為 public abstract。例如:

public interface Swimmer {
    public abstract void swim();
}

介面的預設

介面中的方法沒有實作時,一定得是公開且抽象,為了方便,也可以省略 public abstract

public interface Swimmer {
    void swim();  // 預設就是public abstract
}

編譯器會自動幫你加上 public abstract。在 interface 中,可以定義常數。例如:

package cc.openhome;

public interface Action {
    public static final int STOP = 0;
    public static final int RIGHT = 1;
    public static final int LEFT = 2;
    public static final int UP = 3;
    public static final int DOWN = 4;
}

Java 早期常見到於介面中定義這類常數,稱為列舉常數,不過現在已經不鼓勵,建議透過 enum,這之後會再談到。

事實上,在 interface 也只能定義 public static final 的常數,為了方便,也可以如下撰寫:

public interface Action {
    int STOP = 0;
    int RIGHT = 1;
    int LEFT = 2;
    int UP = 3;
    int DOWN = 4;
}  

編譯器會展開為 public static final,所以在介面中列舉常數,一定要使用 = 指定值,否則就會編譯錯誤。

要在類別中定義列舉常數也是可以的,不過就一定要明確寫出 public static final

類別可以實作兩個以上的介面,如果有兩個介面都定義了某方法,而實作兩個介面的類別會怎樣嗎?程式面上來說,並不會有錯誤,照樣通過編譯:

interface Some {
    void execute();
    void doSome();
}

interface Other {
    void execute();
    void doOther();
}

public class Service implements Some, Other {
    @Override
    public void execute() {
        System.out.println("execute()");
    }
    @Override
    public void doSome() {
        System.out.println("doSome()");
    }
    @Override
    public void doOther() {
        System.out.println("doOther()");
    }
}

但在設計上要思考一下:SomeOther 定義的 execute 是否表示不同的行為?

如果表示不同的行為,那麼 Service 在實作時,應該有不同的方法實作,SomeOtherexecute 方法就得在名稱上有所不同,Service 在實作時才可以有兩個不同的方法實作。

如果表示相同的行為,那可以定義一個父介面,在當中定義 execute 方法,而 SomeOther 繼承該介面,各自定義自己的 doSomedoOther 方法:

import static java.lang.System.out;

interface Action {
    void execute();
}

interface Some extends Action {
    void doSome();
}

interface Other extends Action {
    void doOther();
}

public class Service implements Some, Other {
    @Override
    public void execute() {
        out.println("execute()");
    }
    @Override
    public void doSome() {
        out.println("doSome()");
    }
    @Override
    public void doOther() {
        out.println("doOther()");
    }
}

介面可以繼承別的介面,也可以同時繼承兩個以上的介面,同樣也是使用 extends 關鍵字,這代表了繼承父介面的行為。

匿名類別

在撰寫 Java 程式時,經常會有臨時繼承某個類別或實作某個介面並建立實例的需求,由於這類子類別或介面實作類別只使用一次,不需要為這些類別定義名稱,這時可以使用匿名內部類別(Anonymous inner class)來解決這個需求。匿名內部類別的語法為:

new 父類別() | 介面() {
    // 類別本體實作
};

以繼承 Object 重新定義 toString 方法為例:

Object o = new Object() {  // 繼承 Object 重新定義 toString 並直接產生實例
    @Override
    public String toString() {
        return "無聊的語法示範而已";
    }
};

在以上的程式片段中,有個匿名類別繼承了 Object,接著直接產生該匿名類別的實例,正因為是匿名類別,你沒有類別名稱可用來宣告變數型態,真要指定型態的話,只能用 Object 型態的變數參考至該實例。

然而,透過 Object 型態的變數,能進行的多型操作,就只限於 Object 定義的方法,如上建立匿名類別實例,基本上沒太大用處。

若是使用 var 自動推斷區域變數型態,繼承 Object 來建立匿名類別實例,倒是可以有特別的作用:

jshell> var o = new Object() {
   ...>     String name = "Justin Lin";
   ...>     String getFirstName() {
   ...>         return name.split(" ")[0];
   ...>     }
   ...>     String getLastName() {
   ...>         return name.split(" ")[1];
   ...>     }
   ...> };
o ==> $0@6e06451e

jshell> o.name
$2 ==> "Justin Lin"

jshell> o.getFirstName()
$3 ==> "Justin"

jshell> o.getLastName()
$4 ==> "Lin"

因為編譯器自動推斷出匿名的型態,你就可以直接取得定義的值域、操作新增的方法,若想臨時建立物件來組合某些資料或操作,這是可行的方式之一。

如果是實作某個介面,例如若 Some 介面定義了 doService 方法,要建立匿名類別實例,可以如下:

var some = new Some() {  // 實作Some介面並直接產生實例
    public void doService() {
        System.out.println("作一些事");
    }
};

若介面僅定義一個抽象方法,可以使用 Lambda 來簡化這個程式的撰寫,例如:

Some some = () -> {
    System.out.println("作一些事");
};

有關 Lambda 語法的細節,之後會再說明。

介面預設方法

介面定義時可以有預設實作,或者稱為預設方法(Default methods),使用 default 關鍵字修飾,預設權限為 public,例如,可以如下定義自己的Comparable介面:

public interface Comparable {
    int compareTo(Object that);
 
    default boolean lessThan(Object that) {
        return compareTo(that) < 0;
    }
    default boolean lessOrEquals(Object that) {
        return compareTo(that) <= 0;
    }
    default boolean greaterThan(Object that) {
        return compareTo(that) > 0;
    }
}

預設方法令介面看來像是有抽象方法的抽象類別,然而因為介面本身不能定義資料成員,預設方法的實作中無法直接使用資料成員。

若有個 Ball 類別想實作這個 Comparable 介面,只需要實作 compareTo 方法:

public class Ball implements Comparable {
    private int radius;
    ...
    public int compareTo(Object that) {
        return this.radius - ((Ball) that).radius;
    }
}

這麼一來,每個 Ball 實例就會擁有 Comparable 定義的預設方法。因為類別可以實作多個介面,運用預設方法,就可以在某介面定義可共用的操作,若有個類別需要某些可共用操作,只需要實作相關介面,就可以混入(Mixin)這些共用的操作了。

在實作預設方法時,可能會將演算法定義為更小的流程,而這些流程不用公開,基於此需求,介面可以定義 private 方法,可被預設方法呼叫,然而不用加上 default 修飾。例如:

public interface Some {
    default void doIt() {
        subMethod1();
        subMethod2();
    }
    private void subMethod1() {
        // 私有實作...
    }
    private void subMethod2() {
        // 私有實作...
    }
}

介面沒有實作時,判斷方法來源時會單純許多,為介面定義預設實作,可引入更強大的威力,然而也引入更多的複雜度,你得留意採用的是哪個預設方法。

介面也可以被繼承,而抽象方法或預設方法都會繼承下來,子介面再以抽象方法重新定義父介面已定義的抽象方法,通常是為了文件化,這是常見的實踐(Practice),因為沒有實作,也就沒有辨別實作版本的問題。

若介面定義了預設方法,辨別實作版本時有許多需要注意的地方。例如,父介面的抽象方法,在子介面中可以用預設方法實作,父介面的預設方法,在子介面中可以被重新定義。

若父介面有個預設方法,子介面再度宣告與父介面相同的方法簽署,但沒有寫出 default,也就是沒有方法實作,子介面就是重新定義該方法為抽象方法了。

若有兩個父介面定義了相同方法簽署的預設方法,就會引發衝突。例如,假設 PartCanvas 介面都定義了 defaultdraw 方法,而 Lego 介面繼承 PartCanvas 時,沒有重新定義 draw,就會發生編譯錯誤。

解決的方式是明確重新定義 draw,無論是重新定義為抽象或預設方法,若重新定義為預設方法時,想明確呼叫某個父介面的 draw 方法,必須使用介面名稱與 super 明確指定,例如:

public interface Lego extends Part, Canvas {
    default void draw() {
        Part.super.draw();
    }
}

如果類別實作的兩個介面擁有相同的父介面,其中一個介面重新定義了父介面的預設方法,而另一個介面沒有,那麼實作類別會採用重新定義的版本。

若子類別繼承了父類別又實作了某介面,而父類別的方法與介面中的預設方法具有相同方法簽署,就採用父類別的方法定義。

簡單來說,類別的定義優先於介面的定義,若有重新定義,就以重新定義為主,必要時使用介面與 super 指定預設方法。

靜態方法

介面可以定義靜態方法,介面的公開靜態方法,演算流程可能被拆解為數個小流程,定義於其他靜態方法中,若這些方法不用公開給外界,可以定義為 private 的靜態方法。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter