关于 Java 泛型方法定义的疑惑
在 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
这里该如何理解呢?
----------------------- 以下是精选回复-----------------------
答:如果这样定义的话,就没有问题:
```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`。
答:泛型必须有泛型参数,或者模板参数,这样才能在使用的时候将模板参数替换成实际内容。你标题里面的定义缺少了模板参数,这样定义的时候没问题,但是使用的时候因为没有传递模板参数(也无法传递)导致没法替换。换成你回复里面的定义方式,加上了模板参数,这样使用的时候 “Util<A1> util” 这就把 A1 这个参数传进去了,就能用。
答:interface Util2<T> {
void process(List<? super T> list, Function<T, Boolean> function);
}
答:1.
因为前面的 ? 和 后面的 ? 不一定是一样的类型,所以不行
换成同一个类型参数就可以了:<T> void process(List<T extends A1> list, Function<T extends A1, Boolean> function)
2. super 同上,用同一个类型参数就行。另外 super 一般约束返回,拿来约束参数有点没想好是要什么效果
答: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 {
}
答:运行起来才发现怪怪的,楼主定义的 Util 是个函数式接口,但它的具体方法又继续用函数式接口,这样嵌套下来的场景,貌似我不好举例。所以就把 Util 换成工具类了。然后实际运行中,静态方法无法使用模板参数,所以 Util 又给改成对象类型的。
代码看上面,最终的效果是。A2 的工具类,可以从 A3 的 List 中做读方向处理,可以往 A1 的 List 中做写方向处理。
对于楼主 1 楼的需求,如果是这样,Util 类是个函数式接口,模板参数是“A3 的父类”,这是绝对不行的,因为这样的效果等同于方法的形参定义成了“某某或它的父类”,而这是违反面向对象基本原则的。如果是这样,Util 类是带模板参数的普通类,它的其中一个方法的参数限制为“模板参数的父类”,这是可以的,实际效果就看我上面的代码。
答: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” 是传入参数而不再是模板参数,不再一起定义,而是分开定义。
答:PECS: producer extend consumer super.
Function 改为? super A1 即可。
详细解释可以搜 so
答:回到楼主的最初疑问上,有必要对泛型标记做一个区分。
泛型说到本质,就是模板替换。而模板替换,需要首先定义两个东西:一个是替换什么,即模板变量;一个是在哪里替换,即引用模板变量的地方。
举例来说一下:
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 的父类。这跟泛型都没关系了,已经违反基本准则了,显然是不可实现的。
0条评论