PageImpl<T> 构造函数体现 PECS 原则:content 参数声明为 List<? extends T>(生产者,只读取),total 与 pageable 参数为消费者场景,确保类型安全与灵活协变。

Spring Data 的 PageImpl<T> 构造函数是 PECS(Producer Extends, Consumer Super)原则在真实框架代码中非常典型的体现,尤其体现在其对内容列表(content)参数的泛型设计上。
PageImpl 构造函数明确使用 extends T> 接收 content
查看 PageImpl 的核心构造方法:
public PageImpl(List<? extends T> content, Pageable pageable, long total) { ... }
这里 List<? extends T> 表明:content 是一个只读的数据生产者。它不负责写入新元素,只提供已有的、类型兼容的元素供外部消费(比如遍历、序列化、分页展示)。这完全符合 PECS 中 “Producer → extends” 的逻辑。
- 你可以传入
List<User>构造PageImpl<Person>(假设User extends Person) - 也可以传入
List<Admin>构造PageImpl<Role>(若Admin extends Role) - 但不能往这个
content列表里add(new Person())——编译器会拒绝,因为通配符上限不确定
为什么不用 List<T>?——灵活性与安全性的权衡
如果构造函数写成 List<T> content,调用方就必须严格匹配类型:
立即学习“Java免费学习笔记(深入)”;
- 无法把
List<Customer>传给PageImpl<User>,哪怕Customer是User的子类 - 导致大量不必要的类型转换或包装操作,破坏泛型初衷
而 ? extends T 在不牺牲类型安全的前提下,允许更宽泛的输入,让 PageImpl 成为真正可复用的“结果容器”,而非强绑定的具体实现。
对比:Page 接口中的 getContent() 方法也遵循同一逻辑
Page<T> 接口定义:
List<T> getContent();
注意:返回的是 List<T>,不是 List<? extends T>。这是因为接口使用者需要确信拿到的是 T 类型元素(比如用于流式处理、DTO 转换),而 PageImpl 内部通过构造时的 ? extends T 输入 + 运行时类型擦除保障了这一契约——你放进来的都是 T 或其子类,取出来自然能安全转为 T。
这种“输入宽松(extends)、输出严谨(T)”的设计,正是 PECS 在分层 API 中的自然延伸。
延伸:PageImpl 不暴露写入能力,印证其“纯生产者”角色
PageImpl 没有提供 add()、set() 等修改 content 的方法。它的职责就是封装、呈现已有数据。这也从行为层面印证了 ? extends T 的合理性——既然不消费(写入)T,就不该用 ? super T;既然只产出(读取)T,就该用 ? extends T。


















