PECS原则是Command<T>执行结果安全收集的关键约束:生产者用? extends T(如List<? extends R>),消费者用? super T(如Collection<? super T>),禁止同一容器既读又写,确保编译期类型安全。

在命令模式中使用 Command<T> 时,执行结果的收集环节最容易因泛型设计不当引发类型不安全或调用僵化。PECS 原则不是锦上添花的语法技巧,而是让 execute() 的输出能被下游安全消费、让结果收集器能兼容各类命令返回值的关键设计约束。
执行结果作为生产者:用 ? extends T
命令对象的 T execute() 方法返回单个结果;而批量执行(如 CommandExecutor.executeAll(List<Command<R>>))常需将多个结果统一收集到容器中。此时该容器是“数据提供方”,必须声明为生产者:
-
List<? extends R> results—— 允许传入ArrayList<SuccessResult>、LinkedList<ErrorResponse>(假设二者都继承R),下游可安全遍历为R类型调用公共方法 - 禁止在该集合中
add(new RImpl()),因为编译器无法确认实际类型是否匹配(例如传入的是只读Arrays.asList()) - 若需构建新集合,应在内部用具体类型(如
new ArrayList<R>())完成,再以List<? extends R>返回
结果收集器作为消费者:用 ? super T
当命令执行器需要把不同子类型的返回值统一归并到一个目标容器(如日志汇总、缓存写入、批量落库),该容器是“数据接收方”:
-
void collect(Command<T> cmd, Collection<? super T> sink)—— 调用方可传ArrayList<Object>、Set<Serializable>或LinkedList<T>,所有cmd.execute()返回的T及其子类实例均可安全add() - 从
sink中取元素时只能按Object处理,需配合instanceof或统一接口做有限校验,不可直接强转为T - 避免声明为
Collection<T>:会拒绝Command<Payment>执行后往Collection<Order>(Payment是Order子类)中写入
组合场景:执行 + 收集一体化签名
常见于异步命令批处理或管道式工作流,此时方法同时读命令(生产者)、写结果(消费者):
立即学习“Java免费学习笔记(深入)”;
<R> void runCommands(List<? extends Command<R>> commands, Collection<? super R> output)- 上游可传
List<PayCommand>、List<RefundCommand>(二者均扩展Command<Transaction>) - 下游可接
ArrayList<Transaction>、Vector<Object>,甚至ConcurrentLinkedQueue<Serializable> - 方法体内可安全遍历
commands并对每个cmd.execute()结果调用output.add(result)
避坑要点:别让一个容器既读又写
如果某个字段或参数既要 get() 又要 add(),说明职责混淆:
- 例如
CommandContext<T>中声明List<T> history用于记录中间状态并供后续命令读取 —— 这违反 PECS,应拆分为List<? extends T> view()(只读视图)和void append(T item)(专用写入入口) - 强行用
List<T>或原始List会导致调用方频繁做(T) obj强转,或被迫包装转换,引入运行时ClassCastException - 真正健壮的设计,是让编译器替你守住边界:写错就报错,而不是靠测试或运气发现类型问题


















