在 Java 中建立泛型類型的實例
1. 概述
泛型提供了一種優雅的方式,可以在我們的程式碼庫中引入額外的抽象層,同時提高程式碼的可重複使用性並提高程式碼品質。
當使用通用資料類型時,有時我們想要建立它們的新實例。然而,由於 Java 中泛型的設計方式不同,我們可能會遇到不同的挑戰。
在本教程中,我們將首先分析為什麼實例化泛型類型不像實例化類別那麼簡單。然後,我們將探索創建它的實例的幾種方法。
2. 理解類型擦除
在開始之前,我們應該意識到泛型類型在編譯時和執行時的行為有所不同。
由於稱為類型擦除的技術,泛型類型在運行時不會保留。簡單來說,類型擦除就是在編譯時要求泛型類型並在執行時丟棄這些資訊的過程。編譯器從泛型類別和方法中刪除與類型參數和類型參數相關的所有資訊。
此外,類型擦除使使用泛型的 Java 應用程式能夠保持與引入泛型之前建立的庫的向後相容性。
考慮到上述情況,我們不能使用new關鍵字後接建構函式來建立泛型類型的物件:
public class GenericClass<T> {
private T t;
public GenericClass() {
this.t = new T(); // DOES NOT COMPILE
}
}
由於泛型是編譯時概念,且資訊在執行時被刪除,因此不可能為new T( ) 產生字節碼,因為T是未知類型。
此外,泛型類型將替換為第一個邊界,或者如果未設定邊界,則替換為Object類別。我們範例中的T型別沒有界限,因此 Java 將其視為Object類型。
3. 設定範例
讓我們設定將在本教程中使用的範例。我們將創建一個簡單的服務來發送訊息。
首先,讓我們使用send()方法定義Sender介面:
public interface Sender {
String send();
}
接下來,讓我們建立一個具體的Sender實作來發送電子郵件:
public class EmailSender implements Sender {
private String message;
@Override
public String send() {
return "EMAIL";
}
}
然後,我們將建立另一個用於發送通知的實現,但它將是一個通用類別本身:
public class NotificationSender<T> implements Sender {
private T body;
@Override
public String send() {
return "NOTIFICATION";
}
}
現在我們已經準備就緒,讓我們探索建立泛型類型實例的不同方法。
4. 使用反射
實例化泛型類型最常見的方法之一是透過純 Java 和反射。
要建立泛型類型的實例,我們至少需要知道我們想要建立的物件的類型。
讓我們定義一個SenderServiceReflection泛型類,它將負責建立不同服務的實例:
public class SenderServiceReflection<T extends Sender> {
private Class<T> clazz;
public SenderServiceReflection(Class<T> clazz) {
this.clazz = clazz;
}
}
我們定義了Class<T>的實例變量,我們將在其中儲存有關我們要實例化的類別的類型的資訊。
接下來,讓我們建立一個負責建立類別實例的方法:
public T createInstance() {
try {
return clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("Error while creating an instance.");
}
}
這裡,我們呼叫了getDeclaredConstructor().newInstance()來實例化一個新物件。此外,實際類型應該有一個無參構造函數才能使上述程式碼正常運作。
此外,值得注意的是,自 Java 9 以來,直接呼叫Class<T>上的newInstance()方法已被棄用。
接下來,讓我們測試一下我們的方法:
@Test
void givenEmailSender_whenCreateInstanceUsingReflection_thenReturnResult() {
SenderServiceReflection<EmailSender> service = new SenderServiceReflection<>(EmailSender.class);
Sender emailSender = service.createInstance();
String result = emailSender.send();
assertEquals("EMAIL", result);
}
然而,使用 Java 反射有其限制。例如,如果我們嘗試實例化NotificationSender類,我們的解決方案將無法運作:
SenderServiceReflection<NotificationSender<String>> service = new SenderServiceReflection<>(NotificationSender<String>.class);
如果我們嘗試將NotificationSender<String>.class傳遞給建構函數,我們將得到一個編譯錯誤:
Cannot select from parameterized type
5. 使用Supplier介面
Java 8 提供了一種利用Supplier函數介面建立泛型類型實例的便捷方法:
public class SenderServiceSupplier<T extends Sender> {
private Supplier<T> supplier;
public SenderServiceSupplier(Supplier<T> supplier) {
this.supplier = supplier;
}
public T createInstance() {
return supplier.get();
}
}
在這裡,我們定義了Supplier<T>實例變量,它是透過建構函數的參數設定的。為了檢索通用實例,我們呼叫了它的單一get()方法。
讓我們使用方法引用來建立一個新實例:
@Test
void givenEmailSender_whenCreateInstanceUsingSupplier_thenReturnResult() {
SenderServiceSupplier<EmailSender> service = new SenderServiceSupplier<>(EmailSender::new);
Sender emailSender = service.createInstance();
String result = emailSender.send();
assertEquals("EMAIL", result);
}
此外,如果實際型別T的建構函式帶有參數,我們可以使用 lambda 表達式來取代:
@Test
void givenEmailSenderWithCustomConstructor_whenCreateInstanceUsingSupplier_thenReturnResult() {
SenderServiceSupplier<EmailSender> service = new SenderServiceSupplier<>(() -> new EmailSender("Baeldung"));
Sender emailSender = service.createInstance();
String result = emailSender.send();
assertEquals("EMAIL", result);
}
更重要的是,當我們使用嵌套泛型類別時,這種方法可以正常運作:
@Test
void givenNotificationSender_whenCreateInstanceUsingSupplier_thenReturnCorrectResult() {
SenderServiceSupplier<NotificationSender<String>> service = new SenderServiceSupplier<>(
NotificationSender::new);
Sender notificationSender = service.createInstance();
String result = notificationSender.send();
assertEquals("NOTIFICATION", result);
}
6.使用工廠設計模式
同樣,我們可以利用工廠設計模式來完成相同的行為,而不是使用Supplier介面。
首先,讓我們定義將取代Supplier介面的Factory介面:
public interface Factory<T> {
T create();
}
其次,讓我們建立一個將Factory<T>作為建構函數參數的泛型類別:
public class SenderServiceFactory<T extends Sender> {
private final Factory<T> factory;
public SenderServiceFactory(Factory<T> factory) {
this.factory = factory;
}
public T createInstance() {
return factory.create();
}
}
接下來,讓我們建立一個測試來檢查程式碼是否按預期工作:
@Test
void givenEmailSender_whenCreateInstanceUsingFactory_thenReturnResult() {
SenderServiceFactory<EmailSender> service = new SenderServiceFactory<>(EmailSender::new);
Sender emailSender = service.createInstance();
String result = emailSender.send();
assertEquals("EMAIL", result);
}
此外,實例化NotificationSender可以正常運作,沒有任何問題:
@Test
void givenNotificationSender_whenCreateInstanceUsingFactory_thenReturnResult() {
SenderServiceFactory<NotificationSender<String>> service = new SenderServiceFactory<>(
() -> new NotificationSender<>("Hello from Baeldung"));
NotificationSender<String> notificationSender = service.createInstance();
String result = notificationSender.send();
assertEquals("NOTIFICATION", result);
assertEquals("Hello from Baeldung", notificationSender.getBody());
}
7. 使用番石榴
最後,讓我們看看如何使用 Guava 函式庫來做到這一點。
Guava 提供了TypeToken類,它使用反射來儲存運行時可用的通用資訊。它還提供了用於操作泛型類型的附加實用方法。
讓我們建立SenderServiceGuava類,並將TypeToken<T>作為實例變數:
public class SenderServiceGuava<T extends Sender> {
TypeToken<T> typeToken;
public SenderServiceGuava(Class<T> clazz) {
this.typeToken = TypeToken.of(clazz);
}
public T createInstance() {
try {
return (T) typeToken.getRawType().getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
為了建立一個實例,我們呼叫了getRawType() ,它傳回一個運行時類別類型。
讓我們測試一下我們的例子:
@Test
void givenEmailSender_whenCreateInstanceUsingGuava_thenReturnResult() {
SenderServiceGuava<EmailSender> service = new SenderServiceGuava<>(EmailSender.class);
Sender emailSender = service.createInstance();
String result = emailSender.send();
assertEquals("EMAIL", result);
}
或者,我們可以將TypeToken定義為匿名類別來儲存泛型類型的資訊:
TypeToken<T> typeTokenAnonymous = new TypeToken<T>(getClass()) {
};
public T createInstanceAnonymous() {
try {
return (T) typeTokenAnonymous.getRawType().getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
使用這種方法,我們也可以將SenderServiceGuava建立為匿名類別:
@Test
void givenEmailSender_whenCreateInstanceUsingGuavaAndAnonymous_thenReturnResult() {
SenderServiceGuava<EmailSender> service = new SenderServiceGuava<EmailSender>() {
};
Sender emailSender = service.createInstanceAnonymous();
String result = emailSender.send();
assertEquals("EMAIL", result);
}
如果泛型類別本身俱有類型參數,則上述解決方案效果很好:
@Test
void givenNotificationSender_whenCreateInstanceUsingGuavaAndAnonymous_thenReturnResult() {
SenderServiceGuava<NotificationSender<String>> service = new SenderServiceGuava<NotificationSender<String>>() {
};
Sender notificationSender = service.createInstanceAnonymous();
String result = notificationSender.send();
assertEquals("NOTIFICATION", result);
}
八、結論
在本文中,我們學習如何在 Java 中建立泛型類型的實例。
總而言之,我們研究了為什麼不能使用new關鍵字來建立泛型類型的實例。此外,我們也探討如何使用反射、 Supplier介面、工廠設計模式以及 Guava 函式庫來建立泛型類型的實例。
與往常一樣,完整的源代碼可以在 GitHub 上取得。