)
Java 泛型通配符实战:? extends 与 ? super 到底怎么选(PECS 原则)写 Java 的人几乎都被List? extends Number和List? super Integer绕晕过。明明都是通配符,一个只能读不能写,一个能写不能安全读,IDE 报错还看不懂。这篇不背概念,直接从「你写的方法怎么才能同时收ListInteger和ListDouble」这个真实需求出发,把 PECS 讲成肌肉记忆。先看没有通配符时的死路假设你要写一个把 List 里的数字求和的方法:staticdoublesum(ListNumberlist){doubles0;for(Numbern:list)sn.doubleValue();returns;}看着没问题,但你调用时会发现:ListIntegerintsList.of(1,2,3);sum(ints);// 编译错误!ListInteger 不是 ListNumber很多人这里第一反应是「Integer 是 Number 的子类,List 当然是 List」——错。Java 泛型是不变的(invariant):ListInteger和ListNumber没有继承关系。如果允许它们互相赋值,你就能往ListInteger里塞一个Double,类型安全就崩了。上界通配符 ? extends:生产者,只读要让sum收下任意「元素是 Number 或其子类」的 List,用? extends Number:staticdoublesum(List?extendsNumberlist){doubles0;for(Numbern:list)sn.doubleValue();// 读出来当 Number 用,安全returns;}ListIntegerintsList.of(1,2,3);ListDoubledoublesList.of(1.5,2.5);sum(ints);// 编译通过sum(doubles);// 编译通过? extends Number的含义是:「某个具体但未知的、是 Number 子类型的类型」。因为编译器不知道确切类型,所以:staticvoiddemo(List?extendsNumberlist){Numbernlist.get(0);// 读:一定是 Number,允许list.add(1);// 写:编译错误!list.add(null);// 只有 null 例外}为什么不能写?因为 list 实际可能是ListDouble,你往里 add 一个Integer就破坏了它。编译器无法确认你要加的东西是不是「那个未知子类型」,索性一律禁止。所以? extends的容器只能当数据来源(生产者 Producer),从里面往外拿。下界通配符 ? super:消费者,可写反过来,如果你要写一个「往 List 里塞 Integer」的方法,希望它能接受ListInteger、ListNumber、ListObject,用? super Integer:staticvoidaddNumbers(List?superIntegerlist){for(inti1;i3;i){list.add(i);// 写:一定能装下 Integer,安全}}ListNumbernumsnewArrayList();ListObjectobjsnewArrayList();addNumbers(nums);// 通过addNumbers(objs);// 通过? super Integer是「某个未知的、是 Integer 父类型的类型」。因为无论那个未知父类型是谁,Integer都能放进去,所以写是安全的。但读呢?staticvoiddemo(List?superIntegerlist){list.add(42);// 写:允许Integerxlist.get(0);// 读:编译错误!Objectolist.get(0);// 只能当 Object 读}读出来的东西编译器只知道「是 Integer 的某个父类」,最保守就是Object,拿不到具体类型。所以? super的容器当数据接收方(消费者 Consumer),只往里塞。PECS:一句话记死Effective Java 里的口诀PECS Producer-Extends, Consumer-Super:参数是生产数据给你用的(你从里面读)→ 用? extends参数是消费你给的数据的(你往里面写)→ 用? super经典案例是Collections.copy,一个读一个写,两个通配符一起上:publicstaticTvoidcopy(List?superTdest,List?extendsTsrc){for(inti0;isrc.size();i){dest.set(i,src.get(i));// 从 src 读(生产者 extends),往 dest 写(消费者 super)}}ListObjectdestnewArrayList(List.of(0,0,0));ListIntegersrcList.of(1,2,3);copy(dest,src);// Integer 能读、能塞进 Object 列表src是数据来源用extends,dest是数据去处用super,完美对应。一个真实踩坑:自己的接口忘了加通配符假设你写了个事件处理器接口:interfaceHandlerT{voidhandle(Tevent);}// 想注册:一个能处理 Object 的 handler 应该也能处理 String 事件staticvoidregister(HandlerStringh){/* ... */}HandlerObjectuniversale-System.out.println(e);register(universal);// 编译错误!HandlerObject 不是 HandlerStringuniversal明明能处理任何 Object(包括 String),却传不进去。因为handle是消费event 的(消费者),按 PECS 参数该用super:staticvoidregister(Handler?superStringh){h.handle(hello);}register(universal);// 现在通过了判断依据永远是看方法内部对这个泛型参数是读还是写:handle往里塞(消费)String,就用super。小结Java 泛型不变:ListInteger不是ListNumber,别指望自动向上转。? extends X:生产者,只能读(读出来是 X),不能写(null 除外)。数据从它流出。? super X:消费者,能写(写 X 安全),读只能当 Object。数据往它流入。PECS:你读它就extends,你写它就super;既读又写(如copy)两个一起用。设计自己的泛型方法时,盯住「方法体里对这个参数是 get 还是 add」来决定用哪个。一句话记忆点:Producer-Extends,Consumer-Super——从哪读用 extends,往哪写用 super。