在 https://www.v2ex.com/t/858692 里讨论了 "? super T 和 ? extends T" 的问题。我现在有一个疑惑,在设计接口的时候,应该如何正确使用泛型通配符。
借用引用该回答里的定义,有 3 个类:A1 、A2 、A3 ,A2 继承 A1 ,A3 继承 A2 ,那么有:A1>A2>A3 。如果想设计一个工具方法,接收所有继承自 A1 的元素,以及一个对元素操作的方法。理论上可以这个写:
interface Util {
void process(List<? extends A1> list, Function<? extends A1, Boolean> function);
}
但是实际上使用的时候,下面代码会报错:
Util util = (l, f) -> {
l.forEach(e -> {
f.apply(e); // 这里会报错
});
};
在报错的地方,IDE 提示如下:
Required type:
capture of ? extends A1
Provided:
capture of ? extends A1
这里该如何理解呢?
1
ak1ak OP 如果这样定义的话,就没有问题:
```java // 定义工具类 interface Util<T extends A1> { void process(List<T> list, Function<T, Boolean> function); } //使用工具类 Util<A1> util = (l, f) -> { l.forEach(e -> { Boolean flag = f.apply(e); }); }; ``` 这样使用是没有问题的,那如果改一下需求,Util 类需要接收所有是 A3 父类的 List 和一个 List 元素操作的方法又要如何设计呢? Java 里不允许定义 `T super A3`。 |
2
nothingistrue 2022-06-13 11:09:54 +08:00
泛型必须有泛型参数,或者模板参数,这样才能在使用的时候将模板参数替换成实际内容。你标题里面的定义缺少了模板参数,这样定义的时候没问题,但是使用的时候因为没有传递模板参数(也无法传递)导致没法替换。换成你回复里面的定义方式,加上了模板参数,这样使用的时候 “Util<A1> util” 这就把 A1 这个参数传进去了,就能用。
|
3
ak1ak OP @nothingistrue 请问有没有一种方式可以实现类似 process(List<? super A3> list, Function<? super A3, Boolean> function) 这样的功能。
|
4
nothingistrue 2022-06-13 11:38:47 +08:00
interface Util2<T> {
void process(List<? super T> list, Function<T, Boolean> function); } |
5
chendy 2022-06-13 12:11:18 +08:00
1.
因为前面的 ? 和 后面的 ? 不一定是一样的类型,所以不行 换成同一个类型参数就可以了:<T> void process(List<T extends A1> list, Function<T extends A1, Boolean> function) 2. super 同上,用同一个类型参数就行。另外 super 一般约束返回,拿来约束参数有点没想好是要什么效果 |
6
GuuJiang 2022-06-13 12:35:29 +08:00 via iPhone
@nothingistrue 如果你真正看懂了我在隔壁的回答就不会有这个疑问了,你在#1 和#3 说的这种场景是不可能实现的,不是 Java 语法的限制,而是你假想的这个需求本身就有问题,我们姑且先忽略掉 List<? super A3>是不能进行 get 操作的这一点,退一步讲,哪怕允许 get 了,简化一下需求,要定义一个方法,其参数可能是 A3 及其父类,那你在这个方法的内部能够把这个参数当成什么类型呢?唯一的选择就只有 Object 了,这里的 A3 没有提供任何信息量,因此这样的方法没有任何的意义,也不可能存在,也不可能具有实际应用场景
和你试图假定的这个场景最接近的应该是下面这个 interface Util<T extends A1> { void process(List<? super A3> list, Supplier<? extends A3> function); } 这里的 Supplier<? extends A3>也可以换成 Function<T, ? extends A3>,其中的 T 是任意一个具体类型 然后在你的 process 内部也不能像你想象的那样从 list 中 get 然后交给 function 处理,而只能调用 function 然后将返回值 add 到 list 坦白说这确实是演示 PECS 原则的一个很好的例子 |
7
nothingistrue 2022-06-13 12:40:50 +08:00
class Scratch {
public static void main(String[] args) { Util<A2> util = new Util<>(); List<A3> a3List= new ArrayList<>(); util.getAndProcess(a3List,a3 -> {return true;}); List<A1> a1List = new ArrayList<>(); util.supplyAndSet(a1List,A2::new ); } } class Util<T> { public List<? extends T> getAndProcess(List<? extends T> list, Function<T, Boolean> function) { list.forEach(e->function.apply(e)); return list; } public List<? super T> supplyAndSet(List<? super T> list, Supplier<T> supplier) { list.add(supplier.get()); return list; } } class A1 { } class A2 extends A1 { } class A3 extends A2 { } |
8
nothingistrue 2022-06-13 12:57:07 +08:00
运行起来才发现怪怪的,楼主定义的 Util 是个函数式接口,但它的具体方法又继续用函数式接口,这样嵌套下来的场景,貌似我不好举例。所以就把 Util 换成工具类了。然后实际运行中,静态方法无法使用模板参数,所以 Util 又给改成对象类型的。
代码看上面,最终的效果是。A2 的工具类,可以从 A3 的 List 中做读方向处理,可以往 A1 的 List 中做写方向处理。 对于楼主 1 楼的需求,如果是这样,Util 类是个函数式接口,模板参数是“A3 的父类”,这是绝对不行的,因为这样的效果等同于方法的形参定义成了“某某或它的父类”,而这是违反面向对象基本原则的。如果是这样,Util 类是带模板参数的普通类,它的其中一个方法的参数限制为“模板参数的父类”,这是可以的,实际效果就看我上面的代码。 |
9
nothingistrue 2022-06-13 13:52:07 +08:00
1 楼的需求,变通一下,也是可以实现的。变通后的需求是:接受一个对象,将之转换,然后将转换后的结果加入到 指定类的的父类的 list 。
interface ProcessAndSet<T>{ void processAndSet(T element, Function<T, A3> function, List<? super A3> list); } ProcessAndSet processAndSet = (e,f,l)->{ l.add(f.apply(e)); }; List<A1> a1List = new ArrayList<>(); 这个变通需求与原始需求的区别是:原始需求中 “A3 父类的 List” 作为模板参数,要跟函数式接口一并定义,变通后,“A3 父类的 List” 是传入参数而不再是模板参数,不再一起定义,而是分开定义。 |
10
chonh 2022-06-13 14:06:53 +08:00 via Android
PECS: producer extend consumer super.
Function 改为? super A1 即可。 详细解释可以搜 so |
11
nothingistrue 2022-06-13 14:47:07 +08:00 1
回到楼主的最初疑问上,有必要对泛型标记做一个区分。
泛型说到本质,就是模板替换。而模板替换,需要首先定义两个东西:一个是替换什么,即模板变量;一个是在哪里替换,即引用模板变量的地方。 举例来说一下: public interface List<E> { boolean add(E e); } 左边的<E> 是模板变量,右边的那个 E 是模板变量的引用。 <T> T[] toArray(T[] a) ;(该方法同样在 List 中) 昨天的<T> 是模板变量,右边的那个 T 是模板变量的引用。 上面只是定义了模板,到了使用的时候,你还得再定义第三个东西:替换成什么。 举例: ArrayList<String> = new ArrayList<>(); 这里就定了了将相关的 E 替换成 String 。 通配符,只能用在第二个定义,即模板变量的引用那里。模板变量,和模板要替换的值,都必须是确定的,故不能用通配符。这里有一个特殊的地方,返回值那里可以使用<?>通配符,但此时这个<?>等同于<Object>,是个假的通配符。 当上面区分好之后,再看楼主的需求。 主贴当中之所以错误,是因为没有定义模板变量。 1 楼不允许定义`T super A3`的原因,因为这是模板变量,虽然跟普通变量不一样,但也要遵循一样的原则:你只能将变量的类型限定成具体的。T 可以,这相当于 Object 类型,T extend Base 可以,相当于 Base 类型。T extend Base & SomeInterface 也可以,仍然相当于 Base 类型,只不过额外要求实现了 SomeInterface 。T super Child 不可以,因为无法确定这代表哪种类型。 3 楼的需求,想要的效果本质上是:定义一个方法,方法的参数类型是 A3 的父类。这跟泛型都没关系了,已经违反基本准则了,显然是不可实现的。 |
12
ak1ak OP 感谢各位的回答,总结一下,本质上就两点容易纠结的地方:
@nothingistrue 说的模板,就是在使用定义的方法时,能够让编译器能够推导出具体的类型( type reference ),`? super A3` 确实无法推导出一个具体的类型,因此我之前的方法定义是无意义的,现实中也不会有这种需求。 @GuuJiang 说的意思应该是:定义通用泛型方法时,需要考虑到 PECS 原则,结合 RednaxelaFX 在知乎上的回答「 PECS 原则背后的原理,通俗来说就是八字箴言:宽于律人,严于律己。」以 Java Stream<T> 接口为例:map(Function<? super T, ? extends R> mapper) 方法里,目标是完成 T-> R 的转换。因为 T 在消费方( in/consumer ),允许传入所有的 T 以及 T 的父类型的元素,R 在生产方( out/producer ),允许返回所有 R 以及 R 子类型的元素。 |
13
dk7952638 2022-06-13 16:25:41 +08:00
年轻人,老夫奉劝你不要对 JAVA 的泛型有过高的期望,尤其是灵活性方面,过多的技巧最终你会发现小丑竟是你自己,泛型的尽头就是 @SuppressWarnings("unchecked")
|