Java 中的函數指針
1. 簡介
在 C 或 C++ 等語言中,我們可以將函數儲存在變數中並傳遞——這被稱為函數指標。 Java 沒有函數指針,但我們可以使用其他技術實現相同的行為。在本教程中,我們將探索一些在 Java 中模擬函數指標的常用方法。
2. 介面和匿名類
在 Java 8 之前,模擬函數指標的標準方法是定義單方法介面並使用匿名類別實作它們。對於維護遺留程式碼或在不支援 Java 8+ 的環境中工作,這種方法仍然很有價值。
以下是我們定義簡單操作介面的方法:
public interface MathOperation {
int operate(int a, int b);
}
這個介面只有一個方法operate(),
接收兩個整數,回傳一個結果。
現在我們定義一個使用這個介面的類別:
public class Calculator {
public int calculate(int a, int b, MathOperation operation) {
return operation.operate(a, b);
}
}
calculate()
方法接受一個operation
並將計算邏輯委託給傳遞的實作。
讓我們使用匿名類別來測試它以進行加法運算:
@Test
void givenAnonymousAddition_whenCalculate_thenReturnSum() {
Calculator calculator = new Calculator();
MathOperation addition = new MathOperation() {
@Override
public int operate(int a, int b) {
return a + b;
}
};
int result = calculator.calculate(2, 3, addition);
assertEquals(5, result);
}
在這段程式碼中,介面直接使用匿名類別實作。這樣就可以將行為傳遞給Calculator
。測試確認2 + 3
結果為5
。
介面方法適用於所有 Java 版本並提供明確的類型安全性。
然而,它需要大量的樣板程式碼,尤其是對於簡單的操作。每個操作都需要自己的類別實現,這會導致程式碼庫因許多小類別而變得混亂。
3. Lambda 表達式(Java 8+)
Java 8 引入了 lambda 表達式,它提供了一種更短、更易讀的方式來傳遞行為。
我們可以在這裡重複使用相同的MathOperation
介面:
@Test
void givenLambdaSubtraction_whenCalculate_thenReturnDifference() {
Calculator calculator = new Calculator();
MathOperation subtract = (a, b) -> a - b;
int result = calculator.calculate(10, 4, subtract);
assertEquals(6, result);
}
在這個測試中,我們使用 lambda 表達式來執行減法。表達式(a, b) -> a – b
以內聯方式定義了邏輯,並與介面的方法簽章相符。
Calculator
本身沒有變化——它仍然接受接口並調用其方法。差別在於,現在的行為傳遞方式更加簡潔。
這種方法在現代 Java 程式碼中被廣泛使用。它提高了程式碼的可讀性,並減少了樣板程式碼,尤其是在執行簡單操作時。
4. 內建函數式介面
此外,Java 8 在java.util.function
套件中也引入了預先定義的函數式介面。這讓我們無需編寫自己的介面。
讓我們使用一個名為BiFunction
的內建接口,它接受兩個輸入並返回一個結果:
@Test
void givenBiFunctionMultiply_whenApply_thenReturnProduct() {
BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b;
int result = multiply.apply(6, 7);
assertEquals(42, result);
}
BiFunction<T, U, R>
表示一個接受兩個參數並傳回一個結果的函數。我們將邏輯儲存在變數multiply
中,並使用apply()
方法呼叫它。
我們也可以在方法中使用BiFunction
:
public class AdvancedCalculator {
public int compute(int a, int b, BiFunction<Integer, Integer, Integer> operation) {
return operation.apply(a, b);
}
}
讓我們使用BiFunction
方法來測試除法:
@Test
void givenBiFunctionDivide_whenCompute_thenReturnQuotient() {
AdvancedCalculator calculator = new AdvancedCalculator();
BiFunction<Integer, Integer, Integer> divide = (a, b) -> a / b;
int result = calculator.compute(20, 4, divide);
assertEquals(5, result);
}
使用內建介面可以讓我們保持程式碼簡潔,避免額外的樣板程式碼。當功能需求與某個預先定義介面(例如Function
、 BiFunction
或Predicate
)相符時,這種方法非常有效。
當我們希望函數定義標準化和一致性時,這種模式是理想的。然而,當我們需要自訂參數或傳回值類型不符合這些預定義類型時,我們可能會面臨限制。
5. 方法引用
方法引用在呼叫現有方法時為 lambda 表達式提供了一種簡寫。
讓我們定義一個用於加法的實用方法:
public class MathUtils {
public static int add(int a, int b) {
return a + b;
}
}
我們現在可以使用方法參考來代替寫 lambda:
@Test
void givenMethodReference_whenCalculate_thenReturnSum() {
Calculator calculator = new Calculator();
MathOperation operation = MathUtils::add;
int result = calculator.calculate(5, 10, operation);
assertEquals(15, result);
}
在此程式碼中, MathUtils::add
作為引用傳遞。方法簽章與MathOperation
中的operate()
方法匹配,因此編譯器接受它。
當我們已經有靜態方法或實例方法時,方法參考非常有用。它們透過避免重複來保持程式碼簡潔,尤其是在串流操作或回調模式中。
當邏輯已經存在時,這種方法最有效。但如果行為需要動態或自訂,lambda 或介面可能會提供更大的靈活性。
6. 反思
Java 也允許使用反射來動態呼叫方法。這是一種更高級的方法,通常用於必須在運行時發現和調用方法的框架、工具或庫中。
我們來定義一個動態操作方法:
public class DynamicOps {
public int power(int a, int b) {
return (int) Math.pow(a, b);
}
}
現在讓我們透過反射來呼叫該方法:
@Test
void givenReflection_whenInvokePower_thenReturnResult() throws Exception {
DynamicOps ops = new DynamicOps();
Method method = DynamicOps.class.getMethod("power", int.class, int.class);
int result = (int) method.invoke(ops, 2, 3);
assertEquals(8, result);
}
在此範例中,我們使用DynamicOps
類別的名稱和參數類型檢索power()
方法引用,然後使用參數呼叫該方法。這允許在運行時選擇和執行行為。
當我們在編譯時不知道方法時,例如在插件系統或基於註解的處理中,反射非常強大。然而,它比其他技術更慢、更容易出錯,並且不提供編譯時類型安全性。
我們通常會在一般的應用邏輯中避免使用反射。最好將其保留用於需要動態載入或呼叫的特定用例。
7. 命令模式
在 Java 中模擬函數指標的另一種方法是使用命令模式,它將行為封裝到獨立物件中。
當我們想要參數化操作、延遲執行或動態排隊時,此模式尤其有用。它還能促進操作呼叫者和邏輯本身之間的鬆散耦合。
我們繼續使用MathOperation
範例。在這種情況下,每個數學運算都可以視為一個命令。我們從相同的函數式介面開始:
public interface MathOperation {
int operate(int a, int b);
}
現在,我們不再需要傳遞 lambda 表達式或匿名類,而是定義單獨的命令類來實作此介面。例如,我們可以像下面這樣建立一個AddCommand
類別:
public class AddCommand implements MathOperation {
@Override
public int operate(int a, int b) {
return a + b;
}
}
類似地,我們可以建立其他命令,例如SubtractCommand
、 MultiplyCommand
或DivideCommand
,每個命令都封裝一個特定的操作。
我們可以重複使用現有的Calculator
類別來執行這些命令。讓我們用加法運算來測試指令模式:
@Test
void givenAddCommand_whenCalculate_thenReturnSum() {
Calculator calculator = new Calculator();
MathOperation add = new AddCommand();
int result = calculator.calculate(3, 7, add);
assertEquals(10, result);
}
這裡,我們建立了一個特定的AddCommand
物件並將其傳遞給計算器。這將加法邏輯封裝在一個可重複使用的獨立物件中,就像一個指令一樣。
當我們需要將不同的行為作為物件傳遞時,這種方法非常有效,尤其是在支援撤銷操作、歷史記錄追蹤或延遲執行的架構中。它還使每個操作易於獨立測試和擴展。
8.基於枚舉
此外,Java 枚舉不僅限於表示常數,它們還可以封裝邏輯。透過允許枚舉定義方法,我們可以將相關的行為分組,並像函數指標一樣傳遞它們。
當我們有一組已知的固定操作時,這種方法尤其有效。讓我們回到數學運算的範例,並使用枚舉來實現邏輯。
我們定義一個枚舉MathOperationEnum
,其中每個常數重寫一個抽象方法來提供自己的行為:
public enum MathOperationEnum {
ADD {
@Override
public int apply(int a, int b) {
return a + b;
}
},
SUBTRACT {
@Override
public int apply(int a, int b) {
return a - b;
}
},
MULTIPLY {
@Override
public int apply(int a, int b) {
return a * b;
}
},
DIVIDE {
@Override
public int apply(int a, int b) {
if (b == 0) throw new ArithmeticException("Division by zero");
return a / b;
}
};
public abstract int apply(int a, int b);
}
有了這種結構,每個枚舉常數其實都是一個函數。我們可以輕鬆地在類似計算器的類別中使用它:
public class EnumCalculator {
public int calculate(int a, int b, MathOperationEnum operation) {
return operation.apply(a, b);
}
}
讓我們建立一個使用這種基於枚舉的方法的簡單測試:
@Test
void givenEnumSubtract_whenCalculate_thenReturnResult() {
EnumCalculator calculator = new EnumCalculator();
int result = calculator.calculate(9, 4, MathOperationEnum.SUBTRACT);
assertEquals(5, result);
}
在此範例中,行為透過枚舉常數傳遞,枚舉常數定義了操作的實作。此模式提供了類型安全性,將所有可能的操作集中在一處,並避免了需要多個類別檔案或自訂介面。
當我們有一組預先定義的、有限的、需要以邏輯分組的行為時,以這種方式使用枚舉是理想的。
9.總結
以下是最常用方法的比較:
方法 | 優點 | 缺點 | 何時使用 |
---|---|---|---|
介面 + 匿名類 | 適用於所有 Java 版本 | 詳細語法 | 使用舊程式碼庫或 Java 8 之前的環境時 |
Lambda 表達式 | 簡短、現代、易讀 | 僅限 Java 8+ | 編寫現代、簡潔、易讀的函數式程式碼時 |
內建函數式介面 | 無需編寫自訂接口 | 僅限於預先定義的輸入/輸出類型 | 當BiFunction 或Predicate 等常見函數結構適合使用用例時 |
方法參考 | 使用現有方法的清晰語法 | 自訂邏輯靈活性較差 | 重複使用與函數簽章相符的現有靜態或實例方法時 |
反射 | 活力四射、動力十足 | 緩慢、不安全、複雜 | 當必須在運行時動態發現和調用方法時 |
命令模式 | 將行為封裝到可重複使用物件中 | 需要更多樣板和類別定義 | 當您需要將操作排隊、記錄或參數化為物件時 |
基於枚舉的功能行為 | 類型安全且固定行為的集中定義 | 僅限於有限的、預先定義的操作 | 當操作集已知且按邏輯分組時 |
10. 結論
在本文中,我們探討了 Java 如何使用各種技術模擬函數指標的概念。對於現代 Java 開發而言,lambda 表達式和內建函數介面因其簡單性和可讀性而成為最常用的方法。
在 Java 8 功能不可用的較舊或遺留程式碼庫中,使用具有匿名類別的介面仍然是一種可靠的選擇。
與往常一樣,原始碼可在 GitHub 上取得。