介面語法細節
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()");
}
}
但在設計上要思考一下:Some
與 Other
定義的 execute
是否表示不同的行為?
如果表示不同的行為,那麼 Service
在實作時,應該有不同的方法實作,Some
與 Other
的 execute
方法就得在名稱上有所不同,Service
在實作時才可以有兩個不同的方法實作。
如果表示相同的行為,那可以定義一個父介面,在當中定義 execute
方法,而 Some
與 Other
繼承該介面,各自定義自己的 doSome
與 doOther
方法:
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
,也就是沒有方法實作,子介面就是重新定義該方法為抽象方法了。
若有兩個父介面定義了相同方法簽署的預設方法,就會引發衝突。例如,假設 Part
與 Canvas
介面都定義了 default
的 draw
方法,而 Lego
介面繼承 Part
、Canvas
時,沒有重新定義 draw
,就會發生編譯錯誤。
解決的方式是明確重新定義 draw
,無論是重新定義為抽象或預設方法,若重新定義為預設方法時,想明確呼叫某個父介面的 draw
方法,必須使用介面名稱與 super
明確指定,例如:
public interface Lego extends Part, Canvas {
default void draw() {
Part.super.draw();
}
}
如果類別實作的兩個介面擁有相同的父介面,其中一個介面重新定義了父介面的預設方法,而另一個介面沒有,那麼實作類別會採用重新定義的版本。
若子類別繼承了父類別又實作了某介面,而父類別的方法與介面中的預設方法具有相同方法簽署,就採用父類別的方法定義。
簡單來說,類別的定義優先於介面的定義,若有重新定義,就以重新定義為主,必要時使用介面與 super
指定預設方法。
靜態方法
介面可以定義靜態方法,介面的公開靜態方法,演算流程可能被拆解為數個小流程,定義於其他靜態方法中,若這些方法不用公開給外界,可以定義為 private
的靜態方法。