Java泛型上下界通配符不同形式类型参数的实际使用场景问询
这两种写法不是语法糖秀操作,是实际业务开发、基础组件封装里非常常见的写法,核心是靠泛型通配符做编译期的类型安全约束,同时最大化API的灵活度,也就是大家常说的*PECS(Producer Extends, Consumer Super)*原则的落地。
上界通配符? extends Exception的实际使用场景
你写的第一行List<? extends Exception> list1 = new ArrayList<IOException>();,核心适用场景是这个集合只作为数据生产者,你只会从里面读内容,不会往里面写非null元素。
最常见的落地就是工具类、通用组件的参数定义:比如你要写一个批量打印异常栈轨迹的方法,如果把入参定义成List<Exception>,那调用方传List<IOException>、List<SQLException>的时候会直接编译报错——但逻辑上这些集合里存的全是Exception的子类,遍历打印完全没问题。这时候把入参改成List<? extends Exception>就解决了兼容性问题:
// 全局异常日志打印工具 public void printExceptionStacks(List<? extends Exception> exceptionList) { // 读出来的元素一定是Exception类型,完全安全 for (Exception e : exceptionList) { e.printStackTrace(); } // 编译器会直接拦住写入操作,避免误塞不符合实际泛型类型的元素 // 比如如果实际传入的是ArrayList<IOException>,塞RuntimeException肯定是错的,编译期就给你拦了 // exceptionList.add(new RuntimeException("非法写入")); } // 调用方代码,开发中天天这么写 List<IOException> ioErrorList = new ArrayList<>(); ioErrorList.add(new FileNotFoundException("配置文件不存在")); printExceptionStacks(ioErrorList); // 正常编译通过,不用做任何类型强转
下界通配符? super IOException的实际使用场景
第二行List<? super IOException> list2 = new ArrayList<Exception>();反过来,适用场景是这个集合只作为数据消费者,你只会往里面写元素,读的时候不关心元素的具体类型(最多当Object处理)。
典型落地是异常收集、数据归集类的逻辑:比如你要写一个收集IO类异常的方法,允许调用方用任意能存IOException的容器当收集器。如果入参定义成List<IOException>,那调用方想传全局的List<Exception>异常容器就会编译报错——但逻辑上往List<Exception>里塞IOException是完全合法的。这时候用? super IOException就刚好:
// 收集IO链路异常的方法 public void collectIoErrors(List<? super IOException> errorCollector) { // 可以安全写入IOException及其任意子类 errorCollector.add(new FileNotFoundException("证书文件缺失")); errorCollector.add(new SocketException("网络连接中断")); // 编译器会拦住非法写入,你没法塞IOException的父类进去 // errorCollector.add(new Exception("通用异常")); } // 调用方代码,全局异常容器通常是List<Exception>类型,直接传就行 List<Exception> globalErrorContainer = new ArrayList<>(); collectIoErrors(globalErrorContainer); // 正常编译通过
你可能天天用但没注意的JDK源码里就全是这种写法,比如Collections.copy()的方法签名就是public static <T> void copy(List<? super T> dest, List<? extends T> src):源列表是只出不进的生产者,所以用extends;目标列表是只进不出的消费者,所以用super。你平时调用这个方法的时候,传new ArrayList<IOException>()当源、传new ArrayList<Exception>()当目标是完全合法的,这就是最普遍的落地场景。
这种写法的核心价值就两点:
- 不用写一堆重载方法,就能让API兼容所有逻辑上合法的参数类型,灵活度拉满
- 所有类型校验在编译期完成,不会等到运行时才抛
ClassCastException,类型安全有保障
顺便提一句:直接把局部变量定义成通配符类型再new具体实现类的写法不算多见,大部分场景下通配符都是用在方法入参、返回值的定义上,局部变量这么写一般是做类型兼容的临时中转,底层逻辑和上面说的场景完全一致。
内容的提问来源于stack exchange,提问作者rostIvan

