Java中建立泛型型別的例項

banq發表於2024-06-12

泛型提供了一種優雅的方法,可以在我們的程式碼庫中引入額外的抽象層,同時提高程式碼可重用性和增強程式碼質量。

使用泛型資料型別時,有時我們想建立它們的新例項。然而,由於 Java 中泛型的設計方式,我們可能會遇到不同的挑戰。

在本教程中,我們將首先分析為什麼例項化泛型型別不像例項化類那麼簡單。然後,我們將探索建立泛型型別的幾種方法。

理解型別擦除
在開始之前,我們應該意識到泛型型別在編譯時和執行時的行為不同。

由於一種稱為型別擦除的技術,泛型型別在執行時不會被保留。簡而言之,型別擦除是在編譯時需要泛型型別並在執行時丟棄此資訊的過程。編譯器會從泛型類和方法中刪除與型別引數和型別實參相關的所有資訊。

此外,型別擦除使使用泛型的 Java 應用程式能夠與引入泛型之前建立的庫保持向後相容性。

考慮到上述情況,我們不能使用new關鍵字後跟建構函式來建立泛型型別的物件:

public class GenericClass<T> {
    private T t;
    public GenericClass() {
        this.t = new T(); <font>// DOES NOT COMPILE<i>
    }
}

由於泛型是編譯時概念並且資訊在執行時被刪除,因此為new T( ) 生成位元組碼是不可能的,因為T是未知型別。

此外,泛型型別將替換為第一個邊界,如果未設定邊界,則替換為Object類。我們示例中的T型別沒有邊界,因此 Java 將其視為Object型別。

示例設定
讓我們設定本教程中將使用的示例。我們將建立一個簡單的服務來傳送訊息。

首先,讓我們用send()方法定義Sender介面:

public interface Sender {
    String send();
}

接下來,讓我們建立一個用於傳送電子郵件的具體Sender實現:

public class EmailSender implements Sender {
    private String message;
    @Override
    public String send() {
        return <font>"EMAIL";
    }
}

然後,我們將建立另一個用於傳送通知的實現,但它本身是一個通用類:

public class NotificationSender<T> implements Sender {
    private T body;
    @Override
    public String send() {
        return <font>"NOTIFICATION";
    }
}

現在一切就緒,讓我們探索建立泛型型別例項的不同方法。

使用反射
例項化泛型型別的最常見方法之一是透過普通 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(<font>"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(<font>"EMAIL", result);
}

但是,使用 Java 反射有其侷限性。例如,如果我們嘗試例項化NotificationSender類,我們的解決方案將不起作用:

SenderServiceReflection<NotificationSender<String>> service = new SenderServiceReflection<>(NotificationSender<String>.class);

如果我們嘗試將NotificationSender<String>.class傳遞給建構函式,我們將收到編譯錯誤:

Cannot select from parameterized type

使用供應商介面
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(<font>"EMAIL", result);
}

此外,如果實際型別T的建構函式需要引數,我們可以使用lambda 表示式來代替:

@Test
void givenEmailSenderWithCustomConstructor_whenCreateInstanceUsingSupplier_thenReturnResult() {
    SenderServiceSupplier<EmailSender> service = new SenderServiceSupplier<>(() -> new EmailSender(<font>"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(<font>"NOTIFICATION", result);
}

使用工廠設計模式
類似地,我們可以利用工廠設計模式來完成相同的行為,而不是使用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(<font>"EMAIL", result);
}

此外,例項化NotificationSender沒有任何問題:

@Test
void givenNotificationSender_whenCreateInstanceUsingFactory_thenReturnResult() {
    SenderServiceFactory<NotificationSender<String>> service = new SenderServiceFactory<>(
      () -> new NotificationSender<>(<font>"Hello from Baeldung"));
    NotificationSender<String> notificationSender = service.createInstance();
    String result = notificationSender.send();
    assertEquals(
"NOTIFICATION", result);
    assertEquals(
"Hello from Baeldung", notificationSender.getBody());
}

使用Guava
最後,讓我們看看如何使用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(<font>"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(<font>"EMAIL", result);
}

如果泛型類本身具有型別引數,則上述解決方案可以很好地工作:

@Test
void givenNotificationSender_whenCreateInstanceUsingGuavaAndAnonymous_thenReturnResult() {
    SenderServiceGuava<NotificationSender<String>> service = new SenderServiceGuava<NotificationSender<String>>() {
    };
    Sender notificationSender = service.createInstanceAnonymous();
    String result = notificationSender.send();
    assertEquals(<font>"NOTIFICATION", result);
}

相關文章