1. 導言
函式介面 — 是只包含恰好一個抽象方法(也就是沒有實作的方法)的介面。這樣的介面可以用來精簡地表達方法的實作 — 在 Java 中可透過 Lambda 運算式來完成(我們稍後會學到)。
為什麼只能有一個方法?
因為函式介面描述的就是一個操作。若方法有兩個以上,就不清楚到底要實作哪一個。因此規則很簡單:一個介面 — 一個抽象方法。
標準程式庫中的範例
- Runnable — 用於執行緒中的任務(void run())
- Callable<V> — 用於會回傳結果的任務(V call())
- Comparator<T> — 用於比較物件(int compare(T o1, T o2))
- Consumer<T> — 值的「消費者」(void accept(T t))
- Supplier<T> — 值的「供應者」(T get())
- Function<T, R> — 從 T 對應到 R 的函式(R apply(T t))
- Predicate<T> — 條件判斷(boolean test(T t))
例如,介面 Runnable:
public interface Runnable {
void run();
}
再看介面 Comparator:
public interface Comparator<T> {
int compare(T o1, T o2);
// ... 還有 default 與 static 方法,但抽象方法只有一個!
}
重要提示: default 與 static 方法不算作抽象方法,因此它們可以有任意多個!
2. 註解 @FunctionalInterface
Java — 是一位嚴謹而有原則的女士。為了避免混淆,它允許你用註解 @FunctionalInterface 明確標記某個介面為函式介面。這就像貼上「只用一個按鈕就能運作!」的貼紙 — 以免有人加上多餘的東西。
@FunctionalInterface
public interface Operation {
int apply(int a, int b);
}
此後若你不小心又加入第二個抽象方法,編譯器會立刻報錯:
@FunctionalInterface
public interface Oops {
void doIt();
void doSomethingElse(); // 錯誤!有兩個抽象方法
}
註解是必須的嗎?
不,並非必須。若介面恰好只有一個抽象方法,即使沒有它,介面仍然是函式介面。不過使用註解可以明確表達你的意圖,並保護你免於不小心犯錯。
可以加入 default 和 static 方法嗎?
可以!重點是必須只有 一個 抽象方法。其餘方法都可以是 default 或 static,想加多少都行。
範例:
@FunctionalInterface
public interface FancyOperation {
int apply(int a, int b);
default void printInfo() {
System.out.println("我是個花式操作!");
}
static void description() {
System.out.println("用於算術的函式介面。");
}
}
3. 宣告與使用範例
假設你想描述一個針對兩個數字的操作,可以這樣做:
@FunctionalInterface
public interface Operation {
int apply(int a, int b);
}
現在可以用不同方式來實作這個介面。
以一般類別實作
public class SumOperation implements Operation {
@Override
public int apply(int a, int b) {
return a + b;
}
}
用法:
Operation sum = new SumOperation();
System.out.println(sum.apply(2, 3)); // 5
以匿名類別實作
Operation multiply = new Operation() {
@Override
public int apply(int a, int b) {
return a * b;
}
};
System.out.println(multiply.apply(2, 3)); // 6
關於 Lambda 的備註
自 Java 8 起,這類介面很適合用更精簡的 Lambda 運算式來實作。我們會在接下來幾堂課學到 Lambda;目前只要知道:函式介面的設計目的就是讓你能更方便地與之搭配使用。
4. 實作練習:寫出你自己的函式介面
任務 1:做一個屬於你的 Action!
建立介面 Action,接收一個字串且不回傳任何值。用匿名類別實作,將傳入字串以全大寫輸出。
@FunctionalInterface
interface Action {
void act(String s);
}
public class ActionDemo {
public static void main(String[] args) {
Action shout = new Action() {
@Override
public void act(String text) {
System.out.println(text.toUpperCase());
}
};
shout.act("我在學 Java!"); // 我在學 JAVA!
}
}
(之後我們會看到如何用 Lambda 運算式把它寫得更精簡。)
任務 2:數字過濾器
建立介面 NumberPredicate,其方法為 boolean test(int n)。使用匿名類別實作偶數檢查。
@FunctionalInterface
interface NumberPredicate {
boolean test(int n);
}
public class PredicateDemo {
public static void main(String[] args) {
NumberPredicate isEven = new NumberPredicate() {
@Override
public boolean test(int n) {
return n % 2 == 0;
}
};
System.out.println(isEven.test(4)); // true
System.out.println(isEven.test(7)); // false
}
}
任務 3:使用標準介面
也可以不用自訂介面,直接使用現成的 Predicate<Integer>:
import java.util.function.Predicate;
Predicate<Integer> isPositive = new Predicate<Integer>() {
@Override
public boolean test(Integer x) {
return x > 0;
}
};
System.out.println(isPositive.test(10)); // true
System.out.println(isPositive.test(-5)); // false
表格:標準程式庫中的函式介面
| 介面 | 方法 | 描述 | 應用範例 |
|---|---|---|---|
|
|
無引數且無回傳值的任務 | 執行緒、計時器 |
|
|
有回傳值的任務 | 執行緒、ExecutorService |
|
|
比較兩個物件 | 集合排序 |
|
|
值的「消費者」 | 遍歷集合 |
|
|
值的「供應者」 | 惰性初始化、資料生成 |
|
|
從 T 到 R 的函式 | 資料轉換 |
|
|
條件檢查 | 集合過濾 |
5. 使用函式介面時的常見錯誤
錯誤 1:加入了第二個抽象方法。 若介面包含超過一個抽象方法,就不再是函式介面。編譯器(特別是在有 @FunctionalInterface 時)會立刻報錯。
錯誤 2:忘了 default 與 static 方法不算抽象方法。 可以放心把它們加到函式介面中 — 這不會違反「僅有一個抽象方法」的規則。
錯誤 3:實作的方法簽章不正確。 例如介面要求兩個參數,而你卻只在方法中寫了一個。務必核對簽章。
錯誤 4:沒有使用 @FunctionalInterface,結果不小心破壞了介面。 如果沒有用註解標記,很可能不小心加了多餘的方法 — 然後花很長時間找為什麼程式不運作。為求明確,建議一律加上註解。
GO TO FULL VERSION