如何在 Java 中使用 ParameterizedTypeReference
1. 簡介
在 Java 中使用泛型類型時,我們經常會遇到類型擦除的問題。當我們發出傳回泛型集合或複雜參數化類型的 HTTP 請求時,這個問題尤其棘手。 Spring 的ParameterizedTypeReference
為我們提供了一個優雅的解決方案。
在本教程中,我們將探討如何在RestTemplate
ParameterizedTypeReference
**WebClient** .
我們還將介紹在現代 Java 應用程式中處理複雜泛型類型的基本概念和最佳實踐。
2. 理解類型擦除及其引發的問題
Java 的類型擦除會在執行時期刪除泛型類型資訊。例如, List<String>
和List<Integer>
在執行時都會變成List
。這在我們需要保留泛型類型資訊時帶來了挑戰。
讓我們建立一個User
類別來幫助我們進行程式碼示範:
public class User {
private Long id;
private String name;
private String email;
private String department;
//constructors, getters and setters
}
現在,讓我們考慮一個常見的場景,即從 REST API 檢索使用者清單:
RestTemplate restTemplate = new RestTemplate();
List<User> users = restTemplate.getForObject("/users", List.class);
然而,它得到的是一個List<Object>
而不是我們想要的List<User>
。此外,清單中的每個元素都需要手動進行類型轉換。這種方法可能會導致錯誤,並且違背了使用泛型的初衷。
毫無疑問,這就是ParameterizedTypeReference.
它在編譯時捕獲並保存完整的泛型類型信息,以便在運行時可用。
3. RestTemplate
的基本用法
RestTemplate
在 Spring 應用中仍然被廣泛使用。了解如何使用它來處理泛型類型非常重要。接下來,我們將透過幾個實際場景來更好地理解ParameterizedTypeReference
.
3.1. 使用泛型集合
讓我們來看一個使用RestTemplate
來取得使用者清單的範例:
@Service
public class ApiService {
//properties and constructor
public List<User> fetchUserList() {
ParameterizedTypeReference<List<User>> typeRef =
new ParameterizedTypeReference<List<User>>() {};
ResponseEntity<List<User>> response = restTemplate.exchange(
baseUrl + "/api/users",
HttpMethod.GET,
null,
typeRef
);
return response.getBody();
}
}
在上面的例子中,關鍵元素是創建一個ParameterizedTypeReference
,其類型正是我們期望的泛型類型。空括號建立了一個匿名類,我們將它用作exchange()
方法的參數。 ParameterizedTypeReference 允許exchange()
直接傳回List<User>
ParameterizedTypeReference
無需任何強制型別轉換。
我們來驗證一下這個邏輯:
@Test
void whenFetchingUserList_thenReturnsCorrectType() {
// given
wireMockServer.stubFor(get(urlEqualTo("/api/users"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
[
{
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"department": "Engineering"
},
{
"id": 2,
"name": "Jane Smith",
"email": "[email protected]",
"department": "Marketing"
}
]
""")));
// when
List<User> result = apiService.fetchUserList();
// then
assertEquals(2, result.size());
assertEquals("John Doe", result.get(0).getName());
assertEquals("[email protected]", result.get(1).getEmail());
assertEquals("Engineering", result.get(0).getDepartment());
assertEquals("Marketing", result.get(1).getDepartment());
}
測試確認我們的ParameterizedTypeReference
正確保留了泛型類型信息,從而允許我們使用正確類型的List<User>
而不是原始List.
我們在測試中使用WireMock
來模擬即時 API 端點。這使得我們的RestTemplate
能夠進行真實的 HTTP 呼叫並接收有效的 JSON 回應,然後根據ParameterizedTypeReference
對其進行反序列化。
WireMock
提供了一個可靠的選項來測試 HTTP 用戶端行為,而無需依賴外部服務,確保我們的測試既可靠又快速。
3.2. 比較getForEntity()
與exchange()
一般來說,在使用泛型時,理解getForEntity()
和exchange()
之間的差異至關重要。此外,許多開發人員最初嘗試使用更簡單的getForEntity()
方法,結果卻遇到了類型安全性問題。
關鍵問題是getForEntity()
不接受ParameterizedTypeReference
— 它只接受回應物件的類別類型。
讓我們來看一個**getForEntity()
不當使用的**範例:
public List<User> fetchUsersWrongApproach() {
ResponseEntity response = restTemplate.getForEntity(
baseUrl + "/api/users",
List.class
);
return (List) response.getBody();
}
使用上述解決方案時,我們會遇到一個未經檢查的類型轉換問題。即使程式碼編譯通過,也會遺失類型資訊。
現在,讓我們來看看建議的解決方案,使用exchange()
:
public List<User> fetchUsersCorrectApproach() {
ParameterizedTypeReference<List<User>> typeRef =
new ParameterizedTypeReference<List<User>>() {};
ResponseEntity<List<User>> response = restTemplate.exchange(
baseUrl + "/api/users",
HttpMethod.GET,
null,
typeRef
);
return response.getBody();
}
一些關鍵的差異在於, getForEntity()
接受Class<T>
參數,而該參數無法表示泛型類型。另一方面, exchange()
接受ParameterizedTypeReference<T>,
從而保留了完整的類型資訊。
因此,編譯器可以使用exchange(),
防止在執行時出現ClassCastException
。
4. 使用WebClient
WebClient
提供了一種現代化的、響應式的 HTTP 通訊方法。與RestTemplate,
它傳回的是Mono
和Flux.
在使用泛型時,這些類型需要特殊處理。
4.1. 複雜類型的反應式操作
讓我們看看ParameterizedTypeReference
如何在反應式程式設計中處理嵌套泛型類型:
@Service
public class ReactiveApiService {
private final WebClient webClient;
public ReactiveApiService(String baseUrl) {
this.webClient = WebClient.builder().baseUrl(baseUrl).build();
}
public Mono<Map<String, List<User>>> fetchUsersByDepartment() {
ParameterizedTypeReference<Map<String, List<User>>> typeRef =
new ParameterizedTypeReference<Map<String, List<User>>>() {};
return webClient.get()
.uri("/users/by-department")
.retrieve()
.bodyToMono(typeRef);
}
}
此方法傳回一個Mono
,其中包含一個映射,其中每個鍵代表一個部門名稱,每個值代表該部門的使用者清單。這展示了WebClient's
在保持型別安全的同時反序列化複雜泛型結構的能力。
現在,讓我們測試一下我們的實作:
@Test
void whenFetchingUsersByDepartment_thenReturnsCorrectMap() {
// given
wireMockServer.stubFor(get(urlEqualTo("/users/by-department"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
{
"Engineering": [
{
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"department": "Engineering"
}
],
"Marketing": [
{
"id": 2,
"name": "Jane Smith",
"email": "[email protected]",
"department": "Marketing"
}
]
}
""")));
// when
Mono<Map<String, List<User>>> result = reactiveApiService.fetchUsersByDepartment();
// then
StepVerifier.create(result)
.assertNext(map -> {
assertTrue(map.containsKey("Engineering"));
assertTrue(map.containsKey("Marketing"));
assertEquals("John Doe", map.get("Engineering").get(0).getName());
assertEquals("Jane Smith", map.get("Marketing").get(0).getName());
// Verify proper typing - this would fail if ParameterizedTypeReference didn't work
List engineeringUsers = map.get("Engineering");
User firstUser = engineeringUsers.get(0);
assertEquals(Long.valueOf(1L), firstUser.getId());
})
.verifyComplete();
}
請注意,我們能夠正確地將 JSON 回應反序列化為預期的Map<String, List<User>>
。
4.2. 自訂通用包裝器
現實世界中的 API 經常使用泛型包裝類別。以下是處理它們的方法。首先,讓我們建立包裝器物件:
public record ApiResponse<T>(boolean success, String message, T data) {}
現在,讓我們將此包裝器與ParameterizedTypeReference:
public Mono<ApiResponse<List<User>>> fetchUsersWithWrapper() {
ParameterizedTypeReference<ApiResponse<List<User>>> typeRef =
new ParameterizedTypeReference<ApiResponse<List<User>>>() {};
return webClient.get()
.uri("/users/wrapped")
.retrieve()
.bodyToMono(typeRef);
}
最後,讓我們測試我們的實作以確保它正確處理通用包裝器:
@Test
void whenFetchingUsersWithWrapper_thenReturnsApiResponse() {
// given
wireMockServer.stubFor(get(urlEqualTo("/users/wrapped"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
{
"success": true,
"message": "Success",
"data": [
{
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"department": "Engineering"
},
{
"id": 2,
"name": "Jane Smith",
"email": "[email protected]",
"department": "Marketing"
}
]
}
""")));
// when
Mono<ApiResponse<List<User>>> result = reactiveApiService.fetchUsersWithWrapper();
// then
StepVerifier.create(result)
.assertNext(response -> {
assertTrue(response.success());
assertEquals("Success", response.message());
assertEquals(2, response.data().size());
assertEquals("John Doe", response.data().get(0).getName());
assertEquals("Jane Smith", response.data().get(1).getName());
// Verify proper generic typing - this ensures ParameterizedTypeReference worked
List users = response.data();
User firstUser = users.get(0);
assertEquals(Long.valueOf(1L), firstUser.getId());
assertEquals("Engineering", firstUser.getDepartment());
})
.verifyComplete();
}
5.最佳實踐
在生產應用程式中使用ParameterizedTypeReference
時,遵循某些最佳實踐可以提高效能和可維護性。
讓我們來探索一些最佳實踐。我們介紹的策略專注於優化程式碼和優雅地處理錯誤。
5.1. 何時使用ParameterizedTypeReference
了解何時使用ParameterizedTypeReference
對於編寫簡潔的程式碼至關重要。並非每個 HTTP 呼叫都需要它。此外,不必要地使用它會增加複雜性。
我們應該在以下情況下使用ParameterizedTypeReference
:
- 使用泛型集合(
List<T>, Set<T>, Map<K, V>
) - 處理自訂通用包裝類別(
ApiResponse<T>
) - 處理巢狀泛型類型(
Map<String, List<User>>
)
在以下情況下,我們應該避免使用ParameterizedTypeReference
:
- 使用簡單的非泛型類型
- 響應是一個沒有泛型的單一對象
- 使用原始類型或其包裝器
讓我們來看一些應該避免這種情況的例子:
public User fetchUser(Long id) {
return restTemplate.getForObject(baseUrl + "/api/users/" + id, User.class);
}
public User[] fetchUsersArray() {
return restTemplate.getForObject(baseUrl + "/api/users", User[].class);
}
一眼就能看出為什麼fetchUser()
方法中不需要ParameterizedTypeReference
。與上面的解釋類似,該方法傳回一個簡單的物件User
。此外,這也適用於數組類型。因此,我們也不需要在fetchUsersArray()
方法中使用ParameterizedTypeReference
。
另一方面,讓我們來看一個需要它的例子:
public List<User> fetchUsersList() {
ParameterizedTypeReference<List<User>> typeRef =
new ParameterizedTypeReference<List<User>>() {};
ResponseEntity<List<User>> response = restTemplate.exchange(
baseUrl + "/api/users",
HttpMethod.GET,
null,
typeRef
);
return response.getBody();
}
在上面的例子中,我們需要ParameterizedTypeReference
因為我們正在處理一個通用集合,特別是List<User>
。
我們的主要觀點總結得很好:關鍵的區別在於類型擦除是否會影響我們的用例。如果 Java 運行時無需泛型資訊就能確定類型,那麼ParameterizedTypeReference
就沒有必要了。
5.2. 重用型別引用
建立ParameterizedTypeReference
實例會降低效能。因此,對於常用類型,我們應該建立static
實例:
public class TypeReferences {
public static final ParameterizedTypeReference<List<User>> USER_LIST =
new ParameterizedTypeReference<List<User>>() {};
public static final ParameterizedTypeReference<Map<String, List<User>>> USER_MAP =
new ParameterizedTypeReference<Map<String, List<User>>>() {};
}
以下是如何使用static
實例之一的範例:
public List<User> fetchUsersListWithExistingReference() {
ResponseEntity<List<User>> response =
restTemplate.exchange(baseUrl + "/api/users", HttpMethod.GET, null, USER_LIST);
return response.getBody();
}
6. 結論
在本文中,我們探討如何使用ParameterizedTypeReference
處理 Java 應用程式中的複雜泛型類型。此外,我們還了解了它如何解決類型擦除問題,並使我們能夠無縫地處理泛型集合。
在 Spring HTTP 用戶端中使用泛型類型時, ParameterizedTypeReference
至關重要。它適用於RestTemplate
和WebClient,
並且重複使用類型參考可以提高效能。
因此,透過遵循這些模式,我們可以在 Java 應用程式中使用泛型類型時編寫更健壯、更易於維護的程式碼。
與往常一樣,本文中使用的程式碼可以在 GitHub 上找到。