前言
CRUD 写多了才发现:泛型用对了是神器,用错了是噩梦。在
写业务代码时,泛型是我们每天都在用的东西:
swift
Result<List<UserDTO>> getUsers();
BaseService<User, UserDTO> service;
Response<R<List<Order>>> orders();
看起来很标准,但实际项目中,泛型相关的坑一踩一个准:
- 明明定义了泛型上限,序列化时却变成了
LinkedHashMap - 泛型擦除导致
instanceof判断失效 - 泛型方法里
new T()报编译错误 - 工具类封装时泛型参数对不上
- ......
今天盘一盘泛型封装中 8 个高频踩坑点,看完直接落地。
1. 坑一:泛型擦除------instanceof 判断永远为 false
常见写法
kotlin
public class Response<T> {
private T data;
public boolean isList() {
// 以为是 List 就返回 true?
return data instanceof List; // 永远 false!
}
}
问题在哪
运行期泛型会被擦除为 Object,List<T> 会被擦除成 List,根本不存在 List<String> 这种具体类型。
所以 data instanceof List<String> 语法上就是错的,编译器直接报错。
正确做法
方案一:通过传入 Class 参数判断
csharp
public class Response<T> {
private T data;
private Class<T> clazz;
public Response(Class<T> clazz) {
this.clazz = clazz;
}
public boolean isList() {
return List.class.isAssignableFrom(clazz);
}
}
// 使用
Response<List<UserDTO>> response = new Response<>(new TypeToken<List<UserDTO>>(){}.getType());
方案二:用 TypeReference 保留泛型信息(JSON 序列化场景)
swift
public class Result<T> {
private T data;
// 配合 Jackson 使用
public static <T> Result<T> fromJson(String json, TypeReference<Result<T>> typeRef) {
try {
return new ObjectMapper().readValue(json, typeRef);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
// 调用
Result<List<UserDTO>> result = Result.fromJson(json,
new TypeReference<Result<List<UserDTO>>>() {});
2. 坑二:工具类封装时泛型参数"对不上"
常见写法
swift
public class ResultUtil {
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setData(data);
result.setCode(200);
return result;
}
}
// 调用
List<UserDTO> users = userService.list();
Result<List<UserDTO>> result = ResultUtil.success(users); // 完美
这看起来没问题。但如果这样呢?
typescript
public static <T> T getData(Result<T> result) {
if (result.getCode() == 200) {
return result.getData(); // OK
}
return null; // 这里有问题吗?
}
问题在哪
当 T 是基本类型包装类(如 Integer、Long)时,返回 null 可能导致 NPE。
正确做法
typescript
public static <T> T getDataOrThrow(Result<T> result) {
if (result.getCode() != 200) {
throw new BusinessException(result.getMessage());
}
return result.getData(); // 非空,编译器保证
}
// 或者返回空对象而非 null
public static <T> T getData(Result<T> result, T defaultValue) {
if (result.getCode() != 200) {
return defaultValue;
}
return result.getData();
}
3. 坑三:new T() 永远编译不过
常见写法
csharp
public class BaseService<T> {
public T createEntity() {
// 想动态创建实例
return new T(); // 编译错误!
}
}
问题在哪
泛型擦除后,运行时根本不知道 T 是什么类型,无法调用构造函数。这是 Java 类型系统的限制。
正确做法
方案一:通过 Class 对象创建
csharp
public class BaseService<T> {
private final Class<T> entityClass;
public BaseService(Class<T> entityClass) {
this.entityClass = entityClass;
}
public T createEntity() {
try {
return entityClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("创建实例失败", e);
}
}
}
// 使用
UserService userService = new UserService(User.class);
User user = userService.createEntity();
方案二:用反射工具类封装(推荐)
typescript
public class BeanUtil {
public static <T> T newInstance(Class<T> clazz) {
try {
return clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new IllegalStateException("无法创建实例: " + clazz.getName(), e);
}
}
public static <T> T copyProperties(Object source, Class<T> targetClass) {
T target = newInstance(targetClass);
BeanUtils.copyProperties(source, target);
return target;
}
}
4. 坑四:泛型上限没设对,导致类型转换 ClassCastException
常见写法
csharp
// 随便定义一个泛型
public class DataHolder<T> {
private T data;
public void process() {
// 假设需要调用 Comparable 方法
Comparable<T> comparable = data; // 可能出问题
comparable.compareTo(data); // 如果 T 是 String,OK;如果是 User?User 没实现 Comparable
}
}
问题在哪
没有约束 T 的上限,任何类型都能传,但代码里可能需要特定能力(如 Comparable、Serializable)。
正确做法
明确泛型上限
scala
// 限定 T 必须实现 Comparable
public class DataHolder<T extends Comparable<T>> {
private T data;
public T max(T other) {
return data.compareTo(other) > 0 ? data : other; // 编译器保证安全
}
}
// 限定 T 必须实现序列化
public class CacheWrapper<T extends Serializable> {
private T data;
}
// 限定多重上限
public class Processor<T extends Number & Comparable<T>> {
public double doubleValue(T value) {
return value.doubleValue();
}
}
常见场景:Service 层基类
scala
// 基础 Service,限定 entity 必须继承 BaseEntity
public abstract class BaseService<T extends BaseEntity, DTO> {
protected abstract Mapper<T> getMapper();
public DTO getById(Long id) {
T entity = getMapper().selectById(id);
return convertToDTO(entity); // entity 一定有 id、createTime 等
}
protected abstract DTO convertToDTO(T entity);
}
// 子类实现
public class UserServiceImpl extends BaseService<User, UserDTO> {
@Override
protected Mapper<User> getMapper() {
return userMapper;
}
@Override
protected UserDTO convertToDTO(User user) {
// user 一定有 getId(),因为继承了 BaseEntity
return UserDTO.builder()
.id(user.getId())
.name(user.getName())
.build();
}
}
5. 坑五:泛型方法定义错误,调用时类型推断失败
常见写法
typescript
public class Converter {
// 以为是泛型方法,实际上不是
public static T convert(Object source) {
return (T) source; // 编译警告,运行时可能 ClassCastException
}
}
// 调用
String str = Converter.convert(someObject); // 谁知道转成啥?
问题在哪
这个 T 不是方法级别泛型,而是类级别泛型。如果类没有声明 <T>,这里的 T 就是普通的类型参数(虽然也能编译,但语义完全错误)。
正确做法
正确的泛型方法
typescript
public class Converter {
// 正确的泛型方法:<T> 是方法声明的一部分
public static <T> T convert(Object source, Class<T> targetClass) {
if (source == null) {
return null;
}
return targetClass.cast(source);
}
// 更安全的版本
public static <T, S> T convert(S source, Function<S, T> converter) {
if (source == null) {
return null;
}
return converter.apply(source);
}
}
// 使用
String str = Converter.convert(someObject, String.class);
UserDTO dto = Converter.convert(user, UserDTO::toDTO);
复杂场景:返回多种类型的泛型方法
typescript
// 业务场景:统一处理成功/失败返回
public class ApiResult {
public static <T> T getOrThrow(ApiResponse<T> response) {
if (!response.isSuccess()) {
throw new ApiException(response.getCode(), response.getMessage());
}
return response.getData();
}
// 配合 Optional 使用
public static <T> Optional<T> toOptional(ApiResponse<T> response) {
if (response.isSuccess() && response.getData() != null) {
return Optional.of(response.getData());
}
return Optional.empty();
}
}
6. 坑六:泛型通配符 ? extends 和 ? super 傻傻分不清
常见写法
typescript
// 读取数据时用 extends
public void read(List<? extends Object> list) {
Object item = list.get(0); // 读 OK
list.add(new Object()); // 写?编译错误
}
// 写入数据时用 super
public void write(List<? super String> list) {
list.add("hello"); // 写 OK
String item = list.get(0); // 读?需要强制转型
}
问题在哪
搞不清 PECS 原则(Producer Extends, Consumer Super):
- 读取数据 (生产者)→ 用
? extends - 写入数据 (消费者)→ 用
? super
正确做法
记住 PECS 原则
typescript
// 生产者:用 extends,只能读
public double sumOfPrices(List<? extends Product> products) {
double total = 0;
for (Product p : products) { // 读 OK
total += p.getPrice();
}
// products.add(new Product()); // 编译错误,不能写
return total;
}
// 消费者:用 super,只能写
public void addNumbers(List<? super Integer> list) {
list.add(1); // 写 OK
list.add(2);
// Integer num = list.get(0); // 读出来是 Object
}
// 既读又写?别用通配符
public <T> void copy(List<T> dest, List<? extends T> src) {
for (T item : src) { // src 是生产者,可以读
dest.add(item); // dest 是消费者,可以写
}
}
实际业务场景
scss
// DTO 转换:源列表是生产者,目标列表是消费者
public <S, T> void convertList(List<S> sources, List<T> targets,
Function<S, T> converter) {
for (S source : sources) {
targets.add(converter.apply(source));
}
}
// 使用
List<User> users = userMapper.selectList();
List<UserDTO> dtos = new ArrayList<>();
convertList(users, dtos, User::toDTO);
7. 坑七:泛型与序列化冲突,返回给前端变成了 LinkedHashMap
常见写法
typescript
public class Result<T> {
private T data;
// 序列化给前端
public String toJson() {
return new ObjectMapper().writeValueAsString(this);
}
}
// 接口
@GetMapping("/user")
public Result<UserDTO> getUser() {
Result<UserDTO> result = new Result<>();
result.setData(userDTO);
return result;
}
前端收到的 JSON:
json
{
"data": {
"name": "张三",
"id": 1
}
}
这看起来没问题。但如果前端拿到的是 List<UserDTO> 呢?
问题在哪
当 T 是泛型集合时,Jackson 默认反序列化会丢失具体类型信息,反序列化成 LinkedHashMap 而不是具体 DTO。
swift
// 后端
Result<List<UserDTO>> result = new Result<>();
result.setData(Arrays.asList(userDTO1, userDTO2));
// 前端收到
{
"data": [
{"name": "张三", "id": 1}, // 不再是 UserDTO
{"name": "李四", "id": 2}
]
}
正确做法
方案一:用 TypeReference 显式指定泛型
typescript
public class Result<T> {
public String toJson() {
try {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
return mapper.writeValueAsString(this);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 泛型反序列化方法
public static <T> T fromJson(String json, TypeReference<T> typeRef) {
try {
return new ObjectMapper().readValue(json, typeRef);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
// 后端给前端:直接序列化,不需要改动
// 前端拿到字符串后:
Result<List<UserDTO>> result = Result.fromJson(jsonString,
new TypeReference<Result<List<UserDTO>>>() {});
方案二:用 @JsonTypeInfo 标记具体类型
kotlin
@JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS)
public class Result<T> {
private T data;
}
// 序列化成
{
"data": {
"@c": ".UserDTO",
"name": "张三"
}
}
方案三:返回 ResponseEntity(Spring 官方推荐)
swift
@GetMapping("/users")
public ResponseEntity<Result<List<UserDTO>>> getUsers() {
Result<List<UserDTO>> result = Result.success(userService.list());
return ResponseEntity.ok(result);
}
8. 坑八:泛型嵌套太深,代码可读性灾难
常见写法
scss
// 四层泛型嵌套,你能一眼看出 data 是什么吗?
Response<Result<Page<List<UserDTO>>>> result = userService.query(request);
// 访问数据时
List<UserDTO> users = result.getData().getData().getData().getRecords();
问题在哪
泛型是为了类型安全,但如果嵌套太深,反而降低了可读性,而且修改维护时容易出错。
正确做法
方案一:抽取中间类型
csharp
// 第一层:接口返回统一封装
public class ApiResponse<T> {
private int code;
private String message;
private T data;
}
// 第二层:分页数据统一封装
public class PageResult<T> {
private List<T> records;
private long total;
private int pageNum;
private int pageSize;
}
// 简化后的调用
ApiResponse<PageResult<UserDTO>> result = userService.query(request);
PageResult<UserDTO> page = result.getData();
List<UserDTO> users = page.getRecords();
方案二:用 Optional 消除空判断
kotlin
public class Result<T> {
private T data;
public Optional<T> getOptionalData() {
return Optional.ofNullable(data);
}
}
// 使用
user.getOptionalData()
.map(PageResult::getRecords)
.orElse(Collections.emptyList());
方案三:工具方法封装常用路径
kotlin
public class ResultHelper {
public static <T> List<T> getRecordsOrEmpty(Result<PageResult<T>> result) {
if (result == null || result.getData() == null) {
return Collections.emptyList();
}
return result.getData().getRecords();
}
}
// 调用
List<UserDTO> users = ResultHelper.getRecordsOrEmpty(result);
最佳实践总结
| 坑点 | 问题 | 解决方案 |
|---|---|---|
| instanceof 失效 | 泛型擦除 | 用 Class 或 TypeReference 判断 |
| 工具类泛型失效 | 泛型参数对不上 | 显式传入 Class 或用函数式接口 |
| new T() 编译错误 | 类型擦除限制 | 通过 Class.newInstance() 或构造函数引用 |
| ClassCastException | 泛型上限未设 | 用 约束 |
| 类型推断失败 | 泛型方法定义错误 | 放在返回类型前 |
| extends/super 混淆 | PECS 原则不清 | 记住:读用 extends,写用 super |
| 序列化变成 Map | 泛型信息丢失 | 用 TypeReference 或 ResponseEntity |
| 泛型嵌套太深 | 可读性差 | 抽取中间类型 + 工具方法封装 |
泛型封装黄金法则
arduino
// 1. 永远不要 new T()
// 2. 永远不要写 instance of T
// 3. 永远明确泛型上限
// 4. 永远记住 PECS 原则
// 5. 永远用 TypeReference 处理 JSON 序列化
记住:泛型是给编译器用的,不是给运行时用的。想清楚这一点,大部分坑都能避开。
如果你有更多关于泛型或 Spring Boot 的问题,欢迎继续交流!