关于 Java 泛型方法定义的疑惑

2022-06-13 10:42:26 +08:00
 ak1ak

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

这里该如何理解呢?

2794 次点击
所在节点    Java
13 条回复
ak1ak
2022-06-13 10:51:48 +08:00
如果这样定义的话,就没有问题:
```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`。
nothingistrue
2022-06-13 11:09:54 +08:00
泛型必须有泛型参数,或者模板参数,这样才能在使用的时候将模板参数替换成实际内容。你标题里面的定义缺少了模板参数,这样定义的时候没问题,但是使用的时候因为没有传递模板参数(也无法传递)导致没法替换。换成你回复里面的定义方式,加上了模板参数,这样使用的时候 “Util<A1> util” 这就把 A1 这个参数传进去了,就能用。
ak1ak
2022-06-13 11:30:20 +08:00
@nothingistrue 请问有没有一种方式可以实现类似 process(List<? super A3> list, Function<? super A3, Boolean> function) 这样的功能。
nothingistrue
2022-06-13 11:38:47 +08:00
interface Util2<T> {
void process(List<? super T> list, Function<T, Boolean> function);
}
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 一般约束返回,拿来约束参数有点没想好是要什么效果
GuuJiang
2022-06-13 12:35:29 +08:00
@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 原则的一个很好的例子
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 {
}
nothingistrue
2022-06-13 12:57:07 +08:00
运行起来才发现怪怪的,楼主定义的 Util 是个函数式接口,但它的具体方法又继续用函数式接口,这样嵌套下来的场景,貌似我不好举例。所以就把 Util 换成工具类了。然后实际运行中,静态方法无法使用模板参数,所以 Util 又给改成对象类型的。

代码看上面,最终的效果是。A2 的工具类,可以从 A3 的 List 中做读方向处理,可以往 A1 的 List 中做写方向处理。

对于楼主 1 楼的需求,如果是这样,Util 类是个函数式接口,模板参数是“A3 的父类”,这是绝对不行的,因为这样的效果等同于方法的形参定义成了“某某或它的父类”,而这是违反面向对象基本原则的。如果是这样,Util 类是带模板参数的普通类,它的其中一个方法的参数限制为“模板参数的父类”,这是可以的,实际效果就看我上面的代码。
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” 是传入参数而不再是模板参数,不再一起定义,而是分开定义。
chonh
2022-06-13 14:06:53 +08:00
PECS: producer extend consumer super.

Function 改为? super A1 即可。
详细解释可以搜 so
nothingistrue
2022-06-13 14:47:07 +08:00
回到楼主的最初疑问上,有必要对泛型标记做一个区分。

泛型说到本质,就是模板替换。而模板替换,需要首先定义两个东西:一个是替换什么,即模板变量;一个是在哪里替换,即引用模板变量的地方。
举例来说一下:
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 的父类。这跟泛型都没关系了,已经违反基本准则了,显然是不可实现的。
ak1ak
2022-06-13 15:33:58 +08:00
感谢各位的回答,总结一下,本质上就两点容易纠结的地方:

@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 子类型的元素。
dk7952638
2022-06-13 16:25:41 +08:00
年轻人,老夫奉劝你不要对 JAVA 的泛型有过高的期望,尤其是灵活性方面,过多的技巧最终你会发现小丑竟是你自己,泛型的尽头就是 @SuppressWarnings("unchecked")

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/859220

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX