Java 中具有可變參數清單的抽象方法
1.概述
在 Java 中使用繼承時,我們通常會從抽象類別開始,該抽象類別定義了所有子類別都需要實作的通用方法。例如,假設有一個抽象類別Item
,它定義了use()
方法。 Item 的每個子Item
(例如Book
或Key
)可能都需要各自的use()
方法實作。然而,當每個子類別都需要為use()
方法提供不同的參數時,可能會出現問題。
在本教程中,我們將探討如何在 Java 中實作接受可變數量參數的抽象方法。
2.問題陳述
假設我們有抽象類別Item
:
public abstract class Item {
public abstract void use();
}
在我們的場景中,子類別Book
不需要參數,而子類別Key
需要一個String
(閘識別碼)和一個Queue
(閘佇列) 。然而,**由於Java
是強型別的,我們無法輕易地宣告一個抽象方法,然後用任意參數覆寫它**。
因此,讓我們實施一個解決方法來解決這個問題:
在上面的流程圖中,我們直觀地了解了各個類別之間的連結方式。在最頂部,類別Item<C>
定義了公共基底類別。從這裡開始,每個子類別選擇要使用的上下文類型:
- 子類別
Book
與上下文類型EmptyContext
一起使用,它表示不需要額外資料的情況 - 子類別
Key
與上下文類型KeyContext
一起使用,後者包含閘識別碼和閘佇列
因此,每個子類別都有一個專用的上下文類型來模擬其確切的需求。
3. 解決方案-類型上下文物件方法
這裡,我們實作了類型化上下文物件的方法。抽象基底類別包含一個方法use
( C
context ),其中C
是一個泛型上下文類型。每個子類別都會選擇一個能夠精確建模所需資料的 C,從而保持 API 的精簡,並實現編譯時類型安全。
3.1. Item
– 泛型抽象基底類
Item.java
檔案定義了一次抽象方法簽名,使用通用類型參數C
作為上下文:
public abstract class Item<C> {
public abstract void use(C context);
}
我們現在不再使用許多use(…)
重載,而是使用一個use(C)
方法,每個子類別透過該方法決定自己的C
類型。
3.2. EmptyContext
– 無參數的上下文
EmptyContext.java
檔案為不需要參數的專案提供上下文,例如Book
:
package com.example.items;
public final class EmptyContext {
public static final EmptyContext INSTANCE = new EmptyContext();
private EmptyContext() {}
}
Book
仍然可以實作Item<C>
,但使用C = EmptyContext
,避免 null 並保持 API 的一致性。
3.3. KeyContext
– Key
參數的上下文
KeyContext
包含Key
所需的資料( doorId
和閘Strings
Queue
) :
public final class KeyContext {
private final String doorId;
private final Queue<String> doorsQueue;
public KeyContext(String doorId, Queue<String> doorsQueue) {
this.doorId = doorId;
this.doorsQueue = doorsQueue;
}
public String getDoorId() {
return doorId;
}
public Queue<String> getDoorsQueue() {
return doorsQueue;
}
}
將參數分組到一個命名類型(在本例中為KeyContext
)中,可以指定預期的數據,並避免參數順序錯誤。此外,編譯器也會幫我們檢查類型。
3.4. Book
-不需要數據的物品
Book
檔案表示使用EmptyContext
的項目,因為它不需要任何參數:
public class Book extends Item<EmptyContext> {
private final String title;
public Book(String title) {
this.title = title;
}
@Override
public void use(EmptyContext ctx) {
System.out.println("You read the book: " + title);
}
}
儘管像Book
這樣的子類別不需要任何參數,但我們仍然需要它來適應與其他專案相同的方法模式。這就是為什麼我們創建了EmptyContext
,一個不包含任何資料的佔位符物件。需要澄清的是:
-
Book
可以實作use(EmptyContext context)
而不需要額外的參數 - 其他子類(如
Key
)可以使用它們自己的帶有真實資料的上下文類
因此,所有項目現在都可以共享相同的統一方法簽名。
3.5. Key
– 使用KeyContext
的項目
Key
示範了一個類別如何接收具有多個欄位的類型上下文:
package com.example.items;
public class Key extends Item<KeyContext> {
private final String name;
public Key(String name) {
this.name = name;
}
@Override
public void use(KeyContext ctx) {
System.out.println("Using key '" + name + "' on door: " + ctx.getDoorId());
System.out.println("Doors remaining in queue: " + ctx.getDoorsQueue().size());
}
}
因此, Key
類別只能與KeyContext
一起使用,Java 編譯器會確保這一點。因此,我們無需擔心不安全的類型轉換。
4. 測試
此時,讓我們建立測試類別BookUnitTest.java
和KeyUnitTest.java
來驗證Book
和Key
是否正常運作。
4.1. BookUnitTest.java
在此測試中,讓我們驗證在Book
物件上呼叫use()
是否適用於EmptyContext
:
package com.example.items;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
class BookUnitTest {
@Test
void givenBook_whenUseWithEmptyContext_thenNoException() {
Book book = new Book("The Hobbit");
assertDoesNotThrow(() -> book.use(EmptyContext.INSTANCE));
}
}
測試內容如下:
- 創作了一本名為
“The Hobbit”
的Book
- 呼叫其
use()
方法,傳入EmptyContext
- 如果沒有拋出異常,則測試通過
測試確認不需要額外資料的項目(如Book
)可以安全地使用EmptyContext
而不會出現執行時間錯誤。
這裡,使用assertDoesNotThrow
來驗證該方法是否安全執行且無錯誤。我們的目標是確認型別安全,而不是測試列印輸出。雖然我們可以捕獲System.out
來驗證列印輸出,但保持測試簡單可以更清楚地表明我們只驗證 API 的正確使用。
同時,使用EmptyContext.INSTANCE
也避免了傳遞null
需要,否則可能會導致執行時間問題。因此,即使無參數的情況也能與所有子類別共享的整體方法簽名保持一致。
4.2. KeyUnitTest.java
接下來,讓我們驗證一下,當提供KeyContext
時, Key
是否可以安全地打開門:
package com.example.items;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import java.util.Queue;
import java.util.LinkedList;
public class KeyUnitTest {
@Test
void givenKey_whenUseWithKeyContext_thenNoException() {
Queue<String> doors = new LinkedList<>();
doors.add("front-door");
doors.add("back-door");
Key key = new Key("MasterKey");
KeyContext ctx = new KeyContext("front-door", doors);
assertDoesNotThrow(() -> key.use(ctx));
}
}
測試內容如下:
- 建立門隊列(前門、後門)
- 建立一個名為
“MasterKey”
的金Key
- 使用目前門(前門)和佇列建構一個
KeyContext
- 呼叫
key.use(ctx)
並斷言它不會引發異常
此測試確認Key
需要接收正確的上下文類型 ( KeyContext
),以確保類型安全,並且該方法在管理閘時能夠正確運作。與BookUnitTest
不同,此處的測試展示了一種有狀態的交互,即每次門解鎖時,都會將其從隊列中移除。
此外,它還展示了編譯器如何強制正確使用上下文,因為如果我們嘗試將EmptyContext
傳遞給Key
,程式碼甚至無法編譯。因此,我們可以在運行時之前就發現意外的誤用。
4.3. 測試結果
現在讓我們執行測試:
$ mvn clean test
...
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
...
上面的輸出顯示BookUnitTest.java
使用EmptyContext
驗證無參數的情況,而KeyUnitTest.java
使用KeyContext
驗證有參數的情況。
這些測試共同展示了兩種極端情況:一個物品不需要數據,而另一個物品需要多個資訊。因此,無論子類別需要多少參數,類型化上下文物件方法都是健壯的。例如,新增物品(具有 PotionContext 的 Potion 類別)將遵循相同的模式,只需要新的測試,而無需更改抽象基底類別。
以下是我們的Book
和Key
項目如何與各自的上下文協同工作的摘要:
- 單一抽象方法
use(C)
為所有子類別提供了一致的 API - 每個子類別選擇一個上下文類型來精確地模擬它所需的資料(零個或多個參數)
- 編譯器強制類型,因此,我們避免了運行時強制類型轉換和脆弱的
Object…
處理 -
EmptyContext
使無參數的情況保持明確和乾淨
因此,該方法靈活、類型安全且易於擴展。
5. 結論
在本文中,我們研究如何在 Java 中實作一個使用可變參數集的抽象方法。
因此,我們使用了類型化上下文物件的方法,每個子類別透過其自身的上下文類別精確定義所需的資料。在包含無參數的場景中, EmptyContext
可確保簽章方法保持一致,無需特殊處理。此外,我們不需要不安全的類型轉換,因為編譯器會確保我們始終使用正確的類型。
我們隨時可以在 GitHub 上檢查原始碼。