很多同学第一次学泛型时觉得它只是"把 Object 换成 T",但到了项目里会发现真正难点不在语法,而在 API 设计:一个工具方法到底写 List<T> 还是 List<? extends T>?为什么一个接口改成通配符后,调用方反而更难写?为什么运行时拿不到泛型参数?
如果你最近在DTO 转换器、分页封装、批量导入导出、通用 CRUD 基座,或者你准备重构公共组件,这篇基本能覆盖你会遇到80% 场景。下面按「基础认知 通配符边API 设计套路 类型擦除与反Spring Boot 实战 常见坑与面试题」的顺序往下聊
1. 泛型到底解决了什么问题?
1.1 没有泛型时,错误会延迟到运行
java
List list = new ArrayList();
list.add("alice");
list.add(18);
String name = (String) list.get(0); // 正常
String age = (String) list.get(1); // ClassCastException
上面这段代码在编译期完全合法,问题在运行时才暴露。线上系统里,这种错误往往是"某条脏数据触发一次"
1.2 有了泛型,类型约束前移到编译
java
List<String> names = new ArrayList<>();
names.add("alice");
// names.add(18); // 编译错误
这就是泛型最核心的收益:*把不确定性前
1.3 泛型的三层价
- 类型安全:减少强转、减少运行时类型异常
- *可读:方法签名直接表达"数据意图"
- *可维护:重构时,IDE 能做更准确的检查和补全
2. 基础语法一次过:类、方法、接
2.1 泛型类:对象本身绑定类型
java
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
使用
java
Box<String> box1 = new Box<>("hello");
String s = box1.getValue();
Box<Integer> box2 = new Box<>(100);
Integer n = box2.getValue();
2.2 泛型方法:方法级别声明类型参
注意泛型方法的声明位置在返回值前面:
java
public static <T> T firstOrNull(List<T> list) {
if (list == null || list.isEmpty()) {
return null;
}
return list.get(0);
}
调用
java
String a = firstOrNull(List.of("a", "b"));
Integer b = firstOrNull(List.of(1, 2, 3));
2.3 泛型接口:能力统一,数据类型可
java
public interface Converter<S, T> {
T convert(S source);
}
实现
java
public class UserEntityToDtoConverter implements Converter<UserEntity, UserDTO> {
@Override
public UserDTO convert(UserEntity source) {
UserDTO dto = new UserDTO();
dto.setId(source.getId());
dto.setName(source.getName());
return dto;
}
}
2.4 多类型参数泛
java
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
3. 类型参数命名习惯(别小看
3.1 常见命名
T:Type,任意类E:Element,集合元K:Key,MapV:Value,MapR:Result,返回结S:Source,源对象
3.2 为什么命名重要?
相同逻辑,命名不同,可读性差别很大:
java
public interface Mapper<A, B> {
B map(A a);
}
vs
java
public interface Mapper<S, T> {
T map(S source);
}
第二种在团队协作里更省沟通成本
4. 通配符是 API 设计的关键:?
4.1 List<T>、List<?>、List<Object> 不是一回事
| 写法 | 含义 | 典型用 |
|---|---|---|
List<T> |
明确具体类型参数 | 读写同一类型 |
List<?> |
未知类型列表 | 只读遍历/透传 |
List<Object> |
只能Object 体系元素 |
很少用于泛型抽象 |
这三个经常被误用,特别是List<Object> 当"万能父类型"
4.2 泛型不变性(invariant
下面代码是错误的
java
List<String> strings = new ArrayList<>();
// List<Object> objects = strings; // 编译错误
为什么?因为如果允许赋值,就可能出现:
java
List<String> strings = new ArrayList<>();
// List<Object> objects = strings;
// objects.add(123); // strings 里就混入 Integer
所Java 选择不变性,避免类型污染
5. ? extends T:生产者(读)
5.1 核心含义
? extends Number 表示"某Number 的子类型,但具体哪个我不知道"
java
public static double sum(List<? extends Number> nums) {
double total = 0D;
for (Number n : nums) {
total += n.doubleValue();
}
return total;
}
5.2 读写规则
对于 List<? extends Number>
- 可以安全读成
Number - 不能安全写入
Integer/Double(除null外)
java
List<? extends Number> nums = List.of(1, 2, 3);
Number n = nums.get(0);
// nums.add(1); // 编译错误
5.3 适用场景
- 统计函数(求和、求均值)
- 校验函数(只读扫描)
- 转换函数的源数据入参
6. ? super T:消费者(写)
6.1 核心含义
? super Integer 表示"某Integer 的父类型"
java
public static void fillDefaults(List<? super Integer> target) {
target.add(10);
target.add(20);
}
6.2 读写规则
对于 List<? super Integer>
- 可以安全写入
Integer - 读取时只能当
Object
java
List<? super Integer> target = new ArrayList<Number>();
target.add(1);
target.add(2);
Object v = target.get(0);
6.3 适用场景
- 批量写入
- 收集器(collector
- 目标容器由调用方提供
7. PECS 原则:一句话定边
text
Producer Extends, Consumer Super
生产者用 extends,消费者用 super
7.1 用表格快速决
| 方法角色 | 推荐写法 | 备注 |
|---|---|---|
| 只读输入 | ? extends T |
扩大入参兼容 |
| 只写输出 | ? super T |
扩大可写目标类型 |
| 又读又写 | T |
最清晰 |
7.2 一个完整例
java
public static <T> void copy(List<? extends T> src, List<? super T> dest) {
for (T item : src) {
dest.add(item);
}
}
调用
java
List<Integer> src = List.of(1, 2, 3);
List<Number> dest = new ArrayList<>();
copy(src, dest);
8. API 设计中最常见8 个签名问
8.1 问题 1:返回值写 ? extends 导致调用方难
不推荐:
java
public List<? extends UserDTO> queryUsers() {
return List.of(new UserDTO());
}
更推荐:
java
public List<UserDTO> queryUsers() {
return List.of(new UserDTO());
}
8.2 问题 2:参数写得太
java
// 太窄:只能接List<UserEntity>
public void validate(List<UserEntity> users) { ... }
更好的写法:
java
public void validate(List<? extends UserEntity> users) { ... }
8.3 问题 3:为"通用"而过度抽
java
interface BaseService<T, ID, DTO, VO, Q, C, U> {
VO create(C cmd);
VO update(ID id, U cmd);
List<VO> query(Q query);
}
这种设计容易让业务语义被淹没。公共基础层适合,业务层往往不适合
8.4 问题 4:把 Object 当通用方案
java
public Object convert(Object input) { ... }
这会丢失几乎所有类型信息。一般应替换为泛型:
java
public <S, T> T convert(S input, Class<T> targetType) { ... }
8.5 问题 5:工具方法忘<T>
错误
java
public static T pick(T a, T b) { // 编译错误
return a != null ? a : b;
}
正确
java
public static <T> T pick(T a, T b) {
return a != null ? a : b;
}
8.6 问题 6:复杂嵌套泛型缺typedef 思路
java
Map<String, List<Map<Long, Set<UserDTO>>>> data;
这种签名建议拆成领域对象,减少认知负担
8.7 问题 7:接口暴露实现细
不建议对外暴露具体实现类
java
public ArrayList<UserDTO> query() { ... }
建议返回接口类型
java
public List<UserDTO> query() { ... }
8.8 问题 8:遗漏空集合策略
泛型 API 最好明确"空值语义":
- 返回空集合还
null - 空集合是否可
- 是否允许元素
null
9. 泛型与继承:你以为会"自动向上转型",其实不会
9.1 类继承和泛型参数继承不是一回事
java
class Animal {}
class Dog extends Animal {}
List<Dog> dogs = new ArrayList<>();
// List<Animal> animals = dogs; // 编译错误
9.2 需要协变时extends
java
List<Dog> dogs = List.of(new Dog());
List<? extends Animal> animals = dogs;
Animal a = animals.get(0);
9.3 需要逆变时用 super
java
List<Animal> animals = new ArrayList<>();
List<? super Dog> sink = animals;
sink.add(new Dog());
10. 类型擦除:编译后发生了什么?
10.1 核心理解
Java 的泛型主要发生在编译期,编译后会擦除类型参数,并在必要时插入强转
10.2 擦除示例
源码
java
public class Holder<T> {
private T value;
public T getValue() { return value; }
}
可近似理解为
java
public class Holder {
private Object value;
public Object getValue() { return value; }
}
调用侧由编译器补强转
10.3 擦除带来的限
- 不能
new T() - 不能
T.class - 不能
instanceof List<String> - 不能创建泛型数组
new T[10]
10.4 常见替代方案
java
public class Creator<T> {
private final Class<T> clazz;
public Creator(Class<T> clazz) {
this.clazz = clazz;
}
public T newInstance() throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
}
11. 泛型数组与可变参数警告(实战常见
11.1 泛型数组限制
错误
java
// T[] arr = new T[10]; // 编译错误
可行但需警惕
java
@SuppressWarnings("unchecked")
T[] arr = (T[]) new Object[10];
11.2 泛型 + 可变参数(heap pollution
java
public static <T> List<T> of(T... elements) {
return Arrays.asList(elements);
}
如果方法内部不会elements 暴露到不安全上下文,可用 @SafeVarargs
java
@SafeVarargs
public static <T> List<T> immutableOf(T... elements) {
return List.of(elements);
}
12. 通用工具方法模板(可直接复用
12.1 判空并返回默认
java
public static <T> T defaultIfNull(T value, T defaultValue) {
return value == null ? defaultValue : value;
}
12.2 安全取第一
java
public static <T> Optional<T> first(Collection<T> data) {
if (data == null || data.isEmpty()) {
return Optional.empty();
}
return Optional.ofNullable(data.iterator().next());
}
12.3 批量转换
java
public static <S, T> List<T> mapList(
Collection<? extends S> source,
Function<? super S, ? extends T> mapper
) {
if (source == null || source.isEmpty()) {
return List.of();
}
List<T> result = new ArrayList<>(source.size());
for (S item : source) {
result.add(mapper.apply(item));
}
return result;
}
12.4 分组工具
java
public static <T, K> Map<K, List<T>> groupBy(
Collection<T> source,
Function<? super T, ? extends K> keyExtractor
) {
Map<K, List<T>> map = new HashMap<>();
for (T item : source) {
K key = keyExtractor.apply(item);
map.computeIfAbsent(key, k -> new ArrayList<>()).add(item);
}
return map;
}
13. Spring Boot 场景 1:统一返回
13.1 统一响应对象
java
public class ApiResponse<T> {
private boolean success;
private String code;
private String message;
private T data;
public static <T> ApiResponse<T> ok(T data) {
ApiResponse<T> r = new ApiResponse<>();
r.success = true;
r.code = "0";
r.message = "OK";
r.data = data;
return r;
}
public static <T> ApiResponse<T> fail(String code, String message) {
ApiResponse<T> r = new ApiResponse<>();
r.success = false;
r.code = code;
r.message = message;
return r;
}
// getter/setter 省略
}
13.2 Controller 使用
java
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public ApiResponse<UserDTO> getById(@PathVariable Long id) {
UserDTO dto = new UserDTO();
dto.setId(id);
dto.setName("demo");
return ApiResponse.ok(dto);
}
@GetMapping
public ApiResponse<List<UserDTO>> list() {
List<UserDTO> list = new ArrayList<>();
return ApiResponse.ok(list);
}
}
13.3 统一返回体的泛型边界建议
ApiResponse<T>保持单一类型参数即可,别叠太多泛型参数- 分页直接
ApiResponse<PageResult<UserDTO>>,不要再搞一套"特殊分页响应" - 错误响应也复用同一个泛型壳,减少前端分支判断
14. Spring Boot 场景 2:DTO 映射组件
14.1 基础接口
java
public interface Mapper<S, T> {
T toTarget(S source);
default List<T> toTargetList(Collection<? extends S> sourceList) {
if (sourceList == null || sourceList.isEmpty()) {
return List.of();
}
List<T> result = new ArrayList<>(sourceList.size());
for (S source : sourceList) {
result.add(toTarget(source));
}
return result;
}
}
14.2 具体实现
java
@Component
public class UserMapper implements Mapper<UserEntity, UserDTO> {
@Override
public UserDTO toTarget(UserEntity source) {
UserDTO dto = new UserDTO();
dto.setId(source.getId());
dto.setName(source.getName());
dto.setEmail(source.getEmail());
return dto;
}
}
14.3 Service 中使
java
@Service
public class UserService {
private final UserMapper userMapper;
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
public List<UserDTO> listUsers() {
List<UserEntity> entities = queryFromDb();
return userMapper.toTargetList(entities);
}
private List<UserEntity> queryFromDb() {
return List.of();
}
}
15. Spring Boot 场景 3:分页模型泛型化
15.1 PageResult
java
public class PageResult<T> {
private long total;
private int pageNo;
private int pageSize;
private List<T> records;
public PageResult() {
}
public PageResult(long total, int pageNo, int pageSize, List<T> records) {
this.total = total;
this.pageNo = pageNo;
this.pageSize = pageSize;
this.records = records;
}
// getter/setter 省略
}
15.2 分页转换工具
java
public final class PageMappers {
private PageMappers() {
}
public static <S, T> PageResult<T> mapPage(
PageResult<S> source,
Function<? super S, ? extends T> converter
) {
List<T> records = new ArrayList<>();
if (source.getRecords() != null) {
for (S item : source.getRecords()) {
records.add(converter.apply(item));
}
}
return new PageResult<>(
source.getTotal(),
source.getPageNo(),
source.getPageSize(),
records
);
}
}
15.3 使用示例
java
PageResult<UserEntity> entityPage = repository.pageQuery();
PageResult<UserDTO> dtoPage = PageMappers.mapPage(entityPage, userMapper::toTarget);
16. Spring Boot 场景 4:通用校验器接
16.1 定义
java
public interface Validator<T> {
void validate(T target);
}
16.2 组合校验
java
public class CompositeValidator<T> implements Validator<T> {
private final List<Validator<? super T>> validators;
public CompositeValidator(List<Validator<? super T>> validators) {
this.validators = validators;
}
@Override
public void validate(T target) {
for (Validator<? super T> validator : validators) {
validator.validate(target);
}
}
}
这里使用 ? super T 的好处是:传T 的父类校验器也能复用
17. JSON 反序列化与泛型:类型擦除的实战应
17.1 使用 Class<T>
java
public <T> T read(String json, Class<T> clazz) throws Exception {
// return objectMapper.readValue(json, clazz);
return clazz.getDeclaredConstructor().newInstance();
}
17.2 复杂泛型要用 TypeReference
java
String json = "[{\"id\":1,\"name\":\"Tom\"}]";
// List<UserDTO> list = objectMapper.readValue(
// json,
// new TypeReference<List<UserDTO>>() {}
// );
17.3 常见误区
- 以为
Class<List<UserDTO>>可用,实际上不可表达具体元素泛型 - 接口里只
Class<T>,遇Map<String, List<UserDTO>>就不够用了
18. 反射获取泛型参数(高级但实用
18.1 父类带泛型参
java
public abstract class BaseRepository<T> {
private final Class<T> entityClass;
@SuppressWarnings("unchecked")
protected BaseRepository() {
ParameterizedType type = (ParameterizedType) getClass().getGenericSuperclass();
this.entityClass = (Class<T>) type.getActualTypeArguments()[0];
}
public Class<T> getEntityClass() {
return entityClass;
}
}
18.2 子类固定泛型实参
java
public class UserRepository extends BaseRepository<UserEntity> {
}
18.3 使用效果
java
UserRepository repo = new UserRepository();
Class<UserEntity> clazz = repo.getEntityClass();
System.out.println(clazz.getSimpleName()); // UserEntity
小结
- 泛型核心价值是把类型错误前移到编译期,减少运行时强转风险。
? extends T适合"读",? super T适合"写",PECS 是最稳的判断准则。- API 设计优先"参数宽进、返回严出",尽量少让调用方处理复杂通配符。
- 类型擦除决定了运行时泛型信息有限,序列化和反射场景要显式传类型。
- 在 Spring Boot 项目里,泛型最适合用在统一返回体、转换器、分页模型等公共层。
下一篇(018)预告:异常体系:受检/非受检与业务异常分层------如何定义异常边界、统一错误码,并打通日志与告警链路。