【017】泛型与通配符:API 设计里怎么用省心

很多同学第一次学泛型时觉得它只是"把 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,Map
  • V:Value,Map
  • R: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)预告:异常体系:受检/非受检与业务异常分层------如何定义异常边界、统一错误码,并打通日志与告警链路。

相关推荐
IT利刃出鞘2 小时前
Spring工具类--ObjectUtils的使用
java·后端·spring
MY_TEUCK8 小时前
Sealos 平台部署实战指南:结合 Cursor 与版本发布流程
java·人工智能·学习·aigc
2401_873479408 小时前
如何利用IP查询定位识别电商刷单?4个关键指标+工具配置方案
开发语言·tcp/ip·php
我爱cope9 小时前
【从0开始学设计模式-10| 装饰模式】
java·开发语言·设计模式
菜鸟学Python9 小时前
Python生态在悄悄改变:FastAPI全面反超,Django和Flask还行吗?
开发语言·python·django·flask·fastapi
朝新_9 小时前
【Spring AI 】图像与语音模型实战
java·人工智能·spring
RH2312119 小时前
2026.4.16Linux 管道
java·linux·服务器
zmsofts10 小时前
java面试必问13:MyBatis 一级缓存、二级缓存:从原理到脏数据,一篇讲透
java·面试·mybatis
浪浪小洋10 小时前
c++ qt课设定制
开发语言·c++