知識庫 / HTTP Client-Side RSS 訂閱

使用參數化類型引用(ParameterizedTypeReference)在 Java 中的方法

HTTP Client-Side,Spring Web
HongKong
11
10:43 AM · Dec 06 ,2025

1. 簡介

在 Java 中使用泛型時,我們經常會遇到類型擦除。這在處理返回泛型集合或複雜參數化類型的 HTTP 請求時尤其具有挑戰性。Spring 的 <em>ParameterizedTypeReference</em> 提供了優雅的解決方案。

在本教程中,我們將探討如何使用 <em>ParameterizedTypeReference</em><em>RestTemplate</em><em>WebClient</em> 一起使用。我們還將涵蓋處理現代 Java 應用程序中複雜泛型類型的底層概念和最佳實踐。

2. 理解類型擦除及其帶來的問題

Java 的類型擦除會在運行時移除泛型類型信息。例如,<em List&lt;String&gt;&nbsp;</em>和<em List<Integer> 都會在運行時變為`<em List 。1這在我們需要保留泛型類型信息時,會帶來挑戰。

讓我們創建一個`<em 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>,而無需進行任何類型轉換。

讓我們驗證這個邏輯:

@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();
}

我們使用上述解決方案時會遇到一個未檢查的類型轉換。即使代碼編譯成功,它也會丟失類型信息。

現在,讓我們來看一下推薦的解決方案,使用 <em >exchange()</em>

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 不同,它返回響應式類型,例如 MonoFlux

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在處理複雜泛型結構的同時保持類型安全的能力。

現在,讓我們測試我們的實現:

@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);
}

直觀地看,我們就能明白為什麼ParameterizedTypeReferencefetchUser()方法中是不需要的。 類似於上述解釋,該方法返回一個簡單的對象,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. 重用類型引用

創建 <em>ParameterizedTypeReference</em> 實例會產生性能開銷。因此,對於經常使用的類型,應創建 <em>靜態</em> 實例:

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>>>() {};
}

以下是一個使用靜態實例的示例:

public List<User> fetchUsersListWithExistingReference() {
    ResponseEntity<List<User>> response =
      restTemplate.exchange(baseUrl + "/api/users", HttpMethod.GET, null, USER_LIST);

    return response.getBody();
}

6. 結論

在本文中,我們探討了如何使用ParameterizedTypeReference來處理 Java 應用程序中的複雜泛型類型。此外,我們還看到了這種方法如何解決類型擦除問題,並使我們能夠無縫地與泛型集合進行交互。

ParameterizedTypeReference在與泛型類型一起工作時至關重要,尤其是在 Spring HTTP 客户端中。它與 RestTemplateWebClient 均兼容,並重用類型引用可以提高性能。

因此,通過遵循這些模式,我們可以編寫更健壯和易於維護的代碼,以處理 Java 應用程序中的泛型類型。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.